Skip to content

UK spine: composition-faithful SPI income draw — bridge predictors and rank-preserving replacement (with-children UC residual) #840

Open

Description

@juaristi22

Problem

PR #835 (#832) made the SPI reporter channel truthful — reporter→positive-award conversion is now ≈100% by construction and the 1.24m dead-reporter mass is gone — but the four deferred UC with-children receipts survive it essentially unchanged (v18: single_with_children −34.2%, children_1 −27.9%, children_2 −34.4%, children_5_or_more −29.1%). Decomposing the single_with_children gap on the v18 calibrated weights (target 2,213,760; support 1,460k; gap 754k):

Component Weighted Owner
Dead reporters (reported, award-killed) 0 closed by #835
Take-up residue (award > 0, would_claim_uc false) 59k exhausted — 0.55 stands
Income/eligibility shape: lone parents with no pre-take-up award 400k, of which 195k SPI-drawn and 205k FRS-observed this issue (the SPI-drawn part)
Structural mass shortfall: only 1.919m lone-parent benefit units exist in the spine ≥295k separate item (concealed-family representation)

The income-shape signature is stark: SPI-channel lone parents pass the pre-take-up screen (max(0, uc_maximum_amount − uc_income_reduction) > 0) at 35% (0.351m of 1.004m) versus 69.5% on the base FRS channel (0.768m of 1.105m). The SPI channel hands lone-parent benefit units incomes that a lone parent rarely earns, so #835 correctly refuses to draw them as reporters — the defect moved from a hidden contradiction to a sized shortfall.

Why this happens

  • Stage 17 (spi_support_channel) samples FRS donor households uniformly within (region, capital-gains-clone, clone) strata; composition is not a stratum, and the donor keeps its benefit-unit structure.
  • Stage 18 (hmrc_spi_income_spine) stage 1 then overwrites every SPI-channel adult's 18 income components with a RegimeGatedQRF draw conditioned on age, gender, region only (SPI_SPINE_STAGE1_PREDICTORS; fit_weighted_qrf_stage1 in uk/spec/sources.yaml). Each row's regime is sampled from the gate's probabilities with an RNG uniform (_gate_draw_with_rng, microcosm-fit/qrf.py:950-962) and its magnitude at an RNG quantile (_draw_target_with_rng, qrf.py:965-989: quantiles = rng.random(n)). The donor's own observed income — and with it the composition↔income relationship the FRS carries — is discarded. A lone parent receives the income of a random same-age/gender/region taxpayer, and the taxpayer population is far richer than lone parents are.
  • Composition cannot be conditioned from the SPI side: the SPI 2022-23 public-use tape carries no child, dependant, or family-composition variable (Annex A, UKDS SN 9422 — the word "child" appears only in a footnote on the abolished additional personal allowance). The only relationship signals are MAIND (marriage-allowance claimant) and MCAS (married couple's allowance — partner born before 1935). The signal has to come from the FRS side of the pairing.

Proposed fix — one increment, two mechanisms

  1. Bridge predictors that exist on both sides. Extend the stage-1 predictor list with fields the SPI tape and the FRS spine share:
    • MAINSRCE (1 pay / 2 occupational pension / 3 sole trader / 4 partnership / 5 other / 6 claims case) ↔ FRS employment_status (+ self_employment_income/PENSION-style precedence) — the mapping is a translate-published-facts adjudication, documented coding by coding;
    • INDUSTRY07 ↔ sic_industry_division (frs_employment);
    • SPA ↔ age vs state pension age;
    • SEINC_NUM ↔ self-employment indicator;
    • MAIND ↔ is_married — a documented approximation (claimants are a subset of married couples with one low-earning partner), included as a weak couple signal, never as a lone-parent signal.
      A part-time retail employee then draws from part-time-retail-employee taxpayers, not from every 35–44 woman in the region; most of what composition does to income rides on these fields.
  2. Rank-preserving replacement. Supply the per-row draw quantile from the donor's observed FRS income rank within the predictor cell instead of rng.random(n) (and the gate's uniform likewise), so the FRS lone parent at her cell's 30th percentile receives the SPI cell's 30th-percentile income. This preserves the FRS composition↔income-rank joint while importing SPI's marginal — the fat top tail the channel exists for. Ranks are robust to the FRS's under-reporting of top-tail levels. To adjudicate: rank on the donor's FRS total income vs per component; tie/zero handling within the regime gate; whether the gate keeps its own stream.

Both mechanisms live in stage 18, so they move the pinned stage-1 output surface (_assert_income_stage_parameters), re-draw every SPI income column and, through the stage-2 chain (whose predictors are those incomes), every FRS-only fill on SPI rows — a whole-SPI-surface signed-difference campaign. The twin compare must show the diff confined to SPI-channel rows; the base channel stays byte-unchanged.

Companion (separate, calibration-side, not this increment): a joint target on high-income × has-children from HMRC's High Income Child Benefit Charge / Child Benefit statistics, so the solver cannot leave lone parents rich regardless of how the draw got there.

Explicitly out of scope

  • No take-up changes (the residue is 59k; 0.55 stands).
  • No composition-aware donor selection at stage 17 — it needs taxpayer-composition evidence the SPI tape does not carry.
  • Not the ≥295k structural lone-parent mass shortfall (the ONS lone-parent-household target fits exactly at 1.835m; the missing units are benefit units embedded in larger households) — its own item.
  • Not the B-lane (num_uc_children engine variable + numbered-child rebind, plan D5 of UK spine: condition SPI UC receipt on benefit-unit composition and add a UC-specific child count #832).
  • No SPI/WAS tail suppression; the 208-fact HMRC replay and its fences stay in force.

Acceptance criteria

  • Stage-1 predictors carry the bridge fields with adjudicated, documented mappings from the published SPI codings to the FRS columns.
  • Draw quantiles come from the donor's within-cell income rank; declared in the manifest; twin determinism preserved.
  • Before/after receipt by family type and channel: screened-eligible share of SPI lone parents (35% → toward the 69.5% base rate) and the no-award SPI lone-parent mass (195k → ?).
  • Twin compare: diff confined to SPI-channel person income columns and their stage-2 / reporter / coherence dependents; base-FRS rows byte-unchanged.
  • The stage's HMRC income-band replay and distributional-mass gates stay green (the SPI marginals are still the SPI marginals).
  • Rebuilt candidate: recalibration + T0–T6; re-measure the four with-children receipts with the childless cells, housing benefit, pension bands and CGT on the sibling watch; solver-pressure movement reported from calibration_diagnostics.json.
  • Every moved column signed with receipts before any exclusion changes.

References

🤖 Generated with Claude Code

Activity

  1. juaristi22 commented on Sep 2, 2026

    @juaristi22
    CollaboratorAuthor

    B-lane hand-off from #832: num_uc_children and the numbered-child rebind

    #832 closes with PR #835 (the reporter-channel fix); its second half — the UC-specific child count for the five DWP numbered-child cells — was split off as a separate lane (plan #832 D5 / A4) and is recorded here so it does not live only in a closed issue. It is a target-semantics correction, not a gap-closer: on spine-k about 78k weighted positive-UC benefit units change count band under the UC definition (one-child ≈ +1k support, two-child ≈ −5k, five-plus ≈ −3k).

    Why it has to be an engine variable. The target resolver (uk_runtime/measure_simulation.py, UKMeasureResolver.knows/compute) resolves filter variables only through tax_benefit_system.variables, and a person-grain boolean routed to benefit-unit grain collapses to ANY (bool_any_collapse_person_to_benunit), never a count. So there is no binding-layer escape hatch (value_expression covers +-joined same-grain columns on value_variable only, not filters).

    The change, in order.

    1. policyengine-uk: add num_uc_children — BenUnit, int, YEAR, benunit.sum(benunit.members("is_child_or_qualifying_young_person_for_universal_credit", period)). The flag already excludes looked-after children and is the one the engine's own child element, child-limit and disabled-child logic use (uc_child_index, uc_is_child_limit_affected, …), so the count matches "children on the claim" as DWP counts them, including 16–19 qualifying young persons.
    2. microcosm floor bump to the carrying release (packages/microcosm-build/pyproject.toml uk extra, uv.lock), with the UK WAS stage: split private pension wealth out of corporate_wealth (uk-data#452 M2) #750 lesson applied: check whether the new version newly recognizes loader columns (unknown_export_columns_excluded in the parity extraction) and, if so, re-record the licensed coverage/parity evidence from the same pinned artifacts (the ae9457a3 precedent).
    3. Rebind in uk/uk_population_targets.json: the five cells dwp.uc.households_children_{1,2,3,4,5_or_more} change their filter {"variable": "num_children", …} → {"variable": "num_uc_children", …} (operators unchanged: == 1/2/3/4, >= 5); update each notes — the "16–19 qualifying young persons … declared residue" sentence is discharged for these cells. Regenerate target_references.json and the membership report with tools/generate_uk_target_references.py --contract … --ledger-facts … --period 2025 --output … --membership-report ….
    4. Tests: test_uk_population_targets.py (allowed_filter_variables, currently {universal_credit, num_children, family_type}), the test_uk_ledger_targets.py fixture benunit table.
    5. No register re-freeze: filters are not in the frozen-register TargetSpec hash (registry.py), unlike Re-bind the DWP UC composition and TCL disability-children targets to the publisher's grain (#808, #809) #813's entity/measure flip — verify TargetRegistry.version is unmoved; verify_register_coverage.py's family counts and the headline range are unaffected.

    Declared residue, on purpose. The four family-type cells keep their binding: family_type derives LONE_PARENT / COUPLE_WITH_CHILDREN from the same age<18 is_child, so they carry the QYP omission this lane fixes for the numbered cells. #832 kept them out of scope; whoever takes this lane should say so in the cell notes rather than widen the change.

    Synergy with #835: the reporter-redraw stage already computes its UC child band from the same engine flag (summed in-stage), so once the engine variable exists there is one definition of "children on the claim" across the draw and the targets.

    Owner per the #832 plan: María.

  2. juaristi22 commented on Sep 7, 2026

    @juaristi22
    CollaboratorAuthor

    PR #879 is an incremental input and measurement correction; it does not complete #840 or close the UC support gap. The latest results below supersede the earlier comparison that used the wrong national VOA geography and generic UC family categories.

    What #879 covers

    • SPI income rebasing from the 2022 donor period to the 2024 build period, including the OTHERINV index correction and correct treatment of build-year FRS tax-free interest.
    • Preserving under-16 FRS inputs and conditioning the FRS-only pension fill on SPI pension receipt. The under-16 boundary is a modeling choice based on constructed donor support, not a documented SPI survey minimum; the declared nominal-flow limitations remain.
    • England-only national VOA bands and total, plus relationship-aware UC calibration family and child counts. The UC change corrects how awards are classified for calibration; it leaves the model's calculated awards unchanged.
    • A matched national-only calibration of the retained full control and treatment spines. Before either solve, all nine VOA matrix rows were checked against England masks and all 84 active UC payment-band rows against the corrected family definitions and full-register band edges.

    What changed in the matched comparison

    Both runs use UK 2.94.0/Core 3.31.0, Chronicle 6fb700e, the same 371 active targets, 1,500 epochs, seed 0 and maximum weight ratio 10. Measurement code is 117860ef; the committed comparison is at 4ccaabba.

    Metric Corrected control Corrected SPI treatment
    Calibration loss 0.021724747 0.013076866
    Targets within 10% 337/371 341/371
    England Band A error +0.049% −0.027%
    UC single-with-children error −31.365% −27.923%
    Total UC benefit-unit error −11.189% −10.006%
    Income-tax error −12.067% −12.709%
    Total state-pension error −7.909% −11.430%

    Loss falls 39.8%, but interest contributes 91.5% of the net objective gain. This does not establish that the joint income-tax/state-pension/UC problem is solved. All nine England VOA measures are now within 0.22%; the earlier Band A miss came from comparing a wider geography against England targets.

    Why the UC gap remains

    The main composition-preserving mechanisms proposed in #840—adjudicated shared SPI/FRS bridge predictors and donor-rank-preserving income replacement—are not implemented by this PR. Pension-receipt conditioning is narrower. The PR also does not add the missing benefit-unit composition support identified in this issue, or establish a new channel-by-family decomposition of that shortfall. The original v18 decomposition should therefore not be treated as a measurement of this candidate.

    The high-payment childless-couple problem is a separate support and modeling defect. After correcting family classification, neither spine contains childless-couple support from £26.4k annual UC upwards. The old £27.6–28.8k fit depended on two copies of one lone-parent family whose older child was counted as a partner. Dividend rebasing moved its award across a narrow boundary; it was never a sound childless-couple donor. Reweighting cannot populate an empty cell. The calibration correction also leaves the native model's standard-allowance relationship error unresolved.

    The £26.4–27.6k band was already excluded from calibration. The next two bands remain active and both miss by −100%. Across all families there are 100 payment-band references, of which 84 are active and 16 were already excluded. The highest labeled band's existing open-ended implementation is documented and held fixed in this experiment; its payment-boundary semantics still need correction.

    Exclusion and release status

    No new target exclusions have been signed or added in #879. All 44 measurement exclusions, existing reviewed fit exclusions, target values, take-up and gate thresholds were preserved for the controlled comparison. A fit deferral would change release acceptance, not repair support or improve the measured fit.

    Both runs remain release-blocked and export no calibrated H5. Treatment has four unreviewed fit failures: the two empty childless-couple bands (−100% each), self-employment income in the £50–70k income band (+34.485%), and state-pension income in the £20–30k band (+27.941%). The existing private-pension-count exclusion is stale in both runs; the two-child UC exclusion is also stale in treatment because those targets now fall inside the bound.

    Keep #840 open for the remaining income/composition work, with the wider build deferrals tracked in #796. Separate CGT work remains in #878 and #875.

    Corrected comparison · 371-target paired receipt and support audit · All UC bands and activation statuses.

  3. juaristi22 commented on Sep 16, 2026

    @juaristi22
    CollaboratorAuthor

    A finding from the #882 repairs work that belongs on this issue's SPI-channel lane (branch uk-uc-repairs-882, receipts in experiments/882-uc-repairs-receipts.md, tool tools/diagnose_uk_legacy_benefits.py).

    The A16 legacy-benefit misses (#867 Housing Benefit, #869 JSA, and the ESA rows) are mostly the SPI support channel, not FRS misreporting. FRS 2024-25 at design weights reports 1,559,991 Housing Benefit benefit units, 99.9 percent in rented households, against a DWP GB caseload of 1.94m in January 2025 falling to 1.46m by December; and 63k JSA benefit units (27 records) against DWP's 71,000 New Style claimants. Spine-q carries 1.21m GB Housing Benefit reporters: the frs channel's 758k are all renters, but the spi channel's 451k include 363k in owner-occupied synthetic households, which the engine can never pay. JSA: 161k of the spine's 181k contributory reporters are spi-channel units. ESA runs the other way: 1.06m raw against 791k on the spine, the channel under-carrying them. Household mass is split half and half between the channels (14.66m / 14.58m).

    The mechanism is the FRS-only QRF fill on SPI support rows (spi_support.py, FRS_ONLY_SPI_FILL_PERSON_COLUMNS): it re-imputes the reported benefit columns onto synthetic households from age, gender, region and the six income predictors, with no tenure or eligibility coherence, so a reporter's Housing Benefit lands in an owner-occupied household and JSA receipt is over-drawn. On the Q50f candidate the engine pays Housing Benefit to 235k units (£2.13bn against the OBR £12.10bn) and the JSA row measures 34k against 71,000 after the solve pares the reporters to fit the OBR JSA spend line.

    What the #882 PR does about it: gates the would_claim_uc draw to benefit units with an adult under State Pension age (a second, separable mechanism: 377k pension-age Housing Benefit reporters on Q50f were zeroed by a Universal Credit claim they cannot make), re-adjudicates the two A16 rows to 2026-12-08 on these receipts, and binds the Stat-Xplore Housing Benefit caseload by tenure as measured diagnostics. What it does not do: touch the SPI fill. The repair candidates for this lane are a tenure-coherent fill (zero the housing-cost benefits where the synthetic household does not rent, or add tenure and a means-test proxy to the fill's predictors) and a check of the JSA/ESA incidence the fill produces against the raw FRS shares.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions