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.
@@ -1,4 +1,5 @@
1
1
  from .dataset import Dataset
2
+ from .dataset import YearData as YearData
2
3
  from .dataset import map_to_entity as map_to_entity
3
4
  from .dataset_version import DatasetVersion as DatasetVersion
4
5
  from .dynamic import Dynamic as Dynamic
@@ -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 sum
122
- aggregated = source_df.groupby(join_key, as_index=False)[
123
- agg_cols
124
- ].sum()
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
- aggregated = source_with_target.groupby(
232
- target_link_key, as_index=False
233
- )[agg_cols].sum()
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:
@@ -23,7 +23,10 @@ class Dynamic(BaseModel):
23
23
 
24
24
  # Combine simulation modifiers
25
25
  combined_modifier = None
26
- if self.simulation_modifier is not None and other.simulation_modifier is not None:
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)
@@ -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
@@ -23,7 +23,10 @@ class Policy(BaseModel):
23
23
 
24
24
  # Combine simulation modifiers
25
25
  combined_modifier = None
26
- if self.simulation_modifier is not None and other.simulation_modifier is not None:
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)
@@ -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 BaseModel, ConfigDict
5
+ from pydantic import ConfigDict
6
6
 
7
- from policyengine.core import Dataset, map_to_entity
7
+ from policyengine.core import Dataset, YearData
8
8
 
9
9
 
10
- class UKYearData(BaseModel):
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
- 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 = {
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
- # 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
- }
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 BaseModel, ConfigDict
6
+ from pydantic import ConfigDict
7
7
 
8
- from policyengine.core import Dataset, map_to_entity
8
+ from policyengine.core import Dataset, YearData
9
9
 
10
10
 
11
- class USYearData(BaseModel):
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
- def map_to_entity(
24
- self, source_entity: str, target_entity: str, columns: list[str] = None
25
- ) -> MicroDataFrame:
26
- """Map data from source entity to target entity using join keys.
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
- # Allow custom variable selection, or use defaults
160
- if simulation.variables is not None:
161
- entity_variables = simulation.variables
162
- else:
163
- # Default comprehensive variable set
164
- entity_variables = {
165
- "person": [
166
- # IDs and weights
167
- "person_id",
168
- "marital_unit_id",
169
- "family_id",
170
- "spm_unit_id",
171
- "tax_unit_id",
172
- "household_id",
173
- "person_weight",
174
- # Demographics
175
- "age",
176
- # Income
177
- "employment_income",
178
- # Benefits
179
- "ssi",
180
- "social_security",
181
- "medicaid",
182
- "unemployment_compensation",
183
- ],
184
- "marital_unit": [
185
- "marital_unit_id",
186
- "marital_unit_weight",
187
- ],
188
- "family": [
189
- "family_id",
190
- "family_weight",
191
- ],
192
- "spm_unit": [
193
- "spm_unit_id",
194
- "spm_unit_weight",
195
- "snap",
196
- "tanf",
197
- "spm_unit_net_income",
198
- ],
199
- "tax_unit": [
200
- "tax_unit_id",
201
- "tax_unit_weight",
202
- "income_tax",
203
- "employee_payroll_tax",
204
- "eitc",
205
- "ctc",
206
- ],
207
- "household": [
208
- "household_id",
209
- "household_weight",
210
- "household_net_income",
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
 
@@ -1,6 +1,5 @@
1
1
  """Plotting utilities for PolicyEngine visualisations."""
2
2
 
3
-
4
3
  import plotly.graph_objects as go
5
4
 
6
5
  # PolicyEngine brand colours
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: policyengine
3
- Version: 3.1.1
3
+ Version: 3.1.2
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
@@ -1,16 +1,16 @@
1
1
  policyengine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- policyengine/__pycache__/__init__.cpython-313.pyc,sha256=dmkhqYKcUjDPvirOFgf1L-P-aV3l7JIWK6zZZ6J45LU,175
3
- policyengine/core/__init__.py,sha256=3vabkWXYNNmQmN4Vy1yx2kKk6RKVSsdjtToQ8s8gzUg,870
4
- policyengine/core/dataset.py,sha256=jGjnoyEQ6VcTiCs8Hr6PT22l59CRhK1oUT5RPaz7mC0,9256
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=40RO3N6hmUT6-3tpJy0wSOC-9ITdFSS5LRiqPmi79f0,1605
6
+ policyengine/core/dynamic.py,sha256=ng9BjDzxdwjJ0e7zoqXFmq33E1SRbaaPYfW7pjRSSzI,1641
7
7
  policyengine/core/output.py,sha256=cCW4vbzkLdQaT_nJTyDJBl7Hubm7nZeRuR7aVG1dKvg,643
8
- policyengine/core/parameter.py,sha256=PLDaepytqB7FVSfYQ541vnkVuMDLamFBeHtz2TmQrOE,378
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=xorVf03bcjB-3mBEmwj2YMqF1-6Xzps74SquzqtS0lo,1599
11
- policyengine/core/simulation.py,sha256=x9dU_nKEIgCZa11cxB-VdRNt9DZjjSML7n7M8_3STv8,941
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=AJHFRaSEzR2_0FVkBBb40ID3FAaFFrFLnhSXRYYbtWs,1291
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=LvbHemfhMU1kk9sZU5eAPrix8EmTl07p9fYkwbSasIE,6542
24
- policyengine/tax_benefit_models/uk/model.py,sha256=5GGhscQEX1-YBgkUt4kd2RyFfVIBEd7IihaSP3ia7hg,9247
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=sQ0YsTsmqpaM-j7w6Cea1lKUzjMV3UPqH5Bc3hH71cI,12718
29
- policyengine/tax_benefit_models/us/model.py,sha256=SNQ3_pVLJDF41gRzU4NZ1yZUq8nLMPSHWwb-qGaTae0,16021
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=JbMPwD7SCQqp_qbjYrXixT5zS0eG734n0Sg4JkD9nDc,5336
35
- policyengine-3.1.1.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
36
- policyengine-3.1.1.dist-info/METADATA,sha256=JuCAmXnSyBcTeeqyBm54GYmsYrrOPg8zGhWHdYoleT4,45889
37
- policyengine-3.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
38
- policyengine-3.1.1.dist-info/top_level.txt,sha256=_23UPobfkneHQkpJ0e0OmDJfhCUfoXj_F2sTckCGOH4,13
39
- policyengine-3.1.1.dist-info/RECORD,,
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,,