policyengine 3.1.13__tar.gz → 3.1.15__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.13 → policyengine-3.1.15}/CHANGELOG.md +14 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/PKG-INFO +1 -1
- {policyengine-3.1.13 → policyengine-3.1.15}/changelog.yaml +10 -0
- policyengine-3.1.15/examples/household_impact_example.py +128 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/pyproject.toml +1 -1
- policyengine-3.1.15/src/policyengine/__pycache__/__init__.cpython-313.pyc +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/core/tax_benefit_model_version.py +9 -1
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/outputs/decile_impact.py +22 -2
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/tax_benefit_models/uk/__init__.py +10 -2
- policyengine-3.1.15/src/policyengine/tax_benefit_models/uk/analysis.py +268 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/tax_benefit_models/uk/datasets.py +2 -2
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/tax_benefit_models/uk/model.py +5 -2
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/tax_benefit_models/us/__init__.py +10 -2
- policyengine-3.1.15/src/policyengine/tax_benefit_models/us/analysis.py +288 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/tax_benefit_models/us/datasets.py +2 -2
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/tax_benefit_models/us/model.py +1 -2
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine.egg-info/PKG-INFO +1 -1
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine.egg-info/SOURCES.txt +4 -1
- policyengine-3.1.15/tests/test_household_impact.py +207 -0
- policyengine-3.1.15/tests/test_models.py +77 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/uv.lock +3 -1
- policyengine-3.1.13/src/policyengine/__pycache__/__init__.cpython-313.pyc +0 -0
- policyengine-3.1.13/src/policyengine/tax_benefit_models/uk/analysis.py +0 -97
- policyengine-3.1.13/src/policyengine/tax_benefit_models/us/analysis.py +0 -99
- {policyengine-3.1.13 → policyengine-3.1.15}/.claude/policyengine-guide.md +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/.claude/quick-reference.md +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/.github/CONTRIBUTING.md +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/.github/changelog_template.md +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/.github/fetch_version.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/.github/get-changelog-diff.sh +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/.github/has-functional-changes.sh +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/.github/is-version-number-acceptable.sh +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/.github/publish-git-tag.sh +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/.github/workflows/code_changes.yaml +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/.github/workflows/docs.yml +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/.github/workflows/pr_code_changes.yaml +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/.github/workflows/pr_docs_changes.yaml +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/.github/workflows/versioning.yaml +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/.gitignore +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/CLAUDE.md +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/LICENSE +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/Makefile +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/README.md +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/changelog_entry.yaml +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/docs/.gitignore +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/docs/core-concepts.md +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/docs/country-models-uk.md +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/docs/country-models-us.md +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/docs/dev.md +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/docs/index.md +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/docs/myst.yml +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/docs/visualisation.md +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/examples/employment_income_variation_uk.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/examples/employment_income_variation_us.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/examples/income_bands_uk.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/examples/income_distribution_us.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/examples/policy_change_uk.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/examples/speedtest_us_simulation.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/setup.cfg +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/__init__.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/core/__init__.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/core/cache.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/core/dataset.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/core/dataset_version.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/core/dynamic.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/core/output.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/core/parameter.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/core/parameter_value.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/core/policy.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/core/simulation.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/core/tax_benefit_model.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/core/variable.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/outputs/__init__.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/outputs/aggregate.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/outputs/change_aggregate.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/tax_benefit_models/uk/outputs.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/tax_benefit_models/uk.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/tax_benefit_models/us/outputs.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/tax_benefit_models/us.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/utils/__init__.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/utils/dates.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/utils/parametric_reforms.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/utils/plotting.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine.egg-info/dependency_links.txt +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine.egg-info/requires.txt +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine.egg-info/top_level.txt +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/tests/test_aggregate.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/tests/test_cache.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/tests/test_change_aggregate.py +0 -0
- {policyengine-3.1.13 → policyengine-3.1.15}/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.15] - 2025-12-14 23:51:27
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Household impacts
|
|
13
|
+
|
|
14
|
+
## [3.1.14] - 2025-12-10 21:59:24
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Improvements to loading taxbenefitsystems.
|
|
19
|
+
|
|
8
20
|
## [3.1.13] - 2025-12-10 19:29:28
|
|
9
21
|
|
|
10
22
|
### Fixed
|
|
@@ -275,6 +287,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
275
287
|
|
|
276
288
|
|
|
277
289
|
|
|
290
|
+
[3.1.15]: https://github.com/PolicyEngine/policyengine.py/compare/3.1.14...3.1.15
|
|
291
|
+
[3.1.14]: https://github.com/PolicyEngine/policyengine.py/compare/3.1.13...3.1.14
|
|
278
292
|
[3.1.13]: https://github.com/PolicyEngine/policyengine.py/compare/3.1.12...3.1.13
|
|
279
293
|
[3.1.12]: https://github.com/PolicyEngine/policyengine.py/compare/3.1.11...3.1.12
|
|
280
294
|
[3.1.11]: https://github.com/PolicyEngine/policyengine.py/compare/3.1.10...3.1.11
|
|
@@ -225,3 +225,13 @@
|
|
|
225
225
|
fixed:
|
|
226
226
|
- Naming improvements.
|
|
227
227
|
date: 2025-12-10 19:29:28
|
|
228
|
+
- bump: patch
|
|
229
|
+
changes:
|
|
230
|
+
fixed:
|
|
231
|
+
- Improvements to loading taxbenefitsystems.
|
|
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
|
|
@@ -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()
|
{policyengine-3.1.13 → policyengine-3.1.15}/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(
|
|
@@ -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(
|
{policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/tax_benefit_models/uk/__init__.py
RENAMED
|
@@ -5,7 +5,12 @@ from importlib.util import find_spec
|
|
|
5
5
|
if find_spec("policyengine_uk") is not None:
|
|
6
6
|
from policyengine.core import Dataset
|
|
7
7
|
|
|
8
|
-
from .analysis import
|
|
8
|
+
from .analysis import (
|
|
9
|
+
UKHouseholdInput,
|
|
10
|
+
UKHouseholdOutput,
|
|
11
|
+
calculate_household_impact,
|
|
12
|
+
economic_impact_analysis,
|
|
13
|
+
)
|
|
9
14
|
from .datasets import (
|
|
10
15
|
PolicyEngineUKDataset,
|
|
11
16
|
UKYearData,
|
|
@@ -37,7 +42,10 @@ if find_spec("policyengine_uk") is not None:
|
|
|
37
42
|
"PolicyEngineUKLatest",
|
|
38
43
|
"uk_model",
|
|
39
44
|
"uk_latest",
|
|
40
|
-
"
|
|
45
|
+
"economic_impact_analysis",
|
|
46
|
+
"calculate_household_impact",
|
|
47
|
+
"UKHouseholdInput",
|
|
48
|
+
"UKHouseholdOutput",
|
|
41
49
|
"ProgrammeStatistics",
|
|
42
50
|
]
|
|
43
51
|
else:
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""General utility functions for UK policy reform analysis."""
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from microdf import MicroDataFrame
|
|
9
|
+
from pydantic import BaseModel, Field, create_model
|
|
10
|
+
|
|
11
|
+
from policyengine.core import OutputCollection, Simulation
|
|
12
|
+
from policyengine.core.policy import Policy
|
|
13
|
+
from policyengine.outputs.decile_impact import (
|
|
14
|
+
DecileImpact,
|
|
15
|
+
calculate_decile_impacts,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from .datasets import PolicyEngineUKDataset, UKYearData
|
|
19
|
+
from .model import uk_latest
|
|
20
|
+
from .outputs import ProgrammeStatistics
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _create_entity_output_model(
|
|
24
|
+
entity: str, variables: list[str]
|
|
25
|
+
) -> type[BaseModel]:
|
|
26
|
+
"""Create a dynamic Pydantic model for entity output variables."""
|
|
27
|
+
fields = {var: (float, ...) for var in variables}
|
|
28
|
+
return create_model(f"{entity.title()}Output", **fields)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Create output models dynamically from uk_latest.entity_variables
|
|
32
|
+
PersonOutput = _create_entity_output_model(
|
|
33
|
+
"person", uk_latest.entity_variables["person"]
|
|
34
|
+
)
|
|
35
|
+
BenunitOutput = _create_entity_output_model(
|
|
36
|
+
"benunit", uk_latest.entity_variables["benunit"]
|
|
37
|
+
)
|
|
38
|
+
HouseholdEntityOutput = _create_entity_output_model(
|
|
39
|
+
"household", uk_latest.entity_variables["household"]
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class UKHouseholdOutput(BaseModel):
|
|
44
|
+
"""Output from a UK household calculation with all entity data."""
|
|
45
|
+
|
|
46
|
+
person: list[dict[str, Any]]
|
|
47
|
+
benunit: list[dict[str, Any]]
|
|
48
|
+
household: dict[str, Any]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class UKHouseholdInput(BaseModel):
|
|
52
|
+
"""Input for a UK household calculation."""
|
|
53
|
+
|
|
54
|
+
people: list[dict[str, Any]]
|
|
55
|
+
benunit: dict[str, Any] = Field(default_factory=dict)
|
|
56
|
+
household: dict[str, Any] = Field(default_factory=dict)
|
|
57
|
+
year: int = 2026
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def calculate_household_impact(
|
|
61
|
+
household_input: UKHouseholdInput,
|
|
62
|
+
policy: Policy | None = None,
|
|
63
|
+
) -> UKHouseholdOutput:
|
|
64
|
+
"""Calculate tax and benefit impacts for a single UK household."""
|
|
65
|
+
n_people = len(household_input.people)
|
|
66
|
+
|
|
67
|
+
# Build person data with defaults
|
|
68
|
+
person_data = {
|
|
69
|
+
"person_id": list(range(n_people)),
|
|
70
|
+
"person_benunit_id": [0] * n_people,
|
|
71
|
+
"person_household_id": [0] * n_people,
|
|
72
|
+
"person_weight": [1.0] * n_people,
|
|
73
|
+
}
|
|
74
|
+
# Add user-provided person fields
|
|
75
|
+
for i, person in enumerate(household_input.people):
|
|
76
|
+
for key, value in person.items():
|
|
77
|
+
if key not in person_data:
|
|
78
|
+
person_data[key] = [
|
|
79
|
+
0.0
|
|
80
|
+
] * n_people # Default to 0 for numeric fields
|
|
81
|
+
person_data[key][i] = value
|
|
82
|
+
|
|
83
|
+
# Build benunit data with defaults
|
|
84
|
+
benunit_data = {
|
|
85
|
+
"benunit_id": [0],
|
|
86
|
+
"benunit_weight": [1.0],
|
|
87
|
+
}
|
|
88
|
+
for key, value in household_input.benunit.items():
|
|
89
|
+
benunit_data[key] = [value]
|
|
90
|
+
|
|
91
|
+
# Build household data with defaults (required for uprating)
|
|
92
|
+
household_data = {
|
|
93
|
+
"household_id": [0],
|
|
94
|
+
"household_weight": [1.0],
|
|
95
|
+
"region": ["LONDON"],
|
|
96
|
+
"tenure_type": ["RENT_PRIVATELY"],
|
|
97
|
+
"council_tax": [0.0],
|
|
98
|
+
"rent": [0.0],
|
|
99
|
+
}
|
|
100
|
+
for key, value in household_input.household.items():
|
|
101
|
+
household_data[key] = [value]
|
|
102
|
+
|
|
103
|
+
# Create MicroDataFrames
|
|
104
|
+
person_df = MicroDataFrame(
|
|
105
|
+
pd.DataFrame(person_data), weights="person_weight"
|
|
106
|
+
)
|
|
107
|
+
benunit_df = MicroDataFrame(
|
|
108
|
+
pd.DataFrame(benunit_data), weights="benunit_weight"
|
|
109
|
+
)
|
|
110
|
+
household_df = MicroDataFrame(
|
|
111
|
+
pd.DataFrame(household_data), weights="household_weight"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Create temporary dataset
|
|
115
|
+
tmpdir = tempfile.mkdtemp()
|
|
116
|
+
filepath = str(Path(tmpdir) / "household_impact.h5")
|
|
117
|
+
|
|
118
|
+
dataset = PolicyEngineUKDataset(
|
|
119
|
+
name="Household impact calculation",
|
|
120
|
+
description="Single household for impact calculation",
|
|
121
|
+
filepath=filepath,
|
|
122
|
+
year=household_input.year,
|
|
123
|
+
data=UKYearData(
|
|
124
|
+
person=person_df,
|
|
125
|
+
benunit=benunit_df,
|
|
126
|
+
household=household_df,
|
|
127
|
+
),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Run simulation
|
|
131
|
+
simulation = Simulation(
|
|
132
|
+
dataset=dataset,
|
|
133
|
+
tax_benefit_model_version=uk_latest,
|
|
134
|
+
policy=policy,
|
|
135
|
+
)
|
|
136
|
+
simulation.run()
|
|
137
|
+
|
|
138
|
+
# Extract all output variables defined in entity_variables
|
|
139
|
+
output_data = simulation.output_dataset.data
|
|
140
|
+
|
|
141
|
+
def safe_convert(value):
|
|
142
|
+
"""Convert value to float if numeric, otherwise return as string."""
|
|
143
|
+
try:
|
|
144
|
+
return float(value)
|
|
145
|
+
except (ValueError, TypeError):
|
|
146
|
+
return str(value)
|
|
147
|
+
|
|
148
|
+
person_outputs = []
|
|
149
|
+
for i in range(n_people):
|
|
150
|
+
person_dict = {}
|
|
151
|
+
for var in uk_latest.entity_variables["person"]:
|
|
152
|
+
person_dict[var] = safe_convert(output_data.person[var].iloc[i])
|
|
153
|
+
person_outputs.append(person_dict)
|
|
154
|
+
|
|
155
|
+
benunit_outputs = []
|
|
156
|
+
for i in range(len(output_data.benunit)):
|
|
157
|
+
benunit_dict = {}
|
|
158
|
+
for var in uk_latest.entity_variables["benunit"]:
|
|
159
|
+
benunit_dict[var] = safe_convert(output_data.benunit[var].iloc[i])
|
|
160
|
+
benunit_outputs.append(benunit_dict)
|
|
161
|
+
|
|
162
|
+
household_dict = {}
|
|
163
|
+
for var in uk_latest.entity_variables["household"]:
|
|
164
|
+
household_dict[var] = safe_convert(output_data.household[var].iloc[0])
|
|
165
|
+
|
|
166
|
+
return UKHouseholdOutput(
|
|
167
|
+
person=person_outputs,
|
|
168
|
+
benunit=benunit_outputs,
|
|
169
|
+
household=household_dict,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class PolicyReformAnalysis(BaseModel):
|
|
174
|
+
"""Complete policy reform analysis result."""
|
|
175
|
+
|
|
176
|
+
decile_impacts: OutputCollection[DecileImpact]
|
|
177
|
+
programme_statistics: OutputCollection[ProgrammeStatistics]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def economic_impact_analysis(
|
|
181
|
+
baseline_simulation: Simulation,
|
|
182
|
+
reform_simulation: Simulation,
|
|
183
|
+
) -> PolicyReformAnalysis:
|
|
184
|
+
"""Perform comprehensive analysis of a policy reform.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
PolicyReformAnalysis containing decile impacts and programme statistics
|
|
188
|
+
"""
|
|
189
|
+
baseline_simulation.ensure()
|
|
190
|
+
reform_simulation.ensure()
|
|
191
|
+
|
|
192
|
+
assert len(baseline_simulation.dataset.data.household) > 100, (
|
|
193
|
+
"Baseline simulation must have more than 100 households"
|
|
194
|
+
)
|
|
195
|
+
assert len(reform_simulation.dataset.data.household) > 100, (
|
|
196
|
+
"Reform simulation must have more than 100 households"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Decile impact
|
|
200
|
+
decile_impacts = calculate_decile_impacts(
|
|
201
|
+
dataset=baseline_simulation.dataset,
|
|
202
|
+
tax_benefit_model_version=baseline_simulation.tax_benefit_model_version,
|
|
203
|
+
baseline_policy=baseline_simulation.policy,
|
|
204
|
+
reform_policy=reform_simulation.policy,
|
|
205
|
+
dynamic=baseline_simulation.dynamic,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Major programmes to analyse
|
|
209
|
+
programmes = {
|
|
210
|
+
# Tax
|
|
211
|
+
"income_tax": {"entity": "person", "is_tax": True},
|
|
212
|
+
"national_insurance": {"entity": "person", "is_tax": True},
|
|
213
|
+
"vat": {"entity": "household", "is_tax": True},
|
|
214
|
+
"council_tax": {"entity": "household", "is_tax": True},
|
|
215
|
+
# Benefits
|
|
216
|
+
"universal_credit": {"entity": "person", "is_tax": False},
|
|
217
|
+
"child_benefit": {"entity": "person", "is_tax": False},
|
|
218
|
+
"pension_credit": {"entity": "person", "is_tax": False},
|
|
219
|
+
"income_support": {"entity": "person", "is_tax": False},
|
|
220
|
+
"working_tax_credit": {"entity": "person", "is_tax": False},
|
|
221
|
+
"child_tax_credit": {"entity": "person", "is_tax": False},
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
programme_statistics = []
|
|
225
|
+
|
|
226
|
+
for programme_name, programme_info in programmes.items():
|
|
227
|
+
entity = programme_info["entity"]
|
|
228
|
+
is_tax = programme_info["is_tax"]
|
|
229
|
+
|
|
230
|
+
stats = ProgrammeStatistics(
|
|
231
|
+
baseline_simulation=baseline_simulation,
|
|
232
|
+
reform_simulation=reform_simulation,
|
|
233
|
+
programme_name=programme_name,
|
|
234
|
+
entity=entity,
|
|
235
|
+
is_tax=is_tax,
|
|
236
|
+
)
|
|
237
|
+
stats.run()
|
|
238
|
+
programme_statistics.append(stats)
|
|
239
|
+
|
|
240
|
+
# Create DataFrame
|
|
241
|
+
programme_df = pd.DataFrame(
|
|
242
|
+
[
|
|
243
|
+
{
|
|
244
|
+
"baseline_simulation_id": p.baseline_simulation.id,
|
|
245
|
+
"reform_simulation_id": p.reform_simulation.id,
|
|
246
|
+
"programme_name": p.programme_name,
|
|
247
|
+
"entity": p.entity,
|
|
248
|
+
"is_tax": p.is_tax,
|
|
249
|
+
"baseline_total": p.baseline_total,
|
|
250
|
+
"reform_total": p.reform_total,
|
|
251
|
+
"change": p.change,
|
|
252
|
+
"baseline_count": p.baseline_count,
|
|
253
|
+
"reform_count": p.reform_count,
|
|
254
|
+
"winners": p.winners,
|
|
255
|
+
"losers": p.losers,
|
|
256
|
+
}
|
|
257
|
+
for p in programme_statistics
|
|
258
|
+
]
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
programme_collection = OutputCollection(
|
|
262
|
+
outputs=programme_statistics, dataframe=programme_df
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
return PolicyReformAnalysis(
|
|
266
|
+
decile_impacts=decile_impacts,
|
|
267
|
+
programme_statistics=programme_collection,
|
|
268
|
+
)
|
{policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/tax_benefit_models/uk/datasets.py
RENAMED
|
@@ -196,8 +196,8 @@ def load_datasets(
|
|
|
196
196
|
for year in years:
|
|
197
197
|
filepath = f"{data_folder}/{Path(dataset).stem}_year_{year}.h5"
|
|
198
198
|
uk_dataset = PolicyEngineUKDataset(
|
|
199
|
-
name=f"{dataset}-year-{year}",
|
|
200
|
-
description=f"UK Dataset for year {year} based on {dataset}",
|
|
199
|
+
name=f"{Path(dataset).stem}-year-{year}",
|
|
200
|
+
description=f"UK Dataset for year {year} based on {Path(dataset).stem}",
|
|
201
201
|
filepath=filepath,
|
|
202
202
|
year=int(year),
|
|
203
203
|
)
|
|
@@ -9,12 +9,10 @@ from microdf import MicroDataFrame
|
|
|
9
9
|
|
|
10
10
|
from policyengine.core import (
|
|
11
11
|
Parameter,
|
|
12
|
-
ParameterValue,
|
|
13
12
|
TaxBenefitModel,
|
|
14
13
|
TaxBenefitModelVersion,
|
|
15
14
|
Variable,
|
|
16
15
|
)
|
|
17
|
-
from policyengine.utils import parse_safe_date
|
|
18
16
|
|
|
19
17
|
from .datasets import PolicyEngineUKDataset, UKYearData
|
|
20
18
|
|
|
@@ -108,6 +106,11 @@ class PolicyEngineUKLatest(TaxBenefitModelVersion):
|
|
|
108
106
|
"rent",
|
|
109
107
|
"council_tax",
|
|
110
108
|
"tenure_type",
|
|
109
|
+
# Poverty measures
|
|
110
|
+
"in_poverty_bhc",
|
|
111
|
+
"in_poverty_ahc",
|
|
112
|
+
"in_relative_poverty_bhc",
|
|
113
|
+
"in_relative_poverty_ahc",
|
|
111
114
|
],
|
|
112
115
|
}
|
|
113
116
|
|
{policyengine-3.1.13 → policyengine-3.1.15}/src/policyengine/tax_benefit_models/us/__init__.py
RENAMED
|
@@ -5,7 +5,12 @@ from importlib.util import find_spec
|
|
|
5
5
|
if find_spec("policyengine_us") is not None:
|
|
6
6
|
from policyengine.core import Dataset
|
|
7
7
|
|
|
8
|
-
from .analysis import
|
|
8
|
+
from .analysis import (
|
|
9
|
+
USHouseholdInput,
|
|
10
|
+
USHouseholdOutput,
|
|
11
|
+
calculate_household_impact,
|
|
12
|
+
economic_impact_analysis,
|
|
13
|
+
)
|
|
9
14
|
from .datasets import (
|
|
10
15
|
PolicyEngineUSDataset,
|
|
11
16
|
USYearData,
|
|
@@ -37,7 +42,10 @@ if find_spec("policyengine_us") is not None:
|
|
|
37
42
|
"PolicyEngineUSLatest",
|
|
38
43
|
"us_model",
|
|
39
44
|
"us_latest",
|
|
40
|
-
"
|
|
45
|
+
"economic_impact_analysis",
|
|
46
|
+
"calculate_household_impact",
|
|
47
|
+
"USHouseholdInput",
|
|
48
|
+
"USHouseholdOutput",
|
|
41
49
|
"ProgramStatistics",
|
|
42
50
|
]
|
|
43
51
|
else:
|