policyengine 3.1.1__py3-none-any.whl → 3.1.2__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 +1 -0
- policyengine/core/dataset.py +163 -8
- policyengine/core/dynamic.py +4 -1
- policyengine/core/parameter.py +1 -0
- policyengine/core/policy.py +4 -1
- policyengine/core/simulation.py +8 -5
- policyengine/core/tax_benefit_model_version.py +48 -0
- policyengine/tax_benefit_models/uk/datasets.py +7 -27
- policyengine/tax_benefit_models/uk/model.py +84 -71
- policyengine/tax_benefit_models/us/datasets.py +7 -27
- policyengine/tax_benefit_models/us/model.py +69 -57
- policyengine/utils/plotting.py +0 -1
- {policyengine-3.1.1.dist-info → policyengine-3.1.2.dist-info}/METADATA +1 -1
- {policyengine-3.1.1.dist-info → policyengine-3.1.2.dist-info}/RECORD +18 -18
- {policyengine-3.1.1.dist-info → policyengine-3.1.2.dist-info}/WHEEL +0 -0
- {policyengine-3.1.1.dist-info → policyengine-3.1.2.dist-info}/licenses/LICENSE +0 -0
- {policyengine-3.1.1.dist-info → policyengine-3.1.2.dist-info}/top_level.txt +0 -0
|
Binary file
|
policyengine/core/__init__.py
CHANGED
policyengine/core/dataset.py
CHANGED
|
@@ -8,6 +8,63 @@ from .dataset_version import DatasetVersion
|
|
|
8
8
|
from .tax_benefit_model import TaxBenefitModel
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
class YearData(BaseModel):
|
|
12
|
+
"""Base class for entity-level data for a single year."""
|
|
13
|
+
|
|
14
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def entity_data(self) -> dict[str, MicroDataFrame]:
|
|
18
|
+
"""Return a dictionary of entity names to their data.
|
|
19
|
+
|
|
20
|
+
This should be implemented by subclasses to return the appropriate entities.
|
|
21
|
+
"""
|
|
22
|
+
raise NotImplementedError(
|
|
23
|
+
"Subclasses must implement entity_data property"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def person_entity(self) -> str:
|
|
28
|
+
"""Return the name of the person-level entity.
|
|
29
|
+
|
|
30
|
+
Defaults to 'person' but can be overridden by subclasses.
|
|
31
|
+
"""
|
|
32
|
+
return "person"
|
|
33
|
+
|
|
34
|
+
def map_to_entity(
|
|
35
|
+
self,
|
|
36
|
+
source_entity: str,
|
|
37
|
+
target_entity: str,
|
|
38
|
+
columns: list[str] = None,
|
|
39
|
+
values: list = None,
|
|
40
|
+
how: str = "sum",
|
|
41
|
+
) -> MicroDataFrame:
|
|
42
|
+
"""Map data from source entity to target entity using join keys.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
source_entity (str): The source entity name.
|
|
46
|
+
target_entity (str): The target entity name.
|
|
47
|
+
columns (list[str], optional): List of column names to map. If None, maps all columns.
|
|
48
|
+
values (list, optional): List of values to use instead of column data.
|
|
49
|
+
how (str): Aggregation method ('sum' or 'first') when mapping to higher-level entities (default 'sum').
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
MicroDataFrame: The mapped data at the target entity level.
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
ValueError: If source or target entity is invalid.
|
|
56
|
+
"""
|
|
57
|
+
return map_to_entity(
|
|
58
|
+
entity_data=self.entity_data,
|
|
59
|
+
source_entity=source_entity,
|
|
60
|
+
target_entity=target_entity,
|
|
61
|
+
person_entity=self.person_entity,
|
|
62
|
+
columns=columns,
|
|
63
|
+
values=values,
|
|
64
|
+
how=how,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
11
68
|
class Dataset(BaseModel):
|
|
12
69
|
"""Base class for datasets.
|
|
13
70
|
|
|
@@ -43,6 +100,8 @@ def map_to_entity(
|
|
|
43
100
|
target_entity: str,
|
|
44
101
|
person_entity: str = "person",
|
|
45
102
|
columns: list[str] | None = None,
|
|
103
|
+
values: list | None = None,
|
|
104
|
+
how: str = "sum",
|
|
46
105
|
) -> MicroDataFrame:
|
|
47
106
|
"""Map data from source entity to target entity using join keys.
|
|
48
107
|
|
|
@@ -58,12 +117,17 @@ def map_to_entity(
|
|
|
58
117
|
target_entity: The target entity name
|
|
59
118
|
person_entity: The name of the person entity (default "person")
|
|
60
119
|
columns: List of column names to map. If None, maps all columns
|
|
120
|
+
values: List of values to use instead of column data. If provided, creates a single unnamed column
|
|
121
|
+
how: Aggregation method (default 'sum')
|
|
122
|
+
- For person → group: 'sum' (aggregate), 'first' (take first value)
|
|
123
|
+
- For group → person: 'project' (broadcast), 'divide' (split equally)
|
|
124
|
+
- For group → group: 'sum', 'first', 'project', 'divide'
|
|
61
125
|
|
|
62
126
|
Returns:
|
|
63
127
|
MicroDataFrame: The mapped data at the target entity level
|
|
64
128
|
|
|
65
129
|
Raises:
|
|
66
|
-
ValueError: If source or target entity is invalid
|
|
130
|
+
ValueError: If source or target entity is invalid or unsupported aggregation method
|
|
67
131
|
"""
|
|
68
132
|
valid_entities = set(entity_data.keys())
|
|
69
133
|
|
|
@@ -79,6 +143,18 @@ def map_to_entity(
|
|
|
79
143
|
# Get source data (convert to plain DataFrame to avoid weighted operations during mapping)
|
|
80
144
|
source_df = pd.DataFrame(entity_data[source_entity])
|
|
81
145
|
|
|
146
|
+
# Handle values parameter - create a temporary column with the provided values
|
|
147
|
+
if values is not None:
|
|
148
|
+
if len(values) != len(source_df):
|
|
149
|
+
raise ValueError(
|
|
150
|
+
f"Length of values ({len(values)}) must match source entity length ({len(source_df)})"
|
|
151
|
+
)
|
|
152
|
+
# Create a temporary DataFrame with just ID columns and the values column
|
|
153
|
+
id_cols = {col for col in source_df.columns if col.endswith("_id")}
|
|
154
|
+
source_df = source_df[[col for col in id_cols]]
|
|
155
|
+
source_df["__mapped_value"] = values
|
|
156
|
+
columns = ["__mapped_value"]
|
|
157
|
+
|
|
82
158
|
if columns:
|
|
83
159
|
# Select only requested columns (keep all ID columns for joins)
|
|
84
160
|
id_cols = {col for col in source_df.columns if col.endswith("_id")}
|
|
@@ -118,10 +194,17 @@ def map_to_entity(
|
|
|
118
194
|
if c not in id_cols and c not in weight_cols
|
|
119
195
|
]
|
|
120
196
|
|
|
121
|
-
# Group by join key and
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
197
|
+
# Group by join key and aggregate
|
|
198
|
+
if how == "sum":
|
|
199
|
+
aggregated = source_df.groupby(join_key, as_index=False)[
|
|
200
|
+
agg_cols
|
|
201
|
+
].sum()
|
|
202
|
+
elif how == "first":
|
|
203
|
+
aggregated = source_df.groupby(join_key, as_index=False)[
|
|
204
|
+
agg_cols
|
|
205
|
+
].first()
|
|
206
|
+
else:
|
|
207
|
+
raise ValueError(f"Unsupported aggregation method: {how}")
|
|
125
208
|
|
|
126
209
|
# Rename join key to target key if needed
|
|
127
210
|
if join_key != target_key:
|
|
@@ -146,6 +229,10 @@ def map_to_entity(
|
|
|
146
229
|
|
|
147
230
|
# Group entity to person: expand group-level data to person level
|
|
148
231
|
if source_entity != person_entity and target_entity == person_entity:
|
|
232
|
+
# Default to 'project' (broadcast) for group -> person if 'sum' was provided
|
|
233
|
+
if how == "sum":
|
|
234
|
+
how = "project"
|
|
235
|
+
|
|
149
236
|
source_key = f"{source_entity}_id"
|
|
150
237
|
# Check for both naming patterns
|
|
151
238
|
person_source_key = f"{person_entity}_{source_entity}_id"
|
|
@@ -163,6 +250,40 @@ def map_to_entity(
|
|
|
163
250
|
source_df = source_df.rename(columns={source_key: join_key})
|
|
164
251
|
|
|
165
252
|
result = target_pd.merge(source_df, on=join_key, how="left")
|
|
253
|
+
|
|
254
|
+
# Handle divide operation
|
|
255
|
+
if how == "divide":
|
|
256
|
+
# Get columns to divide (exclude ID and weight columns)
|
|
257
|
+
id_cols = {
|
|
258
|
+
col for col in result.columns if col.endswith("_id")
|
|
259
|
+
}
|
|
260
|
+
weight_cols = {
|
|
261
|
+
col for col in result.columns if col.endswith("_weight")
|
|
262
|
+
}
|
|
263
|
+
value_cols = [
|
|
264
|
+
c
|
|
265
|
+
for c in result.columns
|
|
266
|
+
if c not in id_cols and c not in weight_cols
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
# Count members in each group
|
|
270
|
+
group_counts = (
|
|
271
|
+
target_pd.groupby(join_key, as_index=False)
|
|
272
|
+
.size()
|
|
273
|
+
.rename(columns={"size": "__group_count"})
|
|
274
|
+
)
|
|
275
|
+
result = result.merge(group_counts, on=join_key, how="left")
|
|
276
|
+
|
|
277
|
+
# Divide values by group count
|
|
278
|
+
for col in value_cols:
|
|
279
|
+
result[col] = result[col] / result["__group_count"]
|
|
280
|
+
|
|
281
|
+
result = result.drop(columns=["__group_count"])
|
|
282
|
+
elif how not in ["project"]:
|
|
283
|
+
raise ValueError(
|
|
284
|
+
f"Unsupported aggregation method for group->person: {how}. Use 'project' or 'divide'."
|
|
285
|
+
)
|
|
286
|
+
|
|
166
287
|
return MicroDataFrame(result, weights=target_weight)
|
|
167
288
|
|
|
168
289
|
# Group to group: go through person table
|
|
@@ -228,9 +349,43 @@ def map_to_entity(
|
|
|
228
349
|
if c not in id_cols and c not in weight_cols
|
|
229
350
|
]
|
|
230
351
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
352
|
+
if how == "sum":
|
|
353
|
+
aggregated = source_with_target.groupby(
|
|
354
|
+
target_link_key, as_index=False
|
|
355
|
+
)[agg_cols].sum()
|
|
356
|
+
elif how == "first":
|
|
357
|
+
aggregated = source_with_target.groupby(
|
|
358
|
+
target_link_key, as_index=False
|
|
359
|
+
)[agg_cols].first()
|
|
360
|
+
elif how == "project":
|
|
361
|
+
# Just take first value (broadcast to target groups)
|
|
362
|
+
aggregated = source_with_target.groupby(
|
|
363
|
+
target_link_key, as_index=False
|
|
364
|
+
)[agg_cols].first()
|
|
365
|
+
elif how == "divide":
|
|
366
|
+
# Count persons in each source group
|
|
367
|
+
source_group_counts = (
|
|
368
|
+
person_df.groupby(source_link_key, as_index=False)
|
|
369
|
+
.size()
|
|
370
|
+
.rename(columns={"size": "__source_count"})
|
|
371
|
+
)
|
|
372
|
+
source_with_target = source_with_target.merge(
|
|
373
|
+
source_group_counts, on=source_link_key, how="left"
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
# Divide values by source group count (per-person share)
|
|
377
|
+
for col in agg_cols:
|
|
378
|
+
source_with_target[col] = (
|
|
379
|
+
source_with_target[col]
|
|
380
|
+
/ source_with_target["__source_count"]
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
# Now aggregate (sum of per-person shares) to target level
|
|
384
|
+
aggregated = source_with_target.groupby(
|
|
385
|
+
target_link_key, as_index=False
|
|
386
|
+
)[agg_cols].sum()
|
|
387
|
+
else:
|
|
388
|
+
raise ValueError(f"Unsupported aggregation method: {how}")
|
|
234
389
|
|
|
235
390
|
# Rename target link key to target key if needed
|
|
236
391
|
if target_link_key != target_key:
|
policyengine/core/dynamic.py
CHANGED
|
@@ -23,7 +23,10 @@ class Dynamic(BaseModel):
|
|
|
23
23
|
|
|
24
24
|
# Combine simulation modifiers
|
|
25
25
|
combined_modifier = None
|
|
26
|
-
if
|
|
26
|
+
if (
|
|
27
|
+
self.simulation_modifier is not None
|
|
28
|
+
and other.simulation_modifier is not None
|
|
29
|
+
):
|
|
27
30
|
|
|
28
31
|
def combined_modifier(sim):
|
|
29
32
|
sim = self.simulation_modifier(sim)
|
policyengine/core/parameter.py
CHANGED
|
@@ -8,6 +8,7 @@ from .tax_benefit_model_version import TaxBenefitModelVersion
|
|
|
8
8
|
class Parameter(BaseModel):
|
|
9
9
|
id: str = Field(default_factory=lambda: str(uuid4()))
|
|
10
10
|
name: str
|
|
11
|
+
label: str | None = None
|
|
11
12
|
description: str | None = None
|
|
12
13
|
data_type: type | None = None
|
|
13
14
|
tax_benefit_model_version: TaxBenefitModelVersion
|
policyengine/core/policy.py
CHANGED
|
@@ -23,7 +23,10 @@ class Policy(BaseModel):
|
|
|
23
23
|
|
|
24
24
|
# Combine simulation modifiers
|
|
25
25
|
combined_modifier = None
|
|
26
|
-
if
|
|
26
|
+
if (
|
|
27
|
+
self.simulation_modifier is not None
|
|
28
|
+
and other.simulation_modifier is not None
|
|
29
|
+
):
|
|
27
30
|
|
|
28
31
|
def combined_modifier(sim):
|
|
29
32
|
sim = self.simulation_modifier(sim)
|
policyengine/core/simulation.py
CHANGED
|
@@ -21,10 +21,13 @@ class Simulation(BaseModel):
|
|
|
21
21
|
tax_benefit_model_version: TaxBenefitModelVersion = None
|
|
22
22
|
output_dataset: Dataset | None = None
|
|
23
23
|
|
|
24
|
-
variables: dict[str, list[str]] | None = Field(
|
|
25
|
-
default=None,
|
|
26
|
-
description="Optional dictionary mapping entity names to lists of variable names to calculate. If None, uses model defaults.",
|
|
27
|
-
)
|
|
28
|
-
|
|
29
24
|
def run(self):
|
|
30
25
|
self.tax_benefit_model_version.run(self)
|
|
26
|
+
|
|
27
|
+
def save(self):
|
|
28
|
+
"""Save the simulation's output dataset."""
|
|
29
|
+
self.tax_benefit_model_version.save(self)
|
|
30
|
+
|
|
31
|
+
def load(self):
|
|
32
|
+
"""Load the simulation's output dataset."""
|
|
33
|
+
self.tax_benefit_model_version.load(self)
|
|
@@ -29,6 +29,54 @@ class TaxBenefitModelVersion(BaseModel):
|
|
|
29
29
|
"The TaxBenefitModel class must define a method to execute simulations."
|
|
30
30
|
)
|
|
31
31
|
|
|
32
|
+
def save(self, simulation: "Simulation"):
|
|
33
|
+
raise NotImplementedError(
|
|
34
|
+
"The TaxBenefitModel class must define a method to save simulations."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def load(self, simulation: "Simulation"):
|
|
38
|
+
raise NotImplementedError(
|
|
39
|
+
"The TaxBenefitModel class must define a method to load simulations."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def get_parameter(self, name: str) -> "Parameter":
|
|
43
|
+
"""Get a parameter by name.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
name: The parameter name (e.g., "gov.hmrc.income_tax.allowances.personal_allowance.amount")
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Parameter: The matching parameter
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
ValueError: If parameter not found
|
|
53
|
+
"""
|
|
54
|
+
for param in self.parameters:
|
|
55
|
+
if param.name == name:
|
|
56
|
+
return param
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"Parameter '{name}' not found in {self.model.id} version {self.version}"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def get_variable(self, name: str) -> "Variable":
|
|
62
|
+
"""Get a variable by name.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
name: The variable name (e.g., "income_tax", "household_net_income")
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Variable: The matching variable
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
ValueError: If variable not found
|
|
72
|
+
"""
|
|
73
|
+
for var in self.variables:
|
|
74
|
+
if var.name == name:
|
|
75
|
+
return var
|
|
76
|
+
raise ValueError(
|
|
77
|
+
f"Variable '{name}' not found in {self.model.id} version {self.version}"
|
|
78
|
+
)
|
|
79
|
+
|
|
32
80
|
def __repr__(self) -> str:
|
|
33
81
|
# Give the id and version, and the number of variables, parameters, parameter values
|
|
34
82
|
return f"<TaxBenefitModelVersion id={self.id} variables={len(self.variables)} parameters={len(self.parameters)} parameter_values={len(self.parameter_values)}>"
|
|
@@ -2,12 +2,12 @@ from pathlib import Path
|
|
|
2
2
|
|
|
3
3
|
import pandas as pd
|
|
4
4
|
from microdf import MicroDataFrame
|
|
5
|
-
from pydantic import
|
|
5
|
+
from pydantic import ConfigDict
|
|
6
6
|
|
|
7
|
-
from policyengine.core import Dataset,
|
|
7
|
+
from policyengine.core import Dataset, YearData
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
class UKYearData(
|
|
10
|
+
class UKYearData(YearData):
|
|
11
11
|
"""Entity-level data for a single year."""
|
|
12
12
|
|
|
13
13
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
@@ -16,34 +16,14 @@ class UKYearData(BaseModel):
|
|
|
16
16
|
benunit: MicroDataFrame
|
|
17
17
|
household: MicroDataFrame
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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 = {
|
|
19
|
+
@property
|
|
20
|
+
def entity_data(self) -> dict[str, MicroDataFrame]:
|
|
21
|
+
"""Return a dictionary of entity names to their data."""
|
|
22
|
+
return {
|
|
36
23
|
"person": self.person,
|
|
37
24
|
"benunit": self.benunit,
|
|
38
25
|
"household": self.household,
|
|
39
26
|
}
|
|
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
27
|
|
|
48
28
|
|
|
49
29
|
class PolicyEngineUKDataset(Dataset):
|
|
@@ -83,6 +83,7 @@ class PolicyEngineUKLatest(TaxBenefitModelVersion):
|
|
|
83
83
|
parameter = Parameter(
|
|
84
84
|
id=self.id + "-" + param_node.name,
|
|
85
85
|
name=param_node.name,
|
|
86
|
+
label=param_node.metadata.get("label", param_node.name),
|
|
86
87
|
tax_benefit_model_version=self,
|
|
87
88
|
description=param_node.description,
|
|
88
89
|
data_type=type(
|
|
@@ -152,77 +153,72 @@ class PolicyEngineUKLatest(TaxBenefitModelVersion):
|
|
|
152
153
|
)
|
|
153
154
|
modifier(microsim)
|
|
154
155
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
"
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
"
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
"
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
"rent",
|
|
222
|
-
"council_tax",
|
|
223
|
-
"tenure_type",
|
|
224
|
-
],
|
|
225
|
-
}
|
|
156
|
+
entity_variables = {
|
|
157
|
+
"person": [
|
|
158
|
+
# IDs and weights
|
|
159
|
+
"person_id",
|
|
160
|
+
"benunit_id",
|
|
161
|
+
"household_id",
|
|
162
|
+
"person_weight",
|
|
163
|
+
# Demographics
|
|
164
|
+
"age",
|
|
165
|
+
"gender",
|
|
166
|
+
"is_adult",
|
|
167
|
+
"is_SP_age",
|
|
168
|
+
"is_child",
|
|
169
|
+
# Income
|
|
170
|
+
"employment_income",
|
|
171
|
+
"self_employment_income",
|
|
172
|
+
"pension_income",
|
|
173
|
+
"private_pension_income",
|
|
174
|
+
"savings_interest_income",
|
|
175
|
+
"dividend_income",
|
|
176
|
+
"property_income",
|
|
177
|
+
"total_income",
|
|
178
|
+
"earned_income",
|
|
179
|
+
# Benefits
|
|
180
|
+
"universal_credit",
|
|
181
|
+
"child_benefit",
|
|
182
|
+
"pension_credit",
|
|
183
|
+
"income_support",
|
|
184
|
+
"working_tax_credit",
|
|
185
|
+
"child_tax_credit",
|
|
186
|
+
# Tax
|
|
187
|
+
"income_tax",
|
|
188
|
+
"national_insurance",
|
|
189
|
+
],
|
|
190
|
+
"benunit": [
|
|
191
|
+
# IDs and weights
|
|
192
|
+
"benunit_id",
|
|
193
|
+
"benunit_weight",
|
|
194
|
+
# Structure
|
|
195
|
+
"family_type",
|
|
196
|
+
# Income and benefits
|
|
197
|
+
"universal_credit",
|
|
198
|
+
"child_benefit",
|
|
199
|
+
"working_tax_credit",
|
|
200
|
+
"child_tax_credit",
|
|
201
|
+
],
|
|
202
|
+
"household": [
|
|
203
|
+
# IDs and weights
|
|
204
|
+
"household_id",
|
|
205
|
+
"household_weight",
|
|
206
|
+
# Income measures
|
|
207
|
+
"household_net_income",
|
|
208
|
+
"hbai_household_net_income",
|
|
209
|
+
"equiv_hbai_household_net_income",
|
|
210
|
+
"household_market_income",
|
|
211
|
+
"household_gross_income",
|
|
212
|
+
# Benefits and tax
|
|
213
|
+
"household_benefits",
|
|
214
|
+
"household_tax",
|
|
215
|
+
"vat",
|
|
216
|
+
# Housing
|
|
217
|
+
"rent",
|
|
218
|
+
"council_tax",
|
|
219
|
+
"tenure_type",
|
|
220
|
+
],
|
|
221
|
+
}
|
|
226
222
|
|
|
227
223
|
data = {
|
|
228
224
|
"person": pd.DataFrame(),
|
|
@@ -247,6 +243,7 @@ class PolicyEngineUKLatest(TaxBenefitModelVersion):
|
|
|
247
243
|
)
|
|
248
244
|
|
|
249
245
|
simulation.output_dataset = PolicyEngineUKDataset(
|
|
246
|
+
id=simulation.id,
|
|
250
247
|
name=dataset.name,
|
|
251
248
|
description=dataset.description,
|
|
252
249
|
filepath=str(
|
|
@@ -262,7 +259,23 @@ class PolicyEngineUKLatest(TaxBenefitModelVersion):
|
|
|
262
259
|
),
|
|
263
260
|
)
|
|
264
261
|
|
|
262
|
+
def save(self, simulation: "Simulation"):
|
|
263
|
+
"""Save the simulation's output dataset."""
|
|
265
264
|
simulation.output_dataset.save()
|
|
266
265
|
|
|
266
|
+
def load(self, simulation: "Simulation"):
|
|
267
|
+
"""Load the simulation's output dataset."""
|
|
268
|
+
simulation.output_dataset = PolicyEngineUKDataset(
|
|
269
|
+
id=simulation.id,
|
|
270
|
+
name=simulation.dataset.name,
|
|
271
|
+
description=simulation.dataset.description,
|
|
272
|
+
filepath=str(
|
|
273
|
+
Path(simulation.dataset.filepath).parent
|
|
274
|
+
/ (simulation.id + ".h5")
|
|
275
|
+
),
|
|
276
|
+
year=simulation.dataset.year,
|
|
277
|
+
is_output_dataset=True,
|
|
278
|
+
)
|
|
279
|
+
|
|
267
280
|
|
|
268
281
|
uk_latest = PolicyEngineUKLatest()
|
|
@@ -3,12 +3,12 @@ from pathlib import Path
|
|
|
3
3
|
|
|
4
4
|
import pandas as pd
|
|
5
5
|
from microdf import MicroDataFrame
|
|
6
|
-
from pydantic import
|
|
6
|
+
from pydantic import ConfigDict
|
|
7
7
|
|
|
8
|
-
from policyengine.core import Dataset,
|
|
8
|
+
from policyengine.core import Dataset, YearData
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
class USYearData(
|
|
11
|
+
class USYearData(YearData):
|
|
12
12
|
"""Entity-level data for a single year."""
|
|
13
13
|
|
|
14
14
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
@@ -20,23 +20,10 @@ class USYearData(BaseModel):
|
|
|
20
20
|
tax_unit: MicroDataFrame
|
|
21
21
|
household: MicroDataFrame
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
Args:
|
|
29
|
-
source_entity (str): The source entity name.
|
|
30
|
-
target_entity (str): The target entity name.
|
|
31
|
-
columns (list[str], optional): List of column names to map. If None, maps all columns.
|
|
32
|
-
|
|
33
|
-
Returns:
|
|
34
|
-
MicroDataFrame: The mapped data at the target entity level.
|
|
35
|
-
|
|
36
|
-
Raises:
|
|
37
|
-
ValueError: If source or target entity is invalid.
|
|
38
|
-
"""
|
|
39
|
-
entity_data = {
|
|
23
|
+
@property
|
|
24
|
+
def entity_data(self) -> dict[str, MicroDataFrame]:
|
|
25
|
+
"""Return a dictionary of entity names to their data."""
|
|
26
|
+
return {
|
|
40
27
|
"person": self.person,
|
|
41
28
|
"marital_unit": self.marital_unit,
|
|
42
29
|
"family": self.family,
|
|
@@ -44,13 +31,6 @@ class USYearData(BaseModel):
|
|
|
44
31
|
"tax_unit": self.tax_unit,
|
|
45
32
|
"household": self.household,
|
|
46
33
|
}
|
|
47
|
-
return map_to_entity(
|
|
48
|
-
entity_data=entity_data,
|
|
49
|
-
source_entity=source_entity,
|
|
50
|
-
target_entity=target_entity,
|
|
51
|
-
person_entity="person",
|
|
52
|
-
columns=columns,
|
|
53
|
-
)
|
|
54
34
|
|
|
55
35
|
|
|
56
36
|
class PolicyEngineUSDataset(Dataset):
|
|
@@ -156,63 +156,58 @@ class PolicyEngineUSLatest(TaxBenefitModelVersion):
|
|
|
156
156
|
)
|
|
157
157
|
modifier(microsim)
|
|
158
158
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
"
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
"
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
"
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
"
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
"
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
"
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
"household_benefits",
|
|
212
|
-
"household_tax",
|
|
213
|
-
"household_market_income",
|
|
214
|
-
],
|
|
215
|
-
}
|
|
159
|
+
entity_variables = {
|
|
160
|
+
"person": [
|
|
161
|
+
# IDs and weights
|
|
162
|
+
"person_id",
|
|
163
|
+
"marital_unit_id",
|
|
164
|
+
"family_id",
|
|
165
|
+
"spm_unit_id",
|
|
166
|
+
"tax_unit_id",
|
|
167
|
+
"household_id",
|
|
168
|
+
"person_weight",
|
|
169
|
+
# Demographics
|
|
170
|
+
"age",
|
|
171
|
+
# Income
|
|
172
|
+
"employment_income",
|
|
173
|
+
# Benefits
|
|
174
|
+
"ssi",
|
|
175
|
+
"social_security",
|
|
176
|
+
"medicaid",
|
|
177
|
+
"unemployment_compensation",
|
|
178
|
+
],
|
|
179
|
+
"marital_unit": [
|
|
180
|
+
"marital_unit_id",
|
|
181
|
+
"marital_unit_weight",
|
|
182
|
+
],
|
|
183
|
+
"family": [
|
|
184
|
+
"family_id",
|
|
185
|
+
"family_weight",
|
|
186
|
+
],
|
|
187
|
+
"spm_unit": [
|
|
188
|
+
"spm_unit_id",
|
|
189
|
+
"spm_unit_weight",
|
|
190
|
+
"snap",
|
|
191
|
+
"tanf",
|
|
192
|
+
"spm_unit_net_income",
|
|
193
|
+
],
|
|
194
|
+
"tax_unit": [
|
|
195
|
+
"tax_unit_id",
|
|
196
|
+
"tax_unit_weight",
|
|
197
|
+
"income_tax",
|
|
198
|
+
"employee_payroll_tax",
|
|
199
|
+
"eitc",
|
|
200
|
+
"ctc",
|
|
201
|
+
],
|
|
202
|
+
"household": [
|
|
203
|
+
"household_id",
|
|
204
|
+
"household_weight",
|
|
205
|
+
"household_net_income",
|
|
206
|
+
"household_benefits",
|
|
207
|
+
"household_tax",
|
|
208
|
+
"household_market_income",
|
|
209
|
+
],
|
|
210
|
+
}
|
|
216
211
|
|
|
217
212
|
data = {
|
|
218
213
|
"person": pd.DataFrame(),
|
|
@@ -291,6 +286,7 @@ class PolicyEngineUSLatest(TaxBenefitModelVersion):
|
|
|
291
286
|
)
|
|
292
287
|
|
|
293
288
|
simulation.output_dataset = PolicyEngineUSDataset(
|
|
289
|
+
id=simulation.id,
|
|
294
290
|
name=dataset.name,
|
|
295
291
|
description=dataset.description,
|
|
296
292
|
filepath=str(
|
|
@@ -309,8 +305,24 @@ class PolicyEngineUSLatest(TaxBenefitModelVersion):
|
|
|
309
305
|
),
|
|
310
306
|
)
|
|
311
307
|
|
|
308
|
+
def save(self, simulation: "Simulation"):
|
|
309
|
+
"""Save the simulation's output dataset."""
|
|
312
310
|
simulation.output_dataset.save()
|
|
313
311
|
|
|
312
|
+
def load(self, simulation: "Simulation"):
|
|
313
|
+
"""Load the simulation's output dataset."""
|
|
314
|
+
simulation.output_dataset = PolicyEngineUSDataset(
|
|
315
|
+
id=simulation.id,
|
|
316
|
+
name=simulation.dataset.name,
|
|
317
|
+
description=simulation.dataset.description,
|
|
318
|
+
filepath=str(
|
|
319
|
+
Path(simulation.dataset.filepath).parent
|
|
320
|
+
/ (simulation.id + ".h5")
|
|
321
|
+
),
|
|
322
|
+
year=simulation.dataset.year,
|
|
323
|
+
is_output_dataset=True,
|
|
324
|
+
)
|
|
325
|
+
|
|
314
326
|
def _build_simulation_from_dataset(self, microsim, dataset, system):
|
|
315
327
|
"""Build a PolicyEngine Core simulation from dataset entity IDs.
|
|
316
328
|
|
policyengine/utils/plotting.py
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
policyengine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
policyengine/__pycache__/__init__.cpython-313.pyc,sha256=
|
|
3
|
-
policyengine/core/__init__.py,sha256=
|
|
4
|
-
policyengine/core/dataset.py,sha256=
|
|
2
|
+
policyengine/__pycache__/__init__.cpython-313.pyc,sha256=o8VWizV0RJAh0YGEL98jBlt-vlR2yWepNiM-xbf5ET8,175
|
|
3
|
+
policyengine/core/__init__.py,sha256=KBVhkqzkvjWLDDwk96vquQKL63ZFuLen5AzBOBnO9pg,912
|
|
4
|
+
policyengine/core/dataset.py,sha256=SFicjsZgMeKgJjlt8z98fOUWG2dfod0Q9NBy_m9PSc8,15506
|
|
5
5
|
policyengine/core/dataset_version.py,sha256=6KeFCRGQto_Yyl4QY4Vo2JFythjaXrNAOHQiwRGESyM,378
|
|
6
|
-
policyengine/core/dynamic.py,sha256=
|
|
6
|
+
policyengine/core/dynamic.py,sha256=ng9BjDzxdwjJ0e7zoqXFmq33E1SRbaaPYfW7pjRSSzI,1641
|
|
7
7
|
policyengine/core/output.py,sha256=cCW4vbzkLdQaT_nJTyDJBl7Hubm7nZeRuR7aVG1dKvg,643
|
|
8
|
-
policyengine/core/parameter.py,sha256=
|
|
8
|
+
policyengine/core/parameter.py,sha256=5nBCw-6-BCfW-_uFqvAN5zcP_Vfz1mAmQsi3peWAbZA,407
|
|
9
9
|
policyengine/core/parameter_value.py,sha256=b0ts1kbWcwjPSYnZm2rlCylmTLPJRLxDL8z3RmxM5OI,377
|
|
10
|
-
policyengine/core/policy.py,sha256=
|
|
11
|
-
policyengine/core/simulation.py,sha256=
|
|
10
|
+
policyengine/core/policy.py,sha256=ExMrUDMvNk_uuOL0cSm0UCzDyGka0t_yk6x4U0Kp6Ww,1635
|
|
11
|
+
policyengine/core/simulation.py,sha256=fToaD4ieV99IMMKGU7r6ATCREWuOU2-4k6WqhzAzo7c,971
|
|
12
12
|
policyengine/core/tax_benefit_model.py,sha256=2Yc1RlQrUG7djDMZbJOQH4Ns86_lOnLeISCGR4-9zMo,176
|
|
13
|
-
policyengine/core/tax_benefit_model_version.py,sha256=
|
|
13
|
+
policyengine/core/tax_benefit_model_version.py,sha256=V1CGft5Y6YflMASx0wR3V73jr-WqQu2R8N5QVMRm9yw,2752
|
|
14
14
|
policyengine/core/variable.py,sha256=AjSImORlRkh05xhYxyeT6GFMOfViRzYg0qRQAIj-mxo,350
|
|
15
15
|
policyengine/outputs/__init__.py,sha256=IJUmLP0Og41VrwiqhJF-a9-3fIb4nlXpS7uFuVCINIs,515
|
|
16
16
|
policyengine/outputs/aggregate.py,sha256=exI-U04OF5kVf2BBYV6sf8VldIWnT_IzxgkBs5wtnCw,4846
|
|
@@ -20,20 +20,20 @@ policyengine/tax_benefit_models/uk.py,sha256=MCeJGQCTwUzBYdz0ru7IgT8Mgv-vJMqqVwF
|
|
|
20
20
|
policyengine/tax_benefit_models/us.py,sha256=d6rdhW2awoUvk5Ldp9mHAUiHqlnSfEQR318PkIXS_9c,794
|
|
21
21
|
policyengine/tax_benefit_models/uk/__init__.py,sha256=1L1HBbOTflRefjOcU5vgEYkn6wLWOt_uG9G_PF089S0,732
|
|
22
22
|
policyengine/tax_benefit_models/uk/analysis.py,sha256=O4eYJYF7tsgiuLuiWMU0OXq7ss6U8-vzlg6nC2U8sgU,3175
|
|
23
|
-
policyengine/tax_benefit_models/uk/datasets.py,sha256=
|
|
24
|
-
policyengine/tax_benefit_models/uk/model.py,sha256
|
|
23
|
+
policyengine/tax_benefit_models/uk/datasets.py,sha256=Cn3sBvIBQ_-s5pEM9TDp2FwdVIo20h3do_8biVJLyKI,5748
|
|
24
|
+
policyengine/tax_benefit_models/uk/model.py,sha256=-u7DM9pPndgCcNzOSMD0kftlVLF90mJV1RYCkECiw2c,9483
|
|
25
25
|
policyengine/tax_benefit_models/uk/outputs.py,sha256=2mYLwQW4QNvrOHtHfm_ACqE9gbmuLxvcCyldRU46s0o,3543
|
|
26
26
|
policyengine/tax_benefit_models/us/__init__.py,sha256=ClBkFwRa_nFQ3YU003uxBKiMG3pktaUb8jRvhzfR0vE,968
|
|
27
27
|
policyengine/tax_benefit_models/us/analysis.py,sha256=Xf-DT0QjVySs0QG_koCwgvOeWI_scLtv3S3SP8u8ZWc,3253
|
|
28
|
-
policyengine/tax_benefit_models/us/datasets.py,sha256=
|
|
29
|
-
policyengine/tax_benefit_models/us/model.py,sha256=
|
|
28
|
+
policyengine/tax_benefit_models/us/datasets.py,sha256=StvHtliqhYSIzI2ARqadLxTkp6xDYnf_qquB7aeH55Y,11994
|
|
29
|
+
policyengine/tax_benefit_models/us/model.py,sha256=pfkPEJXm4epccgLQlSxP7Apw9JdvfZgNA9rgYHaNdlc,16236
|
|
30
30
|
policyengine/tax_benefit_models/us/outputs.py,sha256=GT8Eur8DfB9cPQRbSljEl9RpKSNHW80Fq_CBXCybvIU,3519
|
|
31
31
|
policyengine/utils/__init__.py,sha256=1X-VYAWLyB9A0YRHwsGWrqQHns1WfeZ7ISC6DMU5myM,140
|
|
32
32
|
policyengine/utils/dates.py,sha256=HnAqyl8S8EOYp8ibsnMTmECYoDWCSqwL-7A2_qKgxSc,1510
|
|
33
33
|
policyengine/utils/parametric_reforms.py,sha256=4P3U39-4pYTU4BN6JjgmVLUkCkBhRfZJ6UIWTlsjyQE,1155
|
|
34
|
-
policyengine/utils/plotting.py,sha256=
|
|
35
|
-
policyengine-3.1.
|
|
36
|
-
policyengine-3.1.
|
|
37
|
-
policyengine-3.1.
|
|
38
|
-
policyengine-3.1.
|
|
39
|
-
policyengine-3.1.
|
|
34
|
+
policyengine/utils/plotting.py,sha256=ZAzTWz38vIaW0c3Nt4Un1kfrNoXLyHCDd1pEJIlsRg4,5335
|
|
35
|
+
policyengine-3.1.2.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
|
|
36
|
+
policyengine-3.1.2.dist-info/METADATA,sha256=iZkPPB-Tve-DYtGCm7ZDhTCDQyRhBg2nQ9DRer6eZ_A,45889
|
|
37
|
+
policyengine-3.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
38
|
+
policyengine-3.1.2.dist-info/top_level.txt,sha256=_23UPobfkneHQkpJ0e0OmDJfhCUfoXj_F2sTckCGOH4,13
|
|
39
|
+
policyengine-3.1.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|