Repository navigation
Stop SPI-synthetic rows carrying benefit claims nobody observed - #514
Merged
Merged
Conversation
The second-stage QRF draws each SPI-donor person's benefit reports from age, gender, region and incomes, with no view of the benefit unit or of health. Those reports then act as existing claims in policyengine-uk. The take-up anchors, receives_benefits_in_own_right and ssmg_reported were also left as the FRS donor's. On SPI-donor rows, this zeroes the income-related awards (UC, Pension Credit, Housing Benefit, CTR, IS, tax credits, income-related ESA and JSA, SSMG), the out-of-work benefits (contributory ESA and JSA, incapacity benefit, SDA) and Child Benefit, whose only use is the take-up anchor. It then rebuilds the anchors and receives_benefits_in_own_right from the rows' own reports. The zeroed columns stay in the QRF chain, so the values of the reports kept do not change. Also adds hypothesis as a dev extra, for the property tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…efit flag Review of 96672af (subfleet 20261002-011737-spi-514-review): - IIDB, AFCS and bereavement support follow an injury, service or a death, not income, and the QRF drew them at 6.2, 2.4 and 4.6 times the FRS rate by weight. SPI rows now take the donor's own values. - The Child Benefit take-up flag keeps the donor's value: the award does not depend on the replaced incomes and the claim is for the same children. Only the UC and Pension Credit flags are redrawn. - The comment no longer says the model's means test replaces the zeroed income-related reports: in policyengine-uk 2.93.0 housing benefit, CTR, IS, tax credits and income-related ESA and JSA need a report, so SPI rows no longer get them. Evidence is now cited by weight, and Child Benefit counts 16-19 qualifying young people. - One assign_reported_takeup helper serves create_frs and the SPI rows, so the rate and the anchoring rule have one source. The SPI draws no longer depend on which columns are present. - Tests pin the three rule sets, use shuffled, gapped ids and check the rebuilt flags against reports that are not zeroed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e year and the UC redraw Round-2 review of eb14fd0 (subfleet 20261002-041419-spi-514-review-r2, APPROVE): the FRS comparison for ESA (contributory) rested on under 10 records; the take-up rates' year and the stage-two UC redraw had no direct test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
21 of 52 tasks
This was referenced Oct 2, 2026
MaxGhenis
marked this pull request as ready for review
October 4, 2026 10:27
Contributor
Author
|
Hand-off to the UK hub (owner session c56de936 / e09285c9 is closing).
|
Max approved #514 into the batched release with one change: CTR is no longer zeroed on SPI-synthetic rows. In policyengine-uk 2.93.0 a CTR claim needs a report, so zeroing it left those rows no claim route (-21% CTR in the earlier build). SPI CTR stays the stage-two QRF draw, as on main, until it is zeroed together with a household-level CTR imputation (#499). Moves council_tax_benefit_reported out of SPI_DONOR_ZEROED_PERSON_VARIABLES, pins the set of reports kept as drawn, and tests that SPI CTR equals the stage-two draw (not zeroed, not the donor's value). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
added a commit
that referenced
this pull request
Oct 7, 2026
The SPI-donor rules redraw would_claim_uc on SPI-synthetic rows, so a unit whose claimant and any partner have all reached State Pension age could get would_claim_uc there even though create_frs (#490) never gives it one. apply_spi_donor_benefit_rules now takes an optional per-benefit-unit mask, uc_pension_age_excluded, and clears would_claim_uc where it is set. Its one pipeline caller always computes the mask on the real target dataset with the same derivation as create_frs and passes it; a test checks that wiring. Integration commit for the 10/8 uk-data release (d833). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
Queued in release PR #544 for the 10/8 uk-data batch. It lands only on Max's go (d833). |
This was referenced Oct 7, 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.
This changes the published dataset. It ships only in the batched uk-data release on Max's go (d833); don't merge it on its own. No dataset was uploaded or released from this branch.
Ruling (Max, d821, 2026-10-05): approved into the batch with one change, made in deb16e9: council tax reduction (CTR) is no longer zeroed on SPI rows. SPI CTR stays the stage-two QRF draw, as on main. Zeroing it comes later, together with a household-level CTR imputation (alongside #499). Every other rule ships unchanged.
CTR is unchanged against main; its total still moves through reweighting. The impact figures below come from a rebuild of this head (deb16e9) measured against main (b45c373), both with #507's seed. On every record the two builds share, this head's SPI CTR reports are identical to main's, and at main's weights the CTR paid differs on fewer than 10 survey households. The 2025 CTR total is £2.09bn against main's £2.20bn (−£0.11bn, −5.0%), with £0.37bn on SPI rows against £0.49bn. That gap comes from reweighting, driven by the PR's other changes; no target constrains CTR itself. Zeroing SPI CTR moves to a later PR with a household-level CTR imputation (#499). See "Council tax reduction against main" below.
Problem
impute_incomestacks a 10,000-household copy of the FRS (household_is_spi_synthetic), replaces its incomes with SPI draws, and calibration gives those rows weight. On the main build (b45c373, policyengine-uk 2.93.0), they hold 8.62m of 31.09m households (27.7%).Their benefit columns come from two places:
impute_frs_only_variablesfills every*_reportedcolumn inFRS_ONLY_PERSON_VARIABLES, drawing each person's reports from their age, gender, region and six incomes. It sees nothing of their benefit unit (partner, children, rent, capital), their health or their history.create_frsbuildswould_claim_uc,would_claim_pcandwould_claim_child_benefit(anchored on the donor's reports),receives_benefits_in_own_rightandssmg_reportedbefore the SPI rows exist.policyengine-uk 2.93.0 treats a positive report as an existing claim, in three ways:
housing_benefit_eligible,income_support_eligible,is_WTC_eligible,is_CTC_eligible,esa_income_eligible,jsa_income_eligible,sdaandssmg.would_claim_housing_benefit,would_claim_IS,would_claim_WTC,would_claim_CTCandwould_claim_council_tax_reduction. Each isclaims_all_entitled_benefits | report > 0.claims_all_entitled_benefitssums reports over the whole simulation, so it is False in any dataset run.esa_incomeandjsa_income: the reported award less tariff income;council_tax_benefit, where there is no scheme;esa_contrib,jsa_contrib,incapacity_benefit,iidb,afcsandbsp, byadds.What that does on the main build (b45c373)
Data file (2024-25), calibrated weights:
receives_benefits_in_own_rightdisagrees with their own reports, weightedSPI report rates by weight, relative to FRS rows:
Benefits paid on SPI rows, policyengine-uk 2.93.0, 2025:
Change
apply_spi_donor_benefit_rules(imputations/frs_only.py) runs on the SPI rows after the QRF draw. It runs before the disability categories and flags are derived, because ESA, IIDB and AFCS feedis_disabled_for_benefits.Reports zeroed on SPI rows:
active.yaml). HB paid on SPI rows rests on fewer than 10 survey households, so its amount isn't shown. CTR would also need a report, so it is kept for now (see "Draws kept"). SSMG needs one of these awards and was a donor copy.Donor values restored: IIDB, AFCS and bereavement support. They follow from an injury, military service or a death, not income, and the QRF over-draws them (6.2x, 2.4x and 4.6x by weight). No target is set on them.
Draws kept:
would_claim_council_tax_reductionisclaims_all_entitled_benefits | council_tax_benefit_reported > 0, so zeroing the report left SPI rows no way to claim CTR. It will be zeroed together with a household-level CTR imputation (Build council_tax before council tax reduction; stop imputing reported CTR on SPI rows #499).Flags rebuilt from the rows' own reports:
would_claim_ucandwould_claim_pcare redrawn withassign_reported_takeup, the helpercreate_frsnow uses too. With no reports, every SPI unit draws at the take-up rate, because whether a synthetic family claims a means-tested benefit at its imputed income is unobserved. The draws use their own seed (101;create_frsuses 100) and don't depend on which columns exist.would_claim_child_benefitkeeps the donor's flag. The award doesn't depend on the replaced incomes, and the donor's claim is for the same children.receives_benefits_in_own_rightis rebuilt from the rows' own UC, JSA and ESA reports. That makes it False on every SPI row, so SPI 16–19-year-olds can count as qualifying young people.The QRF chain is unchanged. Every column stays in it, so the kept draws are bit-identical, and a test checks this. The cost is that the kept values were drawn alongside values that are later zeroed or restored.
create_frskeeps its draw order, so the base FRS doesn't change; the builds below confirm it table by table.Other effects of zeroing that follow from this change:
Invariants (tested; Hypothesis for 2–6)
receives_benefits_in_own_rightmatches the person's own reports. This is checked with those reports left unzeroed, so the test can fail.impute_frs_only_variables:test_council_tax_reduction_keeps_the_stage_two_draw). The property test for invariant 2 now covers CTR as one of the columns that must pass through unchanged.I broke the code in seven ways, and each failure was caught:
receives_benefits_in_own_rightnot rebuiltAfter the d821 change (deb16e9), three more breaks (patched in-process), each caught:
A one-off differential check ran
origin/main's ownimpute_frs_only_variablesand this head's on the same synthetic inputs (CTR drawn non-zero for some rows): SPI CTR and the other 14 kept columns are bit-identical.Tests (no dataset build): at deb16e9 the five core files (
test_spi_donor_benefit_rules,test_frs_only_imputation,test_reported_takeup_anchors,test_legacy_benefit_proxies,test_imputation_source_flags) give 36 passed. Every test file mentioning SPI,frs_onlyorcouncil_tax_benefit(15 files, run on the same code just before a formatting-only change to one assertion) gives 107 passed, 5 skipped (they need built FRS datasets) and 1 xfailed (the existingtest_hf_destinationsguard).test_frs_only_reported_values_correlate_with_training_patternnow checks the QRF's income gradient on carer's allowance, because UC is zeroed on these rows.Measured with real builds
Two production-settings builds, measured with policyengine-uk 2.93.0 (the lock):
The Problem section's figures come from an earlier build of main without the seed, so they differ slightly from the before column here (for example, CTR is £2.21bn there and £2.20bn here).
Both used 512 calibration epochs,
PE_UK_DATA_OA_CLONES=1and #507's seeded calibration dropout (348dd49, applied locally, at its default seed of 0). Every target download was served from the same frozen cache (0 misses; the request logs are identical). The after build finished on 5 October with exit 0. Re-running the audit and headline scripts on the before build, in the environment used for after2, reproduces its earlier outputs exactly.The CTR check also uses the earlier build of this branch: eb14fd0 + the same seed (ea6f3b5), which zeroed CTR.
Evidence:
~/reviews/uk-data-spi-reported-2026-10-01/on the build host (Max's Mac). The h5 files are FRS-derived, so they stay local.run_gated_build.shbuilds the worktree's commit withPE_UK_DATA_OA_CLONES=1.setup_build_worktree.shapplies the Seed the calibration weight dropout so builds are reproducible #507 seed commit 348dd49.cached_build.pypatchesrequests.getto servehttp_cache/.builds/{before,after2}.commit(exact heads and bases),.http(every request served from cache),.sha256,.started/.finished, and after2's gzipped calibration logs.analysis/):audit_spi_reports.py: benefits, reports and anchors;headline.py: tax, income and poverty;coherence2.pyandreport_rates.py;compare_builds.py: record-level equality, reported as counts;cells_and_decomposition.py: the disclosure counts and the split of each change;spending_check.py: the public-spending parts of household benefits;impact_tables.py: these tables.analysis/*_after2.*, plusaudit_after2_wafter.jsonandheadline_after2_wafter.json(this head's data at the earlier build's weights)cells_and_decomposition.jsonandspending_check.json. All of these are aggregates only.The base FRS is unchanged. All three tables are identical column by column (34,966 people, 18,850 benefit units, 16,288 households). The h5 checksums differ, but the table contents don't.
What else changes in the data. On the records the two builds share, the changes are the SPI rows' reports and flags, as intended, plus two knock-ons. Zeroing the taxable ESA and JSA (contributory) lowers some SPI rows' total income, and two later steps select on total income:
Pension contributions also differ on fewer than 10 records each on SPI and FRS rows, and output-area codes differ on a few households. The rail, bus and road-fuel spending columns differ too, because the build fits them to fixed targets at each build's own weights (see below).
Coherence on SPI rows, after:
receives_benefits_in_own_rightdisagrees with their reports (before: 2.48m weighted).Disclosure: every cell below was checked against the number of distinct survey households behind it. The enhanced FRS holds every household at least twice, as itself and as its capital gains clone. SPI copies, their clones and the CGT band donors repeat households again. Record counts therefore overstate survey records. Any cell resting on 1 to 9 survey households reads "suppressed (<10 records)", and so does any change computed from one.
Benefits, 2025:
Other report-gated benefits, £bn: Income Support 0.38 → 0.38; WTC 0 → 0; CTC 0 → 0; income-related JSA suppressed (<10 records) → suppressed (<10 records); incapacity benefit 0 → 0; SDA suppressed (<10 records) → suppressed (<10 records); SSMG 0.03 → 0.02.
Benefits, 2026:
Other report-gated benefits, £bn: Income Support 0 → 0; WTC 0 → 0; CTC 0 → 0; income-related JSA 0 → 0; incapacity benefit 0 → 0; SDA suppressed (<10 records) → suppressed (<10 records); SSMG 0.03 → 0.02.
Headline:
Where each 2025 change comes from. This splits each change over the 52,596 households in both builds:
Some rows are left out. Housing Benefit and JSA (contributory) are left out because their data change is the suppressed SPI-row amount. Household net income is left out because it includes capital gains, which were redrawn. 2026 splits the same way.
SPI rows' household weight fell from 8.63m to 8.17m (of 31.14m).
Council tax reduction against main
council_tax_benefit_reported, on 2,859 SPI people.What to read into these:
Interactions
would_claim_ucincreate_frs. Whichever of Calibrate Housing Benefit to DWP's GB figures by age group #490 and Stop SPI-synthetic rows carrying benefit claims nobody observed #514 merges second must apply the same exclusion to the SPI redraw, or Calibrate Housing Benefit to DWP's GB figures by age group #490'stest_built_dataset_has_no_pension_age_uc_claimantsfails. This is noted on Calibrate Housing Benefit to DWP's GB figures by age group #490.would_claim_pcfor every unit after imputation, filling SPI rows like non-reporters. So it supersedes this PR's PC redraw, and itsspi_synthetic_benunitsexclusion becomes redundant.council_tax_benefit_reportedon SPI rows by taking it out of the QRF chain. After ruling d821 this PR keeps CTR as drawn, so CTR zeroing lands with Build council_tax before council tax reduction; stop imputing reported CTR on SPI rows #499. Both PRs defineSPI_DONOR_ZEROED_PERSON_VARIABLES, so Build council_tax before council tax reduction; stop imputing reported CTR on SPI rows #499 must be reconciled when it rebases onto this one, andtest_rule_sets_are_pinnedpins CTR as kept, so it fails until Build council_tax before council tax reduction; stop imputing reported CTR on SPI rows #499 updates it. Build council_tax before council tax reduction; stop imputing reported CTR on SPI rows #499's test asserting that the zeroed and imputed lists don't overlap contradicts this PR's design for the other columns, so whichever merges second drops that assertion. Both addhypothesis>=6.168.3the same way.Not in this PR
employment_status,hours_workedand the ESA/JSA proxy flags built from them, which can contradict the imputed incomes.ssmg_reportedis annualised ×52 on a one-off grant; that's harmless, because only> 0is read.Reviews:
REQUEST_CHANGES, addressed in eb14fd0.axiom: n/a: dataset construction in uk-data; no policy rule encoded.
🤖 Generated with Claude Code