Skip to content

Give a simulation its identifiers for a year before its data - #2354

Merged
MaxGhenis merged 11 commits into
mainfrom
fix-earlier-year-identifiers
Oct 23, 2026
Merged

MaxGhenis merged 11 commits into
mainfrom
fix-earlier-year-identifiers

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #2353

Builds on #2349, which merged as ed7b42f95. This branch starts from #2349's head (a9664455e), so the diff here is this PR's own work.

The gap

  1. Build a Simulation from a frame or dataset for 2025.

  2. set_input some variable for an earlier year, such as 2023.

  3. The simulation now holds no identifiers in 2023:

    • PE-UK turns on carry-over (tax_benefit_system.py:75), and policyengine-core carries an input forward but never back. Core 3.32.27, simulations/simulation.py:1199-1250, says: "A later input does not carry backwards."
    • When only later periods are stored, core returns the default (:1241-1247).
    • So person_id, benunit_id, household_id, person_benunit_id and person_household_id are all zero in 2023.
  4. The exported frame loads as a dataset whose 2023 tables hold the people and groups:

    • input_dataframe.py:404-405: a whole year that a column of another variable names becomes a dataset year;
    • :431-443: each year's tables hold the identifiers;
    • simulation.py:554-559: every column of every year is set as an input.

    So the reloaded simulation reads the real identifiers there.

Four formulas read identifiers, so their 2023 values differed between a simulation and the one its export loads back to:

  • is_benunit_eldest_child.py:22
  • months_since_last_birthday.py:36
  • uc_deduction_random_draw.py:25
  • uc_deduction_type_random_draw.py:25

Two children of 10 in one benefit unit, given values for 2024 at representative weight, before this PR:

Variable (2024) Simulation Reloaded export
is_benunit_eldest_child [True, True] [False, True]
months_since_last_birthday [3.0, 9.0] [9.0, 3.0]
uc_deduction_random_draw 0.883 0.567
uc_deduction_type_random_draw 0.362 0.258

After this PR, both give the right-hand column.

Which semantics, and why

There were two options:

  • A. The simulation holds its identifiers in every year it holds an input.
  • B. The export does not invent identifiers for years the simulation lacks them in.

I chose A:

  • B can't make the reloaded simulation agree. The loader puts each whole year a frame names in the dataset, and a dataset year's tables must hold the identifiers. So B would also need the loader to build dataset years without people, or to delete identifiers after building. It would also make zero identifiers, which are only a side effect of carry-over, part of the format.
  • A matches what the loader already does for frames. The loader comment at input_dataframe.py:423-425 says inputs carry forward but not back, so a year before the data that a frame names identifiers for needs them set too. A applies the same rule to any set_input.
  • A keeps Export the identifiers for every year an input frame names, so it round-trips #2349's property and fixes the values. export(load(export(S))) == export(S) still holds, and the reloaded simulation computes the same values. The export is column-for-column what Export the identifiers for every year an input frame names, so it round-trips #2349 gives: the identifiers for the earlier year are now inputs the simulation holds, not copies the export adds.

The fix

  • New function hold_identifiers_in(simulation, variable_name, period) in policyengine_uk/data/input_dataframe.py:454. Some identifiers are held as an input for some period, but for none that starts by 1 January of the year period starts in. Each of those is set for that year, copied from the period it is held for that starts first. From there it carries forward.
    • It does nothing for an input core ignores, one for a period after its variable's end (_ignored, :447, which mirrors core simulation.py:1736-1740).
    • Each identifier is set through simulation.set_input, which calls the function again for that identifier. That call finds the identifier held, or returns at the same check if core ignored it, so the calls end.
    • No PE-UK variable has an end today, so the guard changes nothing now.
    • An input in or after the first year the identifiers are held changes nothing.
    • A simulation that holds no identifier as an input, such as one built from a situation, is left alone.
    • A branch gets the identifiers on its own branch only.
  • Override Simulation.set_input (simulation.py:254-260) calls core's set_input, then hold_identifiers_in. It sits just after delete_arrays.
  • File ownership: simulation.py belongs to the Autumn Budget W1 session (local_37646626). This PR's changes there are only:
  • Docs and changelog:
    • a docstring paragraph and the with_identifiers_in_each_year docstring (input_dataframe.py);
    • one bullet in docs/book/usage/simulations.md;
    • changelog.d/2353.fixed.md.

#2076 also overrides set_input: combine them

#2076 adds its own Simulation.set_input override, which calls forget_model_value.

  • Git doesn't conflict. git merge-tree c10e7e3a1 <#2076 head> exits 0, and the merged class defines set_input twice. Python keeps the later definition, so one PR's behaviour would be lost without an error.
  • Lint might not catch it. ruff check reports this as F811, but when this PR was written Lint ran only ruff format --check. Run ruff check in Lint, with F811 and F821 on for the package #2355 adds ruff check --select F811,F821.
  • The guard. This PR adds policyengine_uk/tests/code_health/test_simulation_methods_defined_once.py. It fails when Simulation or Microsimulation defines a method twice (property setters, deleters and overload stubs aside). I checked that it fails on the tree merged with Lift the benefit cap only for the support component and the people HB reg 75F and UC reg 83 name, and cap only the family's benefits #2076.
  • The fix, for whichever PR lands second: combine the two overrides into one that calls super().set_input(...), then forget_model_value(self, variable_name, period), then hold_identifiers_in(self, variable_name, period).

#2349 review nits folded in

The UK hub passed these on from #2349's Sol review:

  1. The claims that columns sort by period, and that the frame loads back to the same frame, are now scoped to the default export. With include_computed_variables=True, core exports periods in storage order.
  2. A new Hypothesis test, test_the_export_and_the_loader_find_the_same_years, checks that the loader's years and the export helper's years agree.
    • It covers frames with identifier, value, lone-person_id and month columns in different years.
    • Mutation-checked: an export that skips years named only by an identifier fails it.
  3. The docstring says the helper returns df itself when it adds nothing, and otherwise a new frame.
  4. New test: an export whose person_id differs between years, before or after the data, is still refused on load.

Invariants

These hold for every input:

  1. Identifiers in each input year. A simulation that holds identifiers as inputs holds them in every year it is given an input for. They are its people's and groups' own identifiers, whether held as inputs or carried forward.
  2. Differential. In each year a frame-built simulation is given inputs for, it computes the same value as the simulation its export loads back to. This holds for every identifier and every variable whose formula reads one.
  3. Round trip (Export the identifiers for every year an input frame names, so it round-trips #2349). export(load(export(S))) == export(S).
  4. No-op on datasets. A simulation built from a dataset, as H5 files load, holds the identifiers as inputs in exactly the dataset's years, whether the years are stored ascending or descending.
  5. No-op without identifiers. A simulation that holds no identifier as an input is given none.

Tests (policyengine_uk/tests/test_dataframe_dataset.py)

Property (invariants 1 to 3): test_a_reloaded_simulation_reads_the_same_identifiers_in_each_year_given_inputs (Hypothesis).

  • Random populations, all four identifier-column layouts, input years from 2022 to 2027, and weights of 1 or 1,000,000. The million weight reaches the Universal Credit draws' representative-data branch.
  • It finds every variable whose formula reads an identifier by scanning formula source, and asserts the four known readers are among them.
  • Explicit examples:
    • Hypothesis's shrunk counterexample: one person and inputs for 2022, where months_since_last_birthday was 6.48 against 5.59;
    • the twins above at a million weight, where all four readers differed.

Deterministic:

  • a month input two years before the data: identifiers are given for its year only, carry to the next, and none come for later years;
  • a lone identifier before the data brings the others;
  • a branch holds its own;
  • a situation gets none (invariant 5);
  • datasets in ascending and descending year order (invariant 4);
  • an input core ignores gives no identifiers, and an identifier core would ignore is left out. With the _ignored check removed, both tests fail and the second hits RecursionError; I ran it to confirm.

Also added:

  • test_the_export_and_the_loader_find_the_same_years (Hypothesis, see the nits above);
  • test_an_export_whose_identifiers_differ_between_years_is_refused;
  • tests/code_health/test_simulation_methods_defined_once.py.
Check Result
New property test on #2349's head, before the fix fails: shrunk to one person, months_since_last_birthday 6.48 vs 5.59 in 2022
New tests at the test-only commit 426df7b49 (mutation check) 4 fail (property via both explicit examples, month, lone identifier, branch); the 3 no-op tests pass
test_dataframe_dataset.py, the code-health test, test_uc_rebalancing.py and test_input_records.py at d2f48c86f, -n 3 112 passed
test_dataframe_dataset.py at the first fix commit c48f3a9a5, -n 3 85 passed
test_uc_rebalancing.py, test_input_records.py, test_claimant_or_partner_awards_entered_directly.py, test_entered_directly_lifecycle.py, code_health/test_cached_arrays_not_written_in_place.py, test_simulation_dataset_sources.py 105 passed
ruff format / ruff check on changed files clean

I didn't run the full make test or make documentation locally; CI runs both.

H5-built Microsimulations are unaffected

By reading the code: the hook acts only through set_input. It does something only when an input starts in a year before every period an identifier is held for.

  • A dataset build sets every year's tables, each with all five identifiers (simulation.py:554-559). In the first year loaded, no identifier is held when the first column is set. From then on each year holds them, whatever order the years are loaded in.
  • These set_input callers set years the data covers:
    • move_values;
    • the rebalancing in uc_reform.py, for 2026 to 2029.
  • These set the year being calculated: the labour supply and marginal rate branches, and dynamics/.
    • For a year the data covers, the hook does nothing.
    • For a year before the data, only the branch gets the identifiers (the parent holds no input there). That is the intended behaviour, which test_a_branch_given_an_input_before_the_data_holds_the_identifiers_itself pins.

By running it: h5_check.py builds a Microsimulation from an H5 file twice. One build removes the override, which is #2349's behaviour. The other counts hook calls that change any identifier input. Each run then calculates 14 aggregates for 2025, 2026 and 2029:

  • household net income (and HBAI);
  • income tax and National Insurance;
  • Universal Credit, Child Benefit, Pension Credit, Housing Benefit and State Pension;
  • council tax;
  • the four identifier readers.

It then compares a sha256 over every input array before and after the calculations.

Dataset Hook calls Calls that changed an identifier Input digest (off vs on) Aggregates differing
enhanced_frs_2023_24_tiny.h5 (years 2023 to 2030) 1,256 0 identical, 1,232 inputs (174546b7…) 0 of 42
enhanced_frs_2024_25.h5, uk-data 1.58.0 pending
enhanced_frs_2023_24.h5, uk-data main pending

The full-dataset runs are queued on the machine's shared heavy-run lock, as a launchd job that writes to ~/reviews/uk-earlier-year-ids/h5/. I'll fill in these rows when they finish.

Cost: the hook takes about 35 µs per set_input call when it does nothing. That is about 45 ms for the ~1,256 calls of an eight-year build, whatever the number of households. Tiny builds took a median of 1.41 s with the hook against 1.29 s without (4 each, on a loaded host).

Review

Both reviews so far were served by Opus 5.5: Subfleet promoted each hard-tier review from GPT-6.1 Sol when no Codex lane admitted it.

Opus 5.5: APPROVE at 38bfb0d6d (~/reviews/uk-earlier-year-ids/review/opus-review-38bfb0d6d.md), with four minor findings:

  1. The hook ran for an input core ignores, and could recurse without end for an ended identifier. Fixed in d2f48c86f with tests.
  2. A branch given an input before the data holds identifiers its parent doesn't. This is intended, and a test pins it.
  3. The docstring overstated what the dataset holds. Narrowed in d2f48c86f, corrected in 11b806732.
  4. The instrumented H5 run's build times aren't a clean comparison. Agreed. The cost figures above come from bench_hook.py, not from that run.

Opus 5.5: REQUEST_CHANGES at f9ddc20a2 (opus-review-f9ddc20a2.md). It found nothing wrong in the runtime code, but four false or imprecise claims, all fixed in 11b806732 and this description:

  1. The recipe for combining with Lift the benefit cap only for the support component and the people HB reg 75F and UC reg 83 name, and cap only the family's benefits #2076 used the old signature. It now says hold_identifiers_in(self, variable_name, period).
  2. The check that skipped an ended identifier did nothing, and the claims that it prevented a recursion were false. I ran it: both tests pass with that check deleted, so I deleted it. The _ignored check on the input itself is what ends the calls; with it removed too, the ended-identifier case hits RecursionError.
  3. The docstring still overstated what the dataset holds. Corrected.
  4. "Other set_input callers set years the data covers" was imprecise. Reworded above.

It also noted, as information only, that _ignored doesn't mirror core storing nothing for a neutralised variable (holder.py:462-466). An input for a neutralised variable in an earlier year still gives identifiers, which keeps the round trip. It judged a neutralised identifier that already holds inputs unreachable in PE-UK.

GPT-6.1 Sol review of the current head: pending, pinned to a Codex lane so it can't fall back.

axiom: n/a: simulation input and export plumbing, no policy change

🤖 Generated with Claude Code

Reproducer: policyengine_uk/tests/code_health/test_simulation_methods_defined_once.py::test_each_method_of_the_simulation_classes_is_defined_once, policyengine_uk/tests/code_health/test_simulation_methods_defined_once.py::test_a_method_defined_twice_is_found, policyengine_uk/tests/test_dataframe_dataset.py::test_a_reloaded_simulation_reads_the_same_identifiers_in_each_year_given_inputs, policyengine_uk/tests/test_dataframe_dataset.py::test_an_input_before_the_data_gives_the_identifiers_for_its_year, policyengine_uk/tests/test_dataframe_dataset.py::test_an_identifier_given_alone_before_the_data_brings_the_others, policyengine_uk/tests/test_dataframe_dataset.py::test_a_branch_given_an_input_before_the_data_holds_the_identifiers_itself

MaxGhenis and others added 6 commits October 11, 2026 16:36
…year

A simulation built from a frame and given values for a year before its
data holds no identifiers there, because policyengine-core carries
inputs forward but never back. The frame it exports loads back to a
simulation that holds the real identifiers in that year, so formulas
that read them compute differently there.

The property checks that the simulation and its reloaded export compute
the same values in each year given inputs for, both for the identifiers
and for every variable whose formula reads one, and that both hold the
people's own identifiers there. The examples are Hypothesis's shrunk
counterexample (one person, an input three years before the data) and two
children of one age, for whom all four identifier readers differed.
Further tests cover a month input, a lone identifier, a branch, a
situation, and datasets loaded as H5 files load.

Refs #2353.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
policyengine-core carries an input forward but never back, so a
simulation given an input for a year before every year it holds its
identifiers for held none there: every person, benefit unit and
household identifier read as zero. The frame it exports loads as a
dataset that holds the people and groups in that year, so the reloaded
simulation read the real identifiers there, and is_benunit_eldest_child,
months_since_last_birthday and the Universal Credit deduction draws gave
different values.

Simulation.set_input now calls hold_identifiers_in, which gives each
identifier the simulation holds as an input for some period, but for
none starting by the first day of the year the input starts in, for that
year, copied from the period it is held for that starts first. From
there it carries forward. An input in or after the first year the
identifiers are held changes nothing, and a simulation that holds no
identifier as an input, such as one built from a situation, is left
alone. Datasets load each year with its identifiers, so loading one
gives none for any other year.

The change to simulation.py is the set_input override and one import.

Fixes #2353.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of #2349 (the export helper this builds on):
- Limit the claims that the columns sort by period, and that the frame
  loads back to the same frame, to the default export. With
  include_computed_variables=True, core exports each variable's periods
  in the order they are stored.
- Say that with_identifiers_in_each_year returns the frame itself when
  it adds nothing, and otherwise a new frame.
- Test that the loader and the export find the same years for frames
  that name identifiers, values and months for different years. The
  round trip relies on their agreeing, and each works the years out
  separately.
- Test that an export whose person_id differs between years, before or
  after the data, is still refused when loaded.

#2076 also overrides Simulation.set_input, just after delete_arrays.
Put this override in the same place, so that if both PRs land git
reports a conflict to resolve, rather than one override silently
replacing the other in the class body.

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

Without it, every identifier was named for the same years, so the test
passed even with an export that ignored years only an identifier names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous commit put this PR's set_input override beside
delete_arrays expecting git to conflict with #2076's override there. It
does not: git merges the two (`git merge-tree` exits 0), and the class
then defines set_input twice. The later definition, this PR's, replaces
#2076's, so its forget_model_value call would be lost without an error.
CI checks formatting only, not `ruff check`, whose F811 reports this.

This code-health test fails when Simulation or Microsimulation defines a
method more than once (property setters, deleters and overload stubs
aside). It fails on the tree merged with #2076, so whichever of the two
lands second must combine the overrides into one: call core's set_input,
then forget_model_value, then hold_identifiers_in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 3 commits October 11, 2026 17:10
From the review of 38bfb0d: policyengine-core's set_input stores
nothing for a period after the variable ends, but the override still
gave the identifiers for that year. If an identifier itself had ended,
setting it would store nothing, and hold_identifiers_in would call
set_input again without end.

hold_identifiers_in now takes the input's variable and returns when core
ignores the input, and skips an identifier core would ignore for the
year. No variable in policyengine-uk has an end date today, so nothing
changes now; the tests give one for a single test.

Also corrects its docstring: the reloaded simulation holds the people
and groups in a year when the exported frame gives a whole year's value
for it, not in each year the frame gives values for.

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

#2355 adds ruff check --select F811,F821 to Lint, so 'CI checks
formatting only' would go stale once it lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of f9ddc20:

- The check that skipped an identifier core would not store did nothing.
  hold_identifiers_in sets each identifier through simulation.set_input,
  which calls it again for that identifier, and the check that an input
  core ignores gives no identifiers already returns there. With the
  skipped check deleted both tests still pass; with the remaining check
  removed as well, the ended-identifier case raises RecursionError (both
  executed). So the last commit's claim, and the test's comment, that the
  deleted check prevented the recursion were wrong. The docstring now
  says how the calls end.
- The docstring said each whole year a column names is a year of the
  dataset. A year only an identifier column names is not: the loader
  sets the identifiers there as inputs.

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

Copy link
Copy Markdown
Collaborator Author

CI run cancelled to free runners (UK hub, 2026-10-11 23:30Z). The PolicyEngine org is at GitHub's limit of 60 concurrent jobs, with about 360 queued. Fleet ops asked the hub to let the Budget-critical #2305 take runners first. This PR's queued run had not started, so I cancelled it. Nothing is wrong with the PR.

Please don't re-run it or push just to retrigger CI. The hub re-runs this run itself once #2305 has runners. The record is ~/reviews/uk-hub/ci-yield-2026-10-11.json; see notice 17 in ~/reviews/uk-hub/notices.md.

@MaxGhenis
MaxGhenis marked this pull request as ready for review October 22, 2026 18:14
MaxGhenis and others added 2 commits October 22, 2026 14:16
Brings the branch up to date and runs the checks main now requires (Test / Result, Lint); this branch's last CI predates those workflow names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main gained a set_input override (model-value records and CGT input capture) after this branch's own (holding identifiers for the year an input starts in). The merge kept both definitions, and the later one silently replaced the earlier: ruff F811 in CI's Lint. Keep one override that does both: core's set_input, then forget replaced model values, then hold the identifiers, then capture CGT inputs.

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

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Aggregate impact on certified data: pass

Base a29f30a5d9 against head cb76b03f37, both on enhanced_frs_2024_25.h5 (policyengine-uk-data 1.58.1, sha256 056e80fd8e91), fiscal years 2025-26 to 2031-32. Each year is a full microsimulation run on each side.

Checks: 0 broken by this change, 1 failing on both sides, 0 fixed. Moves flagged for review: 0 (thresholds in report_spec.yaml; a flag does not fail the job).

Workflow run and full report

Notes

  • In the years a figure from fewer than 10 records is suppressed (spending.disabled_students_allowance), every total containing it, and the decile incomes, are rebuilt from their other components, so no published total includes it. Those totals are marked †. A change is withheld where the two sides leave out different figures and can't be lined up.

Moves flagged for review

None.

Spending by benefit and service

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Disabled Students' Allowance suppressed suppressed suppressed suppressed suppressed suppressed suppressed

Caseloads

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Disabled Students' Allowance suppressed suppressed suppressed suppressed suppressed suppressed suppressed

Household net income by decile

No household's net income changes in any year.

No change in any year: receipts by tax head; government balance, each component counted once; model totals; household income totals; population; poverty rates; people in poverty; inequality; mean household net income in each side's own deciles; standard reform costings (change in gov_balance); standard reforms: change in each reform's target.

Checks failing on both base and head (1)
  • VAT is never negative (sign:vat): 2025-26: 76 units below zero, a weighted £-54,797,972 in total; 2026-27: 76 units below zero, a weighted £-56,271,349 in total; 2027-28: 76 units below zero, a weighted £-57,609,146 in total
Counted-once check

Each of 84 components was raised by £1 per unit in 2026-27, and every head of its totals was read household by household: its own head must rise by exactly £1, no other tax head may move, no other benefit may rise, and the model's own total must move by what its heads moved. A head that breaks a rule is measured again twice: with every other head held at its baseline value, and with every other head held at its value after the first £1 while the component rises again. It fails where either still shows the response. 0 failed; 0 were skipped.

Provenance
Side Commit Runtime Environment hash Partition hash Time
base main a29f30a5d9 uk 2.147.2 (files 4fae93cfc3ac), core 3.33.2, py 3.13.17 aarch64 5c9a49667c9b d4c1e9f08160 1593.02s
head #2354 merged into main cb76b03f37 uk 2.147.2 (files dcf3fefa35af), core 3.33.2, py 3.13.17 aarch64 5c9a49667c9b d4c1e9f08160 1561.57s

Dataset: hf://policyengine/policyengine-uk-data-private/enhanced_frs_2024_25.h5@2526edbbb687a3b54dbb11272e5389e4b62d2ac2, sha256 056e80fd8e9151d78f432a25cde863340baee81304c6216a37fb575ce8563784 (verified on both sides), policyengine-uk-data 1.58.1.

Receipts by tax head: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Income tax £299.29bn £319.01bn £336.47bn £351.62bn £367.17bn £385.29bn £403.59bn
National Insurance (employee and self-employed) £52.58bn £54.94bn £56.75bn £58.48bn £60.62bn £62.57bn £65.03bn
National Insurance (employer) £152.64bn £159.18bn £164.14bn £168.73bn £176.11bn £181.72bn £188.90bn
Employer NI change borne by capital £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Employer NI change borne by consumers £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Capital gains tax £12.62bn £13.06bn £13.55bn £14.04bn £14.54bn £15.09bn £15.65bn
Student loan repayments £7.83bn £8.12bn £8.35bn £8.56bn £8.79bn £9.04bn £9.38bn
VAT £253.71bn £260.53bn £266.72bn £273.14bn £279.83bn £286.71bn £293.71bn
Change in VAT from its baseline £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
VAT on private school fees £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Fuel duties £24.53bn £24.64bn £26.64bn £26.69bn £26.42bn £25.76bn £26.35bn
Alcohol duties £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Tobacco duties £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Vehicle excise duties £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
TV licence fee £4.28bn £4.43bn £4.44bn £4.46bn £4.48bn £4.51bn £4.53bn
Council tax £49.56bn £52.45bn £55.78bn £59.39bn £62.98bn £66.74bn £70.70bn
High value council tax surcharge £0.00bn £0.00bn £0.00bn £0.48bn £0.49bn £0.51bn £0.52bn
Northern Ireland domestic rates £0.55bn £0.58bn £0.61bn £0.63bn £0.67bn £0.70bn £0.73bn
Business rates £31.73bn £31.73bn £31.73bn £31.73bn £31.73bn £31.73bn £31.73bn
Stamp duty land tax on the household's own purchases £6.93bn £7.42bn £7.85bn £8.26bn £8.68bn £9.09bn £9.51bn
Stamp duty land tax paid by companies £3.13bn £3.13bn £3.13bn £3.13bn £3.13bn £3.13bn £3.13bn
Land transaction tax (Wales) £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Land and buildings transaction tax (Scotland) £0.01bn £0.01bn £0.02bn £0.02bn £0.02bn £0.02bn £0.02bn
Unspecified tax change borne by capital £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Unspecified tax change borne by consumers £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Wealth tax £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Wealth tax excluding the main residence £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Land value tax £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Carbon tax £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Spending by benefit and service: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
State Pension £122.15bn £126.64bn £127.57bn £129.47bn £133.29bn £137.24bn £141.32bn
Pension Credit £6.68bn £7.07bn £7.06bn £7.13bn £7.26bn £7.39bn £7.49bn
Winter Fuel Payment £1.70bn £1.68bn £1.59bn £1.58bn £1.57bn £1.55bn £1.52bn
Pension Age Winter Heating Payment (Scotland) £0.19bn £0.20bn £0.20bn £0.21bn £0.21bn £0.22bn £0.22bn
Universal Credit £75.54bn £80.75bn £82.55bn £84.28bn £85.26bn £86.64bn £87.92bn
Housing Benefit £8.59bn £8.93bn £9.19bn £9.34bn £9.61bn £9.87bn £10.15bn
Council Tax Reduction £1.99bn £2.03bn £2.08bn £2.13bn £2.18bn £2.23bn £2.29bn
Employment and Support Allowance (income-related) £2.12bn £2.18bn £2.23bn £2.28bn £2.33bn £2.39bn £2.45bn
Employment and Support Allowance (contribution-based) £5.22bn £5.36bn £5.49bn £5.62bn £5.76bn £5.90bn £6.04bn
Jobseeker's Allowance (income-based) £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Jobseeker's Allowance (contribution-based) £0.29bn £0.30bn £0.30bn £0.31bn £0.32bn £0.33bn £0.33bn
Income Support £0.12bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Working Tax Credit £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Child Tax Credit £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Incapacity Benefit £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Personal Independence Payment £27.05bn £28.19bn £28.94bn £29.64bn £30.37bn £31.11bn £31.87bn
Disability Living Allowance £7.52bn £7.83bn £8.04bn £8.24bn £8.44bn £8.65bn £8.86bn
Attendance Allowance £8.71bn £9.08bn £9.32bn £9.55bn £9.78bn £10.02bn £10.26bn
Carer's Allowance £4.50bn £4.69bn £4.81bn £4.93bn £5.05bn £5.17bn £5.29bn
Carer Support Payment (Scotland) £0.31bn £0.32bn £0.33bn £0.34bn £0.35bn £0.36bn £0.36bn
Carer's Allowance Supplement (Scotland) £0.00bn £0.04bn £0.04bn £0.04bn £0.04bn £0.04bn £0.04bn
Severe Disablement Allowance £0.03bn £0.03bn £0.03bn £0.03bn £0.03bn £0.03bn £0.03bn
Industrial Injuries Disablement Benefit £0.51bn £0.52bn £0.53bn £0.54bn £0.56bn £0.57bn £0.59bn
Armed Forces Compensation Scheme £0.61bn £0.62bn £0.64bn £0.65bn £0.67bn £0.69bn £0.70bn
War Widow's or Widower's Pension £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Child Benefit £16.23bn £16.91bn £17.32bn £17.73bn £18.16bn £18.60bn £19.04bn
Scottish Child Payment £0.50bn £0.52bn £0.53bn £0.55bn £0.56bn £0.57bn £0.58bn
Two Child Limit Payment (Scotland, counterfactual) £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Healthy Start vouchers £0.05bn £0.05bn £0.05bn £0.05bn £0.05bn £0.05bn £0.05bn
Maternity Allowance £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Statutory Maternity Pay £3.32bn £3.41bn £3.49bn £3.57bn £3.66bn £3.75bn £3.84bn
Statutory Paternity Pay £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Statutory Sick Pay £1.13bn £1.16bn £1.19bn £1.22bn £1.25bn £1.28bn £1.31bn
Sure Start Maternity Grant £0.01bn £0.01bn £0.01bn £0.01bn £0.01bn £0.01bn £0.01bn
Bereavement Support Payment £0.67bn £0.69bn £0.70bn £0.72bn £0.74bn £0.76bn £0.78bn
Tax-Free Childcare £0.55bn £0.56bn £0.57bn £0.58bn £0.60bn £0.61bn £0.59bn
Extended childcare entitlement £2.69bn £3.18bn £3.26bn £3.33bn £3.42bn £3.51bn £3.60bn
Universal childcare entitlement £2.26bn £2.45bn £2.51bn £2.58bn £2.64bn £2.72bn £2.78bn
Targeted childcare entitlement £0.57bn £0.60bn £0.62bn £0.63bn £0.65bn £0.66bn £0.67bn
Care to Learn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Childcare Grant £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Parents' Learning Allowance £0.20bn £0.20bn £0.20bn £0.20bn £0.20bn £0.20bn £0.20bn
Adult Dependants' Grant £0.06bn £0.06bn £0.04bn £0.04bn £0.04bn £0.04bn £0.04bn
Travel Grant £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Disabled Students' Allowance suppressed suppressed suppressed suppressed suppressed suppressed suppressed
16 to 19 Bursary Fund £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
NHS spending £204.68bn £205.46bn £206.22bn £207.04bn £207.96bn £208.89bn £209.79bn
State education spending £0.02bn £0.02bn £0.02bn £0.02bn £0.02bn £0.02bn £0.02bn
Transport subsidy spending £25.15bn £25.25bn £25.34bn £25.44bn £25.55bn £25.67bn £25.78bn
Cost-of-living support payment £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Energy Bills Rebate £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Energy Price Guarantee subsidy £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Basic income £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Disability basic income £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Other public spending budget change £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Caseloads: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
State Pension 11,308k 11,208k 10,906k 10,812k 10,860k 10,908k 10,955k
Pension Credit 1,491k 1,489k 1,478k 1,459k 1,459k 1,457k 1,453k
Winter Fuel Payment 7,091k 7,011k 6,601k 6,517k 6,493k 6,381k 6,254k
Pension Age Winter Heating Payment (Scotland) 806k 807k 807k 805k 809k 812k 816k
Universal Credit 6,361k 6,370k 6,402k 6,412k 6,386k 6,378k 6,363k
Housing Benefit 1,254k 1,252k 1,254k 1,240k 1,245k 1,247k 1,249k
Council Tax Reduction 3,186k 3,211k 3,224k 3,245k 3,264k 3,280k 3,293k
Employment and Support Allowance (income-related) 187k 188k 188k 189k 190k 190k 191k
Employment and Support Allowance (contribution-based) 722k 724k 727k 730k 733k 737k 740k
Jobseeker's Allowance (income-based) 0k 0k 0k 0k 0k 0k 0k
Jobseeker's Allowance (contribution-based) 67k 67k 68k 68k 68k 69k 69k
Income Support 25k 0k 0k 0k 0k 0k 0k
Working Tax Credit 0k 0k 0k 0k 0k 0k 0k
Child Tax Credit 0k 0k 0k 0k 0k 0k 0k
Incapacity Benefit 0k 0k 0k 0k 0k 0k 0k
Personal Independence Payment 3,581k 3,595k 3,608k 3,623k 3,639k 3,655k 3,671k
Disability Living Allowance 1,296k 1,301k 1,306k 1,311k 1,317k 1,323k 1,329k
Attendance Allowance 1,701k 1,708k 1,714k 1,721k 1,729k 1,736k 1,744k
Carer's Allowance 1,038k 1,042k 1,046k 1,050k 1,055k 1,059k 1,064k
Carer Support Payment (Scotland) 71k 72k 72k 72k 73k 73k 73k
Carer's Allowance Supplement (Scotland) 0k 72k 72k 72k 73k 73k 73k
Severe Disablement Allowance 4k 4k 4k 4k 4k 4k 4k
Industrial Injuries Disablement Benefit 137k 138k 139k 139k 140k 140k 141k
Armed Forces Compensation Scheme 94k 94k 94k 95k 95k 95k 96k
War Widow's or Widower's Pension 0k 0k 0k 0k 0k 0k 0k
Child Benefit 8,136k 8,165k 8,185k 8,215k 8,249k 8,282k 8,314k
Scottish Child Payment 352k 354k 355k 356k 357k 358k 357k
Two Child Limit Payment (Scotland, counterfactual) 0k 0k 0k 0k 0k 0k 0k
Healthy Start vouchers 200k 201k 202k 203k 204k 204k 205k
Maternity Allowance 0k 0k 0k 0k 0k 0k 0k
Statutory Maternity Pay 334k 335k 336k 338k 339k 341k 342k
Statutory Paternity Pay 0k 0k 0k 0k 0k 0k 0k
Statutory Sick Pay 182k 183k 183k 184k 185k 186k 186k
Sure Start Maternity Grant 26k 26k 26k 27k 27k 27k 27k
Bereavement Support Payment 98k 98k 99k 99k 100k 100k 101k
Tax-Free Childcare 1,018k 1,029k 1,029k 1,033k 1,035k 1,039k 1,024k
Extended childcare entitlement 718k 724k 728k 732k 737k 741k 743k
Universal childcare entitlement 986k 989k 992k 997k 1,002k 1,013k 1,015k
Targeted childcare entitlement 119k 120k 120k 121k 121k 122k 121k
Care to Learn 0k 0k 0k 0k 0k 0k 0k
Childcare Grant 0k 0k 0k 0k 0k 0k 0k
Parents' Learning Allowance 98k 99k 99k 100k 100k 100k 101k
Adult Dependants' Grant 18k 18k 12k 13k 13k 13k 13k
Travel Grant 0k 0k 0k 0k 0k 0k 0k
Disabled Students' Allowance suppressed suppressed suppressed suppressed suppressed suppressed suppressed
16 to 19 Bursary Fund 0k 0k 0k 0k 0k 0k 0k
Cost-of-living support payment 0k 0k 0k 0k 0k 0k 0k
Energy Bills Rebate 0k 0k 0k 0k 0k 0k 0k
Energy Price Guarantee subsidy 0k 0k 0k 0k 0k 0k 0k
Basic income 0k 0k 0k 0k 0k 0k 0k
Disability basic income 0k 0k 0k 0k 0k 0k 0k
Government balance, each component counted once: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Government receipts, each component once £899.38bn £939.24bn £976.18bn £1,009.38bn £1,045.66bn £1,082.60bn £1,123.48bn
Government spending £531.94bn † £546.98bn † £553.05bn † £560.03bn † £568.60bn † £577.76bn † £586.86bn †
Government balance, each receipt once £367.45bn † £392.26bn † £423.13bn † £449.35bn † £477.06bn † £504.85bn † £536.62bn †
Benefits in household_benefits but not in gov_spending £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Model totals: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
gov_tax £899.38bn £939.24bn £976.18bn £1,009.38bn £1,045.66bn £1,082.60bn £1,123.48bn
household_tax £493.03bn £519.53bn £545.32bn £567.51bn £589.72bn £614.17bn £640.87bn
pre_budget_change_household_tax £485.21bn £511.41bn £536.97bn £558.47bn £580.44bn £604.62bn £630.98bn
gov_spending £531.94bn † £546.98bn † £553.05bn † £560.03bn † £568.60bn † £577.76bn † £586.86bn †
household_benefits £531.94bn † £546.98bn † £553.05bn † £560.03bn † £568.60bn † £577.76bn † £586.86bn †
gov_balance £367.45bn † £392.26bn † £423.13bn † £449.35bn † £477.06bn † £504.85bn † £536.62bn †
Household income totals: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Household market income £1,657.06bn £1,719.25bn £1,771.68bn £1,821.99bn £1,872.88bn £1,930.99bn £2,001.98bn
Household net income £1,656.69bn † £1,705.94bn † £1,737.53bn † £1,771.57bn † £1,807.68bn † £1,849.24bn † £1,900.92bn †
Household net income (HBAI definition) £1,463.15bn £1,510.70bn £1,542.73bn £1,575.67bn £1,609.74bn £1,648.74bn £1,699.14bn
Population: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
People 69,514k 69,778k 70,036k 70,316k 70,626k 70,944k 71,249k
Benefit units 35,247k 35,381k 35,512k 35,654k 35,811k 35,972k 36,127k
Households 31,167k 31,285k 31,401k 31,526k 31,665k 31,808k 31,944k
Poverty rates: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Absolute poverty, before housing costs: everyone 16.2% 15.2% 15.7% 15.8% 15.9% 16.0% 15.9%
Absolute poverty, before housing costs: children 24.6% 21.4% 22.0% 21.9% 22.1% 22.3% 22.2%
Absolute poverty, before housing costs: working-age adults 13.9% 13.5% 14.1% 14.2% 14.3% 14.4% 14.4%
Absolute poverty, before housing costs: pensioners 13.7% 13.2% 13.3% 13.5% 13.6% 13.7% 13.7%
Absolute poverty, after housing costs: everyone 21.5% 20.8% 21.5% 21.8% 22.2% 22.4% 22.5%
Absolute poverty, after housing costs: children 33.4% 30.9% 31.6% 31.8% 32.4% 32.9% 33.2%
Absolute poverty, after housing costs: working-age adults 19.8% 19.6% 20.4% 20.6% 20.9% 21.2% 21.2%
Absolute poverty, after housing costs: pensioners 12.8% 12.6% 12.6% 13.1% 13.4% 13.4% 13.4%
Relative poverty, before housing costs: everyone 17.2% 16.5% 16.6% 16.6% 16.4% 16.5% 16.7%
Relative poverty, before housing costs: children 26.4% 23.7% 23.7% 23.6% 23.2% 23.3% 23.5%
Relative poverty, before housing costs: working-age adults 14.6% 14.4% 14.7% 14.8% 14.7% 14.8% 14.9%
Relative poverty, before housing costs: pensioners 15.0% 14.8% 14.4% 14.3% 14.0% 14.0% 14.4%
Relative poverty, after housing costs: everyone 21.7% 21.1% 21.4% 21.4% 21.5% 21.6% 22.0%
Relative poverty, after housing costs: children 33.5% 31.3% 31.4% 31.3% 31.3% 31.5% 32.1%
Relative poverty, after housing costs: working-age adults 19.9% 19.8% 20.2% 20.4% 20.5% 20.6% 20.9%
Relative poverty, after housing costs: pensioners 13.0% 12.7% 12.6% 12.3% 12.2% 12.2% 12.9%
People in poverty: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Absolute poverty, before housing costs: everyone 11,270k 10,578k 10,975k 11,077k 11,210k 11,339k 11,354k
Absolute poverty, before housing costs: children 3,696k 3,227k 3,332k 3,325k 3,371k 3,407k 3,408k
Absolute poverty, before housing costs: working-age adults 5,940k 5,792k 6,115k 6,209k 6,281k 6,360k 6,367k
Absolute poverty, before housing costs: pensioners 1,634k 1,559k 1,527k 1,543k 1,558k 1,572k 1,579k
Absolute poverty, after housing costs: everyone 14,972k 14,539k 15,080k 15,313k 15,657k 15,918k 16,049k
Absolute poverty, after housing costs: children 5,005k 4,656k 4,776k 4,818k 4,933k 5,035k 5,098k
Absolute poverty, after housing costs: working-age adults 8,445k 8,399k 8,848k 9,001k 9,191k 9,342k 9,400k
Absolute poverty, after housing costs: pensioners 1,521k 1,484k 1,456k 1,493k 1,533k 1,541k 1,551k
Relative poverty, before housing costs: everyone 11,980k 11,485k 11,639k 11,698k 11,596k 11,730k 11,890k
Relative poverty, before housing costs: children 3,967k 3,570k 3,578k 3,587k 3,531k 3,566k 3,616k
Relative poverty, before housing costs: working-age adults 6,227k 6,161k 6,401k 6,485k 6,468k 6,552k 6,616k
Relative poverty, before housing costs: pensioners 1,785k 1,754k 1,660k 1,627k 1,597k 1,611k 1,659k
Relative poverty, after housing costs: everyone 15,057k 14,705k 14,981k 15,071k 15,191k 15,336k 15,683k
Relative poverty, after housing costs: children 5,024k 4,718k 4,751k 4,755k 4,774k 4,816k 4,937k
Relative poverty, after housing costs: working-age adults 8,480k 8,486k 8,784k 8,917k 9,017k 9,112k 9,256k
Relative poverty, after housing costs: pensioners 1,552k 1,501k 1,445k 1,399k 1,399k 1,408k 1,491k
Inequality: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Gini coefficient, equivalised HBAI income before housing costs 0.3087 0.3060 0.3074 0.3080 0.3085 0.3091 0.3102
Mean household net income in each side's own deciles: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Decile 1: mean household_net_income per household £17,832 † £18,015 † £17,980 † £18,037 † £18,364 † £18,545 † £18,723 †
Decile 2: mean household_net_income per household £28,135 † £29,092 † £28,744 † £29,163 † £29,434 † £29,997 † £30,442 †
Decile 3: mean household_net_income per household £34,455 † £35,681 † £36,229 † £36,632 † £36,967 † £37,482 † £38,380 †
Decile 4: mean household_net_income per household £39,398 † £41,043 † £41,970 † £42,713 † £43,280 † £44,024 † £44,550 †
Decile 5: mean household_net_income per household £43,698 † £45,019 † £45,872 † £46,588 † £47,624 † £48,569 † £49,772 †
Decile 6: mean household_net_income per household £48,494 † £49,775 † £50,932 † £51,700 † £52,150 † £53,469 † £54,371 †
Decile 7: mean household_net_income per household £56,360 † £57,552 † £58,380 † £59,120 † £60,160 † £60,882 † £62,979 †
Decile 8: mean household_net_income per household £68,817 † £70,403 † £71,732 † £72,842 † £74,422 † £75,795 † £77,575 †
Decile 9: mean household_net_income per household £79,735 † £82,088 † £83,280 † £84,576 † £86,144 † £87,656 † £90,725 †
Decile 10: mean household_net_income per household £117,772 † £120,640 † £122,567 † £124,692 † £126,940 † £129,209 † £132,023 †
Standard reform costings (change in gov_balance): head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Raise basic rate by 1pp £6.22bn £6.50bn £6.70bn £6.90bn £7.11bn £7.34bn £7.62bn
Raise higher rate by 1pp £3.54bn £3.82bn £4.05bn £4.27bn £4.49bn £4.76bn £4.96bn
Raise personal allowance by ~800GBP/year £-3.83bn £-3.96bn £-4.03bn £-4.10bn £-4.19bn £-4.30bn £-2.02bn
Raise child benefit by 25GBP/week per additional child £-2.00bn £-1.76bn £-1.64bn £-1.54bn £-1.44bn £-1.34bn £-1.24bn
Reduce Universal Credit taper rate to 20% £-20.49bn £-21.05bn £-21.40bn £-21.83bn £-21.91bn £-22.28bn £-22.73bn
Raise Class 1 main employee NICs rate to 10% £11.07bn £11.51bn £11.84bn £12.14bn £12.48bn £12.82bn £13.29bn
Raise VAT standard rate by 2pp £25.01bn £25.68bn £26.29bn £26.92bn £27.58bn £28.26bn £28.95bn
Raise additional rate by 3pp £2.83bn £3.07bn £3.27bn £3.47bn £3.67bn £3.91bn £4.23bn
Raise the Universal Credit standard allowance by 10% £-3.03bn £-3.16bn £-3.24bn £-3.32bn £-3.37bn £-3.42bn £-3.46bn
Raise the State Pension by 10% £-7.81bn £-8.07bn £-7.99bn £-8.11bn £-8.36bn £-8.66bn £-9.02bn
Standard reforms: change in each reform's target: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Raise basic rate by 1pp: change in income_tax £6.36bn £6.65bn £6.86bn £7.07bn £7.28bn £7.52bn £7.80bn
Raise higher rate by 1pp: change in income_tax £3.55bn £3.83bn £4.06bn £4.28bn £4.50bn £4.77bn £4.97bn
Raise personal allowance by ~800GBP/year: change in income_tax £-3.96bn £-4.10bn £-4.19bn £-4.26bn £-4.36bn £-4.47bn £-1.77bn
Raise child benefit by 25GBP/week per additional child: change in child_benefit £2.34bn £2.15bn £2.03bn £1.92bn £1.82bn £1.71bn £1.59bn
Reduce Universal Credit taper rate to 20%: change in universal_credit £20.48bn £21.04bn £21.39bn £21.82bn £21.90bn £22.27bn £22.73bn
Raise Class 1 main employee NICs rate to 10%: change in national_insurance £11.33bn £11.79bn £12.13bn £12.44bn £12.78bn £13.13bn £13.62bn
Raise VAT standard rate by 2pp: change in vat £25.01bn £25.68bn £26.29bn £26.92bn £27.58bn £28.26bn £28.95bn
Raise additional rate by 3pp: change in income_tax £2.83bn £3.07bn £3.27bn £3.47bn £3.67bn £3.91bn £4.23bn
Raise the Universal Credit standard allowance by 10%: change in universal_credit £3.03bn £3.16bn £3.24bn £3.32bn £3.37bn £3.43bn £3.47bn
Raise the State Pension by 10%: change in state_pension £10.72bn £11.23bn £11.37bn £11.62bn £12.08bn £12.54bn £13.07bn

@MaxGhenis
MaxGhenis added this pull request to the merge queue Oct 23, 2026
Merged via the queue into main with commit d26cb11 Oct 23, 2026
17 checks passed
@MaxGhenis
MaxGhenis deleted the fix-earlier-year-identifiers branch October 23, 2026 06:11
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Merged by the UK hub (Claude session 8e19f9d5) at head 470de3040, through the merge queue with the head pinned.

  • Independent review (r1): GPT-6.1 Sol at 11b806732 found no code defect in its static review.
    • Its findings: the identifier backfill preserves the original input period, copies only the five identifiers, leaves later-year inputs authoritative and touches no weights or uprating. Before-load Scenario changes still precede single-year extension. An AST scan found no duplicate methods.
    • It withheld approval for one reason only: its local -n 2 run of test_simulation_methods_defined_once.py, test_dataframe_dataset.py and test_state_pension_components.py couldn't start, because host disk was under 55 GB.
  • Hub merge of main: 2eb8e10cb is a clean merge of current main that runs the checks main now requires (Test / Result, Lint).
  • Hub resolution of a merge conflict: after the main merge, CI's Lint (ruff F811) caught two Simulation.set_input definitions: main's (model-value records and CGT capture) and this PR's (identifier hold); the later one silently replaced the earlier. The hub's 470de3040 keeps one override doing both, in this order: core set_input, forget replaced model values, hold identifiers, capture CGT inputs. A second independent review (GPT-6.1 Sol, r2) of 2eb8e10cb..470de3040 found no defect: both behaviours are preserved, the identifier writes are disjoint from model-value variables and CGT inputs, and the AST guard catches a recurrence. It withheld approval only because host disk blocked its local tests; CI at this head is the evidence.
  • Validation: the PR's own CI at the reviewed head 11b806732 ran those three files, with 113 passed and 0 failed (run 38182166890). All 8 checks pass. The merge queue re-runs the full suite on the merge with current main before landing.
  • Impact: none expected. For data already covering 2025 the backfill does nothing for 2025–2031, and the PR reports 0 of 42 aggregates differing on its comparison.
  • Why now: Max asked on 22 Oct to clear the UK PR backlog before the Budget.

MaxGhenis added a commit that referenced this pull request Oct 23, 2026
From the independent review of the corrections:
- The run used main at c65bced, which is policyengine-uk 2.148.3 (the
  editable install's metadata still said 2.148.2); the only change since
  2.148.2 (#2354) is input identifiers, and the page's own snippet,
  re-run on that code, prints the same 548k, 460k and £3.21bn.
- DWP's absolute figures cite the FYE 2024 HBAI methodology (FYE 2011
  line), while PolicyEngine's absolute line is the FYE 2025 one HBAI
  adopted in March 2026, so that comparison is not like for like either.
- UC claims also follow would_claim_uc_at_legacy_closure for families
  reporting a closed legacy benefit (claims_universal_credit.py).
- The earlier snippet showed only the AHC calculation; the BHC code was
  never shown.
- Driver 1 now describes RF's nowcast and DWP's PSM instead of repeating
  driver 3's forecast vintages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

A simulation given an input for a year before its data holds no identifiers there

1 participant