policyengine 3.1.14__tar.gz → 3.1.16__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {policyengine-3.1.14 → policyengine-3.1.16}/CHANGELOG.md +14 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/PKG-INFO +5 -5
- {policyengine-3.1.14 → policyengine-3.1.16}/changelog.yaml +10 -0
- policyengine-3.1.16/examples/household_impact_example.py +128 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/pyproject.toml +5 -5
- policyengine-3.1.16/src/policyengine/__pycache__/__init__.cpython-313.pyc +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/core/tax_benefit_model_version.py +9 -1
- policyengine-3.1.16/src/policyengine/outputs/__init__.py +49 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/outputs/decile_impact.py +22 -2
- policyengine-3.1.16/src/policyengine/outputs/inequality.py +276 -0
- policyengine-3.1.16/src/policyengine/outputs/poverty.py +238 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/tax_benefit_models/uk/__init__.py +10 -2
- policyengine-3.1.16/src/policyengine/tax_benefit_models/uk/analysis.py +292 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/tax_benefit_models/uk/model.py +15 -4
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/tax_benefit_models/us/__init__.py +10 -2
- policyengine-3.1.16/src/policyengine/tax_benefit_models/us/analysis.py +313 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/tax_benefit_models/us/model.py +15 -4
- policyengine-3.1.16/src/policyengine/utils/__init__.py +7 -0
- policyengine-3.1.16/src/policyengine/utils/parameter_labels.py +213 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine.egg-info/PKG-INFO +5 -5
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine.egg-info/SOURCES.txt +13 -2
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine.egg-info/requires.txt +4 -4
- policyengine-3.1.16/tests/__init__.py +1 -0
- policyengine-3.1.16/tests/fixtures/__init__.py +1 -0
- policyengine-3.1.16/tests/fixtures/parameter_labels_fixtures.py +165 -0
- policyengine-3.1.16/tests/test_household_impact.py +206 -0
- policyengine-3.1.16/tests/test_inequality.py +288 -0
- policyengine-3.1.16/tests/test_models.py +148 -0
- policyengine-3.1.16/tests/test_pandas3_compatibility.py +19 -0
- policyengine-3.1.16/tests/test_parameter_labels.py +894 -0
- policyengine-3.1.16/tests/test_poverty.py +283 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/uv.lock +1 -1
- policyengine-3.1.14/CLAUDE.md +0 -17
- policyengine-3.1.14/src/policyengine/__pycache__/__init__.cpython-313.pyc +0 -0
- policyengine-3.1.14/src/policyengine/outputs/__init__.py +0 -21
- policyengine-3.1.14/src/policyengine/tax_benefit_models/uk/analysis.py +0 -97
- policyengine-3.1.14/src/policyengine/tax_benefit_models/us/analysis.py +0 -99
- policyengine-3.1.14/src/policyengine/utils/__init__.py +0 -3
- policyengine-3.1.14/tests/test_models.py +0 -68
- {policyengine-3.1.14 → policyengine-3.1.16}/.claude/policyengine-guide.md +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/.claude/quick-reference.md +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/.github/CONTRIBUTING.md +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/.github/changelog_template.md +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/.github/fetch_version.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/.github/get-changelog-diff.sh +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/.github/has-functional-changes.sh +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/.github/is-version-number-acceptable.sh +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/.github/publish-git-tag.sh +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/.github/workflows/code_changes.yaml +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/.github/workflows/docs.yml +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/.github/workflows/pr_code_changes.yaml +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/.github/workflows/pr_docs_changes.yaml +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/.github/workflows/versioning.yaml +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/.gitignore +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/LICENSE +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/Makefile +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/README.md +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/changelog_entry.yaml +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/docs/.gitignore +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/docs/core-concepts.md +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/docs/country-models-uk.md +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/docs/country-models-us.md +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/docs/dev.md +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/docs/index.md +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/docs/myst.yml +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/docs/visualisation.md +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/examples/employment_income_variation_uk.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/examples/employment_income_variation_us.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/examples/income_bands_uk.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/examples/income_distribution_us.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/examples/policy_change_uk.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/examples/speedtest_us_simulation.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/setup.cfg +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/__init__.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/core/__init__.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/core/cache.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/core/dataset.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/core/dataset_version.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/core/dynamic.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/core/output.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/core/parameter.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/core/parameter_value.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/core/policy.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/core/simulation.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/core/tax_benefit_model.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/core/variable.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/outputs/aggregate.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/outputs/change_aggregate.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/tax_benefit_models/uk/datasets.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/tax_benefit_models/uk/outputs.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/tax_benefit_models/uk.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/tax_benefit_models/us/datasets.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/tax_benefit_models/us/outputs.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/tax_benefit_models/us.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/utils/dates.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/utils/parametric_reforms.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/utils/plotting.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine.egg-info/dependency_links.txt +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine.egg-info/top_level.txt +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/tests/test_aggregate.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/tests/test_cache.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/tests/test_change_aggregate.py +0 -0
- {policyengine-3.1.14 → policyengine-3.1.16}/tests/test_entity_mapping.py +0 -0
|
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.1.16] - 2026-01-25 14:20:29
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Bumped policyengine-core minimum version to 3.23.5 for pandas 3.0 compatibility
|
|
13
|
+
|
|
14
|
+
## [3.1.15] - 2025-12-14 23:51:27
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- Household impacts
|
|
19
|
+
|
|
8
20
|
## [3.1.14] - 2025-12-10 21:59:24
|
|
9
21
|
|
|
10
22
|
### Fixed
|
|
@@ -281,6 +293,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
281
293
|
|
|
282
294
|
|
|
283
295
|
|
|
296
|
+
[3.1.16]: https://github.com/PolicyEngine/policyengine.py/compare/3.1.15...3.1.16
|
|
297
|
+
[3.1.15]: https://github.com/PolicyEngine/policyengine.py/compare/3.1.14...3.1.15
|
|
284
298
|
[3.1.14]: https://github.com/PolicyEngine/policyengine.py/compare/3.1.13...3.1.14
|
|
285
299
|
[3.1.13]: https://github.com/PolicyEngine/policyengine.py/compare/3.1.12...3.1.13
|
|
286
300
|
[3.1.12]: https://github.com/PolicyEngine/policyengine.py/compare/3.1.11...3.1.12
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: policyengine
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.16
|
|
4
4
|
Summary: A package to conduct policy analysis using PolicyEngine tax-benefit models.
|
|
5
5
|
Author-email: PolicyEngine <hello@policyengine.org>
|
|
6
6
|
License: GNU AFFERO GENERAL PUBLIC LICENSE
|
|
@@ -670,15 +670,15 @@ Description-Content-Type: text/markdown
|
|
|
670
670
|
License-File: LICENSE
|
|
671
671
|
Requires-Dist: pydantic>=2.0.0
|
|
672
672
|
Requires-Dist: pandas>=2.0.0
|
|
673
|
-
Requires-Dist: microdf_python
|
|
673
|
+
Requires-Dist: microdf_python>=1.2.1
|
|
674
674
|
Requires-Dist: plotly>=5.0.0
|
|
675
675
|
Requires-Dist: requests>=2.31.0
|
|
676
676
|
Requires-Dist: psutil>=5.9.0
|
|
677
677
|
Provides-Extra: uk
|
|
678
|
-
Requires-Dist: policyengine_core>=3.
|
|
678
|
+
Requires-Dist: policyengine_core>=3.23.6; extra == "uk"
|
|
679
679
|
Requires-Dist: policyengine-uk>=2.51.0; extra == "uk"
|
|
680
680
|
Provides-Extra: us
|
|
681
|
-
Requires-Dist: policyengine_core>=3.
|
|
681
|
+
Requires-Dist: policyengine_core>=3.23.6; extra == "us"
|
|
682
682
|
Requires-Dist: policyengine-us>=1.213.1; extra == "us"
|
|
683
683
|
Provides-Extra: dev
|
|
684
684
|
Requires-Dist: black; extra == "dev"
|
|
@@ -691,7 +691,7 @@ Requires-Dist: itables; extra == "dev"
|
|
|
691
691
|
Requires-Dist: build; extra == "dev"
|
|
692
692
|
Requires-Dist: pytest-asyncio>=0.26.0; extra == "dev"
|
|
693
693
|
Requires-Dist: ruff>=0.5.0; extra == "dev"
|
|
694
|
-
Requires-Dist: policyengine_core>=3.
|
|
694
|
+
Requires-Dist: policyengine_core>=3.23.6; extra == "dev"
|
|
695
695
|
Requires-Dist: policyengine-uk>=2.51.0; extra == "dev"
|
|
696
696
|
Requires-Dist: policyengine-us>=1.213.1; extra == "dev"
|
|
697
697
|
Dynamic: license-file
|
|
@@ -230,3 +230,13 @@
|
|
|
230
230
|
fixed:
|
|
231
231
|
- Improvements to loading taxbenefitsystems.
|
|
232
232
|
date: 2025-12-10 21:59:24
|
|
233
|
+
- bump: patch
|
|
234
|
+
changes:
|
|
235
|
+
added:
|
|
236
|
+
- Household impacts
|
|
237
|
+
date: 2025-12-14 23:51:27
|
|
238
|
+
- bump: patch
|
|
239
|
+
changes:
|
|
240
|
+
changed:
|
|
241
|
+
- Bumped policyengine-core minimum version to 3.23.5 for pandas 3.0 compatibility
|
|
242
|
+
date: 2026-01-25 14:20:29
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Example: Calculate household tax and benefit impacts.
|
|
2
|
+
|
|
3
|
+
This script demonstrates using calculate_household_impact for both UK and US
|
|
4
|
+
to compute taxes and benefits for custom households.
|
|
5
|
+
|
|
6
|
+
Run: python examples/household_impact_example.py
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from policyengine.tax_benefit_models.uk import (
|
|
10
|
+
UKHouseholdInput,
|
|
11
|
+
)
|
|
12
|
+
from policyengine.tax_benefit_models.uk import (
|
|
13
|
+
calculate_household_impact as calculate_uk_impact,
|
|
14
|
+
)
|
|
15
|
+
from policyengine.tax_benefit_models.us import (
|
|
16
|
+
USHouseholdInput,
|
|
17
|
+
)
|
|
18
|
+
from policyengine.tax_benefit_models.us import (
|
|
19
|
+
calculate_household_impact as calculate_us_impact,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def uk_example():
|
|
24
|
+
"""UK household impact example."""
|
|
25
|
+
print("=" * 60)
|
|
26
|
+
print("UK HOUSEHOLD IMPACT")
|
|
27
|
+
print("=" * 60)
|
|
28
|
+
|
|
29
|
+
# Single adult earning £50,000
|
|
30
|
+
household = UKHouseholdInput(
|
|
31
|
+
people=[{"age": 35, "employment_income": 50_000}],
|
|
32
|
+
year=2026,
|
|
33
|
+
)
|
|
34
|
+
result = calculate_uk_impact(household)
|
|
35
|
+
|
|
36
|
+
print("\nSingle adult, £50k income:")
|
|
37
|
+
print(
|
|
38
|
+
f" Net income: £{result.household['hbai_household_net_income']:,.0f}"
|
|
39
|
+
)
|
|
40
|
+
print(f" Income tax: £{result.person[0]['income_tax']:,.0f}")
|
|
41
|
+
print(
|
|
42
|
+
f" National Insurance: £{result.person[0]['national_insurance']:,.0f}"
|
|
43
|
+
)
|
|
44
|
+
print(f" Total tax: £{result.household['household_tax']:,.0f}")
|
|
45
|
+
|
|
46
|
+
# Family with two children, £30k income, renting
|
|
47
|
+
household = UKHouseholdInput(
|
|
48
|
+
people=[
|
|
49
|
+
{"age": 35, "employment_income": 30_000},
|
|
50
|
+
{"age": 33},
|
|
51
|
+
{"age": 8},
|
|
52
|
+
{"age": 5},
|
|
53
|
+
],
|
|
54
|
+
benunit={
|
|
55
|
+
"would_claim_uc": True,
|
|
56
|
+
"would_claim_child_benefit": True,
|
|
57
|
+
},
|
|
58
|
+
household={
|
|
59
|
+
"rent": 12_000, # £1k/month
|
|
60
|
+
"region": "NORTH_WEST",
|
|
61
|
+
},
|
|
62
|
+
year=2026,
|
|
63
|
+
)
|
|
64
|
+
result = calculate_uk_impact(household)
|
|
65
|
+
|
|
66
|
+
print("\nFamily (2 adults, 2 children), £30k income, renting:")
|
|
67
|
+
print(
|
|
68
|
+
f" Net income: £{result.household['hbai_household_net_income']:,.0f}"
|
|
69
|
+
)
|
|
70
|
+
print(f" Income tax: £{result.person[0]['income_tax']:,.0f}")
|
|
71
|
+
print(f" Child benefit: £{result.benunit[0]['child_benefit']:,.0f}")
|
|
72
|
+
print(f" Universal credit: £{result.benunit[0]['universal_credit']:,.0f}")
|
|
73
|
+
print(f" Total benefits: £{result.household['household_benefits']:,.0f}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def us_example():
|
|
77
|
+
"""US household impact example."""
|
|
78
|
+
print("\n" + "=" * 60)
|
|
79
|
+
print("US HOUSEHOLD IMPACT")
|
|
80
|
+
print("=" * 60)
|
|
81
|
+
|
|
82
|
+
# Single adult earning $50,000
|
|
83
|
+
household = USHouseholdInput(
|
|
84
|
+
people=[
|
|
85
|
+
{"age": 35, "employment_income": 50_000, "is_tax_unit_head": True}
|
|
86
|
+
],
|
|
87
|
+
tax_unit={"filing_status": "SINGLE"},
|
|
88
|
+
household={"state_code_str": "CA"},
|
|
89
|
+
year=2024,
|
|
90
|
+
)
|
|
91
|
+
result = calculate_us_impact(household)
|
|
92
|
+
|
|
93
|
+
print("\nSingle adult, $50k income (California):")
|
|
94
|
+
print(f" Net income: ${result.household['household_net_income']:,.0f}")
|
|
95
|
+
print(f" Income tax: ${result.tax_unit[0]['income_tax']:,.0f}")
|
|
96
|
+
print(f" Payroll tax: ${result.tax_unit[0]['employee_payroll_tax']:,.0f}")
|
|
97
|
+
|
|
98
|
+
# Married couple with children, lower income
|
|
99
|
+
household = USHouseholdInput(
|
|
100
|
+
people=[
|
|
101
|
+
{"age": 35, "employment_income": 40_000, "is_tax_unit_head": True},
|
|
102
|
+
{"age": 33, "is_tax_unit_spouse": True},
|
|
103
|
+
{"age": 8, "is_tax_unit_dependent": True},
|
|
104
|
+
{"age": 5, "is_tax_unit_dependent": True},
|
|
105
|
+
],
|
|
106
|
+
tax_unit={"filing_status": "JOINT"},
|
|
107
|
+
household={"state_code_str": "TX"},
|
|
108
|
+
year=2024,
|
|
109
|
+
)
|
|
110
|
+
result = calculate_us_impact(household)
|
|
111
|
+
|
|
112
|
+
print("\nMarried couple with 2 children, $40k income (Texas):")
|
|
113
|
+
print(f" Net income: ${result.household['household_net_income']:,.0f}")
|
|
114
|
+
print(f" Federal income tax: ${result.tax_unit[0]['income_tax']:,.0f}")
|
|
115
|
+
print(f" EITC: ${result.tax_unit[0]['eitc']:,.0f}")
|
|
116
|
+
print(f" Child tax credit: ${result.tax_unit[0]['ctc']:,.0f}")
|
|
117
|
+
print(f" SNAP: ${result.spm_unit[0]['snap']:,.0f}")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def main():
|
|
121
|
+
uk_example()
|
|
122
|
+
us_example()
|
|
123
|
+
print("\n" + "=" * 60)
|
|
124
|
+
print("Done!")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
if __name__ == "__main__":
|
|
128
|
+
main()
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "policyengine"
|
|
7
|
-
version = "3.1.
|
|
7
|
+
version = "3.1.16"
|
|
8
8
|
description = "A package to conduct policy analysis using PolicyEngine tax-benefit models."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
@@ -15,7 +15,7 @@ requires-python = ">=3.13"
|
|
|
15
15
|
dependencies = [
|
|
16
16
|
"pydantic>=2.0.0",
|
|
17
17
|
"pandas>=2.0.0",
|
|
18
|
-
"microdf_python",
|
|
18
|
+
"microdf_python>=1.2.1",
|
|
19
19
|
"plotly>=5.0.0",
|
|
20
20
|
"requests>=2.31.0",
|
|
21
21
|
"psutil>=5.9.0",
|
|
@@ -23,11 +23,11 @@ dependencies = [
|
|
|
23
23
|
|
|
24
24
|
[project.optional-dependencies]
|
|
25
25
|
uk = [
|
|
26
|
-
"policyengine_core>=3.
|
|
26
|
+
"policyengine_core>=3.23.6",
|
|
27
27
|
"policyengine-uk>=2.51.0",
|
|
28
28
|
]
|
|
29
29
|
us = [
|
|
30
|
-
"policyengine_core>=3.
|
|
30
|
+
"policyengine_core>=3.23.6",
|
|
31
31
|
"policyengine-us>=1.213.1",
|
|
32
32
|
]
|
|
33
33
|
dev = [
|
|
@@ -41,7 +41,7 @@ dev = [
|
|
|
41
41
|
"build",
|
|
42
42
|
"pytest-asyncio>=0.26.0",
|
|
43
43
|
"ruff>=0.5.0",
|
|
44
|
-
"policyengine_core>=3.
|
|
44
|
+
"policyengine_core>=3.23.6",
|
|
45
45
|
"policyengine-uk>=2.51.0",
|
|
46
46
|
"policyengine-us>=1.213.1",
|
|
47
47
|
]
|
{policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/core/tax_benefit_model_version.py
RENAMED
|
@@ -24,7 +24,15 @@ class TaxBenefitModelVersion(BaseModel):
|
|
|
24
24
|
|
|
25
25
|
variables: list["Variable"] = Field(default_factory=list)
|
|
26
26
|
parameters: list["Parameter"] = Field(default_factory=list)
|
|
27
|
-
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def parameter_values(self) -> list["ParameterValue"]:
|
|
30
|
+
"""Aggregate all parameter values from all parameters."""
|
|
31
|
+
return [
|
|
32
|
+
pv
|
|
33
|
+
for parameter in self.parameters
|
|
34
|
+
for pv in parameter.parameter_values
|
|
35
|
+
]
|
|
28
36
|
|
|
29
37
|
# Lookup dicts for O(1) access (excluded from serialization)
|
|
30
38
|
variables_by_name: dict[str, "Variable"] = Field(
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from policyengine.core import Output, OutputCollection
|
|
2
|
+
from policyengine.outputs.aggregate import Aggregate, AggregateType
|
|
3
|
+
from policyengine.outputs.change_aggregate import (
|
|
4
|
+
ChangeAggregate,
|
|
5
|
+
ChangeAggregateType,
|
|
6
|
+
)
|
|
7
|
+
from policyengine.outputs.decile_impact import (
|
|
8
|
+
DecileImpact,
|
|
9
|
+
calculate_decile_impacts,
|
|
10
|
+
)
|
|
11
|
+
from policyengine.outputs.inequality import (
|
|
12
|
+
UK_INEQUALITY_INCOME_VARIABLE,
|
|
13
|
+
US_INEQUALITY_INCOME_VARIABLE,
|
|
14
|
+
Inequality,
|
|
15
|
+
calculate_uk_inequality,
|
|
16
|
+
calculate_us_inequality,
|
|
17
|
+
)
|
|
18
|
+
from policyengine.outputs.poverty import (
|
|
19
|
+
UK_POVERTY_VARIABLES,
|
|
20
|
+
US_POVERTY_VARIABLES,
|
|
21
|
+
Poverty,
|
|
22
|
+
UKPovertyType,
|
|
23
|
+
USPovertyType,
|
|
24
|
+
calculate_uk_poverty_rates,
|
|
25
|
+
calculate_us_poverty_rates,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"Output",
|
|
30
|
+
"OutputCollection",
|
|
31
|
+
"Aggregate",
|
|
32
|
+
"AggregateType",
|
|
33
|
+
"ChangeAggregate",
|
|
34
|
+
"ChangeAggregateType",
|
|
35
|
+
"DecileImpact",
|
|
36
|
+
"calculate_decile_impacts",
|
|
37
|
+
"Poverty",
|
|
38
|
+
"UKPovertyType",
|
|
39
|
+
"USPovertyType",
|
|
40
|
+
"UK_POVERTY_VARIABLES",
|
|
41
|
+
"US_POVERTY_VARIABLES",
|
|
42
|
+
"calculate_uk_poverty_rates",
|
|
43
|
+
"calculate_us_poverty_rates",
|
|
44
|
+
"Inequality",
|
|
45
|
+
"UK_INEQUALITY_INCOME_VARIABLE",
|
|
46
|
+
"US_INEQUALITY_INCOME_VARIABLE",
|
|
47
|
+
"calculate_uk_inequality",
|
|
48
|
+
"calculate_us_inequality",
|
|
49
|
+
]
|
|
@@ -2,6 +2,10 @@ import pandas as pd
|
|
|
2
2
|
from pydantic import ConfigDict
|
|
3
3
|
|
|
4
4
|
from policyengine.core import Output, OutputCollection, Simulation
|
|
5
|
+
from policyengine.core.dataset import Dataset
|
|
6
|
+
from policyengine.core.dynamic import Dynamic
|
|
7
|
+
from policyengine.core.policy import Policy
|
|
8
|
+
from policyengine.core.tax_benefit_model_version import TaxBenefitModelVersion
|
|
5
9
|
|
|
6
10
|
|
|
7
11
|
class DecileImpact(Output):
|
|
@@ -93,8 +97,11 @@ class DecileImpact(Output):
|
|
|
93
97
|
|
|
94
98
|
|
|
95
99
|
def calculate_decile_impacts(
|
|
96
|
-
|
|
97
|
-
|
|
100
|
+
dataset: Dataset,
|
|
101
|
+
tax_benefit_model_version: TaxBenefitModelVersion,
|
|
102
|
+
baseline_policy: Policy | None = None,
|
|
103
|
+
reform_policy: Policy | None = None,
|
|
104
|
+
dynamic: Dynamic | None = None,
|
|
98
105
|
income_variable: str = "equiv_hbai_household_net_income",
|
|
99
106
|
entity: str | None = None,
|
|
100
107
|
quantiles: int = 10,
|
|
@@ -104,6 +111,19 @@ def calculate_decile_impacts(
|
|
|
104
111
|
Returns:
|
|
105
112
|
OutputCollection containing list of DecileImpact objects and DataFrame
|
|
106
113
|
"""
|
|
114
|
+
baseline_simulation = Simulation(
|
|
115
|
+
dataset=dataset,
|
|
116
|
+
tax_benefit_model_version=tax_benefit_model_version,
|
|
117
|
+
policy=baseline_policy,
|
|
118
|
+
dynamic=dynamic,
|
|
119
|
+
)
|
|
120
|
+
reform_simulation = Simulation(
|
|
121
|
+
dataset=dataset,
|
|
122
|
+
tax_benefit_model_version=tax_benefit_model_version,
|
|
123
|
+
policy=reform_policy,
|
|
124
|
+
dynamic=dynamic,
|
|
125
|
+
)
|
|
126
|
+
|
|
107
127
|
results = []
|
|
108
128
|
for decile in range(1, quantiles + 1):
|
|
109
129
|
impact = DecileImpact(
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""Inequality analysis output types."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from pydantic import ConfigDict
|
|
8
|
+
|
|
9
|
+
from policyengine.core import Output, Simulation
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _gini(values: np.ndarray, weights: np.ndarray) -> float:
|
|
13
|
+
"""Calculate weighted Gini coefficient.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
values: Array of income values
|
|
17
|
+
weights: Array of weights
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Gini coefficient between 0 (perfect equality) and 1 (perfect inequality)
|
|
21
|
+
"""
|
|
22
|
+
# Handle edge cases
|
|
23
|
+
if len(values) == 0 or weights.sum() == 0:
|
|
24
|
+
return 0.0
|
|
25
|
+
|
|
26
|
+
# Sort by values
|
|
27
|
+
sorted_indices = np.argsort(values)
|
|
28
|
+
sorted_values = values[sorted_indices]
|
|
29
|
+
sorted_weights = weights[sorted_indices]
|
|
30
|
+
|
|
31
|
+
# Cumulative weights and weighted values
|
|
32
|
+
cumulative_weights = np.cumsum(sorted_weights)
|
|
33
|
+
total_weight = cumulative_weights[-1]
|
|
34
|
+
cumulative_weighted_values = np.cumsum(sorted_values * sorted_weights)
|
|
35
|
+
total_weighted_value = cumulative_weighted_values[-1]
|
|
36
|
+
|
|
37
|
+
if total_weighted_value == 0:
|
|
38
|
+
return 0.0
|
|
39
|
+
|
|
40
|
+
# Calculate Gini using the area formula
|
|
41
|
+
# Gini = 1 - 2 * (area under Lorenz curve)
|
|
42
|
+
lorenz_curve = cumulative_weighted_values / total_weighted_value
|
|
43
|
+
weight_fractions = sorted_weights / total_weight
|
|
44
|
+
|
|
45
|
+
# Area under Lorenz curve using trapezoidal rule
|
|
46
|
+
area = np.sum(weight_fractions * (lorenz_curve - weight_fractions / 2))
|
|
47
|
+
|
|
48
|
+
return float(1 - 2 * area)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Inequality(Output):
|
|
52
|
+
"""Single inequality measure result - represents one database row.
|
|
53
|
+
|
|
54
|
+
This is a single-simulation output type that calculates inequality
|
|
55
|
+
metrics for a given income variable, optionally filtered by
|
|
56
|
+
demographic variables.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
60
|
+
|
|
61
|
+
simulation: Simulation
|
|
62
|
+
income_variable: str
|
|
63
|
+
entity: str = "household"
|
|
64
|
+
|
|
65
|
+
# Optional demographic filters
|
|
66
|
+
filter_variable: str | None = None
|
|
67
|
+
filter_variable_eq: Any | None = None
|
|
68
|
+
filter_variable_leq: Any | None = None
|
|
69
|
+
filter_variable_geq: Any | None = None
|
|
70
|
+
|
|
71
|
+
# Results populated by run()
|
|
72
|
+
gini: float | None = None
|
|
73
|
+
top_10_share: float | None = None
|
|
74
|
+
top_1_share: float | None = None
|
|
75
|
+
bottom_50_share: float | None = None
|
|
76
|
+
|
|
77
|
+
def run(self):
|
|
78
|
+
"""Calculate inequality metrics."""
|
|
79
|
+
# Get income variable info
|
|
80
|
+
income_var_obj = (
|
|
81
|
+
self.simulation.tax_benefit_model_version.get_variable(
|
|
82
|
+
self.income_variable
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Get target entity data
|
|
87
|
+
target_entity = self.entity
|
|
88
|
+
data = getattr(self.simulation.output_dataset.data, target_entity)
|
|
89
|
+
|
|
90
|
+
# Map income variable to target entity if needed
|
|
91
|
+
if income_var_obj.entity != target_entity:
|
|
92
|
+
mapped = self.simulation.output_dataset.data.map_to_entity(
|
|
93
|
+
income_var_obj.entity,
|
|
94
|
+
target_entity,
|
|
95
|
+
columns=[self.income_variable],
|
|
96
|
+
)
|
|
97
|
+
income_series = mapped[self.income_variable]
|
|
98
|
+
else:
|
|
99
|
+
income_series = data[self.income_variable]
|
|
100
|
+
|
|
101
|
+
# Get weights
|
|
102
|
+
weight_col = f"{target_entity}_weight"
|
|
103
|
+
if weight_col in data.columns:
|
|
104
|
+
weights = data[weight_col]
|
|
105
|
+
else:
|
|
106
|
+
weights = pd.Series(np.ones(len(income_series)))
|
|
107
|
+
|
|
108
|
+
# Apply demographic filter if specified
|
|
109
|
+
if self.filter_variable is not None:
|
|
110
|
+
filter_var_obj = (
|
|
111
|
+
self.simulation.tax_benefit_model_version.get_variable(
|
|
112
|
+
self.filter_variable
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if filter_var_obj.entity != target_entity:
|
|
117
|
+
filter_mapped = (
|
|
118
|
+
self.simulation.output_dataset.data.map_to_entity(
|
|
119
|
+
filter_var_obj.entity,
|
|
120
|
+
target_entity,
|
|
121
|
+
columns=[self.filter_variable],
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
filter_series = filter_mapped[self.filter_variable]
|
|
125
|
+
else:
|
|
126
|
+
filter_series = data[self.filter_variable]
|
|
127
|
+
|
|
128
|
+
# Build filter mask
|
|
129
|
+
mask = filter_series.notna()
|
|
130
|
+
if self.filter_variable_eq is not None:
|
|
131
|
+
mask &= filter_series == self.filter_variable_eq
|
|
132
|
+
if self.filter_variable_leq is not None:
|
|
133
|
+
mask &= filter_series <= self.filter_variable_leq
|
|
134
|
+
if self.filter_variable_geq is not None:
|
|
135
|
+
mask &= filter_series >= self.filter_variable_geq
|
|
136
|
+
|
|
137
|
+
# Apply mask
|
|
138
|
+
income_series = income_series[mask]
|
|
139
|
+
weights = weights[mask]
|
|
140
|
+
|
|
141
|
+
# Convert to numpy arrays
|
|
142
|
+
values = np.array(income_series)
|
|
143
|
+
weights_arr = np.array(weights)
|
|
144
|
+
|
|
145
|
+
# Remove NaN values
|
|
146
|
+
valid_mask = ~np.isnan(values) & ~np.isnan(weights_arr)
|
|
147
|
+
values = values[valid_mask]
|
|
148
|
+
weights_arr = weights_arr[valid_mask]
|
|
149
|
+
|
|
150
|
+
# Calculate Gini coefficient
|
|
151
|
+
self.gini = _gini(values, weights_arr)
|
|
152
|
+
|
|
153
|
+
# Calculate income shares
|
|
154
|
+
if len(values) > 0 and weights_arr.sum() > 0:
|
|
155
|
+
total_income = np.sum(values * weights_arr)
|
|
156
|
+
|
|
157
|
+
if total_income > 0:
|
|
158
|
+
# Sort by income
|
|
159
|
+
sorted_indices = np.argsort(values)
|
|
160
|
+
sorted_values = values[sorted_indices]
|
|
161
|
+
sorted_weights = weights_arr[sorted_indices]
|
|
162
|
+
|
|
163
|
+
# Cumulative weight fractions
|
|
164
|
+
cumulative_weights = np.cumsum(sorted_weights)
|
|
165
|
+
total_weight = cumulative_weights[-1]
|
|
166
|
+
weight_fractions = cumulative_weights / total_weight
|
|
167
|
+
|
|
168
|
+
# Top 10% share
|
|
169
|
+
top_10_mask = weight_fractions > 0.9
|
|
170
|
+
self.top_10_share = float(
|
|
171
|
+
np.sum(
|
|
172
|
+
sorted_values[top_10_mask]
|
|
173
|
+
* sorted_weights[top_10_mask]
|
|
174
|
+
)
|
|
175
|
+
/ total_income
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Top 1% share
|
|
179
|
+
top_1_mask = weight_fractions > 0.99
|
|
180
|
+
self.top_1_share = float(
|
|
181
|
+
np.sum(
|
|
182
|
+
sorted_values[top_1_mask] * sorted_weights[top_1_mask]
|
|
183
|
+
)
|
|
184
|
+
/ total_income
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Bottom 50% share
|
|
188
|
+
bottom_50_mask = weight_fractions <= 0.5
|
|
189
|
+
self.bottom_50_share = float(
|
|
190
|
+
np.sum(
|
|
191
|
+
sorted_values[bottom_50_mask]
|
|
192
|
+
* sorted_weights[bottom_50_mask]
|
|
193
|
+
)
|
|
194
|
+
/ total_income
|
|
195
|
+
)
|
|
196
|
+
else:
|
|
197
|
+
self.top_10_share = 0.0
|
|
198
|
+
self.top_1_share = 0.0
|
|
199
|
+
self.bottom_50_share = 0.0
|
|
200
|
+
else:
|
|
201
|
+
self.top_10_share = 0.0
|
|
202
|
+
self.top_1_share = 0.0
|
|
203
|
+
self.bottom_50_share = 0.0
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# Default income variables for each country
|
|
207
|
+
UK_INEQUALITY_INCOME_VARIABLE = "equiv_hbai_household_net_income"
|
|
208
|
+
US_INEQUALITY_INCOME_VARIABLE = "household_net_income"
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def calculate_uk_inequality(
|
|
212
|
+
simulation: Simulation,
|
|
213
|
+
income_variable: str = UK_INEQUALITY_INCOME_VARIABLE,
|
|
214
|
+
filter_variable: str | None = None,
|
|
215
|
+
filter_variable_eq: Any | None = None,
|
|
216
|
+
filter_variable_leq: Any | None = None,
|
|
217
|
+
filter_variable_geq: Any | None = None,
|
|
218
|
+
) -> Inequality:
|
|
219
|
+
"""Calculate inequality metrics for a UK simulation.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
simulation: The simulation to analyse
|
|
223
|
+
income_variable: Income variable to use (default: equiv_hbai_household_net_income)
|
|
224
|
+
filter_variable: Optional variable to filter by
|
|
225
|
+
filter_variable_eq: Filter for exact match
|
|
226
|
+
filter_variable_leq: Filter for less than or equal
|
|
227
|
+
filter_variable_geq: Filter for greater than or equal
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Inequality object with Gini and income share metrics
|
|
231
|
+
"""
|
|
232
|
+
inequality = Inequality(
|
|
233
|
+
simulation=simulation,
|
|
234
|
+
income_variable=income_variable,
|
|
235
|
+
entity="household",
|
|
236
|
+
filter_variable=filter_variable,
|
|
237
|
+
filter_variable_eq=filter_variable_eq,
|
|
238
|
+
filter_variable_leq=filter_variable_leq,
|
|
239
|
+
filter_variable_geq=filter_variable_geq,
|
|
240
|
+
)
|
|
241
|
+
inequality.run()
|
|
242
|
+
return inequality
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def calculate_us_inequality(
|
|
246
|
+
simulation: Simulation,
|
|
247
|
+
income_variable: str = US_INEQUALITY_INCOME_VARIABLE,
|
|
248
|
+
filter_variable: str | None = None,
|
|
249
|
+
filter_variable_eq: Any | None = None,
|
|
250
|
+
filter_variable_leq: Any | None = None,
|
|
251
|
+
filter_variable_geq: Any | None = None,
|
|
252
|
+
) -> Inequality:
|
|
253
|
+
"""Calculate inequality metrics for a US simulation.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
simulation: The simulation to analyse
|
|
257
|
+
income_variable: Income variable to use (default: household_net_income)
|
|
258
|
+
filter_variable: Optional variable to filter by
|
|
259
|
+
filter_variable_eq: Filter for exact match
|
|
260
|
+
filter_variable_leq: Filter for less than or equal
|
|
261
|
+
filter_variable_geq: Filter for greater than or equal
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
Inequality object with Gini and income share metrics
|
|
265
|
+
"""
|
|
266
|
+
inequality = Inequality(
|
|
267
|
+
simulation=simulation,
|
|
268
|
+
income_variable=income_variable,
|
|
269
|
+
entity="household",
|
|
270
|
+
filter_variable=filter_variable,
|
|
271
|
+
filter_variable_eq=filter_variable_eq,
|
|
272
|
+
filter_variable_leq=filter_variable_leq,
|
|
273
|
+
filter_variable_geq=filter_variable_geq,
|
|
274
|
+
)
|
|
275
|
+
inequality.run()
|
|
276
|
+
return inequality
|