policyengine 3.0.0__py3-none-any.whl → 3.1.1__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/__init__.py +22 -0
- policyengine/core/dataset.py +260 -0
- policyengine/core/dataset_version.py +16 -0
- policyengine/core/dynamic.py +43 -0
- policyengine/core/output.py +26 -0
- policyengine/{models → core}/parameter.py +4 -2
- policyengine/{models → core}/parameter_value.py +1 -1
- policyengine/core/policy.py +43 -0
- policyengine/{models → core}/simulation.py +10 -14
- policyengine/core/tax_benefit_model.py +11 -0
- policyengine/core/tax_benefit_model_version.py +34 -0
- policyengine/core/variable.py +15 -0
- policyengine/outputs/__init__.py +21 -0
- policyengine/outputs/aggregate.py +124 -0
- policyengine/outputs/change_aggregate.py +184 -0
- policyengine/outputs/decile_impact.py +140 -0
- policyengine/tax_benefit_models/uk/__init__.py +26 -0
- policyengine/tax_benefit_models/uk/analysis.py +97 -0
- policyengine/tax_benefit_models/uk/datasets.py +176 -0
- policyengine/tax_benefit_models/uk/model.py +268 -0
- policyengine/tax_benefit_models/uk/outputs.py +108 -0
- policyengine/tax_benefit_models/uk.py +33 -0
- policyengine/tax_benefit_models/us/__init__.py +36 -0
- policyengine/tax_benefit_models/us/analysis.py +99 -0
- policyengine/tax_benefit_models/us/datasets.py +307 -0
- policyengine/tax_benefit_models/us/model.py +447 -0
- policyengine/tax_benefit_models/us/outputs.py +108 -0
- policyengine/tax_benefit_models/us.py +32 -0
- policyengine/utils/__init__.py +3 -0
- policyengine/utils/dates.py +40 -0
- policyengine/utils/parametric_reforms.py +39 -0
- policyengine/utils/plotting.py +179 -0
- {policyengine-3.0.0.dist-info → policyengine-3.1.1.dist-info}/METADATA +185 -20
- policyengine-3.1.1.dist-info/RECORD +39 -0
- policyengine/database/__init__.py +0 -56
- policyengine/database/aggregate.py +0 -33
- policyengine/database/baseline_parameter_value_table.py +0 -66
- policyengine/database/baseline_variable_table.py +0 -40
- policyengine/database/database.py +0 -251
- policyengine/database/dataset_table.py +0 -41
- policyengine/database/dynamic_table.py +0 -34
- policyengine/database/link.py +0 -82
- policyengine/database/model_table.py +0 -27
- policyengine/database/model_version_table.py +0 -28
- policyengine/database/parameter_table.py +0 -31
- policyengine/database/parameter_value_table.py +0 -62
- policyengine/database/policy_table.py +0 -34
- policyengine/database/report_element_table.py +0 -48
- policyengine/database/report_table.py +0 -24
- policyengine/database/simulation_table.py +0 -50
- policyengine/database/user_table.py +0 -28
- policyengine/database/versioned_dataset_table.py +0 -28
- policyengine/models/__init__.py +0 -30
- policyengine/models/aggregate.py +0 -92
- policyengine/models/baseline_parameter_value.py +0 -14
- policyengine/models/baseline_variable.py +0 -12
- policyengine/models/dataset.py +0 -18
- policyengine/models/dynamic.py +0 -15
- policyengine/models/model.py +0 -124
- policyengine/models/model_version.py +0 -14
- policyengine/models/policy.py +0 -17
- policyengine/models/policyengine_uk.py +0 -114
- policyengine/models/policyengine_us.py +0 -115
- policyengine/models/report.py +0 -10
- policyengine/models/report_element.py +0 -36
- policyengine/models/user.py +0 -14
- policyengine/models/versioned_dataset.py +0 -12
- policyengine/utils/charts.py +0 -286
- policyengine/utils/compress.py +0 -20
- policyengine/utils/datasets.py +0 -71
- policyengine-3.0.0.dist-info/RECORD +0 -47
- policyengine-3.0.0.dist-info/entry_points.txt +0 -2
- {policyengine-3.0.0.dist-info → policyengine-3.1.1.dist-info}/WHEEL +0 -0
- {policyengine-3.0.0.dist-info → policyengine-3.1.1.dist-info}/licenses/LICENSE +0 -0
- {policyengine-3.0.0.dist-info → policyengine-3.1.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from policyengine.core import Output, Simulation
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AggregateType(str, Enum):
|
|
8
|
+
SUM = "sum"
|
|
9
|
+
MEAN = "mean"
|
|
10
|
+
COUNT = "count"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Aggregate(Output):
|
|
14
|
+
simulation: Simulation
|
|
15
|
+
variable: str
|
|
16
|
+
aggregate_type: AggregateType
|
|
17
|
+
entity: str | None = None
|
|
18
|
+
|
|
19
|
+
filter_variable: str | None = None
|
|
20
|
+
filter_variable_eq: Any | None = None
|
|
21
|
+
filter_variable_leq: Any | None = None
|
|
22
|
+
filter_variable_geq: Any | None = None
|
|
23
|
+
filter_variable_describes_quantiles: bool = False
|
|
24
|
+
|
|
25
|
+
# Convenient quantile specification (alternative to describes_quantiles)
|
|
26
|
+
quantile: int | None = (
|
|
27
|
+
None # Number of quantiles (e.g., 10 for deciles, 5 for quintiles)
|
|
28
|
+
)
|
|
29
|
+
quantile_eq: int | None = None # Exact quantile (e.g., 3 for 3rd decile)
|
|
30
|
+
quantile_leq: int | None = (
|
|
31
|
+
None # Maximum quantile (e.g., 5 for bottom 5 deciles)
|
|
32
|
+
)
|
|
33
|
+
quantile_geq: int | None = (
|
|
34
|
+
None # Minimum quantile (e.g., 9 for top 2 deciles)
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
result: Any | None = None
|
|
38
|
+
|
|
39
|
+
def run(self):
|
|
40
|
+
# Convert quantile specification to describes_quantiles format
|
|
41
|
+
if self.quantile is not None:
|
|
42
|
+
self.filter_variable_describes_quantiles = True
|
|
43
|
+
if self.quantile_eq is not None:
|
|
44
|
+
# For a specific quantile, filter between (quantile-1)/n and quantile/n
|
|
45
|
+
self.filter_variable_geq = (
|
|
46
|
+
self.quantile_eq - 1
|
|
47
|
+
) / self.quantile
|
|
48
|
+
self.filter_variable_leq = self.quantile_eq / self.quantile
|
|
49
|
+
elif self.quantile_leq is not None:
|
|
50
|
+
self.filter_variable_leq = self.quantile_leq / self.quantile
|
|
51
|
+
elif self.quantile_geq is not None:
|
|
52
|
+
self.filter_variable_geq = (
|
|
53
|
+
self.quantile_geq - 1
|
|
54
|
+
) / self.quantile
|
|
55
|
+
|
|
56
|
+
# Get variable object
|
|
57
|
+
var_obj = next(
|
|
58
|
+
v
|
|
59
|
+
for v in self.simulation.tax_benefit_model_version.variables
|
|
60
|
+
if v.name == self.variable
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Get the target entity data
|
|
64
|
+
target_entity = self.entity or var_obj.entity
|
|
65
|
+
data = getattr(self.simulation.output_dataset.data, target_entity)
|
|
66
|
+
|
|
67
|
+
# Map variable to target entity if needed
|
|
68
|
+
if var_obj.entity != target_entity:
|
|
69
|
+
mapped = self.simulation.output_dataset.data.map_to_entity(
|
|
70
|
+
var_obj.entity, target_entity, columns=[self.variable]
|
|
71
|
+
)
|
|
72
|
+
series = mapped[self.variable]
|
|
73
|
+
else:
|
|
74
|
+
series = data[self.variable]
|
|
75
|
+
|
|
76
|
+
# Apply filters
|
|
77
|
+
if self.filter_variable is not None:
|
|
78
|
+
filter_var_obj = next(
|
|
79
|
+
v
|
|
80
|
+
for v in self.simulation.tax_benefit_model_version.variables
|
|
81
|
+
if v.name == self.filter_variable
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if filter_var_obj.entity != target_entity:
|
|
85
|
+
filter_mapped = (
|
|
86
|
+
self.simulation.output_dataset.data.map_to_entity(
|
|
87
|
+
filter_var_obj.entity,
|
|
88
|
+
target_entity,
|
|
89
|
+
columns=[self.filter_variable],
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
filter_series = filter_mapped[self.filter_variable]
|
|
93
|
+
else:
|
|
94
|
+
filter_series = data[self.filter_variable]
|
|
95
|
+
|
|
96
|
+
if self.filter_variable_describes_quantiles:
|
|
97
|
+
if self.filter_variable_eq is not None:
|
|
98
|
+
threshold = filter_series.quantile(self.filter_variable_eq)
|
|
99
|
+
series = series[filter_series <= threshold]
|
|
100
|
+
if self.filter_variable_leq is not None:
|
|
101
|
+
threshold = filter_series.quantile(
|
|
102
|
+
self.filter_variable_leq
|
|
103
|
+
)
|
|
104
|
+
series = series[filter_series <= threshold]
|
|
105
|
+
if self.filter_variable_geq is not None:
|
|
106
|
+
threshold = filter_series.quantile(
|
|
107
|
+
self.filter_variable_geq
|
|
108
|
+
)
|
|
109
|
+
series = series[filter_series >= threshold]
|
|
110
|
+
else:
|
|
111
|
+
if self.filter_variable_eq is not None:
|
|
112
|
+
series = series[filter_series == self.filter_variable_eq]
|
|
113
|
+
if self.filter_variable_leq is not None:
|
|
114
|
+
series = series[filter_series <= self.filter_variable_leq]
|
|
115
|
+
if self.filter_variable_geq is not None:
|
|
116
|
+
series = series[filter_series >= self.filter_variable_geq]
|
|
117
|
+
|
|
118
|
+
# Aggregate - MicroSeries will automatically apply weights
|
|
119
|
+
if self.aggregate_type == AggregateType.SUM:
|
|
120
|
+
self.result = series.sum()
|
|
121
|
+
elif self.aggregate_type == AggregateType.MEAN:
|
|
122
|
+
self.result = series.mean()
|
|
123
|
+
elif self.aggregate_type == AggregateType.COUNT:
|
|
124
|
+
self.result = series.count()
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from policyengine.core import Output, Simulation
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ChangeAggregateType(str, Enum):
|
|
8
|
+
COUNT = "count"
|
|
9
|
+
SUM = "sum"
|
|
10
|
+
MEAN = "mean"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ChangeAggregate(Output):
|
|
14
|
+
baseline_simulation: Simulation
|
|
15
|
+
reform_simulation: Simulation
|
|
16
|
+
variable: str
|
|
17
|
+
aggregate_type: ChangeAggregateType
|
|
18
|
+
entity: str | None = None
|
|
19
|
+
|
|
20
|
+
# Filter by absolute change
|
|
21
|
+
change_geq: float | None = None # Change >= value (e.g., gain >= 500)
|
|
22
|
+
change_leq: float | None = None # Change <= value (e.g., loss <= -500)
|
|
23
|
+
change_eq: float | None = None # Change == value
|
|
24
|
+
|
|
25
|
+
# Filter by relative change (as decimal, e.g., 0.05 = 5%)
|
|
26
|
+
relative_change_geq: float | None = None # Relative change >= value
|
|
27
|
+
relative_change_leq: float | None = None # Relative change <= value
|
|
28
|
+
relative_change_eq: float | None = None # Relative change == value
|
|
29
|
+
|
|
30
|
+
# Filter by another variable (e.g., only count people with age >= 30)
|
|
31
|
+
filter_variable: str | None = None
|
|
32
|
+
filter_variable_eq: Any | None = None
|
|
33
|
+
filter_variable_leq: Any | None = None
|
|
34
|
+
filter_variable_geq: Any | None = None
|
|
35
|
+
filter_variable_describes_quantiles: bool = False
|
|
36
|
+
|
|
37
|
+
# Convenient quantile specification (alternative to describes_quantiles)
|
|
38
|
+
quantile: int | None = (
|
|
39
|
+
None # Number of quantiles (e.g., 10 for deciles, 5 for quintiles)
|
|
40
|
+
)
|
|
41
|
+
quantile_eq: int | None = None # Exact quantile (e.g., 3 for 3rd decile)
|
|
42
|
+
quantile_leq: int | None = (
|
|
43
|
+
None # Maximum quantile (e.g., 5 for bottom 5 deciles)
|
|
44
|
+
)
|
|
45
|
+
quantile_geq: int | None = (
|
|
46
|
+
None # Minimum quantile (e.g., 9 for top 2 deciles)
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
result: Any | None = None
|
|
50
|
+
|
|
51
|
+
def run(self):
|
|
52
|
+
# Convert quantile specification to describes_quantiles format
|
|
53
|
+
if self.quantile is not None:
|
|
54
|
+
self.filter_variable_describes_quantiles = True
|
|
55
|
+
if self.quantile_eq is not None:
|
|
56
|
+
# For a specific quantile, filter between (quantile-1)/n and quantile/n
|
|
57
|
+
self.filter_variable_geq = (
|
|
58
|
+
self.quantile_eq - 1
|
|
59
|
+
) / self.quantile
|
|
60
|
+
self.filter_variable_leq = self.quantile_eq / self.quantile
|
|
61
|
+
elif self.quantile_leq is not None:
|
|
62
|
+
self.filter_variable_leq = self.quantile_leq / self.quantile
|
|
63
|
+
elif self.quantile_geq is not None:
|
|
64
|
+
self.filter_variable_geq = (
|
|
65
|
+
self.quantile_geq - 1
|
|
66
|
+
) / self.quantile
|
|
67
|
+
|
|
68
|
+
# Get variable object
|
|
69
|
+
var_obj = next(
|
|
70
|
+
v
|
|
71
|
+
for v in self.baseline_simulation.tax_benefit_model_version.variables
|
|
72
|
+
if v.name == self.variable
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Get the target entity data
|
|
76
|
+
target_entity = self.entity or var_obj.entity
|
|
77
|
+
baseline_data = getattr(
|
|
78
|
+
self.baseline_simulation.output_dataset.data, target_entity
|
|
79
|
+
)
|
|
80
|
+
reform_data = getattr(
|
|
81
|
+
self.reform_simulation.output_dataset.data, target_entity
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Map variable to target entity if needed
|
|
85
|
+
if var_obj.entity != target_entity:
|
|
86
|
+
baseline_mapped = (
|
|
87
|
+
self.baseline_simulation.output_dataset.data.map_to_entity(
|
|
88
|
+
var_obj.entity, target_entity
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
baseline_series = baseline_mapped[self.variable]
|
|
92
|
+
|
|
93
|
+
reform_mapped = (
|
|
94
|
+
self.reform_simulation.output_dataset.data.map_to_entity(
|
|
95
|
+
var_obj.entity, target_entity
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
reform_series = reform_mapped[self.variable]
|
|
99
|
+
else:
|
|
100
|
+
baseline_series = baseline_data[self.variable]
|
|
101
|
+
reform_series = reform_data[self.variable]
|
|
102
|
+
|
|
103
|
+
# Calculate change (reform - baseline)
|
|
104
|
+
change_series = reform_series - baseline_series
|
|
105
|
+
|
|
106
|
+
# Calculate relative change (handling division by zero)
|
|
107
|
+
# Where baseline is 0, relative change is undefined; we'll mask these out if relative filters are used
|
|
108
|
+
import numpy as np
|
|
109
|
+
|
|
110
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
111
|
+
relative_change_series = change_series / baseline_series
|
|
112
|
+
relative_change_series = relative_change_series.replace(
|
|
113
|
+
[np.inf, -np.inf], np.nan
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Start with all rows
|
|
117
|
+
mask = baseline_series.notna()
|
|
118
|
+
|
|
119
|
+
# Apply absolute change filters
|
|
120
|
+
if self.change_eq is not None:
|
|
121
|
+
mask &= change_series == self.change_eq
|
|
122
|
+
if self.change_leq is not None:
|
|
123
|
+
mask &= change_series <= self.change_leq
|
|
124
|
+
if self.change_geq is not None:
|
|
125
|
+
mask &= change_series >= self.change_geq
|
|
126
|
+
|
|
127
|
+
# Apply relative change filters
|
|
128
|
+
if self.relative_change_eq is not None:
|
|
129
|
+
mask &= relative_change_series == self.relative_change_eq
|
|
130
|
+
if self.relative_change_leq is not None:
|
|
131
|
+
mask &= relative_change_series <= self.relative_change_leq
|
|
132
|
+
if self.relative_change_geq is not None:
|
|
133
|
+
mask &= relative_change_series >= self.relative_change_geq
|
|
134
|
+
|
|
135
|
+
# Apply filter_variable filters
|
|
136
|
+
if self.filter_variable is not None:
|
|
137
|
+
filter_var_obj = next(
|
|
138
|
+
v
|
|
139
|
+
for v in self.baseline_simulation.tax_benefit_model_version.variables
|
|
140
|
+
if v.name == self.filter_variable
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if filter_var_obj.entity != target_entity:
|
|
144
|
+
filter_mapped = (
|
|
145
|
+
self.baseline_simulation.output_dataset.data.map_to_entity(
|
|
146
|
+
filter_var_obj.entity, target_entity
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
filter_series = filter_mapped[self.filter_variable]
|
|
150
|
+
else:
|
|
151
|
+
filter_series = baseline_data[self.filter_variable]
|
|
152
|
+
|
|
153
|
+
if self.filter_variable_describes_quantiles:
|
|
154
|
+
if self.filter_variable_eq is not None:
|
|
155
|
+
threshold = filter_series.quantile(self.filter_variable_eq)
|
|
156
|
+
mask &= filter_series <= threshold
|
|
157
|
+
if self.filter_variable_leq is not None:
|
|
158
|
+
threshold = filter_series.quantile(
|
|
159
|
+
self.filter_variable_leq
|
|
160
|
+
)
|
|
161
|
+
mask &= filter_series <= threshold
|
|
162
|
+
if self.filter_variable_geq is not None:
|
|
163
|
+
threshold = filter_series.quantile(
|
|
164
|
+
self.filter_variable_geq
|
|
165
|
+
)
|
|
166
|
+
mask &= filter_series >= threshold
|
|
167
|
+
else:
|
|
168
|
+
if self.filter_variable_eq is not None:
|
|
169
|
+
mask &= filter_series == self.filter_variable_eq
|
|
170
|
+
if self.filter_variable_leq is not None:
|
|
171
|
+
mask &= filter_series <= self.filter_variable_leq
|
|
172
|
+
if self.filter_variable_geq is not None:
|
|
173
|
+
mask &= filter_series >= self.filter_variable_geq
|
|
174
|
+
|
|
175
|
+
# Apply mask to get filtered data
|
|
176
|
+
filtered_change = change_series[mask]
|
|
177
|
+
|
|
178
|
+
# Aggregate
|
|
179
|
+
if self.aggregate_type == ChangeAggregateType.COUNT:
|
|
180
|
+
self.result = filtered_change.count()
|
|
181
|
+
elif self.aggregate_type == ChangeAggregateType.SUM:
|
|
182
|
+
self.result = filtered_change.sum()
|
|
183
|
+
elif self.aggregate_type == ChangeAggregateType.MEAN:
|
|
184
|
+
self.result = filtered_change.mean()
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
from pydantic import ConfigDict
|
|
3
|
+
|
|
4
|
+
from policyengine.core import Output, OutputCollection, Simulation
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DecileImpact(Output):
|
|
8
|
+
"""Single decile's impact from a policy reform - represents one database row."""
|
|
9
|
+
|
|
10
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
11
|
+
|
|
12
|
+
baseline_simulation: Simulation
|
|
13
|
+
reform_simulation: Simulation
|
|
14
|
+
income_variable: str = "equiv_hbai_household_net_income"
|
|
15
|
+
entity: str | None = None
|
|
16
|
+
decile: int
|
|
17
|
+
quantiles: int = 10
|
|
18
|
+
|
|
19
|
+
# Results populated by run()
|
|
20
|
+
baseline_mean: float | None = None
|
|
21
|
+
reform_mean: float | None = None
|
|
22
|
+
absolute_change: float | None = None
|
|
23
|
+
relative_change: float | None = None
|
|
24
|
+
count_better_off: float | None = None
|
|
25
|
+
count_worse_off: float | None = None
|
|
26
|
+
count_no_change: float | None = None
|
|
27
|
+
|
|
28
|
+
def run(self):
|
|
29
|
+
"""Calculate impact for this specific decile."""
|
|
30
|
+
# Get variable object to determine entity
|
|
31
|
+
var_obj = next(
|
|
32
|
+
v
|
|
33
|
+
for v in self.baseline_simulation.tax_benefit_model_version.variables
|
|
34
|
+
if v.name == self.income_variable
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Get target entity
|
|
38
|
+
target_entity = self.entity or var_obj.entity
|
|
39
|
+
|
|
40
|
+
# Get data from both simulations
|
|
41
|
+
baseline_data = getattr(
|
|
42
|
+
self.baseline_simulation.output_dataset.data, target_entity
|
|
43
|
+
)
|
|
44
|
+
reform_data = getattr(
|
|
45
|
+
self.reform_simulation.output_dataset.data, target_entity
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Map income variable to target entity if needed
|
|
49
|
+
if var_obj.entity != target_entity:
|
|
50
|
+
baseline_mapped = (
|
|
51
|
+
self.baseline_simulation.output_dataset.data.map_to_entity(
|
|
52
|
+
var_obj.entity, target_entity
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
baseline_income = baseline_mapped[self.income_variable]
|
|
56
|
+
|
|
57
|
+
reform_mapped = (
|
|
58
|
+
self.reform_simulation.output_dataset.data.map_to_entity(
|
|
59
|
+
var_obj.entity, target_entity
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
reform_income = reform_mapped[self.income_variable]
|
|
63
|
+
else:
|
|
64
|
+
baseline_income = baseline_data[self.income_variable]
|
|
65
|
+
reform_income = reform_data[self.income_variable]
|
|
66
|
+
|
|
67
|
+
# Calculate deciles based on baseline income
|
|
68
|
+
decile_series = (
|
|
69
|
+
pd.qcut(
|
|
70
|
+
baseline_income,
|
|
71
|
+
self.quantiles,
|
|
72
|
+
labels=False,
|
|
73
|
+
duplicates="drop",
|
|
74
|
+
)
|
|
75
|
+
+ 1
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Calculate changes
|
|
79
|
+
absolute_change = reform_income - baseline_income
|
|
80
|
+
relative_change = (absolute_change / baseline_income) * 100
|
|
81
|
+
|
|
82
|
+
# Filter to this decile
|
|
83
|
+
mask = decile_series == self.decile
|
|
84
|
+
|
|
85
|
+
# Populate results
|
|
86
|
+
self.baseline_mean = float(baseline_income[mask].mean())
|
|
87
|
+
self.reform_mean = float(reform_income[mask].mean())
|
|
88
|
+
self.absolute_change = float(absolute_change[mask].mean())
|
|
89
|
+
self.relative_change = float(relative_change[mask].mean())
|
|
90
|
+
self.count_better_off = float((absolute_change[mask] > 0).sum())
|
|
91
|
+
self.count_worse_off = float((absolute_change[mask] < 0).sum())
|
|
92
|
+
self.count_no_change = float((absolute_change[mask] == 0).sum())
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def calculate_decile_impacts(
|
|
96
|
+
baseline_simulation: Simulation,
|
|
97
|
+
reform_simulation: Simulation,
|
|
98
|
+
income_variable: str = "equiv_hbai_household_net_income",
|
|
99
|
+
entity: str | None = None,
|
|
100
|
+
quantiles: int = 10,
|
|
101
|
+
) -> OutputCollection[DecileImpact]:
|
|
102
|
+
"""Calculate decile-by-decile impact of a reform.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
OutputCollection containing list of DecileImpact objects and DataFrame
|
|
106
|
+
"""
|
|
107
|
+
results = []
|
|
108
|
+
for decile in range(1, quantiles + 1):
|
|
109
|
+
impact = DecileImpact(
|
|
110
|
+
baseline_simulation=baseline_simulation,
|
|
111
|
+
reform_simulation=reform_simulation,
|
|
112
|
+
income_variable=income_variable,
|
|
113
|
+
entity=entity,
|
|
114
|
+
decile=decile,
|
|
115
|
+
quantiles=quantiles,
|
|
116
|
+
)
|
|
117
|
+
impact.run()
|
|
118
|
+
results.append(impact)
|
|
119
|
+
|
|
120
|
+
# Create DataFrame
|
|
121
|
+
df = pd.DataFrame(
|
|
122
|
+
[
|
|
123
|
+
{
|
|
124
|
+
"baseline_simulation_id": r.baseline_simulation.id,
|
|
125
|
+
"reform_simulation_id": r.reform_simulation.id,
|
|
126
|
+
"income_variable": r.income_variable,
|
|
127
|
+
"decile": r.decile,
|
|
128
|
+
"baseline_mean": r.baseline_mean,
|
|
129
|
+
"reform_mean": r.reform_mean,
|
|
130
|
+
"absolute_change": r.absolute_change,
|
|
131
|
+
"relative_change": r.relative_change,
|
|
132
|
+
"count_better_off": r.count_better_off,
|
|
133
|
+
"count_worse_off": r.count_worse_off,
|
|
134
|
+
"count_no_change": r.count_no_change,
|
|
135
|
+
}
|
|
136
|
+
for r in results
|
|
137
|
+
]
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return OutputCollection(outputs=results, dataframe=df)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""PolicyEngine UK tax-benefit model."""
|
|
2
|
+
|
|
3
|
+
from .analysis import general_policy_reform_analysis
|
|
4
|
+
from .datasets import PolicyEngineUKDataset, UKYearData, create_datasets
|
|
5
|
+
from .model import PolicyEngineUK, PolicyEngineUKLatest, uk_latest, uk_model
|
|
6
|
+
from .outputs import ProgrammeStatistics
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"UKYearData",
|
|
10
|
+
"PolicyEngineUKDataset",
|
|
11
|
+
"create_datasets",
|
|
12
|
+
"PolicyEngineUK",
|
|
13
|
+
"PolicyEngineUKLatest",
|
|
14
|
+
"uk_model",
|
|
15
|
+
"uk_latest",
|
|
16
|
+
"general_policy_reform_analysis",
|
|
17
|
+
"ProgrammeStatistics",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
# Rebuild models to resolve forward references
|
|
21
|
+
from policyengine.core import Dataset
|
|
22
|
+
|
|
23
|
+
Dataset.model_rebuild()
|
|
24
|
+
UKYearData.model_rebuild()
|
|
25
|
+
PolicyEngineUKDataset.model_rebuild()
|
|
26
|
+
PolicyEngineUKLatest.model_rebuild()
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""General utility functions for UK policy reform analysis."""
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from policyengine.core import OutputCollection, Simulation
|
|
7
|
+
from policyengine.outputs.decile_impact import (
|
|
8
|
+
DecileImpact,
|
|
9
|
+
calculate_decile_impacts,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from .outputs import ProgrammeStatistics
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PolicyReformAnalysis(BaseModel):
|
|
16
|
+
"""Complete policy reform analysis result."""
|
|
17
|
+
|
|
18
|
+
decile_impacts: OutputCollection[DecileImpact]
|
|
19
|
+
programme_statistics: OutputCollection[ProgrammeStatistics]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def general_policy_reform_analysis(
|
|
23
|
+
baseline_simulation: Simulation,
|
|
24
|
+
reform_simulation: Simulation,
|
|
25
|
+
) -> PolicyReformAnalysis:
|
|
26
|
+
"""Perform comprehensive analysis of a policy reform.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
PolicyReformAnalysis containing decile impacts and programme statistics
|
|
30
|
+
"""
|
|
31
|
+
# Decile impact
|
|
32
|
+
decile_impacts = calculate_decile_impacts(
|
|
33
|
+
baseline_simulation=baseline_simulation,
|
|
34
|
+
reform_simulation=reform_simulation,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Major programmes to analyse
|
|
38
|
+
programmes = {
|
|
39
|
+
# Tax
|
|
40
|
+
"income_tax": {"entity": "person", "is_tax": True},
|
|
41
|
+
"national_insurance": {"entity": "person", "is_tax": True},
|
|
42
|
+
"vat": {"entity": "household", "is_tax": True},
|
|
43
|
+
"council_tax": {"entity": "household", "is_tax": True},
|
|
44
|
+
# Benefits
|
|
45
|
+
"universal_credit": {"entity": "person", "is_tax": False},
|
|
46
|
+
"child_benefit": {"entity": "person", "is_tax": False},
|
|
47
|
+
"pension_credit": {"entity": "person", "is_tax": False},
|
|
48
|
+
"income_support": {"entity": "person", "is_tax": False},
|
|
49
|
+
"working_tax_credit": {"entity": "person", "is_tax": False},
|
|
50
|
+
"child_tax_credit": {"entity": "person", "is_tax": False},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
programme_statistics = []
|
|
54
|
+
|
|
55
|
+
for programme_name, programme_info in programmes.items():
|
|
56
|
+
entity = programme_info["entity"]
|
|
57
|
+
is_tax = programme_info["is_tax"]
|
|
58
|
+
|
|
59
|
+
stats = ProgrammeStatistics(
|
|
60
|
+
baseline_simulation=baseline_simulation,
|
|
61
|
+
reform_simulation=reform_simulation,
|
|
62
|
+
programme_name=programme_name,
|
|
63
|
+
entity=entity,
|
|
64
|
+
is_tax=is_tax,
|
|
65
|
+
)
|
|
66
|
+
stats.run()
|
|
67
|
+
programme_statistics.append(stats)
|
|
68
|
+
|
|
69
|
+
# Create DataFrame
|
|
70
|
+
programme_df = pd.DataFrame(
|
|
71
|
+
[
|
|
72
|
+
{
|
|
73
|
+
"baseline_simulation_id": p.baseline_simulation.id,
|
|
74
|
+
"reform_simulation_id": p.reform_simulation.id,
|
|
75
|
+
"programme_name": p.programme_name,
|
|
76
|
+
"entity": p.entity,
|
|
77
|
+
"is_tax": p.is_tax,
|
|
78
|
+
"baseline_total": p.baseline_total,
|
|
79
|
+
"reform_total": p.reform_total,
|
|
80
|
+
"change": p.change,
|
|
81
|
+
"baseline_count": p.baseline_count,
|
|
82
|
+
"reform_count": p.reform_count,
|
|
83
|
+
"winners": p.winners,
|
|
84
|
+
"losers": p.losers,
|
|
85
|
+
}
|
|
86
|
+
for p in programme_statistics
|
|
87
|
+
]
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
programme_collection = OutputCollection(
|
|
91
|
+
outputs=programme_statistics, dataframe=programme_df
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return PolicyReformAnalysis(
|
|
95
|
+
decile_impacts=decile_impacts,
|
|
96
|
+
programme_statistics=programme_collection,
|
|
97
|
+
)
|