policyengine 3.1.0__tar.gz → 3.1.2__tar.gz
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-3.1.2/.claude/policyengine-guide.md +568 -0
- policyengine-3.1.2/.claude/quick-reference.md +367 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/CHANGELOG.md +16 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/PKG-INFO +1 -1
- {policyengine-3.1.0 → policyengine-3.1.2}/changelog.yaml +13 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/docs/core-concepts.md +113 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/docs/country-models-uk.md +36 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/docs/country-models-us.md +32 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/examples/employment_income_variation_uk.py +0 -32
- {policyengine-3.1.0 → policyengine-3.1.2}/examples/employment_income_variation_us.py +0 -46
- {policyengine-3.1.0 → policyengine-3.1.2}/pyproject.toml +1 -1
- policyengine-3.1.2/src/policyengine/__pycache__/__init__.cpython-313.pyc +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine/core/__init__.py +1 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine/core/dataset.py +163 -8
- policyengine-3.1.2/src/policyengine/core/dynamic.py +46 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine/core/parameter.py +1 -0
- policyengine-3.1.2/src/policyengine/core/policy.py +46 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine/core/simulation.py +8 -5
- policyengine-3.1.2/src/policyengine/core/tax_benefit_model_version.py +82 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine/tax_benefit_models/uk/datasets.py +7 -27
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine/tax_benefit_models/uk/model.py +84 -71
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine/tax_benefit_models/us/datasets.py +7 -27
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine/tax_benefit_models/us/model.py +69 -57
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine/utils/plotting.py +0 -1
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine.egg-info/PKG-INFO +1 -1
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine.egg-info/SOURCES.txt +3 -0
- policyengine-3.1.2/tests/test_get_parameter_variable.py +141 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/tests/test_us_simulation.py +0 -13
- policyengine-3.1.0/src/policyengine/__pycache__/__init__.cpython-313.pyc +0 -0
- policyengine-3.1.0/src/policyengine/core/dynamic.py +0 -17
- policyengine-3.1.0/src/policyengine/core/policy.py +0 -17
- policyengine-3.1.0/src/policyengine/core/tax_benefit_model_version.py +0 -34
- {policyengine-3.1.0 → policyengine-3.1.2}/.github/CONTRIBUTING.md +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/.github/changelog_template.md +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/.github/fetch_version.py +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/.github/get-changelog-diff.sh +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/.github/has-functional-changes.sh +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/.github/is-version-number-acceptable.sh +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/.github/publish-git-tag.sh +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/.github/workflows/code_changes.yaml +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/.github/workflows/docs.yml +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/.github/workflows/pr_code_changes.yaml +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/.github/workflows/pr_docs_changes.yaml +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/.github/workflows/versioning.yaml +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/.gitignore +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/CLAUDE.md +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/LICENSE +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/Makefile +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/README.md +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/changelog_entry.yaml +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/docs/.gitignore +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/docs/dev.md +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/docs/index.md +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/docs/myst.yml +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/docs/visualisation.md +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/examples/income_bands_uk.py +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/examples/income_distribution_us.py +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/examples/policy_change_uk.py +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/examples/speedtest_us_simulation.py +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/setup.cfg +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine/__init__.py +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine/core/dataset_version.py +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine/core/output.py +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine/core/parameter_value.py +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine/core/tax_benefit_model.py +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine/core/variable.py +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine/outputs/__init__.py +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine/outputs/aggregate.py +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine/outputs/change_aggregate.py +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine/outputs/decile_impact.py +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine/tax_benefit_models/uk/__init__.py +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine/tax_benefit_models/uk/analysis.py +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine/tax_benefit_models/uk/outputs.py +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine/tax_benefit_models/uk.py +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine/tax_benefit_models/us/__init__.py +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine/tax_benefit_models/us/analysis.py +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine/tax_benefit_models/us/outputs.py +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine/tax_benefit_models/us.py +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine/utils/__init__.py +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine/utils/dates.py +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine/utils/parametric_reforms.py +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine.egg-info/dependency_links.txt +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine.egg-info/requires.txt +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/src/policyengine.egg-info/top_level.txt +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/tests/test_aggregate.py +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/tests/test_change_aggregate.py +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/tests/test_entity_mapping.py +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/tests/test_uk_dataset.py +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/tests/test_us_datasets.py +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/tests/test_us_entity_mapping.py +0 -0
- {policyengine-3.1.0 → policyengine-3.1.2}/uv.lock +0 -0
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
# PolicyEngine.py - Claude Guide
|
|
2
|
+
|
|
3
|
+
This guide helps you use the policyengine.py package to perform tax-benefit microsimulation analysis.
|
|
4
|
+
|
|
5
|
+
## Core workflow
|
|
6
|
+
|
|
7
|
+
1. **Create or load a dataset** with microdata (person, household, etc.)
|
|
8
|
+
2. **Run a simulation** applying tax-benefit rules to the dataset
|
|
9
|
+
3. **Extract results** using output classes (Aggregate, ChangeAggregate)
|
|
10
|
+
4. **Visualise** using built-in plotting utilities
|
|
11
|
+
|
|
12
|
+
## Package structure
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
policyengine
|
|
16
|
+
├── core/
|
|
17
|
+
│ ├── Dataset, YearData # Data containers
|
|
18
|
+
│ ├── Simulation # Runs tax-benefit calculations
|
|
19
|
+
│ ├── Policy, Parameter # Define reforms
|
|
20
|
+
│ └── map_to_entity() # Entity mapping utility
|
|
21
|
+
├── outputs/
|
|
22
|
+
│ ├── Aggregate # Calculate statistics
|
|
23
|
+
│ └── ChangeAggregate # Analyse reforms
|
|
24
|
+
├── tax_benefit_models/
|
|
25
|
+
│ ├── uk/ # UK-specific models
|
|
26
|
+
│ └── us/ # US-specific models
|
|
27
|
+
└── utils/
|
|
28
|
+
└── plotting # Visualisation tools
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick start patterns
|
|
32
|
+
|
|
33
|
+
### Pattern 1: Synthetic scenario analysis
|
|
34
|
+
|
|
35
|
+
Use when: User wants to analyse specific household scenarios
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import pandas as pd
|
|
39
|
+
from microdf import MicroDataFrame
|
|
40
|
+
from policyengine.tax_benefit_models.uk import (
|
|
41
|
+
PolicyEngineUKDataset,
|
|
42
|
+
UKYearData,
|
|
43
|
+
uk_latest
|
|
44
|
+
)
|
|
45
|
+
from policyengine.core import Simulation
|
|
46
|
+
|
|
47
|
+
# Create synthetic person data
|
|
48
|
+
person_df = MicroDataFrame(
|
|
49
|
+
pd.DataFrame({
|
|
50
|
+
"person_id": [0, 1, 2],
|
|
51
|
+
"person_household_id": [0, 0, 1],
|
|
52
|
+
"person_benunit_id": [0, 0, 1],
|
|
53
|
+
"age": [35, 8, 40],
|
|
54
|
+
"employment_income": [30000, 0, 50000],
|
|
55
|
+
"person_weight": [1.0, 1.0, 1.0],
|
|
56
|
+
}),
|
|
57
|
+
weights="person_weight"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Create household data
|
|
61
|
+
household_df = MicroDataFrame(
|
|
62
|
+
pd.DataFrame({
|
|
63
|
+
"household_id": [0, 1],
|
|
64
|
+
"region": ["LONDON", "SOUTH_EAST"],
|
|
65
|
+
"rent": [15000, 12000],
|
|
66
|
+
"household_weight": [1.0, 1.0],
|
|
67
|
+
}),
|
|
68
|
+
weights="household_weight"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Create benunit data (UK only)
|
|
72
|
+
benunit_df = MicroDataFrame(
|
|
73
|
+
pd.DataFrame({
|
|
74
|
+
"benunit_id": [0, 1],
|
|
75
|
+
"would_claim_uc": [True, True],
|
|
76
|
+
"benunit_weight": [1.0, 1.0],
|
|
77
|
+
}),
|
|
78
|
+
weights="benunit_weight"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Package into dataset
|
|
82
|
+
dataset = PolicyEngineUKDataset(
|
|
83
|
+
name="Custom scenario",
|
|
84
|
+
description="Analysis scenario",
|
|
85
|
+
filepath="./custom.h5",
|
|
86
|
+
year=2026,
|
|
87
|
+
data=UKYearData(
|
|
88
|
+
person=person_df,
|
|
89
|
+
household=household_df,
|
|
90
|
+
benunit=benunit_df,
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Run simulation
|
|
95
|
+
simulation = Simulation(
|
|
96
|
+
dataset=dataset,
|
|
97
|
+
tax_benefit_model_version=uk_latest,
|
|
98
|
+
)
|
|
99
|
+
simulation.run()
|
|
100
|
+
|
|
101
|
+
# Access results
|
|
102
|
+
output = simulation.output_dataset.data
|
|
103
|
+
print(output.household[["household_id", "household_net_income"]])
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Pattern 2: US synthetic scenario
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from policyengine.tax_benefit_models.us import (
|
|
110
|
+
PolicyEngineUSDataset,
|
|
111
|
+
USYearData,
|
|
112
|
+
us_latest
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Create person data (note: US has more entity types)
|
|
116
|
+
person_df = MicroDataFrame(
|
|
117
|
+
pd.DataFrame({
|
|
118
|
+
"person_id": [0, 1, 2, 3],
|
|
119
|
+
"person_household_id": [0, 0, 0, 0],
|
|
120
|
+
"person_tax_unit_id": [0, 0, 0, 0],
|
|
121
|
+
"person_spm_unit_id": [0, 0, 0, 0],
|
|
122
|
+
"person_family_id": [0, 0, 0, 0],
|
|
123
|
+
"person_marital_unit_id": [0, 0, 1, 2],
|
|
124
|
+
"age": [35, 33, 8, 5],
|
|
125
|
+
"employment_income": [60000, 40000, 0, 0],
|
|
126
|
+
"person_weight": [1.0, 1.0, 1.0, 1.0],
|
|
127
|
+
}),
|
|
128
|
+
weights="person_weight"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Create entity dataframes (tax_unit, spm_unit, family, marital_unit, household)
|
|
132
|
+
# ... (see examples/employment_income_variation_us.py for full pattern)
|
|
133
|
+
|
|
134
|
+
dataset = PolicyEngineUSDataset(
|
|
135
|
+
name="US scenario",
|
|
136
|
+
year=2024,
|
|
137
|
+
filepath="./us_scenario.h5",
|
|
138
|
+
data=USYearData(
|
|
139
|
+
person=person_df,
|
|
140
|
+
tax_unit=tax_unit_df,
|
|
141
|
+
spm_unit=spm_unit_df,
|
|
142
|
+
family=family_df,
|
|
143
|
+
marital_unit=marital_unit_df,
|
|
144
|
+
household=household_df,
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Pattern 3: Parameter sweep analysis
|
|
150
|
+
|
|
151
|
+
Use when: User wants to vary one parameter across many values
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
import numpy as np
|
|
155
|
+
|
|
156
|
+
# Create N scenarios with varying parameter
|
|
157
|
+
n_scenarios = 43
|
|
158
|
+
income_values = np.linspace(0, 100000, n_scenarios)
|
|
159
|
+
|
|
160
|
+
# Create person data with all scenarios
|
|
161
|
+
person_df = MicroDataFrame(
|
|
162
|
+
pd.DataFrame({
|
|
163
|
+
"person_id": range(n_scenarios),
|
|
164
|
+
"person_household_id": range(n_scenarios),
|
|
165
|
+
"person_benunit_id": range(n_scenarios),
|
|
166
|
+
"age": [35] * n_scenarios,
|
|
167
|
+
"employment_income": income_values,
|
|
168
|
+
"person_weight": [1.0] * n_scenarios,
|
|
169
|
+
}),
|
|
170
|
+
weights="person_weight"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Create matching household/benunit data
|
|
174
|
+
household_df = MicroDataFrame(
|
|
175
|
+
pd.DataFrame({
|
|
176
|
+
"household_id": range(n_scenarios),
|
|
177
|
+
"region": ["LONDON"] * n_scenarios,
|
|
178
|
+
"rent": [15000] * n_scenarios,
|
|
179
|
+
"household_weight": [1.0] * n_scenarios,
|
|
180
|
+
}),
|
|
181
|
+
weights="household_weight"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# ... create dataset and run simulation once for all scenarios
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Pattern 4: Policy reform analysis
|
|
188
|
+
|
|
189
|
+
Use when: User wants to compare baseline vs reform
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
from policyengine.core import Policy, Parameter, ParameterValue
|
|
193
|
+
import datetime
|
|
194
|
+
|
|
195
|
+
# Define reform
|
|
196
|
+
parameter = Parameter(
|
|
197
|
+
name="gov.hmrc.income_tax.allowances.personal_allowance.amount",
|
|
198
|
+
tax_benefit_model_version=uk_latest,
|
|
199
|
+
description="Personal allowance",
|
|
200
|
+
data_type=float,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
policy = Policy(
|
|
204
|
+
name="Increase personal allowance",
|
|
205
|
+
description="Raises PA to £15,000",
|
|
206
|
+
parameter_values=[
|
|
207
|
+
ParameterValue(
|
|
208
|
+
parameter=parameter,
|
|
209
|
+
start_date=datetime.date(2026, 1, 1),
|
|
210
|
+
end_date=datetime.date(2026, 12, 31),
|
|
211
|
+
value=15000,
|
|
212
|
+
)
|
|
213
|
+
],
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Run baseline
|
|
217
|
+
baseline_sim = Simulation(
|
|
218
|
+
dataset=dataset,
|
|
219
|
+
tax_benefit_model_version=uk_latest,
|
|
220
|
+
)
|
|
221
|
+
baseline_sim.run()
|
|
222
|
+
|
|
223
|
+
# Run reform
|
|
224
|
+
reform_sim = Simulation(
|
|
225
|
+
dataset=dataset,
|
|
226
|
+
tax_benefit_model_version=uk_latest,
|
|
227
|
+
policy=policy,
|
|
228
|
+
)
|
|
229
|
+
reform_sim.run()
|
|
230
|
+
|
|
231
|
+
# Analyse impact
|
|
232
|
+
from policyengine.outputs.change_aggregate import (
|
|
233
|
+
ChangeAggregate,
|
|
234
|
+
ChangeAggregateType
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
winners = ChangeAggregate(
|
|
238
|
+
baseline_simulation=baseline_sim,
|
|
239
|
+
reform_simulation=reform_sim,
|
|
240
|
+
variable="household_net_income",
|
|
241
|
+
aggregate_type=ChangeAggregateType.COUNT,
|
|
242
|
+
change_geq=1, # Gain at least £1
|
|
243
|
+
)
|
|
244
|
+
winners.run()
|
|
245
|
+
print(f"Winners: {winners.result:,.0f}")
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Pattern 5: Extract aggregates
|
|
249
|
+
|
|
250
|
+
Use when: User wants statistics from simulation results
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
from policyengine.outputs.aggregate import Aggregate, AggregateType
|
|
254
|
+
|
|
255
|
+
# Total spending on a benefit
|
|
256
|
+
total_uc = Aggregate(
|
|
257
|
+
simulation=simulation,
|
|
258
|
+
variable="universal_credit",
|
|
259
|
+
entity="benunit",
|
|
260
|
+
aggregate_type=AggregateType.SUM,
|
|
261
|
+
)
|
|
262
|
+
total_uc.run()
|
|
263
|
+
print(f"Total UC: £{total_uc.result / 1e9:.1f}bn")
|
|
264
|
+
|
|
265
|
+
# Mean income in top decile
|
|
266
|
+
top_decile_income = Aggregate(
|
|
267
|
+
simulation=simulation,
|
|
268
|
+
variable="household_net_income",
|
|
269
|
+
entity="household",
|
|
270
|
+
aggregate_type=AggregateType.MEAN,
|
|
271
|
+
filter_variable="household_net_income",
|
|
272
|
+
quantile=10,
|
|
273
|
+
quantile_eq=10, # 10th decile only
|
|
274
|
+
)
|
|
275
|
+
top_decile_income.run()
|
|
276
|
+
print(f"Top decile mean income: £{top_decile_income.result:,.0f}")
|
|
277
|
+
|
|
278
|
+
# Count households below poverty line
|
|
279
|
+
poverty_count = Aggregate(
|
|
280
|
+
simulation=simulation,
|
|
281
|
+
variable="household_id",
|
|
282
|
+
entity="household",
|
|
283
|
+
aggregate_type=AggregateType.COUNT,
|
|
284
|
+
filter_variable="in_absolute_poverty_bhc",
|
|
285
|
+
filter_eq=True,
|
|
286
|
+
)
|
|
287
|
+
poverty_count.run()
|
|
288
|
+
print(f"Households in poverty: {poverty_count.result:,.0f}")
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Pattern 6: Entity mapping
|
|
292
|
+
|
|
293
|
+
Use when: User needs to map data between entity levels
|
|
294
|
+
|
|
295
|
+
```python
|
|
296
|
+
# Map person income to household level (sum)
|
|
297
|
+
household_income = dataset.data.map_to_entity(
|
|
298
|
+
source_entity="person",
|
|
299
|
+
target_entity="household",
|
|
300
|
+
columns=["employment_income"],
|
|
301
|
+
how="sum"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Map household rent to person level (broadcast)
|
|
305
|
+
person_rent = dataset.data.map_to_entity(
|
|
306
|
+
source_entity="household",
|
|
307
|
+
target_entity="person",
|
|
308
|
+
columns=["rent"],
|
|
309
|
+
how="project"
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# Split household savings equally per person
|
|
313
|
+
person_savings_share = dataset.data.map_to_entity(
|
|
314
|
+
source_entity="household",
|
|
315
|
+
target_entity="person",
|
|
316
|
+
columns=["total_savings"],
|
|
317
|
+
how="divide"
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
# Map custom values
|
|
321
|
+
import numpy as np
|
|
322
|
+
custom_values = np.array([100, 200, 150])
|
|
323
|
+
household_totals = dataset.data.map_to_entity(
|
|
324
|
+
source_entity="person",
|
|
325
|
+
target_entity="household",
|
|
326
|
+
values=custom_values,
|
|
327
|
+
how="sum"
|
|
328
|
+
)
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Pattern 7: Visualisation
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
from policyengine.utils.plotting import format_fig, COLORS
|
|
335
|
+
import plotly.graph_objects as go
|
|
336
|
+
|
|
337
|
+
fig = go.Figure()
|
|
338
|
+
fig.add_trace(go.Scatter(
|
|
339
|
+
x=income_values,
|
|
340
|
+
y=net_income_values,
|
|
341
|
+
mode='lines',
|
|
342
|
+
name='Net income',
|
|
343
|
+
line=dict(color=COLORS["primary"], width=3)
|
|
344
|
+
))
|
|
345
|
+
|
|
346
|
+
format_fig(
|
|
347
|
+
fig,
|
|
348
|
+
title="Net income by employment income",
|
|
349
|
+
xaxis_title="Employment income (£)",
|
|
350
|
+
yaxis_title="Net income (£)",
|
|
351
|
+
height=600,
|
|
352
|
+
width=1000,
|
|
353
|
+
)
|
|
354
|
+
fig.show()
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
## Entity structures
|
|
358
|
+
|
|
359
|
+
### UK entities
|
|
360
|
+
```
|
|
361
|
+
household
|
|
362
|
+
└── benunit (benefit unit - family claiming benefits together)
|
|
363
|
+
└── person
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### US entities
|
|
367
|
+
```
|
|
368
|
+
household
|
|
369
|
+
├── tax_unit (federal tax filing unit)
|
|
370
|
+
├── spm_unit (Supplemental Poverty Measure unit)
|
|
371
|
+
├── family (Census definition)
|
|
372
|
+
└── marital_unit (married couple or single)
|
|
373
|
+
└── person
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
## Key concepts
|
|
377
|
+
|
|
378
|
+
### 1. MicroDataFrame
|
|
379
|
+
All entity data uses `MicroDataFrame` which automatically handles survey weights:
|
|
380
|
+
```python
|
|
381
|
+
df = MicroDataFrame(pd_dataframe, weights="weight_column_name")
|
|
382
|
+
df.sum() # Automatically weighted
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### 2. Entity mapping
|
|
386
|
+
When variables are at different entity levels, automatic mapping occurs:
|
|
387
|
+
- **Person → Group**: Sum values within each group
|
|
388
|
+
- **Group → Person**: Replicate group value to all members
|
|
389
|
+
|
|
390
|
+
### 3. Required fields
|
|
391
|
+
|
|
392
|
+
**UK person:**
|
|
393
|
+
- `person_id`, `person_household_id`, `person_benunit_id`
|
|
394
|
+
- `age`, `employment_income`
|
|
395
|
+
- `person_weight`
|
|
396
|
+
|
|
397
|
+
**UK household:**
|
|
398
|
+
- `household_id`
|
|
399
|
+
- `region` (e.g., "LONDON", "SOUTH_EAST")
|
|
400
|
+
- `rent` (annual)
|
|
401
|
+
- `household_weight`
|
|
402
|
+
|
|
403
|
+
**UK benunit:**
|
|
404
|
+
- `benunit_id`
|
|
405
|
+
- `would_claim_uc` (boolean - CRITICAL for UC calculations)
|
|
406
|
+
- `benunit_weight`
|
|
407
|
+
|
|
408
|
+
**US person:**
|
|
409
|
+
- `person_id`, `person_household_id`, `person_tax_unit_id`, `person_spm_unit_id`, `person_family_id`, `person_marital_unit_id`
|
|
410
|
+
- `age`, `employment_income`
|
|
411
|
+
- `person_weight`
|
|
412
|
+
|
|
413
|
+
**US household:**
|
|
414
|
+
- `household_id`
|
|
415
|
+
- `state_code` (e.g., "CA", "NY")
|
|
416
|
+
- `household_weight`
|
|
417
|
+
|
|
418
|
+
### 4. Common pitfalls
|
|
419
|
+
|
|
420
|
+
**Always set would_claim variables:**
|
|
421
|
+
```python
|
|
422
|
+
"would_claim_uc": [True] * n_benunits # UK
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
**Set disability variables to avoid spikes:**
|
|
426
|
+
```python
|
|
427
|
+
"is_disabled_for_benefits": [False] * n_people
|
|
428
|
+
"uc_limited_capability_for_WRA": [False] * n_people
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
**Use consistent ID linkages:**
|
|
432
|
+
```python
|
|
433
|
+
# Person 0 must link to valid household_id and benunit_id
|
|
434
|
+
person_df["person_household_id"] = [0, 0, 1] # Persons 0,1 in household 0
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
## Finding parameters
|
|
438
|
+
|
|
439
|
+
### UK common parameters
|
|
440
|
+
```
|
|
441
|
+
gov.hmrc.income_tax.allowances.personal_allowance.amount
|
|
442
|
+
gov.hmrc.national_insurance.class_1.rates.main
|
|
443
|
+
gov.dwp.universal_credit.means_test.reduction_rate
|
|
444
|
+
gov.dwp.universal_credit.elements.child.first_child
|
|
445
|
+
gov.dwp.child_benefit.amount.first_child
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### US common parameters
|
|
449
|
+
```
|
|
450
|
+
gov.irs.income.standard_deduction.single
|
|
451
|
+
gov.irs.income.standard_deduction.joint
|
|
452
|
+
gov.irs.credits.ctc.amount.base
|
|
453
|
+
gov.irs.credits.ctc.refundable.amount.max
|
|
454
|
+
gov.irs.credits.eitc.max[0] # 0 children
|
|
455
|
+
gov.usda.snap.normal_allotment.max[1] # 1 person
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
## Aggregation methods for entity mapping
|
|
459
|
+
|
|
460
|
+
- `how='sum'`: Aggregate by summing (person → group default)
|
|
461
|
+
- `how='first'`: Take first value in group
|
|
462
|
+
- `how='project'`: Broadcast group value to members (group → person default)
|
|
463
|
+
- `how='divide'`: Split equally among members
|
|
464
|
+
|
|
465
|
+
## Response patterns
|
|
466
|
+
|
|
467
|
+
When user asks to:
|
|
468
|
+
|
|
469
|
+
1. **"Analyse a family with £X income"** → Use Pattern 1 (synthetic scenario)
|
|
470
|
+
2. **"How does income vary from £0 to £100k"** → Use Pattern 3 (parameter sweep)
|
|
471
|
+
3. **"What if we increased personal allowance?"** → Use Pattern 4 (policy reform)
|
|
472
|
+
4. **"How many people benefit?"** → Use Pattern 5 (extract aggregates)
|
|
473
|
+
5. **"Compare US vs UK"** → Create both datasets, run separately
|
|
474
|
+
6. **"Show me the phase-out"** → Use Pattern 3 + Pattern 7 (sweep + visualise)
|
|
475
|
+
|
|
476
|
+
## Debugging tips
|
|
477
|
+
|
|
478
|
+
1. **Check dataset shape**: `len(dataset.data.person)` should match expectations
|
|
479
|
+
2. **Verify linkages**: All person IDs should map to valid household IDs
|
|
480
|
+
3. **Check weights**: `dataset.data.household["household_weight"].sum()`
|
|
481
|
+
4. **Inspect output columns**: `list(simulation.output_dataset.data.person.columns)`
|
|
482
|
+
5. **Test small first**: Use 3-5 scenarios before scaling to 100+
|
|
483
|
+
|
|
484
|
+
## Example responses
|
|
485
|
+
|
|
486
|
+
**User: "What's the net income of a single person earning £30k in London?"**
|
|
487
|
+
|
|
488
|
+
```python
|
|
489
|
+
# I'll create a synthetic dataset with one person earning £30k in London
|
|
490
|
+
# and run a UK simulation to calculate their net income.
|
|
491
|
+
|
|
492
|
+
import pandas as pd
|
|
493
|
+
from microdf import MicroDataFrame
|
|
494
|
+
from policyengine.tax_benefit_models.uk import (
|
|
495
|
+
PolicyEngineUKDataset, UKYearData, uk_latest
|
|
496
|
+
)
|
|
497
|
+
from policyengine.core import Simulation
|
|
498
|
+
|
|
499
|
+
# Create person data
|
|
500
|
+
person_df = MicroDataFrame(
|
|
501
|
+
pd.DataFrame({
|
|
502
|
+
"person_id": [0],
|
|
503
|
+
"person_household_id": [0],
|
|
504
|
+
"person_benunit_id": [0],
|
|
505
|
+
"age": [30],
|
|
506
|
+
"employment_income": [30000],
|
|
507
|
+
"person_weight": [1.0],
|
|
508
|
+
}),
|
|
509
|
+
weights="person_weight"
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
# Create household data
|
|
513
|
+
household_df = MicroDataFrame(
|
|
514
|
+
pd.DataFrame({
|
|
515
|
+
"household_id": [0],
|
|
516
|
+
"region": ["LONDON"],
|
|
517
|
+
"rent": [12000], # Typical London rent
|
|
518
|
+
"household_weight": [1.0],
|
|
519
|
+
}),
|
|
520
|
+
weights="household_weight"
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
# Create benunit data
|
|
524
|
+
benunit_df = MicroDataFrame(
|
|
525
|
+
pd.DataFrame({
|
|
526
|
+
"benunit_id": [0],
|
|
527
|
+
"would_claim_uc": [True],
|
|
528
|
+
"benunit_weight": [1.0],
|
|
529
|
+
}),
|
|
530
|
+
weights="benunit_weight"
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
# Create and run simulation
|
|
534
|
+
dataset = PolicyEngineUKDataset(
|
|
535
|
+
name="Single person £30k",
|
|
536
|
+
filepath="./temp_scenario.h5",
|
|
537
|
+
year=2026,
|
|
538
|
+
data=UKYearData(
|
|
539
|
+
person=person_df,
|
|
540
|
+
household=household_df,
|
|
541
|
+
benunit=benunit_df,
|
|
542
|
+
)
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
simulation = Simulation(
|
|
546
|
+
dataset=dataset,
|
|
547
|
+
tax_benefit_model_version=uk_latest,
|
|
548
|
+
)
|
|
549
|
+
simulation.run()
|
|
550
|
+
|
|
551
|
+
# Extract results
|
|
552
|
+
output = simulation.output_dataset.data
|
|
553
|
+
net_income = output.household["household_net_income"].iloc[0]
|
|
554
|
+
income_tax = output.person["income_tax"].iloc[0]
|
|
555
|
+
ni = output.person["national_insurance"].iloc[0]
|
|
556
|
+
|
|
557
|
+
print(f"Employment income: £30,000")
|
|
558
|
+
print(f"Income tax: £{income_tax:,.0f}")
|
|
559
|
+
print(f"National Insurance: £{ni:,.0f}")
|
|
560
|
+
print(f"Net income: £{net_income:,.0f}")
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
## Additional resources
|
|
564
|
+
|
|
565
|
+
- Full examples in `examples/` directory
|
|
566
|
+
- Core concepts: `docs/core-concepts.md`
|
|
567
|
+
- UK model: `docs/country-models-uk.md`
|
|
568
|
+
- US model: `docs/country-models-us.md`
|