What happens
A UK population run through policyengine.py (ensure_datasets → Simulation → outputs) does not reproduce policyengine_uk.Microsimulation on the same certified dataset and year. The State Pension differs, and so does everything downstream of it.
| 2026, £bn |
direct policyengine-uk |
policyengine.py |
gap |
| policyengine 6.2.1 (uk 2.102.3, data 1.56.16): State Pension |
133.69 |
127.50 |
−6.19 |
| Pension Credit |
6.99 |
7.51 |
+0.52 |
| income tax |
313.45 |
312.35 |
−1.10 |
| household net income |
1,764.75 |
1,760.25 |
−4.50 |
| #555 branch (uk 2.123.4, data 1.58.0): State Pension |
128.31 |
124.42 |
−3.89 |
| Pension Credit |
7.05 |
7.42 |
+0.37 |
Universal Credit and Child Benefit match.
Mechanism (shown record by record, real runs)
create_datasets builds policyengine_uk.Microsimulation(dataset=<base file>), takes the projected frame sim.dataset[year] and saves it as the year file. The year file is an exact copy of that frame: no input is dropped or frozen, and the weights are identical.
PolicyEngineUKLatest.run() passes that frame back to policyengine-uk as UKSingleYearDataset(fiscal_year=year).
- policyengine-uk's
basic_state_pension, new_state_pension and additional_state_pension take the data year as min(simulation.dataset.years). They split state_pension_reported at the data year against that year's legislated rates, and scale the share to the period's rates (the triple lock). The direct run's data year is the survey year, 2024. The wrapper's is 2026, so the split uses the projected frame's state_pension_reported, which the dataset projection uprates by CPI.
- So the wrapper's State Pension is the reported amount uprated by CPI, and the direct run's is the reported amount uprated by the triple lock. For every uncapped flat-rate recipient (5,040 NEW and 2,295 BASIC records), the wrapper/direct ratio is 0.96967 = CPI growth 2024→26 (1.0578) / rate growth (1.0909). Under 2.123.4, 128.31 × 0.96968 = 124.42 exactly. All 27,875 (27,347) recipient records differ.
- Set the wrapper's four State Pension variables to the direct run's values, and income tax, Pension Credit, Housing Benefit, UC, Council Tax Reduction, household net income, HBAI net income and
gov_balance all match the direct run exactly. State Pension is the entire discrepancy.
Across the wrapper's default output variables, 26 of 68 differ, including in_poverty_* for 313–372 households and income deciles. Region-scoped runs carry the same error.
Fix
Year files keep the data year and its tables. run() then rebuilds the input that policyengine-uk builds itself: it projects the data-year tables forward and puts the stored year frame at the target year. policyengine-uk then anchors the State Pension on the survey year, as it does in a direct run. Year files written before the fix are regenerated by ensure_datasets and refused by load_datasets.
What happens
A UK population run through
policyengine.py(ensure_datasets→Simulation→ outputs) does not reproducepolicyengine_uk.Microsimulationon the same certified dataset and year. The State Pension differs, and so does everything downstream of it.Universal Credit and Child Benefit match.
Mechanism (shown record by record, real runs)
create_datasetsbuildspolicyengine_uk.Microsimulation(dataset=<base file>), takes the projected framesim.dataset[year]and saves it as the year file. The year file is an exact copy of that frame: no input is dropped or frozen, and the weights are identical.PolicyEngineUKLatest.run()passes that frame back to policyengine-uk asUKSingleYearDataset(fiscal_year=year).basic_state_pension,new_state_pensionandadditional_state_pensiontake the data year asmin(simulation.dataset.years). They splitstate_pension_reportedat the data year against that year's legislated rates, and scale the share to the period's rates (the triple lock). The direct run's data year is the survey year, 2024. The wrapper's is 2026, so the split uses the projected frame'sstate_pension_reported, which the dataset projection uprates by CPI.gov_balanceall match the direct run exactly. State Pension is the entire discrepancy.Across the wrapper's default output variables, 26 of 68 differ, including
in_poverty_*for 313–372 households and income deciles. Region-scoped runs carry the same error.Fix
Year files keep the data year and its tables.
run()then rebuilds the input that policyengine-uk builds itself: it projects the data-year tables forward and puts the stored year frame at the target year. policyengine-uk then anchors the State Pension on the survey year, as it does in a direct run. Year files written before the fix are regenerated byensure_datasetsand refused byload_datasets.