Repository navigation
Restore HMRC salary sacrifice relief calibration targets - #533
Merged
Merged
Conversation
GOV.UK withdrew the July 2025 Tables 6.1/6.2 CSV (410 Gone) when HMRC
published the July 2026 private pension statistics, and get_targets()
logged the error and returned without the relief targets, so every build
since has calibrated without hmrc/salary_sacrifice_it_relief_* and the
HMRC NICs relief targets.
- Point sources.yaml at the July 2026 CSV (tax year 2024-25) and commit
a copy in storage; a failed download uses it, and a table without the
expected rows raises instead of returning fewer targets.
- Map the tax year from the table: PolicyEngine year N is tax year
N to N+1, so 2024-25 is 2024 (the old comment put 2023-24 at 2024).
- Split IT relief across the bands each person's relief straddles,
as HMRC does, instead of classifying by adjusted net income against
taxable-income thresholds (which put every basic-rate taxpayer with
income between 37,700 and 50,270 in the higher-rate target).
- Drop obr/salary_sacrifice_{employee,employer}_ni_relief: static copies
of HMRC's 2023-24 Table 6.2 figures computed on the same column as the
restored HMRC NICs targets.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ish bands - Employer NICs relief targets follow the Class 1 rate in PolicyEngine's parameters (13.8% in 2024-25, 15% from April 2025), so the 2025 target is £3.81bn, not £3.50bn; employee targets weight the main and additional rates by HMRC's split (unchanged, 8% and 2%). - Income tax relief is the rise in tax on earned income, allocated to HMRC's basic/higher/additional categories under the person's rUK or Scottish rates (starter and intermediate with basic, advanced with higher, top with additional). HMRC applies income tax rates to ASHE pay; the previous measure (change in income_tax) included the High Income Child Benefit Charge and savings and dividend tax, and split Scottish relief on rUK boundaries. - Drop the IT relief total target: it is the sum of the bands, which HMRC rounds separately (£8.9bn vs £8.8bn in 2024-25). - The table's tax year must equal _TAX_YEAR, which names the committed copy, so pointing sources.yaml at a new release without refreshing the copy fails instead of giving online and offline builds different targets. - Test that every relief target produces a loss-matrix column on the built dataset (create_target_matrix skips with a warning otherwise). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… PE threshold - _relief rejects a blank (NaN) cell instead of returning a NaN target. - Test that dividends pushed into a higher band by the sacrifice do not count as relief (a revert to the change in income_tax passed every test before). - The Scottish category test reads the thresholds from PolicyEngine's schedule instead of stating its top-rate threshold (£112,570) as law (policyengine-uk#2130: the statutory threshold is £125,140). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only the dividends taxed in the basic rate band move to the higher band, not the whole £10k. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
marked this pull request as ready for review
October 4, 2026 07:31
Contributor
Author
|
Hand-off to the UK hub. Head
|
This was referenced Oct 4, 2026
Contributor
Author
|
Queued in release PR #544 for the 10/8 uk-data batch. It lands only on Max's go (d833). |
MaxGhenis
added a commit
that referenced
this pull request
Oct 7, 2026
…ount (F1) test_full_target_set_available_offline (from #536) asserted at least 30 OBR targets offline. The release combines four member PRs that each replace OBR targets with better sources, which no single PR's CI saw together: #490 swaps obr/housing_benefit for DWP Housing Benefit targets, #510 swaps obr/pension_credit for DWP Pension Credit spend and caseload, #533 swaps the two OBR salary-sacrifice NI relief targets for HMRC's, and #530 merges the two OBR UC targets into obr/universal_credit. That leaves 29 OBR targets (34 on main), so release CI failed with 29 >= 30. The test now checks what it was for: offline, the committed workbooks give exactly the OBR targets that the same workbooks give when served, plus the receipts and NICs names. A broken fallback still fails it. Release-only fix for the 10/8 uk-data release (d833). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Summary
Six HMRC salary sacrifice calibration targets have been missing from every published build since 1.56.16 (18 August 2026).
targets/sources/hmrc_salary_sacrifice.pydownloads HMRC private pension statistics Tables 6.1 and 6.2. GOV.UK withdrew the July 2025 CSV (HTTP 410 Gone) when HMRC published the July 2026 release on 30 July. The module caught the error, logged it, and returned only the static £24bn contributions target, so the builds kept passing.This PR restores the targets from the July 2026 release and stops a dead URL from dropping them silently. It also fixes errors that would otherwise come back with them:
Five of the six targets return; the income tax relief total is now implied by the bands (change 6).
Merging is a data release. Restoring these targets changes calibration, so this PR waits for Max's go on the uk-data release batch (d833). HMRC's method leaves two readings open (the allowance taper, and the Scottish advanced rate); they are queued for Max as d925, with the sizes in the Readings paragraph.
What was missing
hmrc/salary_sacrifice_it_relief_totalhmrc/salary_sacrifice_it_relief_basic_ratehmrc/salary_sacrifice_it_relief_higher_ratehmrc/salary_sacrifice_it_relief_additional_ratehmrc/salary_sacrifice_employee_nics_reliefhmrc/salary_sacrifice_employer_nics_reliefSources: the national rows of
constituency_calibration_log.csvfrom push runs 30202165235 (1.56.14,ebf733c) and 36131888928 (1.57.4,b45c373). The 1.57.4 log has 637 national targets and none of these six. The 1.56.16 build log (run 32138754415) records the 410.hmrc/salary_sacrifice_contributionsand theobr/salary_sacrifice_*targets stayed in every build.Changes
Source.
sources.yamlnow points at the July 2026 CSV (tax year 2024-25). The columns and category labels the parser reads are unchanged from the July 2025 file (compared against the archived copy at the UK Government Web Archive).Failure behaviour. A failed download falls back to a committed copy,
storage/hmrc_pension_relief_tables_6_1_6_2_2024_25.csv(byte-identical to the live asset; sha25604e0dab6…). This follows the OBR EFO fallback (test_obr_efo_fallback.py). The build now fails instead of returning fewer targets when:_TAX_YEAR(which also names the committed copy), so pointingsources.yamlat next year's release without refreshing the copy fails rather than giving online and offline builds different targets;except Exceptionis gone.create_target_matrixstill skips, with a warning, any target whose compute raises or returns nothing. A new test checks on the built dataset that every relief target produces a finite, positive column.Year. The base year now comes from the table's
tax_year, mapped to PolicyEngine's year. PolicyEngine UK's year N is tax year N to N+1: a year-2017 simulation reads the personal allowance in force from 6 April 2017 (£11,500), andobr.pyandfrs.pyuse the same mapping. So 2024-25 maps to 2024. The old constant put the 2023-24 table at 2024, one year late;hmrc_spi.pyandhmrc_cgt.pyshare that mapping (follow-up below). Growth stays at 3% a year.Income tax relief by rate.
compute_ss_it_reliefused to measure relief as the change in totalincome_taxand put each person's whole relief in one band. It chose the band by comparing counterfactual adjusted net income with the rate thresholds (0, £37,700, £125,140), which apply to taxable income, after the £12,570 personal allowance. So every basic-rate taxpayer with income between £37,700 and £50,270 counted as higher rate.HMRC applies income tax rates to employees' pay from ASHE, and models contributions that "straddle" bands (private pension statistics, background and methodology). Relief is now the rise in tax on earned income (
earned_taxable_income) when the sacrifice is paid as salary, under the person's own rUK or Scottish schedule. It is allocated bracket by bracket to HMRC's categories:The bands add up exactly to the change in earned-income tax. That leaves out effects a pay-based estimate cannot see, which the change in
income_taxincludes: savings, dividend and property income tax, the High Income Child Benefit Charge, and the pension annual allowance charge (which falls when the sacrifice is paid as salary). The personal allowance taper stays in, because it follows from pay. On this PR's build the change inincome_taxis £9.12bn, against £9.45bn of band relief. Examples, checked in policyengine-uk (2025):Duplicate NICs targets.
obr/salary_sacrifice_employee_ni_relief(£1.2bn) andobr/salary_sacrifice_employer_ni_relief(£2.9bn) at 2024 equal HMRC's 2023-24 Table 6.2 figures to the £0.1bn. They use the same compute column as the HMRC NICs targets, and in 1.56.14 both pairs had identical targets and estimates. With the 2024-25 table the pairs would disagree (employee £1.03bn vs £1.24bn in 2025, employer £3.81bn vs £2.99bn), which would ask calibration to hit two values for one quantity. This PR removes the OBR-labelled copies. Their cited PDF (2025 SPP Review) returns 404 and has no UK Government Web Archive copy.No total target. The income tax relief total is the sum of the bands, and HMRC rounds each cell separately: in 2024-25 the bands sum to £8.9bn and the total is £8.8bn. Targeting both would ask calibration for two values, so only the bands are targets.
NICs rates. Each year's NICs relief target is scaled by that year's Class 1 rate over the 2024-25 rate, read from PolicyEngine's parameters and weighted by HMRC's main/additional split. The employer rate rose from 13.8% to 15% on 6 April 2025, so the 2025 employer target is £3.4bn × 1.03 × 15/13.8 = £3.81bn rather than £3.50bn. The employee rates (8% and 2%) did not change. Not adjusted: the secondary threshold cut from £9,100 to £5,000, which affects only sacrifices by people paid under £9,100.
Target values for 2025 (the calibration year)
HMRC's 2024-25 table shows £8.8bn of income tax relief on salary-sacrificed contributions, up from £7.2bn for 2023-24. It also shows £1.0bn employee NICs relief (from £1.2bn) and £3.4bn employer NICs relief (from £2.9bn). The July 2026 Table 6 time series leaves the 2023-24 salary sacrifice figures unchanged (£7.2bn, £1.2bn, £2.9bn), although it labels that column "revised".
Calibration fit
Real runs. Two production-settings builds with the same harness (this PR's adds network retries, and routes pandas URL reads through the cache) and seed (512 epochs,
PE_UK_DATA_OA_CLONES=1, torch seed 0, frozen HTTP cache):b45c373, the code of published 1.57.4.138c72a. The head adds only tests and the blank-cell guard, which no real table triggers.The new downloads are the 2024-25 CSV (byte-identical to the committed copy) and the SLC student support workbook, which both builds read live.
National rows of
constituency_calibration_log.csv, last epoch (510). "Before" is each target on main's calibrated data: this PR's code run on main's saved calibration-year dataset with main's final constituency weights. That reconstruction reproduces main's logged estimates for the targets main has.hmrc/salary_sacrifice_contributionsobr/salary_sacrifice_users_totalOther targets. The 635 targets common to both builds barely move:
Five targets worsen by more than a percentage point and four improve. After salary sacrifice contributions, the largest worsenings are:
Leaving aside
ons/multi_family_households(−2.6 points, but more than 1,000% off in both builds), the largest improvements are dividend income £200k–£300k (+2.0% → +0.2%) and £100k–£150k (+6.3% → +5.1%), and the property income count for £150k–£200k (+18.7% → +17.5%).The local-authority calibration agrees: 86.0% → 86.1% of common targets within 10%, the restored targets between −2.4% and +5.2%, and salary sacrifice contributions +8.8% → +20.0%.
The cost: salary sacrifice contributions overshoot more. On main's calibration, which does not target relief, the data's relief is mostly basic rate (£3.09bn against £1.65bn), with too little at higher and additional rates. HMRC's ASHE-based figures put most relief at higher and additional rates. To match them, calibration upweights higher-paid users: those with pre-sacrifice pay of £100k or more go from 0.24m people holding £1.92bn to 0.57m holding £6.73bn (review r3's aggregates). Total contributions rise from +9.4% to +20.6% over the £24.7bn target. That target is cited to a 2025 SPP Review PDF that now returns 404; re-sourcing it is a follow-up.
On this build, £1.44bn of the relief comes from people with no pay. Those are the imputed amounts #518 removes. A build with #518 applied, the release configuration, is reported below.
Readings HMRC's method leaves open, sized on this build (relief in each band totals £9.45bn):
With #518, the release configuration. A third build ran on #518's reviewed head at the time (
9910ad4) merged with this PR's138c72a. #518's two later commits (now14e2c22) change only comments, the changelog and a.copy()with no effect. The comparison is a build of9910ad4alone, which used main's harness (without the retries and cache routing); both builds read identical inputs and their pre-calibration datasets are identical in every column, so the two differ only through calibration. "Before" is each target on #518's calibrated data.hmrc/salary_sacrifice_contributionsobr/salary_sacrifice_users_totalCommon targets:
Six targets improve by more than a point and two worsen: salary sacrifice contributions, and employment income £50k–£70k (+10.0% → +11.3%).
Local authorities:
Calibration again upweights higher-paid users: those with pre-sacrifice pay of £100k or more go from 0.44m holding £5.93bn to 0.63m holding £8.20bn (review r4's aggregates). With #518, no relief comes from people with no pay. The readings are: band relief £9.58bn, the allowance taper £0.39bn of it (0.37m people), and Scottish advanced-rate relief £0.18bn of £0.74bn.
Invariants
tax_by_band: the categories add up to the schedule's tax (checked against PolicyEngine'sMarginalRateTaxScale.calcon 5,000 seeded incomes per schedule and year, rUK and Scottish, 2023 to 2025). Each category's tax is non-decreasing in income, so each band's relief is non-negative when the sacrifice is.compute_ss_it_relief: with pay as the only income, the bands add up to the fall inincome_tax(a differential check against policyengine-uk on three households)._relief_targets: exactly one positive value per required row, a single tax year equal to_TAX_YEAR, and a main/additional split for each NICs class, or it raises.Hypothesis is not a dependency on main (six batch PRs add it), so the property checks use seeded random draws. Each of these mutants of the current code fails at least one test: no NICs rate factor, no tax-year check, advanced rate as additional, Scottish taxpayers on rUK bands, uncapped brackets, relief measured as the change in
income_tax, and a blank cell accepted. Review r2 ran 13 mutants of its own; the three that survived were two equivalent under current parameters and theincome_taxmeasure, which the dividend test now catches.Tests
tests/test_hmrc_salary_sacrifice_targets.py(23 tests):_TAX_YEAR;tax_by_bandagainst the PolicyEngine scales, monotonicity, and the Scottish categories;compute_ss_it_reliefon four real policyengine-uk households, one with dividends that the sacrifice pushes into the higher band (that extra dividend tax is not relief);tests/test_target_registry.py::test_hmrc_salary_sacrifice_relief_targets: the five targets are in the registry for 2025, and the total and the OBR copies are not.Testjob builds the TESTING datasets and runs the whole suite, including the loss-matrix test. Atb224171it gave 655 passed, 3 skipped, 1 xfailed, with every salary sacrifice test run (review r3). Against the first head's production build, the full suite gave 640 passed; the 4 failures were all network timeouts in unrelated sources (explore-education-statistics, ONS).Reviews
40ea945): REQUEST_CHANGES. The findings and the response, finding by finding, are answered in the code at585402d.585402d, ran the code): APPROVE_WITH_NITS. Its three in-PR nits are fixed inb224171: blank cells raise, a dividend household pins the measure, and the Scottish test reads PolicyEngine's thresholds.b224171, ran the code and recomputed every number in this body from the evidence): APPROVE_WITH_NITS. All three r2 nits are fixed and checked with 14 mutants. Its nits were one wrong figure and some wording in this body, fixed here, and a test docstring, fixed in0863a6a.0863a6a, reran the evidence scripts and checked the docstring in policyengine-uk): APPROVE_WITH_NITS. Its nits were all in this body (one rounding, four phrasings) and are fixed.Follow-ups (not in this PR)
policyengine-uk starts the Scottish 48% rate at £116,760 of income instead of £125,140: Scottish top rate (48%) starts at £116,760: threshold stored as £112,570 instead of £125,140 policyengine-uk#2130.
From 2029 the salary sacrifice cap changes the counterfactual in
build_loss_matrix._SimContext(review r2: the sacrifice above £2,000 is added back twice), and the 2029–31 NICs relief targets ignore the cap. Calibration runs only at 2025, so current builds are unaffected. This predates this PR.Bands are classified by rate value, so a reform that changed the rates could reclassify relief. Production builds calibrate the baseline only.
hmrc_spi.pyandhmrc_cgt.pymap tax year 2023-24 to year 2024. Under the mapping above that is one year late. Tracked as a separate task.hmrc/salary_sacrifice_contributions(£24bn) cites the same dead SPP Review PDF. Tracked as a separate task.axiom: n/a: data
🤖 Generated with Claude Code