Skip to content

Keep FRS-reported dividends and key them on person_id - #498

Draft
MaxGhenis wants to merge 5 commits into
mainfrom
frs-dividends-keep-reported
Draft

MaxGhenis wants to merge 5 commits into
mainfrom
frs-dividends-keep-reported

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

WIP — keep this PR a draft. Impact against the published 1.58.0 dataset is pending the hub's queued post-batch build and independent review. This changes a published dataset, including the base FRS H5 files, and lands only in a batch on Max's go (d833).

Part of #495; fixes the data cause of PolicyEngine/policyengine-uk#1948. Original author and commits retained. Round 1 merged main 2b9c1c2c, including #544 / 1.58.0 and the frozen policyengine-uk 2.122.2 / core 3.32.13 lock. The engine already contains uk#1950. Round 2 folds in the investigation session's review patch on top of a0d0e697, with no history rewrite or relock.

Mechanism

FRS account and person keys are household_id * 1000 + person. The old dividend calculation passed person.index to sum_to_entity; unmatched grouped keys became zero. frs_dividend_income and its create_frs call key on person.person_id, preserving account eligibility/gross-up rules. Raw 2024-25 checks reproduce £0.0398bn → £8.015bn, across 1,717 people, with gross4 weights and no orphan accounts. Annualisation uses 365.25 / 7 rather than 52: a 0.34% difference (0.343%).

The second loss was an SPI draw overwriting every original FRS person's reported dividends. impute_income now imputes incomes only on the SPI-donor copy and stacks it with unchanged FRS reports. Since #529, the SPI model predicts from age, gender and region within earnings groups; the former “alone” description is stale. Conservation tests cover FRS reports before calibration; SPI draws and calibrated weights can change total weighted dividends.

Conflict resolutions and review changes

Round 1's main merge kept both the dividend and Pension Credit helpers in frs.py; combined dividend person-ID keying with main's property-income helper; and kept main's SPI gainful-self-employment update while removing the FRS dividend overwrite.

Round 2's patch conflicts were resolved as follows:

  • imputations/income.py: retained Round 1's corrected gift-aid initialisation comment, removed the stale “Impute wealth” comment, and applied the earnings-group description.
  • test_imputation_source_flags.py: added capture/assertion of the stage-2 FRS training dividends before the appended Round 1 tiny-fixture and Hypothesis tests; all tests from both sources are retained.

The create_frs fixture now uses the real nonempty grouped join and a type-8 account. sum_to_entity documents the required key space and ndarray return. INVERTED_BASIC_RATE is defined once at module level; the duplicate WEEKS_IN_YEAR definition is removed. Precise small-cell aggregates remain permitted by Max's d932 / hub notice 9; no revision deletion or suppression is requested.

Tests at the Round 2 head

39 passed, 5 skipped across four targeted files (44 cases): test_frs_dividend_income.py, test_imputation_source_flags.py, test_legacy_benefit_proxies.py and test_cgt_band_donors.py. The five skips explicitly require absent FRS/enhanced-FRS files; no dataset build was run. Frozen worktree venv: Python 3.13.9, UK 2.122.2/core 3.32.13/data 1.58.0. Serial pytest (xdist is absent from the lock), one numerical thread and bytecode writes disabled. uvx ruff format --check . passes for all 293 files. Both targeted regression checks reject deliberately incorrect code: reverting the call site to person.index fails the holder assertion; training stage 2 on the SPI copy fails the reported-dividend assertion. The source files were restored after each check.

Tests retain the real income-draw application and ID-remapping stack path. Hypothesis checks eligible annual gross account dividends over 100 examples and FRS-half totals/person mappings over 50 examples. New coverage pins the actual create_frs call site (adult account holder gets dividends; child gets zero) and proves stage 2 trains on FRS-reported dividends rather than SPI draws.

#535 is resolved: main's d39371d5, included in the 10/8 batch (#544), bounds reduced-build gains within a factor of 6 in either direction, covering #498's historical 3.6–3.9× values. Full builds retain the strict 50% bound. The old local “main fails too” claim used two OA clones rather than CI's one and was incorrect for CI. The explicit TESTING=1 PE_UK_DATA_OA_CLONES=1 test_built_total_gains attempt produced 1 skip: Enhanced FRS dataset not available; its gains assertion was not exercised. No reduced dataset build was run by this job.

Impact pending the queued post-batch build

.job/BUILD_PLAN.md, .job/QUEUE_ROW.md and the aggregate-only measurement helper pin 7a9174a for the hub. Update existing queue row 13, currently QUEUED / NO GO, then run one seeded branch build via setup_build_worktree.sh and seed patch 348dd49, 512 epochs, one OA clone, frozen baseline inputs/cache and cached survey-trained models. Compare with the saved 1.58.0 artifact ~/PolicyEngine/_worktrees/uk-data-combined-1008/_out/efrs_combined.h5. Measure 2025/2026/2030 dividends split FRS/SPI, dividend income tax, UC caseload/spending, and weighted/unweighted counts of UC units with member dividends above £50k. No build or impact microsimulation has started.

The figures below are historical, comparing main b45c373 (before #529) with c9b203e, one unseeded production build per side. They establish mechanisms to measure again, not current-release impact:

Historical 2026 measure Main build Earlier #498 build
Dividends, FRS half £47.13bn £17.88bn (−£29.25bn)
Dividends, SPI-donor half £17.67bn £44.97bn
Dividend income tax, old PE-UK main £14.01bn £13.37bn (−£0.63bn before rounding)
UC units >£50k dividends, old PE-UK main 16.1k (12 records) 0 (0 records)
UC units >£50k dividends, #1950 35.8k (51 records) 6.6k (13 records)
SPI-donor dividends in units reporting positive UC £3.92bn £16.34bn
SPI-donor UC-reporting units >£20k dividends 44k 209k
SPI-donor UC spending >£20k dividends, #1950 £0.34bn £0.47bn
SPI-donor UC spending >£50k dividends, #1950 £0.054bn £0.110bn

Historical UC changed +£0.38bn under old PE-UK main versus −£0.66bn under #1950, where dividends never enter UC unearned income. The difference, about +£1.04bn, is the direct means-test channel from removing imputed dividends. The total movements also include these channels, alongside calibration:

  • Wealth re-imputation: wealth runs before income with capital_income as a predictor; it sees roughly £8.0bn rather than £0.04bn of base FRS dividends. At historical main weights, 1,018 FRS households (about 6%) had changed wealth; gross financial wealth rose £424bn (+7.8%), corporate wealth £131bn (+2.0%), and households above £16k savings plus corporate wealth were 13.72m → 13.71m. Corporate wealth also gates the old UC dividend term.
  • Stage 2: impute_frs_only_variables trains afresh on the FRS half's reported dividends during the build. Cached .pkl models do not freeze stage 2. It re-imputes benefit reports, pension contributions and tax-free savings income on SPI donors, while donor wealth and take-up remain separate coherence work.
  • CGT donors and downstream income: gains blending and band-donor selection use total income; historical donor sets were 61,212 → 61,210 benefit units. Consumption, VAT and services use household net income.
  • Base FRS files: both frs_2024_25.h5 and its tiny companion change, from roughly £0.04bn → £8.0bn of weighted dividends, separately from enhanced-FRS income preservation.

The historical metric named dividends_in_frs_uc_reporter_units_bn covers both halves (£7.81bn → £16.34bn). Small movements such as UC +0.5%, net income +0.7% or poverty ±0.2pp are not established from one unseeded build per side. Historical calibration comparisons use epoch 510, the last logged epoch, from 512 total epochs; the £12,570–£200k range contains 9 bands. The historical £300k–£500k dividend amount fit worsened 1.021 → 0.510 and banded dividends moved £60.6bn → £59.8bn against £71.8bn; the queued comparison must reassess these trade-offs.

Evidence remains in ~/reviews/uc-dividends-1948-2026-09-30/; public text contains aggregates only. Director dividends/net-to-gross conversion, owner-manager inputs, SPI donor wealth/take-up coherence and the matching microcosm change remain outside this PR (#495). The hub dispatches the independent Sol review at the final pushed head. Keep this WIP draft pending review, queued impact and d833 batch go.

MaxGhenis and others added 2 commits September 30, 2026 17:12
The SPI income model predicts from age, gender and region only; applying it to
the FRS half gave Universal Credit claimants six-figure dividends unrelated to
anything they reported (policyengine-uk#1948). Only the SPI-donor copy is now
imputed, as in the microcosm UK build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frs.py summed investment-account dividends to person.index while
account.person_id holds household_id * 1e3 + person, so only 8 people got
any (about £40m against £8bn). Extract the calculation into a tested helper
and annualise with WEEKS_IN_YEAR like the savings lines beside it. Correct
the income.py comments that relied on the near-zero FRS dividends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

UK hub (2026-10-12 12:05Z): the hub adopted this PR under d1280's fallback, because it has no live owner session. A Subfleet build job (adopt-ud498) is finishing it on top of this branch, with no force-push and the author kept. Its plan is in the triage at ~/reviews/uk-hub/jobs/stale-pr-triage/REPORT.md. An independent GPT-6.1 Sol review will follow at the job's final head. uk-data PRs land only in a batched release on Max's go (d833).

Retain both dividend and Pension Credit capital helpers; combine person_id dividend keying with main's property-income helper. Keep SPI gainful-self-employment updates while removing the FRS dividend overwrite. Keep the 1.58.0 frozen lock (policyengine-uk 2.122.2, core 3.32.13).
Add a tiny account-to-output regression using real income-draw application and ID-remapping stack output. Property tests conserve eligible annual gross dividends and FRS-half reported amounts. Ignore workspace-local hub job artifacts. Dividend regressions and repository formatting pass; merge-neighbour checks continue in the locked environment.
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

UK hub adoption hand-off — #498

Final head: a0d0e697b20a1845a1cf0be663942e2a61c11152 on frs-dividends-keep-reported, pushed with a normal fast-forward; original author and commits preserved. Main 2b9c1c2c merged in b73c585b; test/ignore step is a0d0e697. Draft status retained; do not merge outside a batch on Max’s go (d833).

Mechanism: account dividends now join to person.person_id at datasets/frs.py:1353, using the helper at frs.py:793; the grouped join’s unmatched-key zero-fill is at utils/datasets.py:24. The FRS dividend overwrite is removed at datasets/imputations/income.py:553; SPI-only imputation and real stacking retain FRS reports. Conflicts keep both dividend/Pension Credit helpers, main’s property helper, and its SPI gainful-self-employment update. Lock unchanged: UK 2.122.2/core 3.32.13.

Tests: 1,353 passed, 8 absent-data fixture skips across seven targeted files, including all eight dividend regressions. New tiny fixture goes through actual income draws and real ID-remapping stack output; two properties conserve eligible gross account amounts (100 examples) and FRS-half reports/person mappings (50 examples). Worktree venv reconciled with frozen all-extras sync; source/versions verified; serial pytest, one numerical thread, bytecode writes disabled. uvx ruff format --check . passes (293 files). The explicit TESTING=1 test_built_total_gains check produced 1 skip: Enhanced FRS dataset not available; its gains assertion was not exercised. #535 records the historic unrelated reduced-build failure; main already changes that tolerance to a factor of six. No fresh reduced-build result is claimed.

Impact remains pending. No dataset build or impact microsimulation ran. .job/BUILD_PLAN.md and .job/QUEUE_ROW.md pin this final head for ONE seeded 512-epoch build (348dd49, frozen baseline inputs/cache, one OA clone) versus the 1.58.0 uk-data-combined-1008/_out/efrs_combined.h5, then sequential aggregate measurements for 2025/2026/2030. Report FRS/SPI dividends, dividend income tax, UC caseload/spending, and UC units above £50k of member dividends. Historical £29.25bn/16.1k→0 figures are not fresh impact on the now-locked #1950 engine.

Hub next: dispatch the independent GPT-6.1 Sol review at this head; copy the explicit .job/ notes/scripts/logs (exclude caches/venvs), install the proposed queue row, write GO and run its build/finisher, then obtain d833 batch go. Queue installation/GO have not been performed by this job. No in-job review was run.

Workspace: /Users/maxghenis/.subfleet/worktrees/20261012-080035-adopt-ud498. Sandbox protects the original .git pointer; all job Git mutations used .job/git. Recover from the pushed branch or .job/adopt-ud498-final.bundle, not the default detached pointer. HANDOFF.md, PROGRESS.md, MECHANISM.md, exact test commands/JUnit logs and one-line handoffs.jsonl are in .job/.

Fold in the investigation patch on top of Round 1 without rewriting history.
Keep the account-to-output and conservation regressions, add create_frs
holder-keying coverage and assert stage 2 learns from FRS-reported dividends.
Correct earnings-group and join documentation, hoist the gross-up constant,
and remove the redundant annualisation definition.

Conflict resolutions retain the existing gift-aid explanation and all
Round 1 tests. Keep precise historical aggregates per d932/notice 9.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

UK hub adoption hand-off — #498 Round 2

Final head: 7a9174a56e43b94101a72f6062d49d08319d8c8c on frs-dividends-keep-reported, normally pushed above a0d0e697. Original authors/history and Round 1 main merge retained. PR remains a WIP draft; this changes a published dataset and lands only in a batch on Max's go (d833).

Folded in the investigation patch. income.py conflict resolution retains Round 1's corrected gift-aid comment and removes the stale wealth comment; the source-flags conflict adds the stage-2 training assertion before retaining all Round 1 account/output and conservation tests. Other patch hunks applied cleanly. The type-8 account fixture now pins the actual create_frs call with a real nonempty grouped join. Corrected earnings-group and ndarray/key-space documentation, hoisted INVERTED_BASIC_RATE and removed the duplicate WEEKS_IN_YEAR. Kept precise small-cell aggregates under d932 / notice 9.

Mechanism: utils/datasets.py:26 zero-fills unmatched grouped keys; datasets/frs.py:1350 now joins account dividends using person.person_id via the helper at line 795. imputations/income.py:511 imputes the SPI copy; stage 2 at line 531 trains on reported FRS dividends, and stacking at line 564 retains those reports.

Tests: 39 passed, 5 explicit absent-dataset skips across four targeted files. Frozen worktree venv: Python 3.13.9 / UK 2.122.2 / core 3.32.13; serial pytest and one numerical thread. uvx ruff format --check . passes for all 293 files, and whitespace checks pass. Both deliberate regression checks fail at the intended assertion with the wrong person key or training half; source restored after each. The explicit TESTING=1 PE_UK_DATA_OA_CLONES=1 test_built_total_gains attempt is 1 skipped: Enhanced FRS dataset not available; its assertion was not exercised. #535 is closed/resolved: d39371d in #544 uses a sixfold reduced-build guard, covering historical #498 values. The incorrect CI “main fails too” account is removed.

Impact pending: the corrected body marks b45c373/c9b203e results stale before #529. It explains historical UC +£0.38bn with old PE main versus −£0.66bn under #1950, alongside wealth re-imputation, fresh stage-2 training, CGT donors/downstream income and the base FRS H5/tiny change. Precise SPI-donor aggregates remain; the small corrections are 0.34%, 9 bands and last logged epoch 510.

No dataset build, impact microsimulation or in-job review ran. .job/BUILD_PLAN.md, QUEUE_ROW.md and measure_498.py pin this head for one seeded 512-epoch branch build (348dd49) versus the saved 1.58.0 artifact, then sequential aggregate measurements of FRS/SPI dividends, dividend tax, UC caseload/spending and >£50k dividend UC counts. The hub must copy the explicit notes/scripts/logs, update existing queue row 13 (currently NO GO), dispatch the independent Sol review at this head, grant GO, and run the build/finisher before d833 batch go.

Workspace: /Users/maxghenis/.subfleet/worktrees/20261012-231228-adopt-ud498-r2c. Git mutations used .job/git because the default metadata is sandbox-protected. Recover the final head from the pushed branch or .job/adopt-ud498-r2-final.bundle (prerequisite a0d0e69); the default detached Git pointer is unchanged. HANDOFF.md, PROGRESS.md, one-line handoffs.jsonl, test commands/JUnit logs and the precise build plan are in .job/. Task-created venv/caches are being removed after validation; hub should copy only the explicit artifacts.

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Independent code review (GPT-6.1 Sol, UK hub): APPROVE at 7a9174a5 for the next uk-data batch (d833). The PR stays a draft until the queued post-batch build (hub rebuild queue row 13) measures the impact. The build-plan nit (hash the raw prerequisites too) will be applied when the hub runs the build.

VERDICT: APPROVE
Head reviewed: 7a9174a

  1. [nit] job-r2/BUILD_PLAN.md:157: raw-input provenance records shared symlink paths, while HTTP inputs and cached models receive hashes. Include all raw prerequisites, including nested SPI inputs, in the hash manifest. Both builds currently reference the same raw targets; this does not block code approval.

No blocker or should-fix findings. The code is approved for the batch; keep the PR draft until the queued build is measured.

The mechanism is correct. datasets.py:26 groups by account-holder ID, reindexes to the supplied keys and zero-fills misses. The old positional keys lost almost all account dividends. frs.py:1350 now supplies person.person_id. The call-site test:615 uses a real nonempty join: the adult receives the type-8 account’s annual dividends and the child receives zero. With person.index, the adult would receive zero and assertion 635 would fail.

Stage 2 receives the untouched FRS dataset at income.py:531. Dividends enter its predictor frame and model fit. test_imputation_source_flags.py:199 explicitly asserts the reported training vector. The conservation property test:355 checks FRS totals and person mappings through real income assignment and stacking. No assertions or validations were weakened, and no skips or xfails were introduced.

Prior findings 1, 2 and 10 are addressed. test_cgt_band_donors.py:230 contains the inherited sixfold reduced-build guard; full builds retain the strict 50% bound. job-r2/PR_BODY.md:28 honestly reports the absent-dataset skip. Lines 34–55 mark b45c373 figures historical, correct the impact channels and numerical errors, and disclose SPI-donor outcomes. Current impact remains pending. Finding 3 is excluded under d932.

The build plan:5 supports one seed-0, 512-epoch branch build against the specified saved 1.58.0 artifact. Its baseline-model reuse and fresh stage-2 training decisions have evidence and hash checks. measure_498.py:137 covers FRS/SPI dividends, dividend income tax, UC caseload and spend, and weighted/unweighted UC-unit counts above £50k of member dividends, with source-additivity assertions.

Read-only static review; I ran no tests, builds or microsimulations. Existing job logs record 39 passes, five absent-dataset skips and both expected mutation failures. gh pr view --comments could not connect to GitHub; I reviewed the saved PR body and handoff for this exact head.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant