policyengine 3.1.15__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/outputs/__init__.py +28 -0
- policyengine/outputs/inequality.py +276 -0
- policyengine/outputs/poverty.py +238 -0
- policyengine/tax_benefit_models/uk/analysis.py +24 -0
- policyengine/tax_benefit_models/uk/model.py +10 -2
- policyengine/tax_benefit_models/us/analysis.py +26 -1
- policyengine/tax_benefit_models/us/model.py +15 -2
- policyengine/utils/__init__.py +4 -0
- policyengine/utils/parameter_labels.py +213 -0
- {policyengine-3.1.15.dist-info → policyengine-3.1.16.dist-info}/METADATA +5 -5
- {policyengine-3.1.15.dist-info → policyengine-3.1.16.dist-info}/RECORD +15 -12
- {policyengine-3.1.15.dist-info → policyengine-3.1.16.dist-info}/WHEEL +1 -1
- {policyengine-3.1.15.dist-info → policyengine-3.1.16.dist-info}/licenses/LICENSE +0 -0
- {policyengine-3.1.15.dist-info → policyengine-3.1.16.dist-info}/top_level.txt +0 -0
|
Binary file
|
policyengine/outputs/__init__.py
CHANGED
|
@@ -8,6 +8,22 @@ from policyengine.outputs.decile_impact import (
|
|
|
8
8
|
DecileImpact,
|
|
9
9
|
calculate_decile_impacts,
|
|
10
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
|
+
)
|
|
11
27
|
|
|
12
28
|
__all__ = [
|
|
13
29
|
"Output",
|
|
@@ -18,4 +34,16 @@ __all__ = [
|
|
|
18
34
|
"ChangeAggregateType",
|
|
19
35
|
"DecileImpact",
|
|
20
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",
|
|
21
49
|
]
|
|
@@ -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
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Poverty analysis output types."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from pydantic import ConfigDict
|
|
8
|
+
|
|
9
|
+
from policyengine.core import Output, OutputCollection, Simulation
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UKPovertyType(str, Enum):
|
|
13
|
+
"""UK poverty measure types."""
|
|
14
|
+
|
|
15
|
+
ABSOLUTE_BHC = "absolute_bhc"
|
|
16
|
+
ABSOLUTE_AHC = "absolute_ahc"
|
|
17
|
+
RELATIVE_BHC = "relative_bhc"
|
|
18
|
+
RELATIVE_AHC = "relative_ahc"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class USPovertyType(str, Enum):
|
|
22
|
+
"""US poverty measure types."""
|
|
23
|
+
|
|
24
|
+
SPM = "spm"
|
|
25
|
+
SPM_DEEP = "spm_deep"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Mapping from poverty type to variable name
|
|
29
|
+
UK_POVERTY_VARIABLES = {
|
|
30
|
+
UKPovertyType.ABSOLUTE_BHC: "in_poverty_bhc",
|
|
31
|
+
UKPovertyType.ABSOLUTE_AHC: "in_poverty_ahc",
|
|
32
|
+
UKPovertyType.RELATIVE_BHC: "in_relative_poverty_bhc",
|
|
33
|
+
UKPovertyType.RELATIVE_AHC: "in_relative_poverty_ahc",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
US_POVERTY_VARIABLES = {
|
|
37
|
+
USPovertyType.SPM: "spm_unit_is_in_spm_poverty",
|
|
38
|
+
USPovertyType.SPM_DEEP: "spm_unit_is_in_deep_spm_poverty",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Poverty(Output):
|
|
43
|
+
"""Single poverty measure result - represents one database row.
|
|
44
|
+
|
|
45
|
+
This is a single-simulation output type that calculates poverty
|
|
46
|
+
headcount and rate for a given poverty measure, optionally filtered
|
|
47
|
+
by demographic variables.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
51
|
+
|
|
52
|
+
simulation: Simulation
|
|
53
|
+
poverty_variable: str
|
|
54
|
+
entity: str = "person"
|
|
55
|
+
|
|
56
|
+
# Optional demographic filters
|
|
57
|
+
filter_variable: str | None = None
|
|
58
|
+
filter_variable_eq: Any | None = None
|
|
59
|
+
filter_variable_leq: Any | None = None
|
|
60
|
+
filter_variable_geq: Any | None = None
|
|
61
|
+
|
|
62
|
+
# Results populated by run()
|
|
63
|
+
headcount: float | None = None
|
|
64
|
+
total_population: float | None = None
|
|
65
|
+
rate: float | None = None
|
|
66
|
+
|
|
67
|
+
def run(self):
|
|
68
|
+
"""Calculate poverty headcount and rate."""
|
|
69
|
+
# Get poverty variable info
|
|
70
|
+
poverty_var_obj = (
|
|
71
|
+
self.simulation.tax_benefit_model_version.get_variable(
|
|
72
|
+
self.poverty_variable
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Get target entity data
|
|
77
|
+
target_entity = self.entity
|
|
78
|
+
data = getattr(self.simulation.output_dataset.data, target_entity)
|
|
79
|
+
|
|
80
|
+
# Map poverty variable to target entity if needed
|
|
81
|
+
if poverty_var_obj.entity != target_entity:
|
|
82
|
+
mapped = self.simulation.output_dataset.data.map_to_entity(
|
|
83
|
+
poverty_var_obj.entity,
|
|
84
|
+
target_entity,
|
|
85
|
+
columns=[self.poverty_variable],
|
|
86
|
+
)
|
|
87
|
+
poverty_series = mapped[self.poverty_variable]
|
|
88
|
+
else:
|
|
89
|
+
poverty_series = data[self.poverty_variable]
|
|
90
|
+
|
|
91
|
+
# Apply demographic filter if specified
|
|
92
|
+
if self.filter_variable is not None:
|
|
93
|
+
filter_var_obj = (
|
|
94
|
+
self.simulation.tax_benefit_model_version.get_variable(
|
|
95
|
+
self.filter_variable
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if filter_var_obj.entity != target_entity:
|
|
100
|
+
filter_mapped = (
|
|
101
|
+
self.simulation.output_dataset.data.map_to_entity(
|
|
102
|
+
filter_var_obj.entity,
|
|
103
|
+
target_entity,
|
|
104
|
+
columns=[self.filter_variable],
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
filter_series = filter_mapped[self.filter_variable]
|
|
108
|
+
else:
|
|
109
|
+
filter_series = data[self.filter_variable]
|
|
110
|
+
|
|
111
|
+
# Build filter mask
|
|
112
|
+
mask = filter_series.notna()
|
|
113
|
+
if self.filter_variable_eq is not None:
|
|
114
|
+
mask &= filter_series == self.filter_variable_eq
|
|
115
|
+
if self.filter_variable_leq is not None:
|
|
116
|
+
mask &= filter_series <= self.filter_variable_leq
|
|
117
|
+
if self.filter_variable_geq is not None:
|
|
118
|
+
mask &= filter_series >= self.filter_variable_geq
|
|
119
|
+
|
|
120
|
+
# Apply mask
|
|
121
|
+
poverty_series = poverty_series[mask]
|
|
122
|
+
|
|
123
|
+
# Calculate results using weighted counts
|
|
124
|
+
self.headcount = float((poverty_series == True).sum()) # noqa: E712
|
|
125
|
+
self.total_population = float(poverty_series.count())
|
|
126
|
+
self.rate = (
|
|
127
|
+
self.headcount / self.total_population
|
|
128
|
+
if self.total_population > 0
|
|
129
|
+
else 0.0
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def calculate_uk_poverty_rates(
|
|
134
|
+
simulation: Simulation,
|
|
135
|
+
filter_variable: str | None = None,
|
|
136
|
+
filter_variable_eq: Any | None = None,
|
|
137
|
+
filter_variable_leq: Any | None = None,
|
|
138
|
+
filter_variable_geq: Any | None = None,
|
|
139
|
+
) -> OutputCollection[Poverty]:
|
|
140
|
+
"""Calculate all UK poverty rates for a simulation.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
simulation: The simulation to analyse
|
|
144
|
+
filter_variable: Optional variable to filter by (e.g., "is_child")
|
|
145
|
+
filter_variable_eq: Filter for exact match
|
|
146
|
+
filter_variable_leq: Filter for less than or equal
|
|
147
|
+
filter_variable_geq: Filter for greater than or equal
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
OutputCollection containing Poverty objects for each UK poverty type
|
|
151
|
+
"""
|
|
152
|
+
results = []
|
|
153
|
+
|
|
154
|
+
for poverty_variable in UK_POVERTY_VARIABLES.values():
|
|
155
|
+
poverty = Poverty(
|
|
156
|
+
simulation=simulation,
|
|
157
|
+
poverty_variable=poverty_variable,
|
|
158
|
+
entity="person",
|
|
159
|
+
filter_variable=filter_variable,
|
|
160
|
+
filter_variable_eq=filter_variable_eq,
|
|
161
|
+
filter_variable_leq=filter_variable_leq,
|
|
162
|
+
filter_variable_geq=filter_variable_geq,
|
|
163
|
+
)
|
|
164
|
+
poverty.run()
|
|
165
|
+
results.append(poverty)
|
|
166
|
+
|
|
167
|
+
df = pd.DataFrame(
|
|
168
|
+
[
|
|
169
|
+
{
|
|
170
|
+
"simulation_id": r.simulation.id,
|
|
171
|
+
"poverty_variable": r.poverty_variable,
|
|
172
|
+
"filter_variable": r.filter_variable,
|
|
173
|
+
"filter_variable_eq": r.filter_variable_eq,
|
|
174
|
+
"filter_variable_leq": r.filter_variable_leq,
|
|
175
|
+
"filter_variable_geq": r.filter_variable_geq,
|
|
176
|
+
"headcount": r.headcount,
|
|
177
|
+
"total_population": r.total_population,
|
|
178
|
+
"rate": r.rate,
|
|
179
|
+
}
|
|
180
|
+
for r in results
|
|
181
|
+
]
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
return OutputCollection(outputs=results, dataframe=df)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def calculate_us_poverty_rates(
|
|
188
|
+
simulation: Simulation,
|
|
189
|
+
filter_variable: str | None = None,
|
|
190
|
+
filter_variable_eq: Any | None = None,
|
|
191
|
+
filter_variable_leq: Any | None = None,
|
|
192
|
+
filter_variable_geq: Any | None = None,
|
|
193
|
+
) -> OutputCollection[Poverty]:
|
|
194
|
+
"""Calculate all US poverty rates for a simulation.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
simulation: The simulation to analyse
|
|
198
|
+
filter_variable: Optional variable to filter by (e.g., "is_child")
|
|
199
|
+
filter_variable_eq: Filter for exact match
|
|
200
|
+
filter_variable_leq: Filter for less than or equal
|
|
201
|
+
filter_variable_geq: Filter for greater than or equal
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
OutputCollection containing Poverty objects for each US poverty type
|
|
205
|
+
"""
|
|
206
|
+
results = []
|
|
207
|
+
|
|
208
|
+
for poverty_variable in US_POVERTY_VARIABLES.values():
|
|
209
|
+
poverty = Poverty(
|
|
210
|
+
simulation=simulation,
|
|
211
|
+
poverty_variable=poverty_variable,
|
|
212
|
+
entity="person",
|
|
213
|
+
filter_variable=filter_variable,
|
|
214
|
+
filter_variable_eq=filter_variable_eq,
|
|
215
|
+
filter_variable_leq=filter_variable_leq,
|
|
216
|
+
filter_variable_geq=filter_variable_geq,
|
|
217
|
+
)
|
|
218
|
+
poverty.run()
|
|
219
|
+
results.append(poverty)
|
|
220
|
+
|
|
221
|
+
df = pd.DataFrame(
|
|
222
|
+
[
|
|
223
|
+
{
|
|
224
|
+
"simulation_id": r.simulation.id,
|
|
225
|
+
"poverty_variable": r.poverty_variable,
|
|
226
|
+
"filter_variable": r.filter_variable,
|
|
227
|
+
"filter_variable_eq": r.filter_variable_eq,
|
|
228
|
+
"filter_variable_leq": r.filter_variable_leq,
|
|
229
|
+
"filter_variable_geq": r.filter_variable_geq,
|
|
230
|
+
"headcount": r.headcount,
|
|
231
|
+
"total_population": r.total_population,
|
|
232
|
+
"rate": r.rate,
|
|
233
|
+
}
|
|
234
|
+
for r in results
|
|
235
|
+
]
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
return OutputCollection(outputs=results, dataframe=df)
|
|
@@ -14,6 +14,14 @@ from policyengine.outputs.decile_impact import (
|
|
|
14
14
|
DecileImpact,
|
|
15
15
|
calculate_decile_impacts,
|
|
16
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
|
+
)
|
|
17
25
|
|
|
18
26
|
from .datasets import PolicyEngineUKDataset, UKYearData
|
|
19
27
|
from .model import uk_latest
|
|
@@ -175,6 +183,10 @@ class PolicyReformAnalysis(BaseModel):
|
|
|
175
183
|
|
|
176
184
|
decile_impacts: OutputCollection[DecileImpact]
|
|
177
185
|
programme_statistics: OutputCollection[ProgrammeStatistics]
|
|
186
|
+
baseline_poverty: OutputCollection[Poverty]
|
|
187
|
+
reform_poverty: OutputCollection[Poverty]
|
|
188
|
+
baseline_inequality: Inequality
|
|
189
|
+
reform_inequality: Inequality
|
|
178
190
|
|
|
179
191
|
|
|
180
192
|
def economic_impact_analysis(
|
|
@@ -262,7 +274,19 @@ def economic_impact_analysis(
|
|
|
262
274
|
outputs=programme_statistics, dataframe=programme_df
|
|
263
275
|
)
|
|
264
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
|
+
|
|
265
285
|
return PolicyReformAnalysis(
|
|
266
286
|
decile_impacts=decile_impacts,
|
|
267
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,
|
|
268
292
|
)
|
|
@@ -13,6 +13,10 @@ from policyengine.core import (
|
|
|
13
13
|
TaxBenefitModelVersion,
|
|
14
14
|
Variable,
|
|
15
15
|
)
|
|
16
|
+
from policyengine.utils.parameter_labels import (
|
|
17
|
+
build_scale_lookup,
|
|
18
|
+
generate_label_for_parameter,
|
|
19
|
+
)
|
|
16
20
|
|
|
17
21
|
from .datasets import PolicyEngineUKDataset, UKYearData
|
|
18
22
|
|
|
@@ -146,17 +150,21 @@ class PolicyEngineUKLatest(TaxBenefitModelVersion):
|
|
|
146
150
|
|
|
147
151
|
from policyengine_core.parameters import Parameter as CoreParameter
|
|
148
152
|
|
|
153
|
+
scale_lookup = build_scale_lookup(system)
|
|
154
|
+
|
|
149
155
|
for param_node in system.parameters.get_descendants():
|
|
150
156
|
if isinstance(param_node, CoreParameter):
|
|
151
157
|
parameter = Parameter(
|
|
152
158
|
id=self.id + "-" + param_node.name,
|
|
153
159
|
name=param_node.name,
|
|
154
|
-
label=
|
|
160
|
+
label=generate_label_for_parameter(
|
|
161
|
+
param_node, system, scale_lookup
|
|
162
|
+
),
|
|
155
163
|
tax_benefit_model_version=self,
|
|
156
164
|
description=param_node.description,
|
|
157
165
|
data_type=type(param_node(2025)),
|
|
158
166
|
unit=param_node.metadata.get("unit"),
|
|
159
|
-
_core_param=param_node,
|
|
167
|
+
_core_param=param_node,
|
|
160
168
|
)
|
|
161
169
|
self.add_parameter(parameter)
|
|
162
170
|
|
|
@@ -14,6 +14,14 @@ from policyengine.outputs.decile_impact import (
|
|
|
14
14
|
DecileImpact,
|
|
15
15
|
calculate_decile_impacts,
|
|
16
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
|
+
)
|
|
17
25
|
|
|
18
26
|
from .datasets import PolicyEngineUSDataset, USYearData
|
|
19
27
|
from .model import us_latest
|
|
@@ -193,6 +201,10 @@ class PolicyReformAnalysis(BaseModel):
|
|
|
193
201
|
|
|
194
202
|
decile_impacts: OutputCollection[DecileImpact]
|
|
195
203
|
program_statistics: OutputCollection[ProgramStatistics]
|
|
204
|
+
baseline_poverty: OutputCollection[Poverty]
|
|
205
|
+
reform_poverty: OutputCollection[Poverty]
|
|
206
|
+
baseline_inequality: Inequality
|
|
207
|
+
reform_inequality: Inequality
|
|
196
208
|
|
|
197
209
|
|
|
198
210
|
def economic_impact_analysis(
|
|
@@ -283,6 +295,19 @@ def economic_impact_analysis(
|
|
|
283
295
|
outputs=program_statistics, dataframe=program_df
|
|
284
296
|
)
|
|
285
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
|
+
|
|
286
306
|
return PolicyReformAnalysis(
|
|
287
|
-
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,
|
|
288
313
|
)
|
|
@@ -13,6 +13,10 @@ from policyengine.core import (
|
|
|
13
13
|
TaxBenefitModelVersion,
|
|
14
14
|
Variable,
|
|
15
15
|
)
|
|
16
|
+
from policyengine.utils.parameter_labels import (
|
|
17
|
+
build_scale_lookup,
|
|
18
|
+
generate_label_for_parameter,
|
|
19
|
+
)
|
|
16
20
|
|
|
17
21
|
from .datasets import PolicyEngineUSDataset, USYearData
|
|
18
22
|
|
|
@@ -55,6 +59,8 @@ class PolicyEngineUSLatest(TaxBenefitModelVersion):
|
|
|
55
59
|
"person_weight",
|
|
56
60
|
# Demographics
|
|
57
61
|
"age",
|
|
62
|
+
"is_child",
|
|
63
|
+
"is_adult",
|
|
58
64
|
# Income
|
|
59
65
|
"employment_income",
|
|
60
66
|
# Benefits
|
|
@@ -77,6 +83,9 @@ class PolicyEngineUSLatest(TaxBenefitModelVersion):
|
|
|
77
83
|
"snap",
|
|
78
84
|
"tanf",
|
|
79
85
|
"spm_unit_net_income",
|
|
86
|
+
# Poverty measures
|
|
87
|
+
"spm_unit_is_in_spm_poverty",
|
|
88
|
+
"spm_unit_is_in_deep_spm_poverty",
|
|
80
89
|
],
|
|
81
90
|
"tax_unit": [
|
|
82
91
|
"tax_unit_id",
|
|
@@ -134,17 +143,21 @@ class PolicyEngineUSLatest(TaxBenefitModelVersion):
|
|
|
134
143
|
|
|
135
144
|
from policyengine_core.parameters import Parameter as CoreParameter
|
|
136
145
|
|
|
146
|
+
scale_lookup = build_scale_lookup(system)
|
|
147
|
+
|
|
137
148
|
for param_node in system.parameters.get_descendants():
|
|
138
149
|
if isinstance(param_node, CoreParameter):
|
|
139
150
|
parameter = Parameter(
|
|
140
151
|
id=self.id + "-" + param_node.name,
|
|
141
152
|
name=param_node.name,
|
|
142
|
-
label=
|
|
153
|
+
label=generate_label_for_parameter(
|
|
154
|
+
param_node, system, scale_lookup
|
|
155
|
+
),
|
|
143
156
|
tax_benefit_model_version=self,
|
|
144
157
|
description=param_node.description,
|
|
145
158
|
data_type=type(param_node(2025)),
|
|
146
159
|
unit=param_node.metadata.get("unit"),
|
|
147
|
-
_core_param=param_node,
|
|
160
|
+
_core_param=param_node,
|
|
148
161
|
)
|
|
149
162
|
self.add_parameter(parameter)
|
|
150
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
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Utilities for generating human-readable labels for tax-benefit parameters."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def generate_label_for_parameter(param_node, system, scale_lookup):
|
|
7
|
+
"""
|
|
8
|
+
Generate a label for a parameter that doesn't have one.
|
|
9
|
+
|
|
10
|
+
For breakdown parameters: Uses parent label + enum value
|
|
11
|
+
For bracket parameters: Uses scale label + bracket info
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
param_node: The CoreParameter object
|
|
15
|
+
system: The tax-benefit system (has variables and parameters)
|
|
16
|
+
scale_lookup: Dict mapping scale names to ParameterScale objects
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
str or None: Generated label, or None if cannot generate
|
|
20
|
+
"""
|
|
21
|
+
if param_node.metadata.get("label"):
|
|
22
|
+
return param_node.metadata.get("label")
|
|
23
|
+
|
|
24
|
+
param_name = param_node.name
|
|
25
|
+
|
|
26
|
+
if "[" in param_name:
|
|
27
|
+
return _generate_bracket_label(param_name, scale_lookup)
|
|
28
|
+
|
|
29
|
+
# Check for breakdown - either direct child or nested
|
|
30
|
+
breakdown_parent = _find_breakdown_parent(param_node)
|
|
31
|
+
if breakdown_parent:
|
|
32
|
+
return _generate_breakdown_label(param_node, system, breakdown_parent)
|
|
33
|
+
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _find_breakdown_parent(param_node):
|
|
38
|
+
"""
|
|
39
|
+
Walk up the tree to find the nearest ancestor with breakdown metadata.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
param_node: The CoreParameter object
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
The breakdown parent node, or None if not found
|
|
46
|
+
"""
|
|
47
|
+
current = param_node.parent
|
|
48
|
+
while current:
|
|
49
|
+
if current.metadata.get("breakdown"):
|
|
50
|
+
return current
|
|
51
|
+
current = getattr(current, "parent", None)
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _generate_breakdown_label(param_node, system, breakdown_parent=None):
|
|
56
|
+
"""
|
|
57
|
+
Generate label for a breakdown parameter using enum values.
|
|
58
|
+
|
|
59
|
+
Handles both single-level and nested breakdowns by walking up to the
|
|
60
|
+
breakdown parent and collecting all dimension values.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
param_node: The CoreParameter object
|
|
64
|
+
system: The tax-benefit system
|
|
65
|
+
breakdown_parent: The ancestor node with breakdown metadata (optional)
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
str or None: Generated label, or None if cannot generate
|
|
69
|
+
"""
|
|
70
|
+
# Find breakdown parent if not provided
|
|
71
|
+
if breakdown_parent is None:
|
|
72
|
+
breakdown_parent = _find_breakdown_parent(param_node)
|
|
73
|
+
if not breakdown_parent:
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
parent_label = breakdown_parent.metadata.get("label")
|
|
77
|
+
if not parent_label:
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
breakdown_vars = breakdown_parent.metadata.get("breakdown", [])
|
|
81
|
+
breakdown_labels = breakdown_parent.metadata.get("breakdown_labels", [])
|
|
82
|
+
|
|
83
|
+
# Collect dimension values from breakdown parent to param_node
|
|
84
|
+
dimension_values = _collect_dimension_values(
|
|
85
|
+
param_node, breakdown_parent
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if not dimension_values:
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
# Generate labels for each dimension
|
|
92
|
+
formatted_parts = []
|
|
93
|
+
for i, (dim_key, dim_value) in enumerate(dimension_values):
|
|
94
|
+
var_name = breakdown_vars[i] if i < len(breakdown_vars) else None
|
|
95
|
+
dim_label = breakdown_labels[i] if i < len(breakdown_labels) else None
|
|
96
|
+
|
|
97
|
+
formatted_value = _format_dimension_value(
|
|
98
|
+
dim_value, var_name, dim_label, system
|
|
99
|
+
)
|
|
100
|
+
formatted_parts.append(formatted_value)
|
|
101
|
+
|
|
102
|
+
return f"{parent_label} ({', '.join(formatted_parts)})"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _collect_dimension_values(param_node, breakdown_parent):
|
|
106
|
+
"""
|
|
107
|
+
Collect dimension keys and values from breakdown parent to param_node.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
param_node: The CoreParameter object
|
|
111
|
+
breakdown_parent: The ancestor node with breakdown metadata
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
list of (dimension_key, value) tuples, ordered from parent to child
|
|
115
|
+
"""
|
|
116
|
+
# Build path from param_node up to breakdown_parent
|
|
117
|
+
path = []
|
|
118
|
+
current = param_node
|
|
119
|
+
while current and current != breakdown_parent:
|
|
120
|
+
path.append(current)
|
|
121
|
+
current = getattr(current, "parent", None)
|
|
122
|
+
|
|
123
|
+
# Reverse to get parent-to-child order
|
|
124
|
+
path.reverse()
|
|
125
|
+
|
|
126
|
+
# Extract dimension values
|
|
127
|
+
dimension_values = []
|
|
128
|
+
for i, node in enumerate(path):
|
|
129
|
+
key = node.name.split(".")[-1]
|
|
130
|
+
dimension_values.append((i, key))
|
|
131
|
+
|
|
132
|
+
return dimension_values
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _format_dimension_value(value, var_name, dim_label, system):
|
|
136
|
+
"""
|
|
137
|
+
Format a single dimension value with semantic label if available.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
value: The raw dimension value (e.g., "SINGLE", "1", "CA")
|
|
141
|
+
var_name: The breakdown variable name (e.g., "filing_status", "range(1, 9)")
|
|
142
|
+
dim_label: The human-readable label for this dimension (e.g., "Household size")
|
|
143
|
+
system: The tax-benefit system
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
str: Formatted dimension value
|
|
147
|
+
"""
|
|
148
|
+
# First, try to get enum display value
|
|
149
|
+
if var_name and isinstance(var_name, str) and not var_name.startswith("range(") and not var_name.startswith("list("):
|
|
150
|
+
var = system.variables.get(var_name)
|
|
151
|
+
if var and hasattr(var, "possible_values") and var.possible_values:
|
|
152
|
+
try:
|
|
153
|
+
enum_value = var.possible_values[value].value
|
|
154
|
+
return str(enum_value)
|
|
155
|
+
except (KeyError, AttributeError):
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
# For range() dimensions or when no enum found, use breakdown_label if available
|
|
159
|
+
if dim_label:
|
|
160
|
+
return f"{dim_label} {value}"
|
|
161
|
+
|
|
162
|
+
return value
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _generate_bracket_label(param_name, scale_lookup):
|
|
166
|
+
"""Generate label for a bracket parameter."""
|
|
167
|
+
match = re.match(r"^(.+)\[(\d+)\]\.(\w+)$", param_name)
|
|
168
|
+
if not match:
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
scale_name = match.group(1)
|
|
172
|
+
bracket_index = int(match.group(2))
|
|
173
|
+
field_name = match.group(3)
|
|
174
|
+
|
|
175
|
+
scale = scale_lookup.get(scale_name)
|
|
176
|
+
if not scale:
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
scale_label = scale.metadata.get("label")
|
|
180
|
+
scale_type = scale.metadata.get("type", "")
|
|
181
|
+
|
|
182
|
+
if not scale_label:
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
bracket_num = bracket_index + 1
|
|
186
|
+
|
|
187
|
+
if scale_type in ("marginal_rate", "marginal_amount"):
|
|
188
|
+
bracket_desc = f"bracket {bracket_num}"
|
|
189
|
+
elif scale_type == "single_amount":
|
|
190
|
+
bracket_desc = f"tier {bracket_num}"
|
|
191
|
+
else:
|
|
192
|
+
bracket_desc = f"bracket {bracket_num}"
|
|
193
|
+
|
|
194
|
+
return f"{scale_label} ({bracket_desc} {field_name})"
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def build_scale_lookup(system):
|
|
198
|
+
"""
|
|
199
|
+
Build a lookup dict mapping scale names to ParameterScale objects.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
system: The tax-benefit system
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
dict: Mapping of scale name -> ParameterScale object
|
|
206
|
+
"""
|
|
207
|
+
from policyengine_core.parameters import ParameterScale
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
p.name: p
|
|
211
|
+
for p in system.parameters.get_descendants()
|
|
212
|
+
if isinstance(p, ParameterScale)
|
|
213
|
+
}
|
|
@@ -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
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
policyengine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
policyengine/__pycache__/__init__.cpython-313.pyc,sha256=
|
|
2
|
+
policyengine/__pycache__/__init__.cpython-313.pyc,sha256=pXlfNHK9qaW6hIe40yAYhaHxkBtl6c_uvAIzt-n6hzM,175
|
|
3
3
|
policyengine/core/__init__.py,sha256=KBVhkqzkvjWLDDwk96vquQKL63ZFuLen5AzBOBnO9pg,912
|
|
4
4
|
policyengine/core/cache.py,sha256=DcVVFaCt7k9PmqwlhXoNDMtJ8sF4neYP1uRqWik5QYg,1812
|
|
5
5
|
policyengine/core/dataset.py,sha256=iJr9-J6w11uMRYy3EEJO9Gveku1m71AA1yzeo-0SiCs,16094
|
|
@@ -13,28 +13,31 @@ policyengine/core/simulation.py,sha256=h6QbFt3uEvyfRXRVbSFBlrOd6Ze03OeZkwX9oElmO
|
|
|
13
13
|
policyengine/core/tax_benefit_model.py,sha256=2Yc1RlQrUG7djDMZbJOQH4Ns86_lOnLeISCGR4-9zMo,176
|
|
14
14
|
policyengine/core/tax_benefit_model_version.py,sha256=iVzEKWzQxoPVicwxcqo9Fy8PfVX07faBvyL9NhVIjuU,3212
|
|
15
15
|
policyengine/core/variable.py,sha256=AjSImORlRkh05xhYxyeT6GFMOfViRzYg0qRQAIj-mxo,350
|
|
16
|
-
policyengine/outputs/__init__.py,sha256=
|
|
16
|
+
policyengine/outputs/__init__.py,sha256=fcqkl1iK4lMpkdS0OBj3wWGAd1zZjc6IiJ-nrXy9VU8,1254
|
|
17
17
|
policyengine/outputs/aggregate.py,sha256=exI-U04OF5kVf2BBYV6sf8VldIWnT_IzxgkBs5wtnCw,4846
|
|
18
18
|
policyengine/outputs/change_aggregate.py,sha256=tK4K87YlByKikqFaB7OHyh1SqAuGtUnLL7cSF_EhrOs,7373
|
|
19
19
|
policyengine/outputs/decile_impact.py,sha256=f8nR3pea8_qDuQ-M6kaKnVKxbGnfL0IzpRfFTdi7TqA,5522
|
|
20
|
+
policyengine/outputs/inequality.py,sha256=W_yc9Ibeavx7KA3reJTFArK3fR1kf_YFV0jAaC121w0,9356
|
|
21
|
+
policyengine/outputs/poverty.py,sha256=h8dHj-S8YeEQ6CXqmWje3gEz30H8jgE-gmXQ0NoJTUU,7866
|
|
20
22
|
policyengine/tax_benefit_models/uk.py,sha256=HzAG_dORmsj1NJ9pd9WrqwgZPe9DUDrZ1wV5LuVCKAg,950
|
|
21
23
|
policyengine/tax_benefit_models/us.py,sha256=G51dAmHo8NJLb2mnbne6iO5eNaatCGUd_2unvawwF84,946
|
|
22
24
|
policyengine/tax_benefit_models/uk/__init__.py,sha256=StjVt4mV0n2QxlM_2oCp_OqHJu7eyWNbdPndezC7ve0,1294
|
|
23
|
-
policyengine/tax_benefit_models/uk/analysis.py,sha256=
|
|
25
|
+
policyengine/tax_benefit_models/uk/analysis.py,sha256=uPQt2EI2y6obibLfZfV-fHuN-FVzvUVUgeZY9kKSB5E,9527
|
|
24
26
|
policyengine/tax_benefit_models/uk/datasets.py,sha256=N8pMrlhQFec_cbgvVf5HE2owU14VF1i8-ZUwZYBSeio,9043
|
|
25
|
-
policyengine/tax_benefit_models/uk/model.py,sha256=
|
|
27
|
+
policyengine/tax_benefit_models/uk/model.py,sha256=8byY9n8rEWA2DxG3uq7N3SogZWvl_o9JqKcL0xfQ6fk,8984
|
|
26
28
|
policyengine/tax_benefit_models/uk/outputs.py,sha256=2mYLwQW4QNvrOHtHfm_ACqE9gbmuLxvcCyldRU46s0o,3543
|
|
27
29
|
policyengine/tax_benefit_models/us/__init__.py,sha256=0RtqCl01j-Z_T4i9LITBSePegO97gZ4IIYqt-nsv2O0,1290
|
|
28
|
-
policyengine/tax_benefit_models/us/analysis.py,sha256=
|
|
30
|
+
policyengine/tax_benefit_models/us/analysis.py,sha256=qJ9pZjyEY1xS7HCpT5-AETdPTtd-k7hWePxJz-NpXDE,10344
|
|
29
31
|
policyengine/tax_benefit_models/us/datasets.py,sha256=OWqiYK8TWwdYP2qgUNIv6nIpqN5FVtyd8aYkVMUkAno,14757
|
|
30
|
-
policyengine/tax_benefit_models/us/model.py,sha256=
|
|
32
|
+
policyengine/tax_benefit_models/us/model.py,sha256=t8YPeiEzOskzSnEIwCSZFPvdxFubtLTAUPkTEcG_JN8,15945
|
|
31
33
|
policyengine/tax_benefit_models/us/outputs.py,sha256=GT8Eur8DfB9cPQRbSljEl9RpKSNHW80Fq_CBXCybvIU,3519
|
|
32
|
-
policyengine/utils/__init__.py,sha256=
|
|
34
|
+
policyengine/utils/__init__.py,sha256=qq9ElvVnZtmM0CAjbkJV_QFBHz3bAjOSCTJGqx29F0c,311
|
|
33
35
|
policyengine/utils/dates.py,sha256=HnAqyl8S8EOYp8ibsnMTmECYoDWCSqwL-7A2_qKgxSc,1510
|
|
36
|
+
policyengine/utils/parameter_labels.py,sha256=_QTCTOjOdaW-pPVOsYMn7VyN-75QTD6IILfH-6oAd7U,6549
|
|
34
37
|
policyengine/utils/parametric_reforms.py,sha256=4P3U39-4pYTU4BN6JjgmVLUkCkBhRfZJ6UIWTlsjyQE,1155
|
|
35
38
|
policyengine/utils/plotting.py,sha256=ZAzTWz38vIaW0c3Nt4Un1kfrNoXLyHCDd1pEJIlsRg4,5335
|
|
36
|
-
policyengine-3.1.
|
|
37
|
-
policyengine-3.1.
|
|
38
|
-
policyengine-3.1.
|
|
39
|
-
policyengine-3.1.
|
|
40
|
-
policyengine-3.1.
|
|
39
|
+
policyengine-3.1.16.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
|
|
40
|
+
policyengine-3.1.16.dist-info/METADATA,sha256=rmdahoK--THkMMXjckeYhFpagrIk6w_oANMFgTE9rts,45932
|
|
41
|
+
policyengine-3.1.16.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
42
|
+
policyengine-3.1.16.dist-info/top_level.txt,sha256=_23UPobfkneHQkpJ0e0OmDJfhCUfoXj_F2sTckCGOH4,13
|
|
43
|
+
policyengine-3.1.16.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|