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,176 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from microdf import MicroDataFrame
|
|
5
|
+
from pydantic import BaseModel, ConfigDict
|
|
6
|
+
|
|
7
|
+
from policyengine.core import Dataset, map_to_entity
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class UKYearData(BaseModel):
|
|
11
|
+
"""Entity-level data for a single year."""
|
|
12
|
+
|
|
13
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
14
|
+
|
|
15
|
+
person: MicroDataFrame
|
|
16
|
+
benunit: MicroDataFrame
|
|
17
|
+
household: MicroDataFrame
|
|
18
|
+
|
|
19
|
+
def map_to_entity(
|
|
20
|
+
self, source_entity: str, target_entity: str, columns: list[str] = None
|
|
21
|
+
) -> MicroDataFrame:
|
|
22
|
+
"""Map data from source entity to target entity using join keys.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
source_entity (str): The source entity name ('person', 'benunit', 'household').
|
|
26
|
+
target_entity (str): The target entity name ('person', 'benunit', 'household').
|
|
27
|
+
columns (list[str], optional): List of column names to map. If None, maps all columns.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
MicroDataFrame: The mapped data at the target entity level.
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
ValueError: If source or target entity is invalid.
|
|
34
|
+
"""
|
|
35
|
+
entity_data = {
|
|
36
|
+
"person": self.person,
|
|
37
|
+
"benunit": self.benunit,
|
|
38
|
+
"household": self.household,
|
|
39
|
+
}
|
|
40
|
+
return map_to_entity(
|
|
41
|
+
entity_data=entity_data,
|
|
42
|
+
source_entity=source_entity,
|
|
43
|
+
target_entity=target_entity,
|
|
44
|
+
person_entity="person",
|
|
45
|
+
columns=columns,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class PolicyEngineUKDataset(Dataset):
|
|
50
|
+
"""UK dataset with multi-year entity-level data."""
|
|
51
|
+
|
|
52
|
+
data: UKYearData | None = None
|
|
53
|
+
|
|
54
|
+
def model_post_init(self, __context):
|
|
55
|
+
"""Called after Pydantic initialization."""
|
|
56
|
+
# Make sure we are synchronised between in-memory and storage, at least on initialisation
|
|
57
|
+
if self.data is not None:
|
|
58
|
+
self.save()
|
|
59
|
+
elif self.filepath and not self.data:
|
|
60
|
+
try:
|
|
61
|
+
self.load()
|
|
62
|
+
except FileNotFoundError:
|
|
63
|
+
# File doesn't exist yet, that's OK
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
def save(self) -> None:
|
|
67
|
+
"""Save dataset to HDF5 file."""
|
|
68
|
+
filepath = Path(self.filepath)
|
|
69
|
+
if not filepath.parent.exists():
|
|
70
|
+
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
with pd.HDFStore(filepath, mode="w") as store:
|
|
72
|
+
store["person"] = pd.DataFrame(self.data.person)
|
|
73
|
+
store["benunit"] = pd.DataFrame(self.data.benunit)
|
|
74
|
+
store["household"] = pd.DataFrame(self.data.household)
|
|
75
|
+
|
|
76
|
+
def load(self) -> None:
|
|
77
|
+
"""Load dataset from HDF5 file into this instance."""
|
|
78
|
+
filepath = self.filepath
|
|
79
|
+
with pd.HDFStore(filepath, mode="r") as store:
|
|
80
|
+
self.data = UKYearData(
|
|
81
|
+
person=MicroDataFrame(
|
|
82
|
+
store["person"], weights="person_weight"
|
|
83
|
+
),
|
|
84
|
+
benunit=MicroDataFrame(
|
|
85
|
+
store["benunit"], weights="benunit_weight"
|
|
86
|
+
),
|
|
87
|
+
household=MicroDataFrame(
|
|
88
|
+
store["household"], weights="household_weight"
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def __repr__(self) -> str:
|
|
93
|
+
if self.data is None:
|
|
94
|
+
return f"<PolicyEngineUKDataset id={self.id} year={self.year} filepath={self.filepath} (not loaded)>"
|
|
95
|
+
else:
|
|
96
|
+
n_people = len(self.data.person)
|
|
97
|
+
n_benunits = len(self.data.benunit)
|
|
98
|
+
n_households = len(self.data.household)
|
|
99
|
+
return f"<PolicyEngineUKDataset id={self.id} year={self.year} filepath={self.filepath} people={n_people} benunits={n_benunits} households={n_households}>"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def create_datasets(
|
|
103
|
+
datasets: list[str] = [
|
|
104
|
+
"hf://policyengine/policyengine-uk-data/frs_2023_24.h5",
|
|
105
|
+
"hf://policyengine/policyengine-uk-data/enhanced_frs_2023_24.h5",
|
|
106
|
+
],
|
|
107
|
+
years: list[int] = [2026, 2027, 2028, 2029, 2030],
|
|
108
|
+
) -> None:
|
|
109
|
+
for dataset in datasets:
|
|
110
|
+
from policyengine_uk import Microsimulation
|
|
111
|
+
|
|
112
|
+
sim = Microsimulation(dataset=dataset)
|
|
113
|
+
for year in years:
|
|
114
|
+
year_dataset = sim.dataset[year]
|
|
115
|
+
|
|
116
|
+
# Convert to pandas DataFrames and add weight columns
|
|
117
|
+
person_df = pd.DataFrame(year_dataset.person)
|
|
118
|
+
benunit_df = pd.DataFrame(year_dataset.benunit)
|
|
119
|
+
household_df = pd.DataFrame(year_dataset.household)
|
|
120
|
+
|
|
121
|
+
# Map household weights to person and benunit levels
|
|
122
|
+
person_df = person_df.merge(
|
|
123
|
+
household_df[["household_id", "household_weight"]],
|
|
124
|
+
left_on="person_household_id",
|
|
125
|
+
right_on="household_id",
|
|
126
|
+
how="left",
|
|
127
|
+
)
|
|
128
|
+
person_df = person_df.rename(
|
|
129
|
+
columns={"household_weight": "person_weight"}
|
|
130
|
+
)
|
|
131
|
+
person_df = person_df.drop(columns=["household_id"])
|
|
132
|
+
|
|
133
|
+
# Get household_id for each benunit from person table
|
|
134
|
+
benunit_household_map = person_df[
|
|
135
|
+
["person_benunit_id", "person_household_id"]
|
|
136
|
+
].drop_duplicates()
|
|
137
|
+
benunit_df = benunit_df.merge(
|
|
138
|
+
benunit_household_map,
|
|
139
|
+
left_on="benunit_id",
|
|
140
|
+
right_on="person_benunit_id",
|
|
141
|
+
how="left",
|
|
142
|
+
)
|
|
143
|
+
benunit_df = benunit_df.merge(
|
|
144
|
+
household_df[["household_id", "household_weight"]],
|
|
145
|
+
left_on="person_household_id",
|
|
146
|
+
right_on="household_id",
|
|
147
|
+
how="left",
|
|
148
|
+
)
|
|
149
|
+
benunit_df = benunit_df.rename(
|
|
150
|
+
columns={"household_weight": "benunit_weight"}
|
|
151
|
+
)
|
|
152
|
+
benunit_df = benunit_df.drop(
|
|
153
|
+
columns=[
|
|
154
|
+
"person_benunit_id",
|
|
155
|
+
"person_household_id",
|
|
156
|
+
"household_id",
|
|
157
|
+
],
|
|
158
|
+
errors="ignore",
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
uk_dataset = PolicyEngineUKDataset(
|
|
162
|
+
name=f"{dataset}-year-{year}",
|
|
163
|
+
description=f"UK Dataset for year {year} based on {dataset}",
|
|
164
|
+
filepath=f"./data/{Path(dataset).stem}_year_{year}.h5",
|
|
165
|
+
year=year,
|
|
166
|
+
data=UKYearData(
|
|
167
|
+
person=MicroDataFrame(person_df, weights="person_weight"),
|
|
168
|
+
benunit=MicroDataFrame(
|
|
169
|
+
benunit_df, weights="benunit_weight"
|
|
170
|
+
),
|
|
171
|
+
household=MicroDataFrame(
|
|
172
|
+
household_df, weights="household_weight"
|
|
173
|
+
),
|
|
174
|
+
),
|
|
175
|
+
)
|
|
176
|
+
uk_dataset.save()
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from importlib.metadata import version
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
import requests
|
|
8
|
+
from microdf import MicroDataFrame
|
|
9
|
+
|
|
10
|
+
from policyengine.core import (
|
|
11
|
+
Parameter,
|
|
12
|
+
ParameterValue,
|
|
13
|
+
TaxBenefitModel,
|
|
14
|
+
TaxBenefitModelVersion,
|
|
15
|
+
Variable,
|
|
16
|
+
)
|
|
17
|
+
from policyengine.utils import parse_safe_date
|
|
18
|
+
|
|
19
|
+
from .datasets import PolicyEngineUKDataset, UKYearData
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from policyengine.core.simulation import Simulation
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PolicyEngineUK(TaxBenefitModel):
|
|
26
|
+
id: str = "policyengine-uk"
|
|
27
|
+
description: str = "The UK's open-source dynamic tax and benefit microsimulation model maintained by PolicyEngine."
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
uk_model = PolicyEngineUK()
|
|
31
|
+
|
|
32
|
+
pkg_version = version("policyengine-uk")
|
|
33
|
+
|
|
34
|
+
# Get published time from PyPI
|
|
35
|
+
response = requests.get("https://pypi.org/pypi/policyengine-uk/json")
|
|
36
|
+
data = response.json()
|
|
37
|
+
upload_time = data["releases"][pkg_version][0]["upload_time_iso_8601"]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class PolicyEngineUKLatest(TaxBenefitModelVersion):
|
|
41
|
+
model: TaxBenefitModel = uk_model
|
|
42
|
+
version: str = pkg_version
|
|
43
|
+
created_at: datetime.datetime = datetime.datetime.fromisoformat(
|
|
44
|
+
upload_time
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def __init__(self, **kwargs: dict):
|
|
48
|
+
super().__init__(**kwargs)
|
|
49
|
+
from policyengine_core.enums import Enum
|
|
50
|
+
from policyengine_uk.system import system
|
|
51
|
+
|
|
52
|
+
self.id = f"{self.model.id}@{self.version}"
|
|
53
|
+
|
|
54
|
+
self.variables = []
|
|
55
|
+
for var_obj in system.variables.values():
|
|
56
|
+
variable = Variable(
|
|
57
|
+
id=self.id + "-" + var_obj.name,
|
|
58
|
+
name=var_obj.name,
|
|
59
|
+
tax_benefit_model_version=self,
|
|
60
|
+
entity=var_obj.entity.key,
|
|
61
|
+
description=var_obj.documentation,
|
|
62
|
+
data_type=var_obj.value_type
|
|
63
|
+
if var_obj.value_type is not Enum
|
|
64
|
+
else str,
|
|
65
|
+
)
|
|
66
|
+
if (
|
|
67
|
+
hasattr(var_obj, "possible_values")
|
|
68
|
+
and var_obj.possible_values is not None
|
|
69
|
+
):
|
|
70
|
+
variable.possible_values = list(
|
|
71
|
+
map(
|
|
72
|
+
lambda x: x.name,
|
|
73
|
+
var_obj.possible_values._value2member_map_.values(),
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
self.variables.append(variable)
|
|
77
|
+
|
|
78
|
+
self.parameters = []
|
|
79
|
+
from policyengine_core.parameters import Parameter as CoreParameter
|
|
80
|
+
|
|
81
|
+
for param_node in system.parameters.get_descendants():
|
|
82
|
+
if isinstance(param_node, CoreParameter):
|
|
83
|
+
parameter = Parameter(
|
|
84
|
+
id=self.id + "-" + param_node.name,
|
|
85
|
+
name=param_node.name,
|
|
86
|
+
tax_benefit_model_version=self,
|
|
87
|
+
description=param_node.description,
|
|
88
|
+
data_type=type(
|
|
89
|
+
param_node(2025)
|
|
90
|
+
), # Example year to infer type
|
|
91
|
+
unit=param_node.metadata.get("unit"),
|
|
92
|
+
)
|
|
93
|
+
self.parameters.append(parameter)
|
|
94
|
+
|
|
95
|
+
for i in range(len(param_node.values_list)):
|
|
96
|
+
param_at_instant = param_node.values_list[i]
|
|
97
|
+
if i + 1 < len(param_node.values_list):
|
|
98
|
+
next_instant = param_node.values_list[i + 1]
|
|
99
|
+
else:
|
|
100
|
+
next_instant = None
|
|
101
|
+
parameter_value = ParameterValue(
|
|
102
|
+
parameter=parameter,
|
|
103
|
+
start_date=parse_safe_date(
|
|
104
|
+
param_at_instant.instant_str
|
|
105
|
+
),
|
|
106
|
+
end_date=parse_safe_date(next_instant.instant_str)
|
|
107
|
+
if next_instant
|
|
108
|
+
else None,
|
|
109
|
+
value=param_at_instant.value,
|
|
110
|
+
)
|
|
111
|
+
self.parameter_values.append(parameter_value)
|
|
112
|
+
|
|
113
|
+
def run(self, simulation: "Simulation") -> "Simulation":
|
|
114
|
+
from policyengine_uk import Microsimulation
|
|
115
|
+
from policyengine_uk.data import UKSingleYearDataset
|
|
116
|
+
|
|
117
|
+
from policyengine.utils.parametric_reforms import (
|
|
118
|
+
simulation_modifier_from_parameter_values,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
assert isinstance(simulation.dataset, PolicyEngineUKDataset)
|
|
122
|
+
|
|
123
|
+
dataset = simulation.dataset
|
|
124
|
+
dataset.load()
|
|
125
|
+
input_data = UKSingleYearDataset(
|
|
126
|
+
person=dataset.data.person,
|
|
127
|
+
benunit=dataset.data.benunit,
|
|
128
|
+
household=dataset.data.household,
|
|
129
|
+
fiscal_year=dataset.year,
|
|
130
|
+
)
|
|
131
|
+
microsim = Microsimulation(dataset=input_data)
|
|
132
|
+
|
|
133
|
+
if (
|
|
134
|
+
simulation.policy
|
|
135
|
+
and simulation.policy.simulation_modifier is not None
|
|
136
|
+
):
|
|
137
|
+
simulation.policy.simulation_modifier(microsim)
|
|
138
|
+
elif simulation.policy:
|
|
139
|
+
modifier = simulation_modifier_from_parameter_values(
|
|
140
|
+
simulation.policy.parameter_values
|
|
141
|
+
)
|
|
142
|
+
modifier(microsim)
|
|
143
|
+
|
|
144
|
+
if (
|
|
145
|
+
simulation.dynamic
|
|
146
|
+
and simulation.dynamic.simulation_modifier is not None
|
|
147
|
+
):
|
|
148
|
+
simulation.dynamic.simulation_modifier(microsim)
|
|
149
|
+
elif simulation.dynamic:
|
|
150
|
+
modifier = simulation_modifier_from_parameter_values(
|
|
151
|
+
simulation.dynamic.parameter_values
|
|
152
|
+
)
|
|
153
|
+
modifier(microsim)
|
|
154
|
+
|
|
155
|
+
# Allow custom variable selection, or use defaults
|
|
156
|
+
if simulation.variables is not None:
|
|
157
|
+
entity_variables = simulation.variables
|
|
158
|
+
else:
|
|
159
|
+
# Default comprehensive variable set
|
|
160
|
+
entity_variables = {
|
|
161
|
+
"person": [
|
|
162
|
+
# IDs and weights
|
|
163
|
+
"person_id",
|
|
164
|
+
"benunit_id",
|
|
165
|
+
"household_id",
|
|
166
|
+
"person_weight",
|
|
167
|
+
# Demographics
|
|
168
|
+
"age",
|
|
169
|
+
"gender",
|
|
170
|
+
"is_adult",
|
|
171
|
+
"is_SP_age",
|
|
172
|
+
"is_child",
|
|
173
|
+
# Income
|
|
174
|
+
"employment_income",
|
|
175
|
+
"self_employment_income",
|
|
176
|
+
"pension_income",
|
|
177
|
+
"private_pension_income",
|
|
178
|
+
"savings_interest_income",
|
|
179
|
+
"dividend_income",
|
|
180
|
+
"property_income",
|
|
181
|
+
"total_income",
|
|
182
|
+
"earned_income",
|
|
183
|
+
# Benefits
|
|
184
|
+
"universal_credit",
|
|
185
|
+
"child_benefit",
|
|
186
|
+
"pension_credit",
|
|
187
|
+
"income_support",
|
|
188
|
+
"working_tax_credit",
|
|
189
|
+
"child_tax_credit",
|
|
190
|
+
# Tax
|
|
191
|
+
"income_tax",
|
|
192
|
+
"national_insurance",
|
|
193
|
+
],
|
|
194
|
+
"benunit": [
|
|
195
|
+
# IDs and weights
|
|
196
|
+
"benunit_id",
|
|
197
|
+
"benunit_weight",
|
|
198
|
+
# Structure
|
|
199
|
+
"family_type",
|
|
200
|
+
# Income and benefits
|
|
201
|
+
"universal_credit",
|
|
202
|
+
"child_benefit",
|
|
203
|
+
"working_tax_credit",
|
|
204
|
+
"child_tax_credit",
|
|
205
|
+
],
|
|
206
|
+
"household": [
|
|
207
|
+
# IDs and weights
|
|
208
|
+
"household_id",
|
|
209
|
+
"household_weight",
|
|
210
|
+
# Income measures
|
|
211
|
+
"household_net_income",
|
|
212
|
+
"hbai_household_net_income",
|
|
213
|
+
"equiv_hbai_household_net_income",
|
|
214
|
+
"household_market_income",
|
|
215
|
+
"household_gross_income",
|
|
216
|
+
# Benefits and tax
|
|
217
|
+
"household_benefits",
|
|
218
|
+
"household_tax",
|
|
219
|
+
"vat",
|
|
220
|
+
# Housing
|
|
221
|
+
"rent",
|
|
222
|
+
"council_tax",
|
|
223
|
+
"tenure_type",
|
|
224
|
+
],
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
data = {
|
|
228
|
+
"person": pd.DataFrame(),
|
|
229
|
+
"benunit": pd.DataFrame(),
|
|
230
|
+
"household": pd.DataFrame(),
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
for entity, variables in entity_variables.items():
|
|
234
|
+
for var in variables:
|
|
235
|
+
data[entity][var] = microsim.calculate(
|
|
236
|
+
var, period=simulation.dataset.year, map_to=entity
|
|
237
|
+
).values
|
|
238
|
+
|
|
239
|
+
data["person"] = MicroDataFrame(
|
|
240
|
+
data["person"], weights="person_weight"
|
|
241
|
+
)
|
|
242
|
+
data["benunit"] = MicroDataFrame(
|
|
243
|
+
data["benunit"], weights="benunit_weight"
|
|
244
|
+
)
|
|
245
|
+
data["household"] = MicroDataFrame(
|
|
246
|
+
data["household"], weights="household_weight"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
simulation.output_dataset = PolicyEngineUKDataset(
|
|
250
|
+
name=dataset.name,
|
|
251
|
+
description=dataset.description,
|
|
252
|
+
filepath=str(
|
|
253
|
+
Path(simulation.dataset.filepath).parent
|
|
254
|
+
/ (simulation.id + ".h5")
|
|
255
|
+
),
|
|
256
|
+
year=simulation.dataset.year,
|
|
257
|
+
is_output_dataset=True,
|
|
258
|
+
data=UKYearData(
|
|
259
|
+
person=data["person"],
|
|
260
|
+
benunit=data["benunit"],
|
|
261
|
+
household=data["household"],
|
|
262
|
+
),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
simulation.output_dataset.save()
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
uk_latest = PolicyEngineUKLatest()
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""UK-specific output templates."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from pydantic import ConfigDict
|
|
6
|
+
|
|
7
|
+
from policyengine.core import Output
|
|
8
|
+
from policyengine.outputs.aggregate import Aggregate, AggregateType
|
|
9
|
+
from policyengine.outputs.change_aggregate import (
|
|
10
|
+
ChangeAggregate,
|
|
11
|
+
ChangeAggregateType,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from policyengine.core.simulation import Simulation
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ProgrammeStatistics(Output):
|
|
19
|
+
"""Single programme's statistics from a policy reform - represents one database row."""
|
|
20
|
+
|
|
21
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
22
|
+
|
|
23
|
+
baseline_simulation: "Simulation"
|
|
24
|
+
reform_simulation: "Simulation"
|
|
25
|
+
programme_name: str
|
|
26
|
+
entity: str
|
|
27
|
+
is_tax: bool = False
|
|
28
|
+
|
|
29
|
+
# Results populated by run()
|
|
30
|
+
baseline_total: float | None = None
|
|
31
|
+
reform_total: float | None = None
|
|
32
|
+
change: float | None = None
|
|
33
|
+
baseline_count: float | None = None
|
|
34
|
+
reform_count: float | None = None
|
|
35
|
+
winners: float | None = None
|
|
36
|
+
losers: float | None = None
|
|
37
|
+
|
|
38
|
+
def run(self):
|
|
39
|
+
"""Calculate statistics for this programme."""
|
|
40
|
+
# Baseline totals
|
|
41
|
+
baseline_total = Aggregate(
|
|
42
|
+
simulation=self.baseline_simulation,
|
|
43
|
+
variable=self.programme_name,
|
|
44
|
+
aggregate_type=AggregateType.SUM,
|
|
45
|
+
entity=self.entity,
|
|
46
|
+
)
|
|
47
|
+
baseline_total.run()
|
|
48
|
+
|
|
49
|
+
# Reform totals
|
|
50
|
+
reform_total = Aggregate(
|
|
51
|
+
simulation=self.reform_simulation,
|
|
52
|
+
variable=self.programme_name,
|
|
53
|
+
aggregate_type=AggregateType.SUM,
|
|
54
|
+
entity=self.entity,
|
|
55
|
+
)
|
|
56
|
+
reform_total.run()
|
|
57
|
+
|
|
58
|
+
# Count of recipients/payers (baseline)
|
|
59
|
+
baseline_count = Aggregate(
|
|
60
|
+
simulation=self.baseline_simulation,
|
|
61
|
+
variable=self.programme_name,
|
|
62
|
+
aggregate_type=AggregateType.COUNT,
|
|
63
|
+
entity=self.entity,
|
|
64
|
+
filter_variable=self.programme_name,
|
|
65
|
+
filter_variable_geq=0.01,
|
|
66
|
+
)
|
|
67
|
+
baseline_count.run()
|
|
68
|
+
|
|
69
|
+
# Count of recipients/payers (reform)
|
|
70
|
+
reform_count = Aggregate(
|
|
71
|
+
simulation=self.reform_simulation,
|
|
72
|
+
variable=self.programme_name,
|
|
73
|
+
aggregate_type=AggregateType.COUNT,
|
|
74
|
+
entity=self.entity,
|
|
75
|
+
filter_variable=self.programme_name,
|
|
76
|
+
filter_variable_geq=0.01,
|
|
77
|
+
)
|
|
78
|
+
reform_count.run()
|
|
79
|
+
|
|
80
|
+
# Winners and losers
|
|
81
|
+
winners = ChangeAggregate(
|
|
82
|
+
baseline_simulation=self.baseline_simulation,
|
|
83
|
+
reform_simulation=self.reform_simulation,
|
|
84
|
+
variable=self.programme_name,
|
|
85
|
+
aggregate_type=ChangeAggregateType.COUNT,
|
|
86
|
+
entity=self.entity,
|
|
87
|
+
change_geq=0.01 if not self.is_tax else -0.01,
|
|
88
|
+
)
|
|
89
|
+
winners.run()
|
|
90
|
+
|
|
91
|
+
losers = ChangeAggregate(
|
|
92
|
+
baseline_simulation=self.baseline_simulation,
|
|
93
|
+
reform_simulation=self.reform_simulation,
|
|
94
|
+
variable=self.programme_name,
|
|
95
|
+
aggregate_type=ChangeAggregateType.COUNT,
|
|
96
|
+
entity=self.entity,
|
|
97
|
+
change_leq=-0.01 if not self.is_tax else 0.01,
|
|
98
|
+
)
|
|
99
|
+
losers.run()
|
|
100
|
+
|
|
101
|
+
# Populate results
|
|
102
|
+
self.baseline_total = float(baseline_total.result)
|
|
103
|
+
self.reform_total = float(reform_total.result)
|
|
104
|
+
self.change = float(reform_total.result - baseline_total.result)
|
|
105
|
+
self.baseline_count = float(baseline_count.result)
|
|
106
|
+
self.reform_count = float(reform_count.result)
|
|
107
|
+
self.winners = float(winners.result)
|
|
108
|
+
self.losers = float(losers.result)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""PolicyEngine UK tax-benefit model - imports from uk/ module."""
|
|
2
|
+
|
|
3
|
+
from .uk import (
|
|
4
|
+
PolicyEngineUK,
|
|
5
|
+
PolicyEngineUKDataset,
|
|
6
|
+
PolicyEngineUKLatest,
|
|
7
|
+
ProgrammeStatistics,
|
|
8
|
+
UKYearData,
|
|
9
|
+
create_datasets,
|
|
10
|
+
general_policy_reform_analysis,
|
|
11
|
+
uk_latest,
|
|
12
|
+
uk_model,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"UKYearData",
|
|
17
|
+
"PolicyEngineUKDataset",
|
|
18
|
+
"create_datasets",
|
|
19
|
+
"PolicyEngineUK",
|
|
20
|
+
"PolicyEngineUKLatest",
|
|
21
|
+
"uk_model",
|
|
22
|
+
"uk_latest",
|
|
23
|
+
"general_policy_reform_analysis",
|
|
24
|
+
"ProgrammeStatistics",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
# Rebuild models to resolve forward references
|
|
28
|
+
from policyengine.core import Dataset
|
|
29
|
+
|
|
30
|
+
Dataset.model_rebuild()
|
|
31
|
+
UKYearData.model_rebuild()
|
|
32
|
+
PolicyEngineUKDataset.model_rebuild()
|
|
33
|
+
PolicyEngineUKLatest.model_rebuild()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""PolicyEngine US tax-benefit model."""
|
|
2
|
+
|
|
3
|
+
from importlib.util import find_spec
|
|
4
|
+
|
|
5
|
+
if find_spec("policyengine_us") is not None:
|
|
6
|
+
from policyengine.core import Dataset
|
|
7
|
+
|
|
8
|
+
from .analysis import general_policy_reform_analysis
|
|
9
|
+
from .datasets import PolicyEngineUSDataset, USYearData, create_datasets
|
|
10
|
+
from .model import (
|
|
11
|
+
PolicyEngineUS,
|
|
12
|
+
PolicyEngineUSLatest,
|
|
13
|
+
us_latest,
|
|
14
|
+
us_model,
|
|
15
|
+
)
|
|
16
|
+
from .outputs import ProgramStatistics
|
|
17
|
+
|
|
18
|
+
# Rebuild Pydantic models to resolve forward references
|
|
19
|
+
Dataset.model_rebuild()
|
|
20
|
+
USYearData.model_rebuild()
|
|
21
|
+
PolicyEngineUSDataset.model_rebuild()
|
|
22
|
+
PolicyEngineUSLatest.model_rebuild()
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"USYearData",
|
|
26
|
+
"PolicyEngineUSDataset",
|
|
27
|
+
"create_datasets",
|
|
28
|
+
"PolicyEngineUS",
|
|
29
|
+
"PolicyEngineUSLatest",
|
|
30
|
+
"us_model",
|
|
31
|
+
"us_latest",
|
|
32
|
+
"general_policy_reform_analysis",
|
|
33
|
+
"ProgramStatistics",
|
|
34
|
+
]
|
|
35
|
+
else:
|
|
36
|
+
__all__ = []
|