Repository navigation
Impute UC deductions in constituency and local-authority runs - #1905
Merged
Merged
Conversation
…t simulation uc_deduction_random_draw, uc_deduction_type_random_draw and attends_private_school decided between microdata imputation and household defaults by testing whether total weight was below 1e6. policyengine.py builds constituency and local-authority simulations by filtering rows from the national data (RowFilterStrategy), so those fell below the threshold and got household defaults: no UC deductions and no private school attendance. Each now reads Simulation.built_from_dataset (added in #1899). filter_dataset carries each person's attends_private_school into an extract, since one household alone would rank at the 100th income percentile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- attends_private_school no longer raises when no household has weight (the old 1e6 gate returned early; MicroSeries cannot rank zero weight). - The weight-scale property scales by powers of two, so invariance is exact; the national fixture carries national-scale weight, so the region test compares a national run with a constituency-sized one. - Replace the vacuous attends_private_school YAML cases with situation cases that fail under the old gate. - Document what filter_dataset carries, and that the changelog's filtered regions rank private school attendance locally. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…datasets - build_from_situation clears Simulation.built_from_dataset and build_from_ids (every data path) sets it, so a simulation rebuilt in place follows its new source. __init__ no longer sets it separately. - utils.data_source.built_from_data falls back to policyengine-core's is_over_dataset, so a core Simulation built over the UK system from data gets the imputations. The four gates (both UC draws, private school attendance, months_since_last_birthday) use it. - The region test's population now sits below the old threshold for people as well as benefit units, so its private school assertion fails under the old gate. New tests cover both rebuild directions and a core simulation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A calculated clone rebuilt from data kept core's simulation-level _fast_cache, so months_since_last_birthday (now enabled by the data-built flag) read a two-person person_weight against an 80-person population and raised. build_from_situation and build_from_ids now record the source and reset _fast_cache and _user_input_keys (a clone shares the latter with its original). The rebuild test calculates on a clone before rebuilding, in both directions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…al rank exists Measured on the Enhanced FRS 1.58.0 (2026), imputing private school attendance in every constituency run, ranked against the constituency's own incomes, sums to 1,485k pupils against 751k in the national run. So data under a million people of weight keeps imputing none, as on main, while household situations now never impute it (main imputed for a situation with over a million of weight). UC deduction draws are unaffected: constituency runs match the national run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
marked this pull request as ready for review
October 12, 2026 19:20
Collaborator
Author
|
Hand-off to the UK hub at head
Merge method: merge commit. |
Collaborator
Author
|
Merged by the UK hub (audit). Merged with a merge commit at the tested head
|
MaxGhenis
added a commit
that referenced
this pull request
Oct 13, 2026
Brings in #2119 (UC claims at legacy closure), #2373, #1905, #2177, #2305, #2306, #2325, #2376, #2379, #2380, #2388 and #2364. One conflict, in the UC docs notebook: kept main's new paragraph on claims at legacy closure and this branch's benefit cap sentence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
added a commit
that referenced
this pull request
Oct 13, 2026
This was referenced Oct 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1902
Reproducer: policyengine_uk/tests/test_data_built_imputations.py::test_region_filtered_from_data_matches_the_national_run; policyengine_uk/tests/test_data_built_imputations.py::test_small_data_built_simulation_gets_hashed_draws
Builds on #1899 (merged), which adds
Simulation.built_from_dataset. Current main is merged in.Summary
Three variables chose between microdata imputation and household defaults by testing whether total weight was below 1e6:
uc_deduction_random_drawuc_deduction_type_random_drawattends_private_schoolperson.household(...)), so in effect peoplepolicyengine.py builds constituency and local-authority simulations by filtering rows from the national data:
src/policyengine/countries/uk/regions.pyusesRowFilterStrategyonconstituency_code_oa/la_code_oa.filter_dataset_by_household_idsinsrc/policyengine/utils/entity_utils.pykeeps the rows without rescaling weights.run()insrc/policyengine/tax_benefit_models/uk/model.pywraps them inUKSingleYearDatasetand callsMicrosimulation.So every constituency and local-authority run fell below the threshold and modelled no UC deductions at all.
What changes
built_from_data(simulation)(new,policyengine_uk/utils/data_source.py). It is true for any simulation built from data, however little weight it carries. It readsSimulation.built_from_datasetand falls back to policyengine-core'sis_over_dataset, so a coreSimulationbuilt over the UK system from data counts too. Set State Pension age from date of birth, including the rise to 67 #1899'smonths_since_last_birthdaynow uses the same helper. UC draws hashbenunit_id, so a filtered region gets exactly the national run's draws.build_from_situationclears it, andbuild_from_ids, which every data path goes through, sets it. Previously only__init__set it, so a simulation rebuilt in place kept a stale value. The builders also drop core's simulation-level caches (_fast_cache,_user_input_keys), so a calculated clone rebuilt in place never reads arrays sized for its old population.clone()andget_branch()copy the instance__dict__, so the flag reachesbaselineand branch simulations.hasattr(person.simulation, "dataset")check is replaced bybuilt_from_data.filter_datasetcarries each person'sattends_private_schoolinto the household it extracts, as Set State Pension age from date of birth, including the rise to 67 #1899 does formonths_since_last_birthday. For the extracted year, the extract keeps the attendance it had in the full simulation. A multi-year dataset holding only that year recomputes later years.Invariants (stated and tested)
policyengine_uk/tests/test_data_built_imputations.py:uc_has_deduction,uc_deduction_combinationanduc_deductionsunchanged (Hypothesis). That covers totals from about a hundred to about 10^15. Powers of two scale exactly in floating point, so the property holds exactly.splitmix64_uniform(benunit_id)draws and some deductions.filter_datasetextract reproduces the full simulation's UC deductions and private school attendance. The test asserts that both sets are non-empty.Simulation(tax_benefit_system=system, dataset=<DataFrame>)gets hashed draws and private school attendance.Against a
git archiveof main (db2c36b), 7 of the 9 fail. Two pass on main by design: test 4 pins main's behaviour, and test 9's weight is large enough for main's draws. The first new YAML case (a 1e9-weight situation) also fails on main.Each fix was also checked by removing it and confirming its test fails:
filter_datasetcarry.__init__, and again without the cache reset (it reproduces the broadcast error).is_over_datasetfallback.Impact: constituency and local-authority runs, 2026
Real runs, following policyengine.py's path: filter the national tables by
constituency_code_oa/la_code_oa/region, buildUKSingleYearDataset+Microsimulation, and calculate 2026.UC deductions, summed over 632 constituency runs
UC deductions, summed over 363 local-authority runs
months_since_last_birthday, which spreads birthdays within the simulated population and so can move State Pension age status near the boundary. I did not trace every case.Private school attendance (decision d1435). The same runs, with attendance imputed in every data-built simulation as first proposed:
Caveats and follow-ups
shareholdingandcorporate_land_value, which feedcorporate_tax_incidenceandbusiness_rates. In a filtered constituency the whole national total lands on the constituency: for one constituency of 87 records, household net income is −£35.6bn in the constituency run against +£2.3bn for the same households nationally (1.56.16 data). This affects every constituency run in policyengine.py (follow-up task). The net-income change above is a difference, in which it cancels.SimulationBuilder.build_from_dict(..., simulation=existing)leavesis_over_datasetset when it overwrites a data simulation with a situation, so the fallback treats that core-only path as data. No caller does this, and policyengine_uk.Simulation's own rebuild is correct and tested. Replace a simulation's population when the builder writes a situation into it policyengine-core#611 fixes it in core. A follow-up policyengine-uk PR after this one will raise the core floor, drop the caveat indata_source.py, and test that path.Tests run
test_data_built_imputations.py,test_uc_deductions.py,test_state_pension_age.py,test_uc_rebalancing.py,test_dataframe_dataset.py(-n 4): 146 passed.tests/policy/baseline/contrib/labour/YAML: 5 passed.ruff format/ruff checkclean.~/reviews/policyengine-uk-1905-constituency-imputations/:review4_response.md: the fix belongs in core (now policyengine-core#611), and dropping the fallback would bring back the earlier finding.axiom: n/a: microsimulation imputation
🤖 Generated with Claude Code