policyengine 3.1.14__py3-none-any.whl → 3.1.16__py3-none-any.whl
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/__pycache__/__init__.cpython-313.pyc +0 -0
- policyengine/core/tax_benefit_model_version.py +9 -1
- policyengine/outputs/__init__.py +28 -0
- policyengine/outputs/decile_impact.py +22 -2
- policyengine/outputs/inequality.py +276 -0
- policyengine/outputs/poverty.py +238 -0
- policyengine/tax_benefit_models/uk/__init__.py +10 -2
- policyengine/tax_benefit_models/uk/analysis.py +199 -4
- policyengine/tax_benefit_models/uk/model.py +15 -4
- policyengine/tax_benefit_models/us/__init__.py +10 -2
- policyengine/tax_benefit_models/us/analysis.py +219 -5
- policyengine/tax_benefit_models/us/model.py +15 -4
- policyengine/utils/__init__.py +4 -0
- policyengine/utils/parameter_labels.py +213 -0
- {policyengine-3.1.14.dist-info → policyengine-3.1.16.dist-info}/METADATA +5 -5
- {policyengine-3.1.14.dist-info → policyengine-3.1.16.dist-info}/RECORD +19 -16
- {policyengine-3.1.14.dist-info → policyengine-3.1.16.dist-info}/WHEEL +1 -1
- {policyengine-3.1.14.dist-info → policyengine-3.1.16.dist-info}/licenses/LICENSE +0 -0
- {policyengine-3.1.14.dist-info → policyengine-3.1.16.dist-info}/top_level.txt +0 -0
|
@@ -1,25 +1,195 @@
|
|
|
1
1
|
"""General utility functions for UK policy reform analysis."""
|
|
2
2
|
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
3
7
|
import pandas as pd
|
|
4
|
-
from
|
|
8
|
+
from microdf import MicroDataFrame
|
|
9
|
+
from pydantic import BaseModel, Field, create_model
|
|
5
10
|
|
|
6
11
|
from policyengine.core import OutputCollection, Simulation
|
|
12
|
+
from policyengine.core.policy import Policy
|
|
7
13
|
from policyengine.outputs.decile_impact import (
|
|
8
14
|
DecileImpact,
|
|
9
15
|
calculate_decile_impacts,
|
|
10
16
|
)
|
|
17
|
+
from policyengine.outputs.inequality import (
|
|
18
|
+
Inequality,
|
|
19
|
+
calculate_uk_inequality,
|
|
20
|
+
)
|
|
21
|
+
from policyengine.outputs.poverty import (
|
|
22
|
+
Poverty,
|
|
23
|
+
calculate_uk_poverty_rates,
|
|
24
|
+
)
|
|
11
25
|
|
|
26
|
+
from .datasets import PolicyEngineUKDataset, UKYearData
|
|
27
|
+
from .model import uk_latest
|
|
12
28
|
from .outputs import ProgrammeStatistics
|
|
13
29
|
|
|
14
30
|
|
|
31
|
+
def _create_entity_output_model(
|
|
32
|
+
entity: str, variables: list[str]
|
|
33
|
+
) -> type[BaseModel]:
|
|
34
|
+
"""Create a dynamic Pydantic model for entity output variables."""
|
|
35
|
+
fields = {var: (float, ...) for var in variables}
|
|
36
|
+
return create_model(f"{entity.title()}Output", **fields)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Create output models dynamically from uk_latest.entity_variables
|
|
40
|
+
PersonOutput = _create_entity_output_model(
|
|
41
|
+
"person", uk_latest.entity_variables["person"]
|
|
42
|
+
)
|
|
43
|
+
BenunitOutput = _create_entity_output_model(
|
|
44
|
+
"benunit", uk_latest.entity_variables["benunit"]
|
|
45
|
+
)
|
|
46
|
+
HouseholdEntityOutput = _create_entity_output_model(
|
|
47
|
+
"household", uk_latest.entity_variables["household"]
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class UKHouseholdOutput(BaseModel):
|
|
52
|
+
"""Output from a UK household calculation with all entity data."""
|
|
53
|
+
|
|
54
|
+
person: list[dict[str, Any]]
|
|
55
|
+
benunit: list[dict[str, Any]]
|
|
56
|
+
household: dict[str, Any]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class UKHouseholdInput(BaseModel):
|
|
60
|
+
"""Input for a UK household calculation."""
|
|
61
|
+
|
|
62
|
+
people: list[dict[str, Any]]
|
|
63
|
+
benunit: dict[str, Any] = Field(default_factory=dict)
|
|
64
|
+
household: dict[str, Any] = Field(default_factory=dict)
|
|
65
|
+
year: int = 2026
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def calculate_household_impact(
|
|
69
|
+
household_input: UKHouseholdInput,
|
|
70
|
+
policy: Policy | None = None,
|
|
71
|
+
) -> UKHouseholdOutput:
|
|
72
|
+
"""Calculate tax and benefit impacts for a single UK household."""
|
|
73
|
+
n_people = len(household_input.people)
|
|
74
|
+
|
|
75
|
+
# Build person data with defaults
|
|
76
|
+
person_data = {
|
|
77
|
+
"person_id": list(range(n_people)),
|
|
78
|
+
"person_benunit_id": [0] * n_people,
|
|
79
|
+
"person_household_id": [0] * n_people,
|
|
80
|
+
"person_weight": [1.0] * n_people,
|
|
81
|
+
}
|
|
82
|
+
# Add user-provided person fields
|
|
83
|
+
for i, person in enumerate(household_input.people):
|
|
84
|
+
for key, value in person.items():
|
|
85
|
+
if key not in person_data:
|
|
86
|
+
person_data[key] = [
|
|
87
|
+
0.0
|
|
88
|
+
] * n_people # Default to 0 for numeric fields
|
|
89
|
+
person_data[key][i] = value
|
|
90
|
+
|
|
91
|
+
# Build benunit data with defaults
|
|
92
|
+
benunit_data = {
|
|
93
|
+
"benunit_id": [0],
|
|
94
|
+
"benunit_weight": [1.0],
|
|
95
|
+
}
|
|
96
|
+
for key, value in household_input.benunit.items():
|
|
97
|
+
benunit_data[key] = [value]
|
|
98
|
+
|
|
99
|
+
# Build household data with defaults (required for uprating)
|
|
100
|
+
household_data = {
|
|
101
|
+
"household_id": [0],
|
|
102
|
+
"household_weight": [1.0],
|
|
103
|
+
"region": ["LONDON"],
|
|
104
|
+
"tenure_type": ["RENT_PRIVATELY"],
|
|
105
|
+
"council_tax": [0.0],
|
|
106
|
+
"rent": [0.0],
|
|
107
|
+
}
|
|
108
|
+
for key, value in household_input.household.items():
|
|
109
|
+
household_data[key] = [value]
|
|
110
|
+
|
|
111
|
+
# Create MicroDataFrames
|
|
112
|
+
person_df = MicroDataFrame(
|
|
113
|
+
pd.DataFrame(person_data), weights="person_weight"
|
|
114
|
+
)
|
|
115
|
+
benunit_df = MicroDataFrame(
|
|
116
|
+
pd.DataFrame(benunit_data), weights="benunit_weight"
|
|
117
|
+
)
|
|
118
|
+
household_df = MicroDataFrame(
|
|
119
|
+
pd.DataFrame(household_data), weights="household_weight"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Create temporary dataset
|
|
123
|
+
tmpdir = tempfile.mkdtemp()
|
|
124
|
+
filepath = str(Path(tmpdir) / "household_impact.h5")
|
|
125
|
+
|
|
126
|
+
dataset = PolicyEngineUKDataset(
|
|
127
|
+
name="Household impact calculation",
|
|
128
|
+
description="Single household for impact calculation",
|
|
129
|
+
filepath=filepath,
|
|
130
|
+
year=household_input.year,
|
|
131
|
+
data=UKYearData(
|
|
132
|
+
person=person_df,
|
|
133
|
+
benunit=benunit_df,
|
|
134
|
+
household=household_df,
|
|
135
|
+
),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Run simulation
|
|
139
|
+
simulation = Simulation(
|
|
140
|
+
dataset=dataset,
|
|
141
|
+
tax_benefit_model_version=uk_latest,
|
|
142
|
+
policy=policy,
|
|
143
|
+
)
|
|
144
|
+
simulation.run()
|
|
145
|
+
|
|
146
|
+
# Extract all output variables defined in entity_variables
|
|
147
|
+
output_data = simulation.output_dataset.data
|
|
148
|
+
|
|
149
|
+
def safe_convert(value):
|
|
150
|
+
"""Convert value to float if numeric, otherwise return as string."""
|
|
151
|
+
try:
|
|
152
|
+
return float(value)
|
|
153
|
+
except (ValueError, TypeError):
|
|
154
|
+
return str(value)
|
|
155
|
+
|
|
156
|
+
person_outputs = []
|
|
157
|
+
for i in range(n_people):
|
|
158
|
+
person_dict = {}
|
|
159
|
+
for var in uk_latest.entity_variables["person"]:
|
|
160
|
+
person_dict[var] = safe_convert(output_data.person[var].iloc[i])
|
|
161
|
+
person_outputs.append(person_dict)
|
|
162
|
+
|
|
163
|
+
benunit_outputs = []
|
|
164
|
+
for i in range(len(output_data.benunit)):
|
|
165
|
+
benunit_dict = {}
|
|
166
|
+
for var in uk_latest.entity_variables["benunit"]:
|
|
167
|
+
benunit_dict[var] = safe_convert(output_data.benunit[var].iloc[i])
|
|
168
|
+
benunit_outputs.append(benunit_dict)
|
|
169
|
+
|
|
170
|
+
household_dict = {}
|
|
171
|
+
for var in uk_latest.entity_variables["household"]:
|
|
172
|
+
household_dict[var] = safe_convert(output_data.household[var].iloc[0])
|
|
173
|
+
|
|
174
|
+
return UKHouseholdOutput(
|
|
175
|
+
person=person_outputs,
|
|
176
|
+
benunit=benunit_outputs,
|
|
177
|
+
household=household_dict,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
15
181
|
class PolicyReformAnalysis(BaseModel):
|
|
16
182
|
"""Complete policy reform analysis result."""
|
|
17
183
|
|
|
18
184
|
decile_impacts: OutputCollection[DecileImpact]
|
|
19
185
|
programme_statistics: OutputCollection[ProgrammeStatistics]
|
|
186
|
+
baseline_poverty: OutputCollection[Poverty]
|
|
187
|
+
reform_poverty: OutputCollection[Poverty]
|
|
188
|
+
baseline_inequality: Inequality
|
|
189
|
+
reform_inequality: Inequality
|
|
20
190
|
|
|
21
191
|
|
|
22
|
-
def
|
|
192
|
+
def economic_impact_analysis(
|
|
23
193
|
baseline_simulation: Simulation,
|
|
24
194
|
reform_simulation: Simulation,
|
|
25
195
|
) -> PolicyReformAnalysis:
|
|
@@ -28,10 +198,23 @@ def general_policy_reform_analysis(
|
|
|
28
198
|
Returns:
|
|
29
199
|
PolicyReformAnalysis containing decile impacts and programme statistics
|
|
30
200
|
"""
|
|
201
|
+
baseline_simulation.ensure()
|
|
202
|
+
reform_simulation.ensure()
|
|
203
|
+
|
|
204
|
+
assert len(baseline_simulation.dataset.data.household) > 100, (
|
|
205
|
+
"Baseline simulation must have more than 100 households"
|
|
206
|
+
)
|
|
207
|
+
assert len(reform_simulation.dataset.data.household) > 100, (
|
|
208
|
+
"Reform simulation must have more than 100 households"
|
|
209
|
+
)
|
|
210
|
+
|
|
31
211
|
# Decile impact
|
|
32
212
|
decile_impacts = calculate_decile_impacts(
|
|
33
|
-
|
|
34
|
-
|
|
213
|
+
dataset=baseline_simulation.dataset,
|
|
214
|
+
tax_benefit_model_version=baseline_simulation.tax_benefit_model_version,
|
|
215
|
+
baseline_policy=baseline_simulation.policy,
|
|
216
|
+
reform_policy=reform_simulation.policy,
|
|
217
|
+
dynamic=baseline_simulation.dynamic,
|
|
35
218
|
)
|
|
36
219
|
|
|
37
220
|
# Major programmes to analyse
|
|
@@ -91,7 +274,19 @@ def general_policy_reform_analysis(
|
|
|
91
274
|
outputs=programme_statistics, dataframe=programme_df
|
|
92
275
|
)
|
|
93
276
|
|
|
277
|
+
# Calculate poverty rates for both simulations
|
|
278
|
+
baseline_poverty = calculate_uk_poverty_rates(baseline_simulation)
|
|
279
|
+
reform_poverty = calculate_uk_poverty_rates(reform_simulation)
|
|
280
|
+
|
|
281
|
+
# Calculate inequality for both simulations
|
|
282
|
+
baseline_inequality = calculate_uk_inequality(baseline_simulation)
|
|
283
|
+
reform_inequality = calculate_uk_inequality(reform_simulation)
|
|
284
|
+
|
|
94
285
|
return PolicyReformAnalysis(
|
|
95
286
|
decile_impacts=decile_impacts,
|
|
96
287
|
programme_statistics=programme_collection,
|
|
288
|
+
baseline_poverty=baseline_poverty,
|
|
289
|
+
reform_poverty=reform_poverty,
|
|
290
|
+
baseline_inequality=baseline_inequality,
|
|
291
|
+
reform_inequality=reform_inequality,
|
|
97
292
|
)
|
|
@@ -9,12 +9,14 @@ 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
|
|
16
|
+
from policyengine.utils.parameter_labels import (
|
|
17
|
+
build_scale_lookup,
|
|
18
|
+
generate_label_for_parameter,
|
|
19
|
+
)
|
|
18
20
|
|
|
19
21
|
from .datasets import PolicyEngineUKDataset, UKYearData
|
|
20
22
|
|
|
@@ -108,6 +110,11 @@ class PolicyEngineUKLatest(TaxBenefitModelVersion):
|
|
|
108
110
|
"rent",
|
|
109
111
|
"council_tax",
|
|
110
112
|
"tenure_type",
|
|
113
|
+
# Poverty measures
|
|
114
|
+
"in_poverty_bhc",
|
|
115
|
+
"in_poverty_ahc",
|
|
116
|
+
"in_relative_poverty_bhc",
|
|
117
|
+
"in_relative_poverty_ahc",
|
|
111
118
|
],
|
|
112
119
|
}
|
|
113
120
|
|
|
@@ -143,17 +150,21 @@ class PolicyEngineUKLatest(TaxBenefitModelVersion):
|
|
|
143
150
|
|
|
144
151
|
from policyengine_core.parameters import Parameter as CoreParameter
|
|
145
152
|
|
|
153
|
+
scale_lookup = build_scale_lookup(system)
|
|
154
|
+
|
|
146
155
|
for param_node in system.parameters.get_descendants():
|
|
147
156
|
if isinstance(param_node, CoreParameter):
|
|
148
157
|
parameter = Parameter(
|
|
149
158
|
id=self.id + "-" + param_node.name,
|
|
150
159
|
name=param_node.name,
|
|
151
|
-
label=
|
|
160
|
+
label=generate_label_for_parameter(
|
|
161
|
+
param_node, system, scale_lookup
|
|
162
|
+
),
|
|
152
163
|
tax_benefit_model_version=self,
|
|
153
164
|
description=param_node.description,
|
|
154
165
|
data_type=type(param_node(2025)),
|
|
155
166
|
unit=param_node.metadata.get("unit"),
|
|
156
|
-
_core_param=param_node,
|
|
167
|
+
_core_param=param_node,
|
|
157
168
|
)
|
|
158
169
|
self.add_parameter(parameter)
|
|
159
170
|
|
|
@@ -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:
|
|
@@ -1,25 +1,213 @@
|
|
|
1
1
|
"""General utility functions for US policy reform analysis."""
|
|
2
2
|
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
3
7
|
import pandas as pd
|
|
4
|
-
from
|
|
8
|
+
from microdf import MicroDataFrame
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
5
10
|
|
|
6
11
|
from policyengine.core import OutputCollection, Simulation
|
|
12
|
+
from policyengine.core.policy import Policy
|
|
7
13
|
from policyengine.outputs.decile_impact import (
|
|
8
14
|
DecileImpact,
|
|
9
15
|
calculate_decile_impacts,
|
|
10
16
|
)
|
|
17
|
+
from policyengine.outputs.inequality import (
|
|
18
|
+
Inequality,
|
|
19
|
+
calculate_us_inequality,
|
|
20
|
+
)
|
|
21
|
+
from policyengine.outputs.poverty import (
|
|
22
|
+
Poverty,
|
|
23
|
+
calculate_us_poverty_rates,
|
|
24
|
+
)
|
|
11
25
|
|
|
26
|
+
from .datasets import PolicyEngineUSDataset, USYearData
|
|
27
|
+
from .model import us_latest
|
|
12
28
|
from .outputs import ProgramStatistics
|
|
13
29
|
|
|
14
30
|
|
|
31
|
+
class USHouseholdOutput(BaseModel):
|
|
32
|
+
"""Output from a US household calculation with all entity data."""
|
|
33
|
+
|
|
34
|
+
person: list[dict[str, Any]]
|
|
35
|
+
marital_unit: list[dict[str, Any]]
|
|
36
|
+
family: list[dict[str, Any]]
|
|
37
|
+
spm_unit: list[dict[str, Any]]
|
|
38
|
+
tax_unit: list[dict[str, Any]]
|
|
39
|
+
household: dict[str, Any]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class USHouseholdInput(BaseModel):
|
|
43
|
+
"""Input for a US household calculation."""
|
|
44
|
+
|
|
45
|
+
people: list[dict[str, Any]]
|
|
46
|
+
marital_unit: dict[str, Any] = Field(default_factory=dict)
|
|
47
|
+
family: dict[str, Any] = Field(default_factory=dict)
|
|
48
|
+
spm_unit: dict[str, Any] = Field(default_factory=dict)
|
|
49
|
+
tax_unit: dict[str, Any] = Field(default_factory=dict)
|
|
50
|
+
household: dict[str, Any] = Field(default_factory=dict)
|
|
51
|
+
year: int = 2024
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def calculate_household_impact(
|
|
55
|
+
household_input: USHouseholdInput,
|
|
56
|
+
policy: Policy | None = None,
|
|
57
|
+
) -> USHouseholdOutput:
|
|
58
|
+
"""Calculate tax and benefit impacts for a single US household."""
|
|
59
|
+
n_people = len(household_input.people)
|
|
60
|
+
|
|
61
|
+
# Build person data with defaults
|
|
62
|
+
person_data = {
|
|
63
|
+
"person_id": list(range(n_people)),
|
|
64
|
+
"person_household_id": [0] * n_people,
|
|
65
|
+
"person_marital_unit_id": [0] * n_people,
|
|
66
|
+
"person_family_id": [0] * n_people,
|
|
67
|
+
"person_spm_unit_id": [0] * n_people,
|
|
68
|
+
"person_tax_unit_id": [0] * n_people,
|
|
69
|
+
"person_weight": [1.0] * n_people,
|
|
70
|
+
}
|
|
71
|
+
# Add user-provided person fields
|
|
72
|
+
for i, person in enumerate(household_input.people):
|
|
73
|
+
for key, value in person.items():
|
|
74
|
+
if key not in person_data:
|
|
75
|
+
person_data[key] = [
|
|
76
|
+
0.0
|
|
77
|
+
] * n_people # Default to 0 for numeric fields
|
|
78
|
+
person_data[key][i] = value
|
|
79
|
+
|
|
80
|
+
# Build entity data with defaults
|
|
81
|
+
household_data = {
|
|
82
|
+
"household_id": [0],
|
|
83
|
+
"household_weight": [1.0],
|
|
84
|
+
}
|
|
85
|
+
for key, value in household_input.household.items():
|
|
86
|
+
household_data[key] = [value]
|
|
87
|
+
|
|
88
|
+
marital_unit_data = {
|
|
89
|
+
"marital_unit_id": [0],
|
|
90
|
+
"marital_unit_weight": [1.0],
|
|
91
|
+
}
|
|
92
|
+
for key, value in household_input.marital_unit.items():
|
|
93
|
+
marital_unit_data[key] = [value]
|
|
94
|
+
|
|
95
|
+
family_data = {
|
|
96
|
+
"family_id": [0],
|
|
97
|
+
"family_weight": [1.0],
|
|
98
|
+
}
|
|
99
|
+
for key, value in household_input.family.items():
|
|
100
|
+
family_data[key] = [value]
|
|
101
|
+
|
|
102
|
+
spm_unit_data = {
|
|
103
|
+
"spm_unit_id": [0],
|
|
104
|
+
"spm_unit_weight": [1.0],
|
|
105
|
+
}
|
|
106
|
+
for key, value in household_input.spm_unit.items():
|
|
107
|
+
spm_unit_data[key] = [value]
|
|
108
|
+
|
|
109
|
+
tax_unit_data = {
|
|
110
|
+
"tax_unit_id": [0],
|
|
111
|
+
"tax_unit_weight": [1.0],
|
|
112
|
+
}
|
|
113
|
+
for key, value in household_input.tax_unit.items():
|
|
114
|
+
tax_unit_data[key] = [value]
|
|
115
|
+
|
|
116
|
+
# Create MicroDataFrames
|
|
117
|
+
person_df = MicroDataFrame(
|
|
118
|
+
pd.DataFrame(person_data), weights="person_weight"
|
|
119
|
+
)
|
|
120
|
+
household_df = MicroDataFrame(
|
|
121
|
+
pd.DataFrame(household_data), weights="household_weight"
|
|
122
|
+
)
|
|
123
|
+
marital_unit_df = MicroDataFrame(
|
|
124
|
+
pd.DataFrame(marital_unit_data), weights="marital_unit_weight"
|
|
125
|
+
)
|
|
126
|
+
family_df = MicroDataFrame(
|
|
127
|
+
pd.DataFrame(family_data), weights="family_weight"
|
|
128
|
+
)
|
|
129
|
+
spm_unit_df = MicroDataFrame(
|
|
130
|
+
pd.DataFrame(spm_unit_data), weights="spm_unit_weight"
|
|
131
|
+
)
|
|
132
|
+
tax_unit_df = MicroDataFrame(
|
|
133
|
+
pd.DataFrame(tax_unit_data), weights="tax_unit_weight"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Create temporary dataset
|
|
137
|
+
tmpdir = tempfile.mkdtemp()
|
|
138
|
+
filepath = str(Path(tmpdir) / "household_impact.h5")
|
|
139
|
+
|
|
140
|
+
dataset = PolicyEngineUSDataset(
|
|
141
|
+
name="Household impact calculation",
|
|
142
|
+
description="Single household for impact calculation",
|
|
143
|
+
filepath=filepath,
|
|
144
|
+
year=household_input.year,
|
|
145
|
+
data=USYearData(
|
|
146
|
+
person=person_df,
|
|
147
|
+
household=household_df,
|
|
148
|
+
marital_unit=marital_unit_df,
|
|
149
|
+
family=family_df,
|
|
150
|
+
spm_unit=spm_unit_df,
|
|
151
|
+
tax_unit=tax_unit_df,
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Run simulation
|
|
156
|
+
simulation = Simulation(
|
|
157
|
+
dataset=dataset,
|
|
158
|
+
tax_benefit_model_version=us_latest,
|
|
159
|
+
policy=policy,
|
|
160
|
+
)
|
|
161
|
+
simulation.run()
|
|
162
|
+
|
|
163
|
+
# Extract all output variables defined in entity_variables
|
|
164
|
+
output_data = simulation.output_dataset.data
|
|
165
|
+
|
|
166
|
+
def safe_convert(value):
|
|
167
|
+
"""Convert value to float if numeric, otherwise return as string."""
|
|
168
|
+
try:
|
|
169
|
+
return float(value)
|
|
170
|
+
except (ValueError, TypeError):
|
|
171
|
+
return str(value)
|
|
172
|
+
|
|
173
|
+
def extract_entity_outputs(
|
|
174
|
+
entity_name: str, entity_data, n_rows: int
|
|
175
|
+
) -> list[dict[str, Any]]:
|
|
176
|
+
outputs = []
|
|
177
|
+
for i in range(n_rows):
|
|
178
|
+
row_dict = {}
|
|
179
|
+
for var in us_latest.entity_variables[entity_name]:
|
|
180
|
+
row_dict[var] = safe_convert(entity_data[var].iloc[i])
|
|
181
|
+
outputs.append(row_dict)
|
|
182
|
+
return outputs
|
|
183
|
+
|
|
184
|
+
return USHouseholdOutput(
|
|
185
|
+
person=extract_entity_outputs("person", output_data.person, n_people),
|
|
186
|
+
marital_unit=extract_entity_outputs(
|
|
187
|
+
"marital_unit", output_data.marital_unit, 1
|
|
188
|
+
),
|
|
189
|
+
family=extract_entity_outputs("family", output_data.family, 1),
|
|
190
|
+
spm_unit=extract_entity_outputs("spm_unit", output_data.spm_unit, 1),
|
|
191
|
+
tax_unit=extract_entity_outputs("tax_unit", output_data.tax_unit, 1),
|
|
192
|
+
household={
|
|
193
|
+
var: safe_convert(output_data.household[var].iloc[0])
|
|
194
|
+
for var in us_latest.entity_variables["household"]
|
|
195
|
+
},
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
15
199
|
class PolicyReformAnalysis(BaseModel):
|
|
16
200
|
"""Complete policy reform analysis result."""
|
|
17
201
|
|
|
18
202
|
decile_impacts: OutputCollection[DecileImpact]
|
|
19
203
|
program_statistics: OutputCollection[ProgramStatistics]
|
|
204
|
+
baseline_poverty: OutputCollection[Poverty]
|
|
205
|
+
reform_poverty: OutputCollection[Poverty]
|
|
206
|
+
baseline_inequality: Inequality
|
|
207
|
+
reform_inequality: Inequality
|
|
20
208
|
|
|
21
209
|
|
|
22
|
-
def
|
|
210
|
+
def economic_impact_analysis(
|
|
23
211
|
baseline_simulation: Simulation,
|
|
24
212
|
reform_simulation: Simulation,
|
|
25
213
|
) -> PolicyReformAnalysis:
|
|
@@ -28,10 +216,23 @@ def general_policy_reform_analysis(
|
|
|
28
216
|
Returns:
|
|
29
217
|
PolicyReformAnalysis containing decile impacts and program statistics
|
|
30
218
|
"""
|
|
219
|
+
baseline_simulation.ensure()
|
|
220
|
+
reform_simulation.ensure()
|
|
221
|
+
|
|
222
|
+
assert len(baseline_simulation.dataset.data.household) > 100, (
|
|
223
|
+
"Baseline simulation must have more than 100 households"
|
|
224
|
+
)
|
|
225
|
+
assert len(reform_simulation.dataset.data.household) > 100, (
|
|
226
|
+
"Reform simulation must have more than 100 households"
|
|
227
|
+
)
|
|
228
|
+
|
|
31
229
|
# Decile impact (using household_net_income for US)
|
|
32
230
|
decile_impacts = calculate_decile_impacts(
|
|
33
|
-
|
|
34
|
-
|
|
231
|
+
dataset=baseline_simulation.dataset,
|
|
232
|
+
tax_benefit_model_version=baseline_simulation.tax_benefit_model_version,
|
|
233
|
+
baseline_policy=baseline_simulation.policy,
|
|
234
|
+
reform_policy=reform_simulation.policy,
|
|
235
|
+
dynamic=baseline_simulation.dynamic,
|
|
35
236
|
income_variable="household_net_income",
|
|
36
237
|
)
|
|
37
238
|
|
|
@@ -94,6 +295,19 @@ def general_policy_reform_analysis(
|
|
|
94
295
|
outputs=program_statistics, dataframe=program_df
|
|
95
296
|
)
|
|
96
297
|
|
|
298
|
+
# Calculate poverty rates for both simulations
|
|
299
|
+
baseline_poverty = calculate_us_poverty_rates(baseline_simulation)
|
|
300
|
+
reform_poverty = calculate_us_poverty_rates(reform_simulation)
|
|
301
|
+
|
|
302
|
+
# Calculate inequality for both simulations
|
|
303
|
+
baseline_inequality = calculate_us_inequality(baseline_simulation)
|
|
304
|
+
reform_inequality = calculate_us_inequality(reform_simulation)
|
|
305
|
+
|
|
97
306
|
return PolicyReformAnalysis(
|
|
98
|
-
decile_impacts=decile_impacts,
|
|
307
|
+
decile_impacts=decile_impacts,
|
|
308
|
+
program_statistics=program_collection,
|
|
309
|
+
baseline_poverty=baseline_poverty,
|
|
310
|
+
reform_poverty=reform_poverty,
|
|
311
|
+
baseline_inequality=baseline_inequality,
|
|
312
|
+
reform_inequality=reform_inequality,
|
|
99
313
|
)
|
|
@@ -9,12 +9,14 @@ 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
|
|
16
|
+
from policyengine.utils.parameter_labels import (
|
|
17
|
+
build_scale_lookup,
|
|
18
|
+
generate_label_for_parameter,
|
|
19
|
+
)
|
|
18
20
|
|
|
19
21
|
from .datasets import PolicyEngineUSDataset, USYearData
|
|
20
22
|
|
|
@@ -57,6 +59,8 @@ class PolicyEngineUSLatest(TaxBenefitModelVersion):
|
|
|
57
59
|
"person_weight",
|
|
58
60
|
# Demographics
|
|
59
61
|
"age",
|
|
62
|
+
"is_child",
|
|
63
|
+
"is_adult",
|
|
60
64
|
# Income
|
|
61
65
|
"employment_income",
|
|
62
66
|
# Benefits
|
|
@@ -79,6 +83,9 @@ class PolicyEngineUSLatest(TaxBenefitModelVersion):
|
|
|
79
83
|
"snap",
|
|
80
84
|
"tanf",
|
|
81
85
|
"spm_unit_net_income",
|
|
86
|
+
# Poverty measures
|
|
87
|
+
"spm_unit_is_in_spm_poverty",
|
|
88
|
+
"spm_unit_is_in_deep_spm_poverty",
|
|
82
89
|
],
|
|
83
90
|
"tax_unit": [
|
|
84
91
|
"tax_unit_id",
|
|
@@ -136,17 +143,21 @@ class PolicyEngineUSLatest(TaxBenefitModelVersion):
|
|
|
136
143
|
|
|
137
144
|
from policyengine_core.parameters import Parameter as CoreParameter
|
|
138
145
|
|
|
146
|
+
scale_lookup = build_scale_lookup(system)
|
|
147
|
+
|
|
139
148
|
for param_node in system.parameters.get_descendants():
|
|
140
149
|
if isinstance(param_node, CoreParameter):
|
|
141
150
|
parameter = Parameter(
|
|
142
151
|
id=self.id + "-" + param_node.name,
|
|
143
152
|
name=param_node.name,
|
|
144
|
-
label=
|
|
153
|
+
label=generate_label_for_parameter(
|
|
154
|
+
param_node, system, scale_lookup
|
|
155
|
+
),
|
|
145
156
|
tax_benefit_model_version=self,
|
|
146
157
|
description=param_node.description,
|
|
147
158
|
data_type=type(param_node(2025)),
|
|
148
159
|
unit=param_node.metadata.get("unit"),
|
|
149
|
-
_core_param=param_node,
|
|
160
|
+
_core_param=param_node,
|
|
150
161
|
)
|
|
151
162
|
self.add_parameter(parameter)
|
|
152
163
|
|
policyengine/utils/__init__.py
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
from .dates import parse_safe_date as parse_safe_date
|
|
2
|
+
from .parameter_labels import build_scale_lookup as build_scale_lookup
|
|
3
|
+
from .parameter_labels import (
|
|
4
|
+
generate_label_for_parameter as generate_label_for_parameter,
|
|
5
|
+
)
|
|
2
6
|
from .plotting import COLORS as COLORS
|
|
3
7
|
from .plotting import format_fig as format_fig
|