Skip to content

Release the third uk-data batch - #608

Merged
MaxGhenis merged 105 commits into
mainfrom
release/uk-data-2026-10-19
Oct 19, 2026
Merged

MaxGhenis merged 105 commits into
mainfrom
release/uk-data-2026-10-19

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

This PR releases the third uk-data batch from release/uk-data-2026-10-19, based on main a43e8c519 (1.58.2): the 14 independently approved heads below, plus three release-only commits (21d5dbf9, cb92e401, 6ae9639b).

Max's go: d1621 (2026-10-17) for the batch, and d1678 = A (2026-10-19): publish with #571, merging past the failing 32-epoch CI fit gate.

Release decision and measured impact (UK hub, 2026-10-19)

The failing check. "Calibration fit against the base" fails at 6ae9639b (run 39194494219; comparison comment).

  • Local targets within 10% fall from 79.2% to 75.0% over the 11,228 targets both builds have. The allowance is 2 points.
  • National targets within 10% move from 65.1% to 64.8% over 627.
  • Every other check passes.

CI builds with TESTING=1, which trains for 32 epochs. The publish build (push.yaml, make data) sets no TESTING, so it trains for 512 (create_datasets.py:80).

Cause. Seeded leave-one-out CI builds at 32 epochs (#610–#617) trace the drop to #571, mainly constituency self-employment amounts and ages 30–70.

Fit at production settings. These are seeded CI builds at production settings (TESTING=0, 512 epochs), on diagnostic branches. Published 1.58.2's figures come from its own unseeded publish build (run 38741509997).

Build Local within 10% (11,228 common) National within 10% (627 common)
Published main, 1.58.2 92.7% 88.0%
This release (cb92e401, #621) 91.9% 86.6%
Without #571 (#619) 92.1% 88.0%
Without #571's prior raking (#620) 92.2% 86.9%
  • Against published 1.58.2: the release is 0.8 points lower on local fit and 1.4 points lower on national fit, inside the gate's own 2-point allowance.
  • These builds measure the release code. 6ae9639b differs from cb92e401 only in one known-gaps entry's reason text; no target, status or build setting changes.
  • The release coverage gate passes at production settings at cb92e401 ([DO NOT MERGE] Batch-3 gate check: fixed release cb92e401 at production settings #621). Its 17 stale named gaps only warn.

Aggregates at production settings (policyengine-uk 2.122.2, £bn, 2025 and 2030). "Without #571" (#619) is the same seeded build with #571's merge reverted, on the release before cb92e401. cb92e401 leaves the fit and these aggregates unchanged: #618 and #621 agree. "#571's effect" is the difference between the two builds, and it also carries the build-to-build spread that #598 describes.

Metric Release 2025 Release 2030 Without #571 2025 Without #571 2030 #571's effect 2025 #571's effect 2030
Capital gains tax 25.71 30.62 12.67 15.15 +13.04 +15.47
Income tax 315.33 405.78 313.02 402.96 +2.31 +2.82
VAT 317.68 359.01 315.17 356.18 +2.51 +2.83
Universal Credit 79.46 84.54 79.42 84.34 +0.04 +0.20
Household market income 1,799.67 2,113.10 1,729.81 2,030.36 +69.86 +82.74
Household net income 1,764.58 1,966.29 1,707.78 1,899.07 +56.80 +67.22

Not measured here. No matched production-setting comparison of the whole batch against 1.58.2's aggregates was run.

  • CI's 32-epoch aggregate comparison in run 39194494219 hasn't converged: its base build gives CGT £52.3bn, against £12.7bn at 512 epochs without Start capital gains on HMRC's 2024-25 size-of-gain table #571. Don't read it as this release's impact.
  • After publication, the hub will compare the published 1.58.2 and new files on policyengine-uk 2.122.2, as was done for 1.58.1.

Publication. push.yaml builds the published files at production settings without a seed (#507 isn't in this batch). Published totals can therefore differ from the seeded figures above by the build-to-build spread (#598).

Follow-ups:

Assembly record

The assembly job ran no dataset build and no production calibration. The hub's builds and measurements are in the decision section above.

Assembly-time CI at 21d5dbf9 (superseded by the decision section above): Run 39040600634 completed: six checks passed; the final comparison check failed because the share of 11,228 common local targets within 10% fell from 79.4% to 75.1% (−4.3 percentage points), exceeding the 2-point allowance. National fit moved from 64.9% to 64.6% over 627 common targets. The comparison ran successfully and deliberately failed its regression gate; no missing artifact or test-integration error was reported. Distribution comparison passed with no flags; aggregate checks passed with zero newly broken checks, four failures present on both sides and 446 moves flagged for review. The cause of the local-fit change is unresolved, particularly given #598. Approved member computations and gate thresholds are unchanged. See the CI comparison comment.

Members and merge order

Each member is merged with git merge --no-ff, preserving its approved commits. Order: #604, #557, #595, #571, #589, #537, #561, #592, #594, #599, #600, #601, #602, #603. #601 is included as specified in ~/reviews/uk-data-release-batch-2026-10/batch3-decision.md and the 14-member d1621 approval. #599, #600 and #602 each receive a merge commit; #602 contains the first two.

Order PR Approved head Title Independent review Effect
1 #604 29ca7542 Match ONS age targets to the FRS top code of 80 ~/reviews/uk-hub/reviews/ud604/review-r2-final.md Combines national and regional ages 80+ to match the FRS adult-age top code.
2 #557 473adafc Calibrate SPI income bands to their projected values, and map the 2023-24 outturn to 2023 ~/reviews/uk-hub/reviews/ud557/review-r2-final.md Combines SPI target years by name so calibration uses projected income-band values.
3 #595 0785612c Fail the calibration build on targets it drops or cannot fit ~/reviews/uk-hub/reviews/ud595/review-delta-full.md Adds target-coverage checks; integrates #604/#557 with 17 zero-signal and three duplicate exemptions.
4 #571 77b6527b Start capital gains on HMRC's 2024-25 size-of-gain table ~/reviews/uk-hub/reviews/ud571/review-r2-full.md Uses HMRC’s 2024–25 gain distribution and preserves calibration priors through twinning.
5 #589 a2eedce4 Fix local UC target alignment by area code ~/reviews/uk-hub/reviews/ud589/review-r2-final.md Aligns local UC targets and corrects Scottish income, age and housing target ordering.
6 #537 f5de403e Stop calibrating OBR total NICs on ni_employee ~/reviews/uk-hub/batch-reviews/ud537-r2/review-r2-full.md Stops calibrating total OBR NIC receipts against employee NICs alone.
7 #561 98b2583c Read approved training from the FRS TRAINEE flag for dependants ~/reviews/uk-hub/batch-reviews/ud561-r2/review-r2-full.md Derives dependants’ education/training eligibility proxy from FRS TRAINEE.
8 #592 133632af Build OBR targets only from hash-verified EFO workbooks ~/reviews/w3-uk-data-gates-2026-10-11/sol-delta-592b.md Reads OBR targets from hash-verified EFO workbooks with recorded provenance.
9 #594 cac704bb Save the calibrated weights from the final optimiser step ~/reviews/w3-uk-data-gates-2026-10-11/sol-delta-p3s.md Saves weights from the final optimiser step.
10 #599 dc81b55e Compare each pull request's calibration fit with its base build's ~/reviews/w3-uk-data-gates-2026-10-11/sol-delta-p3s.md Adds before/after calibration-fit comparison in CI.
11 #600 45d47428 Measure each build's HBAI distribution and single ages against its base ~/reviews/w3-uk-data-gates-2026-10-11/sol-delta-p3s.md Measures HBAI distributions and single-age fit alongside the comparison.
12 #601 0cf62aaa Read Scottish council tax targets from a hash-verified committed workbook ~/reviews/w3-uk-data-gates-2026-10-11/sol-review-600-601.md Pins and hash-verifies the Scottish council-tax workbook, removing the download fallback.
13 #602 08226999 Compare each build's national aggregates with its base's, model fixed ~/reviews/w3-uk-data-gates-2026-10-11/sol-delta-602b-603c.md Adds national aggregate comparison on the same locked model, withholding suppressed HBAI totals and deciles.
14 #603 5881ce3e Stop uploading raw calibration logs as public Actions artifacts ~/reviews/w3-uk-data-gates-2026-10-11/sol-delta-603d.md Stops uploading raw calibration logs as public workflow artifacts.

All member heads were checked against GitHub before merging. Main is unchanged from the approved base. #595’s approval at 6e03d4c1 carries to 0785612c: the hub verified that its additional commit changes only a YAML header comment and leaves parsed YAML identical (BATCH.md, 2026-10-16 10:30Z entry).

Merge resolutions

All 14 members have been merged in the stated order with merge commits, and every approved head is verified as its merge’s second parent. Resolutions retain the approved members’ computations:

Each conflict resolution was recorded with git show --remerge-diff; details are in .job/RESOLUTIONS.md.

Integration tests found one additional interaction, corrected in release-only commit 21d5dbf9: policyengine_uk_data/targets/build_loss_matrix.py:175 now iterates over #595’s selected all_targets, retaining #537’s helper for its invariant tests. The automatic merge had selected the helper’s list instead and bypassed the coverage gate’s selected/emitted list. The correction preserves #595’s approved first-name/level selection without changing target formulas or values. #537’s offline OBR mapping/conflict fixtures at policyengine_uk_data/tests/test_obr_target_conflicts.py:85 and test_obr_target_mapping.py:42 now use #592’s verified committed-workbook API, retaining their assertions and blocking HTTP. The workbook-target expectations in test_obr_sources.py:68 and :367 change from 25 to 24 because #537 removes obr/ni.

Generated-file checks reproduce approved bytes: the production SPI generator reproduces incomes_projection.csv from committed public inputs, SHA256 39445e49c9cb101fffd99185cf41d94a78d6c5a9fd93c99d8ea027593a7ea475, identical to #557. The production ONS generator reproduces the 80+ CSV from four existing pinned public ZIPs, identical to #604’s committed bytes. The OBR manifest has the semantic parity described above. No generated-file diff remains.

Release-only changes

Release notes and limitations

The limitation wording and release requirements come from ~/reviews/uk-data-release-batch-2026-10/BATCH.md and batch3-decision.md. The annual allowance limitation above is reproduced verbatim from BATCH.md.

Validation

  • Release-only UC known-gap fix cb92e401: locked Python 3.13 suite (test_target_coverage.py, including all coverage-gate tests; test_target_registry.py; test_uc_payment_distribution_targets.py) with four workers: 57 passed, 1 dataset-dependent skip. uvx ruff format --check . (295 files), uv lock --check, typed changelog validation and git diff --check pass. Release named allowances are now 38 zero_signal / 3 duplicate / 15 no_value_for_year; testing keeps 17 named zero_signal plus its UC pattern. No dataset build ran. The hub's production-setting gate check passed ([DO NOT MERGE] Batch-3 gate check: fixed release cb92e401 at production settings #621), and the independent review of cb92e401 approved it with one wording nit, folded in at 6ae9639b.

  • All 14 approved heads checked before assembly. Main refreshed before publication and remains a43e8c519 (1.58.2).

  • uv lock --check passes with Python 3.13; locked and installed policyengine-uk is 2.122.2. The only lock refresh updates this project's stale version from 1.58.0 to 1.58.2; all dependency versions are unchanged.

  • Every member's own tests ran in one foreground suite with four workers. The integration corrections pass a targeted rerun: 48 passed, 1 dataset-dependent skip.

  • Standard make test, four workers with one numerical thread each and frozen public-input replay: 2,532 passed, 134 skipped, 1 expected failure. Missing datasets account for 132 skips; the other skips are the known NISRA endpoint and optional bundle tooling. No dataset was downloaded or built locally, and no production calibration ran.

  • Fit/distribution tests with -W error::RuntimeWarning: 41 passed. Artifact guard: 93 passed. Coverage gate: 29 passed, 1 dataset-dependent skip.

  • Combined-tree named known gaps reproduce zero_signal 17, duplicates 3, and no_value_for_year 15. A fresh source-registry replay independently confirms exactly 3 duplicate names, with live HTTP disabled. Actual combined-matrix signal measurement remains pending the hub's seeded build.

  • SPI projection regenerated with the production generator from committed public aggregates: SHA-256 39445e49c9cb101fffd99185cf41d94a78d6c5a9fd93c99d8ea027593a7ea475, exactly the approved member output. ONS 80+ CSV regenerated from existing pinned public sources and is byte-identical. OBR manifest regenerated with every field unchanged except removing Stop calibrating OBR total NICs on ni_employee #537's obr/ni requirement.

  • uvx ruff format --check .: 294 files already formatted. Changelog-fragment check against origin/main, and git diff --check: pass.

  • Draft release PR CI completed: six checks pass; calibration-fit comparison fails on the local-fit regression described above. Both CI builds, repository tests (2,662 passed, 4 skipped, 1 expected failure) and all report producers passed. The built-data coverage test passed, though CI logs do not print actual zero-signal/duplicate counts. The completed logs and generated comparison comment were read; no integration-only fix is indicated. The hub must investigate the measured regression in the pending matched-seed build.

Reproducer: policyengine_uk_data/tests/test_obr_universal_credit_target.py::test_target_matrix_counts_gb_households_only

axiom: n/a: data release integration with no policy rule changes

Reviews.

  • The release-only review at 21d5dbf9 was APPROVE WITH NITS (comment).
  • The review of cb92e401 was APPROVE WITH NITS (~/reviews/uk-hub/reviews/ud608/review-r2-full.md). Its one wording nit is folded in at 6ae9639b.
  • The hub checked that delta: it changes only the reason text in known_gaps.yaml.

🤖 Generated with Claude Code

MaxGhenis and others added 30 commits October 4, 2026 21:37
The cash-receipts parser targeted Table 3.8 "National insurance
contributions" (total NICs, £200.08bn in 2025-26) on the ni_employee
variable, which _parse_nics also targets at the Class 1 employee figure
(£49.54bn). The two targets on one matrix column contradicted each other;
the seeded constituency calibration on main b45c373 ended with obr/ni at
0.242 of target.

Drop the row rather than retarget it: the Table 3.4 class targets
(employee, employer, self-employed) already cover every NIC class the data
populates, and a total target on total_national_insurance would conflict
with them through the statutory payment recoveries and "Other NIC" lines
PE-UK does not model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- test_obr_target_mapping: pin the variable of every target parsed from the
  committed receipts workbook, the Table 3.4 rows behind the NIC class
  targets, and the source identity that rules out a total-NICs target.
- test_obr_target_conflicts: no two calibration targets whose matrix column
  is the same plain variable sum or count may disagree on a year's value,
  checked over the OBR targets offline and over every target the national
  matrix uses; it flags the removed obr/ni mapping, and a seeded random
  check holds the grouped check to its pairwise definition.
- build_loss_matrix.calibration_targets: the target list create_target_matrix
  already built inline, extracted so the test checks the same set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PolicyEngine UK's year N is tax year N to N+1: convert_to_fiscal_year_parameters
gives every parameter, the uprating indices included, its value at 30 April N.
hmrc_spi mapped the 2023-24 SPI to 2024 and hmrc_cgt followed, so both treated
2023-24 outturns as 2024-25 values and every projected year carried one year
too little uprating. The FRS release (2024-25 -> 2024), OBR targets, the SPI
microdata (2022-23 -> 2022) and #533's salary sacrifice tables already use the
start year; the end-year mapping arrived without stated reasoning in the
February 2026 registry refactor (de061fc).

- _SPI_YEAR and _CGT_BASE_YEAR move to 2023; incomes_projection.csv is
  regenerated from the same ODS (its 2023 rows equal main's 2024 rows).
- The CGT base year is now gated at the £6,000 AEA the outturn was realised
  under.
- Tests tie _SPI_YEAR to the tax year in the configured ODS name, check the
  CGT base-year AEA against policyengine-uk, and (as in #541) check that the
  committed projection is the SPI table uprated with the committed factors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
build_loss_matrix fetches targets without a year filter, and the registry
keeps only the first target of each name. hmrc_spi emitted each band twice:
the ODS outturn (base year only), then the projection (later years). The
projected target was dropped, so _resolve_value carried the base-year outturn
forward unuprated: the 2025 calibration has targeted raw 2023-24 SPI values
since the February 2026 registry refactor. get_targets now merges the two
into one target per band, and a test checks every projected band, variable
and year through the calibration's own resolution path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
derive_is_in_approved_training_from_frs_person read a `train` column. No FRS
release has one (2020-21 to 2024-25 carry TRAIN2 and TRAINEE), and the
function returned False when the column was missing, so every record was
coded as not in approved training.

DWP's FRS counts a 16- to 19-year-old as a dependent child only when in
full-time non-advanced education or unwaged government training. In FRS
2024-25 all 20 child-table records aged 16 to 19 outside full-time education
have TRAINEE = 1 (33.0k grossed), and uk-data coded every one as failing the
qualifying-young-person education-or-training condition.

Read TRAINEE = 1 (yes; the same yes/no format as EDUCFT), and fail the build
when the column is missing instead of defaulting to False.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #561 (Subfleet 20261010-193520-ud561-review-r1) found that an
adult-table trainee aged 16 to 19 would become a qualifying young person of
their own benefit unit. FRS 2024-25 has none, but 2020-21 has one and 2023-24
three. The FRS has already decided those records are not dependants, and the
adult TRAINEE question does not say the training is unwaged, so it may be
training under a contract of employment (UC reg. 5(3)).

- Flag TRAINEE = 1 on child-table records only. On FRS 2020-21, 2022-23,
  2023-24 and 2024-25 the result equals the child-table records aged 16 to 19
  outside full-time education exactly (8, 26, 16 and 20 records).
- Say why TRAINEE and not TRAIN2: `train` was TRAIN2's question name, only the
  adult table has TRAIN2, and its value labels are not in the repo.
- Property test over numeric, float and string codes and adult/child status;
  a create_frs test with an adult-table trainee in the same household.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Delta review r2 of #561 (Subfleet 20261010-201405-ud561-review-r2, APPROVE
WITH NITS at c952694):

- The docstring said a dependant outside full-time education "carries
  TRAINEE = 1" as if it were guaranteed. It is what the four FRS releases
  checked show, so say that, and call the adult exclusion a proxy.
- The adult-trainee fixture set age80 = 18 but kept age = 30, and create_frs
  adds the two, so it built a 48-year-old. Set age = 0 and assert age 18.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r2 (APPROVE_WITH_NITS): with the live ODS download failing, the merge
test passed even without the merge, because only the projection targets were
emitted. The test now serves get_income_band_table from incomes.csv, checks the
outturn year as well as the projected years through the calibration resolver,
and requires every SPI target to carry all of them. The comment now names
30 April N, the date convert_to_fiscal_year_parameters samples.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 1.58.0 enhanced FRS carries £53bn of gains above the annual exempt
amount, and -£15bn of losses on 1.4m people, against the £65.9bn the
build targeted and the £119.3bn HMRC now reports for individuals in
2024-25 (uk-data#570).

The calibration started 17 times over on CGT taxpayers and 5 times over
on gains: the has-gains copy of each zero-weight SPI household kept a
zero weight, so it received a full zero-weight prior, the same as its
no-gains original. After 512 epochs the taxpayer count was still 56%
over while the £12,300 to £250,000 bands had overshot to 23-64% under.

- Split every household's mass between itself and a has-gains twin by
  the Advani-Summers share for its income band, in closed form, and
  split a zero-weight household the same way.
- Give no twin to draws at or under the lowest HMRC band limit, losses
  included: no target describes them.
- Stack donors only for the bands from £500,000 up.
- Scale the calibration's starting weights so each HMRC size band's
  taxpayer count begins on target (rake_priors_to_targets).
- Target Table 2.1a for 2024-25 (2026 release), individuals only, from
  the £3,000 exempt amount up, with the aggregates as the band sums.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The has-gains copies of zero-weight SPI households now carry a weight,
so that the calibration starts from the right share of each. Salary
sacrifice, student loan plans and Pension Credit take-up count people on
survey weights and draw per record. Run before capital gains, they see
neither those weights nor the copies, and a copy matches its original in
everything but the gain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- test_obr_efo_fallback: the receipts and NICs set from #536 listed
  obr/ni, which this branch no longer emits.
- test_obr_target_conflicts: #490/#530 added Target.countries, and
  create_target_matrix zeroes a column outside them, so the column key
  now includes countries. The random differential check uses Hypothesis,
  now a dev dependency, and draws country restrictions too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- obr.py: name what PE-UK lacks (statutory payment recoveries, Class 1A,
  1B, settlements, unallocated) and say ni_class_3 exists but is unfilled;
  the _parse_nics docstring no longer claims it covers Class 3.
- test_obr_target_conflicts: compare every pair in a shared column (isclose
  is not transitive), key countries as a set (restrict_to_countries uses
  np.isin), guard the registry-wide test against a silently empty registry,
  draw carry_forward and both orders of a two-country restriction, and pin
  the tolerance edge with an example.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sol review r2: the pairwise reference checked a pair only in its own
listed years, so two carried-forward targets that disagree in a year only
a third target lists were reported by the grouped check alone. The
reference now uses the column's full year set, and a regression pins the
reviewer's three-target case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The OBR targets came from whatever obr.uk served, falling back to
committed workbooks matched by URL substring, with the vintage label
taken from sources.yaml rather than the bytes, year columns fixed by
letter, and missing rows logged and dropped.

A manifest per EFO vintage (targets/source_manifests/) now names the two
workbooks with their SHA-256, size and own title, the unit and financial
years every parsed table must head, and the targets they must yield.
Builds read only the verified committed bytes; each table's years come
from its header row; a missing target or year, or an undeclared one,
fails the build. `python -m policyengine_uk_data.targets.obr_sources
ingest` adopts a new vintage from browser-downloaded workbooks and
prints each target's change.

On the March 2026 bytes all 29 OBR targets are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
calibrate_local_areas saved and returned the weights only when
epoch % 10 == 0, so a 512-epoch build shipped the state after step 511
and a 32-epoch build the state after step 31, dropping the last update
(#456). Save at the last epoch too, in both the verbose and the
non-verbose loop, so the saved file, the returned dataset and the
logged fit are the final optimiser state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
create_target_matrix skipped a target whose year did not resolve with no
log line (#456), skipped one whose column raised with a warning, and
accepted columns no household contributes to (#593). It now records each
offered target's status and fails unless every target is in the matrix
with a finite value and signal, or is listed in targets/known_gaps.yaml
under that status with a reason and an issue. Errors and non-finite
values can never be listed; a listed gap that no longer applies is
logged as stale.

known_gaps.yaml lists the 34 gaps measured on uk-data 1.58.1 at the
calibration year 2025: 15 two-child-limit targets dated 2026 only, and
19 targets with no signal. The matrix built is unchanged. The diagnostic
get_loss_results and the CPS transfer reweighting opt out.

`python -m policyengine_uk_data.targets.coverage` reports every target's
status on a dataset (names, statuses and counts only).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pull request CI now also builds the PR's base, in the same way as the
head (TESTING, one clone) and with the head's lockfile, so both run one
model and environment. A new job compares the two final-epoch fits from
their calibration logs. It fails when the share of national or local
targets within 10% falls by more than 2pp, flags national targets whose
error rises by more than 0.2, and posts the comparison on the PR.

The thresholds (targets/fit_policy.yaml, adopted under d1344) come from
five builds of the same data code: their shares moved by at most 0.93pp
(national) and 0.69pp (local), and they flagged 0 to 6 targets per pair.
Per-target moves only flag while #598 (two weight sets from one code)
is open. Only calibration logs are uploaded, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n unit

Sol review of #592:
- The manifest's vintage, its label and both workbook titles must agree
  (march_2026, "March 2026", "March 2026 Economic and fiscal outlook ...
  receipts"), at load and at ingest, so verified March bytes cannot be
  labelled October.
- A row whose label states its own unit ("Standard colour licence fee
  (£)") is refused under a "£ billion unless otherwise stated" table, and
  the TV licence target reads the "BBC licence fee receipts" row only.
- Ingest checks both workbooks and parses them in a staging folder before
  copying anything, and never replaces an ingested vintage.
- Coverage checks every workbook target, including one without the
  vintage.
- The NICs docstring no longer claims Class 3 or that partial results
  pass.

All 29 OBR targets are still identical to main's on the March bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Calibration fits grouped targets; these checks measure what the groups
hide, on the built dataset. Each PR build and its base build report
person-weighted equivalised income medians, means and Gini, and relative
low income for everyone, children, working-age adults and pensioners,
against DWP's HBAI FYE 2025 estimates and 95% intervals (tables 8a and
8b, read from the published ODS files), at period 2024. They also report
people by sex and single year of age against the ONS 2024-based
projection at the calibration year. The comparison job adds them to the
PR comment and flags a measure that leaves its interval; it fails only on
a broken or mismatched report until the same-code spread is measured
(#598).

On uk-data 1.58.1, 5 of 14 HBAI measures are outside their intervals
(median AHC, both Gini coefficients, child low income BHC and AHC), and
single ages 0 to 79 are off by 19.5% on average.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first CI build of this branch failed the gate on three single-claimant
UC payment bands (24,000 to 28,800 a year). Pull request (TESTING) builds
hold fewer records than the release: across the final calibration logs of
12 recent ones, these three have no signal in 11 or all 12, while the
released 1.58.1 build has signal in all three. A listed gap that applies
to one kind of build only warns as stale in the other.

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

The Scottish band targets came from a live gov.scot download. When
gov.scot's CloudFront challenge blocked it, as in two of five recent pull
request builds of the same code, the loader substituted a fixed 2025
total spread by fixed shares, so identical code calibrated to different
targets (#596). Commit the September 2025 chargeable-dwellings workbook,
verify its SHA-256, and fail if it is missing or changed. Its Scotland row
equals the targets of the builds whose download succeeded.

Fixes #596.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both builds are measured with policyengine-uk's aggregate-impact tool at
a pinned commit (policyengine-uk#2425, stacked on #2335), under the head's
locked model: receipts by tax head, spending and caseloads, poverty,
inequality, deciles and the ten standard reform costings, 2025-26 to
2031-32. The comparison job runs `compare --axis data`, which fails unless
both sides ran one model, runtime and set of definitions, and adds the
result to the PR comment. It fails on a check the head breaks that the
base passes. Only each side's result.json (aggregates) is uploaded; the
tool's private household detail stays on the runner.

Under the locked 2.122.2 the tool falls back to policyengine-uk main's
partition, whose lists differ from that model's, so closure and two
identities fail on both sides (pre-existing) until uk-data locks a model
that ships its own partition.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
obr.uk refuses scripted downloads, so on Budget day a person downloads the
two EFO workbooks in a browser. The manifest now records each workbook's
download time with its SHA-256 and size: the file's modification time,
which a browser sets on saving, unless --downloaded-at is given. `ingest
--dry-run` checks everything and prints titles, hashes, sizes, download
times and every target's change, writing nothing; on the active vintage
it says whether the bytes match the committed workbooks, so a March 2026
download rehearses the Budget-day path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sol review of #600:
- Negative incomes before housing costs are reset to zero, and income
  after housing costs is derived from the adjusted figure (DWP HBAI
  methodology FYE 2025, "Negative incomes"). Weekly amounts use 365.25 / 7,
  as the FRS loader annualises them.
- A report missing any reference measure, or with a missing or
  non-finite single-age summary, fails the comparison.
- A missing distribution report fails the job.

On uk-data 1.58.1, four of 14 measures are now outside their intervals
(both Gini coefficients, child low income BHC and AHC); median AHC moves
inside.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rint commit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Raw calibration logs stay on the runner. Each build uploads a summary:
  whether every national and local target is within 10%, and the error of
  national targets supported by at least 10 households only (the
  constituency log gains a records column). An estimate from a few records,
  divided by a published count, could give one household's value back.
  The test job's raw-log uploads are replaced by the summary too.
- The gate compares shares over the targets both builds have; added and
  removed targets are notes.
- An empty national or local group, or a last epoch with fewer rows than
  the one before, fails.
- The comment step writes a failure comment when the comparison never ran.

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

Copy link
Copy Markdown
Contributor Author

UK hub: the fit-gate failure, broken down by target family (from the CI fit summaries, run 39040600634). All 11,228 common local targets are constituency targets. Shares within 10%, base against head:

Family n Base Head Change in count
hmrc/self_employment_income/amount 650 78.8% 41.8% −240
age/60_70 650 74.6% 53.2% −139
age/40_50 650 76.2% 58.6% −114
age/50_60 650 80.3% 67.8% −81
age/30_40 650 68.6% 57.7% −71
hmrc/employment_income/count 650 12.6% 7.4% −34
hmrc/employment_income/amount 650 53.2% 73.2% +130
uc_hh_0_children 650 80.9% 88.5% +49
hmrc/self_employment_income/count 650 86.6% 91.7% +33

The drop is concentrated in constituency self-employment amounts and working ages 30–70, while constituency employment amounts improve. It is about six times the same-code spread W3 measured, so it is not #598 noise.

The national aggregate moves look consistent with the members' intent, though that isn't verified:

Next (hub): seeded leave-one-out builds in the Monday hold (main, the release, the release without #557, the release without #604) to attribute the local drop. Then a merge decision. This release stays a draft until then.

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Hub: release-only review at 21d5dbf9: APPROVE WITH NITS (independent GPT-6.1 Sol review, executed).

  • All 14 members are merged at their approved heads, and remerge diffs find exactly the three recorded resolutions.
  • The final matrix target selection equals Fail the calibration build on targets it drops or cannot fit #595 byte for byte, and Build OBR targets only from hash-verified EFO workbooks #592's resolution drops only obr/ni, keeping the other 24 targets and the hashes.
  • The SPI, ONS age and OBR manifest generators reproduce the committed bytes, and the lock matches main apart from this project's version.
  • make test: 2,532 passed, 134 skipped, 1 expected failure. CI: six pass, and the calibration-fit gate fails as known.
  • No release-only change explains the local fit drop. Attribution still waits on the seeded builds from Monday 10/19.

Nits:

  1. The artifact guard accepts a raw log copied over an allowed report name. Stop uploading raw calibration logs as public Actions artifacts #603 disclosed this limit, and nothing at this head does it. Filed as Workflow artifact guard accepts a raw log copied over an allowed report name #609.
  2. The £1m+ disclosure is now exactly the BATCH text; one apostrophe character differed. The PR body is updated.

This stays a draft until the attribution builds report.

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Hub, 10/19 00:40Z: attribution status, and a release blocker found before merge.

The local fit drop. Seeded leave-one-out builds (#610–#617), all 32-epoch TESTING builds as in this PR's CI:

An independent investigation found no implementation defect in #571. Its prior raking of gain-bearing twins against the 12 CGT band targets shifts their pairs' mass at the start of calibration. Whether 512 epochs undo that is being measured in #618 (release) and #619 (−#571).

The release blocker. At production settings (TESTING unset, as in the publish workflow), #595's coverage gate stops the release build. Three UC payment bands have no signal and aren't excused for release builds:

  • dwp/uc_payment_dist/SINGLE_annual_payment_25_200_to_26_400
  • dwp/uc_payment_dist/SINGLE_annual_payment_27_600_to_28_800
  • dwp/uc_payment_dist/COUPLE_WITH_CHILDREN_annual_payment_27_600_to_28_800

That's runs 39180561028 and 39180562951, with and without #571. Zero signal is decided before calibration, so the seed and the epoch count don't affect it. This PR needs a release-only fix before merge. It stays a draft.

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Hub, 01:05Z, update on the release blocker: the zero-signal set varies between release builds. A second production-setting run passed the three bands above, then lost dwp/uc_payment_dist/COUPLE_NO_CHILDREN_annual_payment_26_400_to_27_600, while another build of nearly the same code passed. This is #598's nondeterminism, but #595's known gaps allow patterns only for testing builds. So the release-only fix must name every top UC payment band (£24,000 and above, all four family types) as a release-build known gap, not only the bands one build lost. Bands that do have signal are only reported as stale, which is a warning.

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Release-only fix pushed at cb92e401ebef0fbe67bb86382fb8afc325c0cb2e on release/uk-data-2026-10-19, one commit directly above 21d5dbf9.

The production workbook loader derives 24 UC bands starting at £24,000: six bands for each of SINGLE, LONE_PARENT, COUPLE_NO_CHILDREN and COUPLE_WITH_CHILDREN, through £30,000–infinity. The new #598 entry names 21 release-only zero-signal allowances, retaining the three existing COUPLE_NO_CHILDREN entries without duplication. Total release named zero-signal allowances: 38; testing retains its 17 named targets and existing pattern. The £24,000 cutoff uses SINGLE £24,000–£25,200 being zero in 20/21 testing builds and the varying release failures in runs 39180561028, 39180562951, 39181798638 and 39181880705. Bands retaining signal only warn as stale. This declares missing support; it does not restore it or resolve #598.

Validation under the frozen lock (Python 3.13, policyengine-uk 2.122.2):

  • uv run --frozen --with pytest-xdist==3.8.0 pytest -n 4 policyengine_uk_data/tests/test_target_coverage.py policyengine_uk_data/tests/test_target_registry.py policyengine_uk_data/tests/test_uc_payment_distribution_targets.py: 57 passed, 1 skipped (Enhanced FRS absent).
  • uvx ruff format --check .: 295 files formatted; lock, changelog-fragment and whitespace checks pass.
  • Regression derives names from the workbook, verifies every top band in release, keeps the release gate strict below £24,000, and checks the new entry does not apply to testing.

No dataset build, workflow dispatch, self-review, data-file push or merge ran. The body now has the release-only bullet and zero-signal limitation. The verified incremental recovery bundle and evidence are in .job/; .job/HANDOFF.md and handoffs.jsonl record the final head. Shared Git metadata is read-only, so commits use workspace-local .job/git metadata.

Hub handoff: run independent final-head review and verify the gate in a production-setting CI run (TESTING unset). The existing #571 fit investigation and #598 nondeterminism remain open. #608 remains a draft.

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

Copy link
Copy Markdown
Contributor Author

Head 6ae9639: reworded the release-only zero_signal reason in known_gaps.yaml (review r2 P3) so it no longer says the cited runs share one commit; only #618's two runs (39180561028, 39181798638) are a controlled pair, and the PR body sentence matches. Wording only: test_target_coverage.py 30 passed, 1 skipped (dataset-dependent); ruff format clean. Still a draft, not merged.

@MaxGhenis
MaxGhenis marked this pull request as ready for review October 19, 2026 20:04
@MaxGhenis
MaxGhenis merged commit cbf50ea into main Oct 19, 2026
6 of 7 checks passed
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Merged by the UK hub (session local_8e19f9d5, Opus 5.5) on Max's d1621 (batch go, 2026-10-17) and d1678 = A (2026-10-19). It merged with a merge commit, cbf50eaa, pinned with --match-head-commit 6ae9639b9f1b2ea6c04e3bf5a4ef7d88787b5c83.

  • Checks at the head (run 39194494219): 6 of 7 pass. "Calibration fit against the base" fails at 32 epochs, and d1678 rules that it is merged past. The 512-epoch fit and aggregates are in the body.
  • Reviews:
    • the release-only review at 21d5dbf9: APPROVE WITH NITS;
    • the review of cb92e401: APPROVE WITH NITS, with one wording nit folded in at 6ae9639b;
    • the hub checked that delta: known_gaps.yaml reason text only, with no target or status change.
  • Main was unchanged since assembly: a43e8c519 (1.58.2). Mergeable, not a draft, no changes requested.
  • Next:

🤖 Generated with Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

UK hub, 22:50Z: the 1.59.0 publish failed; nothing was uploaded or tagged. Push run 39320323369 built at 512 epochs, then make test failed one test: test_built_cgt_taxpayer_count, "375k weighted CGT taxpayers against HMRC's 548k (ratio 0.68)". The test's bound is ±20%. The other 2,663 tests passed.

  • Why it wasn't caught earlier: the test skips on CI's 32-epoch builds. The hub's 512-epoch diagnostic builds ran with tests switched off, so this was the test's first full-settings run on the release. The seeded release builds' fit summaries show the same miss, 25–50% off the CGT taxpayer total.
  • Status: queued for Max as d1735 (A: widen the bound and publish with the limitation stated; B: revert Start capital gains on HMRC's 2024-25 size-of-gain table #571; C: hold for a fix). 1.58.2 is still the latest published version.

Note: ~/reviews/uk-hub/jobs/release-batch3/publish-failure-1.59.0.md.

🤖 Generated with Claude Code

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