Repository navigation
Conversation
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>
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>
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>
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>
6 tasks done
…test the matrix against main's Answers Sol's review of #595 (F2, F3, F5, F6, F7). - Duplicates (F2). build_target_matrix now reads every target the sources emit (registry.emitted_targets) and select_matrix_targets keeps the first of each name, national then region then country, exactly as before. Each target it drops gets the status `duplicate`, which fails the build unless known_gaps.yaml lists it. test_no_duplicate_names counts the raw emission, so it can fail. On the current registry 159 targets are dropped this way: 156 HMRC SPI projections (#557) and 3 ONS copies (#605). They are listed; a name may be listed under two statuses, since the ten SPI £1m+ bands are both without signal and duplicated. - Build kinds (F3). A known gap may set `builds: [testing]` or `[release]`; the build kind comes from TESTING, as in create_datasets. A testing-only gap may name fnmatch `patterns`, and one now covers every UC payment band in testing builds (#598), replacing the three SINGLE bands; release builds still fail on any band without signal but the three COUPLE_NO_CHILDREN ones. A pattern is stale only if it matches no target. Errors and non-finite values are never excused, whatever a gap says. Stale gaps and problems are also written, once each, to $GITHUB_STEP_SUMMARY. - Status order (F5). The status is computed after the column enters the matrix, and a column whose status fails leaves it, so each offered target ends with exactly one status. - Reasons (F6). The 85-90 age gaps are permanent while ages are top-coded at 80 (#604); no record can hold a postgraduate loan. - A Hypothesis differential test checks that require_coverage=False builds main's matrix from the same sources, duplicates included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep main's #541 uprating factors, regenerate the aggregate projection CSV with base year 2023, and retain both branches' projection regressions. Isolate SPI registry discovery in the deterministic merge regression.
With #557 each SPI income band is emitted once, so its 156 duplicate entries go; with #604 the ages are 80 and over, so the two ons/*_85_90 zero-signal entries go. The shipped-file counts move from 19 to 17 without signal and from 159 to 3 duplicates (the ONS three, #605). The ten SPI bands of £1m and over keep their zero-signal entries: #557 gives them no signal. On uk-data 1.58.1 at 2025 the coverage CLI reports 626 in the matrix, 3 duplicates, 15 with no 2025 value and 17 without signal, with no problems and no stale gaps, as a release build and as a testing build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…(review nit) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
51 of 76 tasks
This branch has not been deployed
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.
Do not merge on its own. Once approved on its gates, this lands with the d833 uk-data batch on Max's go. It is part of workstream W3 of the Autumn Budget 2026 CI plan: the required-target-coverage gate.
Integration. This branch merges main (
a43e8c51), #604 (29ca7542) and #557 (473adafc), and its last commit drops the known gaps those two close. Land it after both, in the order #604, #557, #595.Why
create_target_matrixbuilds the national half of the calibration loss. It let targets go in four ways, and none of them fails a build:dwp/uc/two_child_limit/*targets, which are dated 2026 against the calibration year 2025;Duplicate names
The registry keeps the first target of each name, and the matrix keeps the first across national, region and country levels. On main's registry 159 targets are dropped this way. With #557 merged, 3 are:
hmrc_spiemits each band twice: the 2023-24 outturn with a 2024 value only, then theincomes_projection.csvcopy for 2025–2029. The matrix keeps the outturn, so calibration at 2025 carries the 2024 outturn forward and never sees the projection. Calibrate SPI income bands to their projected values, and map the 2023-24 outturn to 2023 #557 fixes this.ons/uk_populationand the region Scottish targets, so the 2025 matrix is unaffected. The dropped copies are:ons/uk_populationread fromdemographics.csv1,000 times too small;What changes
build_target_matrixrecords every offered target's status:in_matrix;duplicate, a target dropped because an earlier one has its name;no_value_for_year;no_column;error;non_finite;zero_signal, meaning a non-zero target with no household contributing.It reads every target the sources emit (
registry.emitted_targets) and keeps the same targets in the same order as before. So known gaps and duplicates' winners still enter the matrix. Every offered target ends with exactly one status. The status is computed after the column enters the matrix, and a column whose status cannot be computed (a non-numeric column) leaves it as anerror, where main kept it.create_target_matrix(require_coverage=Trueby default, as on the Enhanced FRS build paths increate_datasets.pyand the local-area calibrate modules) fails withTargetCoverageErroron any target not in the matrix, unlesstargets/known_gaps.yamllists it under that same status, with a reason and an issue URL.buildslimits it totesting(TESTING=1, as pull request builds set) orrelease. The build kind is read from TESTING, ascreate_datasetsreads it.patternsinstead of targets. Release gaps must name each target.$GITHUB_STEP_SUMMARY), so they don't get buried in a multi-hour log.utils/loss.get_loss_results(a diagnostic of any dataset) andutils/reweight(only the CPS-based PolicyBench transfer dataset) opt out withrequire_coverage=False.targets/known_gaps.yamllists:impute_student_loan_plannever assigningPOSTGRADUATEis permanent.dwp/uc_payment_dist/*underzero_signal, for UC payment bands whose signal flickers between pull request builds (Builds of the same code calibrate to two different weight sets #598). It replaces the three single-claimant bands this PR listed before.python -m policyengine_uk_data.targets.coverage <dataset.h5> <year> [out.json]reports every target's status, problems and stale gaps, for the build kind TESTING sets. It outputs names, statuses and counts only.UC payment bands in pull request builds
The gate failed twice on this PR:
SINGLEbands;COUPLE_WITH_CHILDREN_annual_payment_27_600_to_28_800.The final calibration logs of 21 pull request builds (8–15 October) show which bands had a national estimate of exactly zero:
COUPLE_NO_CHILDREN27,600–28,800COUPLE_NO_CHILDREN28,800–30,000COUPLE_NO_CHILDREN30,000+SINGLE24,000–25,200SINGLE25,200–26,400SINGLE27,600–28,800COUPLE_WITH_CHILDREN27,600–28,800Listing these bands one by one would keep failing pull requests, so the testing-only pattern covers the family. Release builds still fail on any UC band without signal, except the three
COUPLE_NO_CHILDRENbands listed by name. Pull request and release builds both setPE_UK_DATA_OA_CLONES=1, so the clone count is not a difference between them. The TESTING differences are the epochs (32, not 512), the SPI income-model sample (10,000 records, not 100,000) and the SLC snapshot in place of a live fetch.Evidence
On the released uk-data 1.58.1
enhanced_frs_2024_25.h5(sha256056e80fd), with the locked policyengine-uk 2.122.2 at 2025:in_matrix, 3duplicate, 15no_value_for_year, 17zero_signal. There are no problems and no stale gaps, run as a release build and again as a testing build. The output is incoverage-1.58.1-2025-integration-{release,testing}.json.in_matrix, 159duplicate, 15no_value_for_year, 19zero_signal, with no problems and no stale gaps. The 660 targets that are not duplicates had the same statuses as in the run before eb247c6.The data test
test_built_data_has_no_target_outside_the_matrix_and_the_known_gapsruns the same check wherever the built dataset is present, as in CI's build-and-test job.Not yet checked: a release build of current main. No non-TESTING pre-calibration dataset built from current main has been run through the gate. The evidence above covers the released 1.58.1 file, which older code built, plus the TESTING builds in CI. So the next release build is the gate's first full-size run. A release build fails safe: if it meets a target the gaps don't cover, it stops at the calibration step.
Invariants (tested)
column_status:zero_signalonly when its target is non-zero.require_coverage=Falsebuilds exactly the matrix and values that main'screate_target_matrixandget_all_targetsbuild from the same sources, over random names, levels, years, values, columns and country scopes. These include duplicate names within and across levels, unresolved years, missing and raising columns, and NaN and infinite values. Every target offered at a matrix level gets exactly one status.create_target_matrixon a fake simulation:duplicate, and so does a regional target named like a national one, which loses to the national one even when emitted first;errorstatus and no column;test_no_duplicate_namescounts what the sources emit, before the registry drops anything. It fails on any duplicate not listed inknown_gaps.yaml. Before Calibrate SPI income bands to their projected values, and map the 2023-24 outturn to 2023 #557 it reported all 159 without the entries; with Calibrate SPI income bands to their projected values, and map the 2023-24 outturn to 2023 #557 merged, the three ONS names remain.Interactions
duplicateentries are removed. The ten £1m+ bands keep theirzero_signalentries, because Calibrate SPI income bands to their projected values, and map the 2023-24 outturn to 2023 #557 gives them no signal.ons/{female,male}_85_90, and those two entries are removed.known_gaps.yaml.Tests
At 6e03d4c, run serially:
pytest policyengine_uk_data/tests/test_target_coverage.py policyengine_uk_data/tests/test_obr_universal_credit_target.py: 39 passed, 1 skipped (needs the built dataset).pytest policyengine_uk_data/tests/test_target_registry.py: 17 passed (live source downloads).At a590573, before the integration:
pytest policyengine_uk_data/tests, no built datasets locally): 2247 passed, 132 skipped, 1 xfailed. Without the opt-outs ofget_loss_resultsand the CPS transfer reweighting, the PolicyBench transfer test fails, because a synthetic dataset has hundreds of zero-signal columns. (That last point was observed at an earlier head and not re-run at a590573.)axiom: n/a: calibration build gate; no policy change
🤖 Generated with Claude Code