Conversation
basic_state_pension and new_state_pension split the data year's reported State Pension by state_pension_type in the period, but additional_state_pension used the data year's type. Survey ages are held fixed across projected years, so a record's cohort, and its type, can change: for records on the basic State Pension in the data year and the new State Pension later, the band between the two flat rates was paid twice (£0.84bn in 2025-26 rising to £6.77bn in 2030-31 on the enhanced FRS 2024-25). All three components now use the period's type and add up to the reported amount uprated by that type's flat rate. Adds property tests (Hypothesis) for that identity on simulations built from data, including datasets that carry other ages in later years, and examples at hand-worked cohorts. Updates the State Pension docs with the split across years and a sourced comparison with the OBR's March 2026 forecast. Fixes #1921 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Address review of the State Pension docs: - Compare the model with DWP's Spring Forecast 2026 less payments abroad, which agrees with the OBR's March 2026 forecast from 2026-27; the OBR's 2024-25 and 2025-26 figures are £1.4bn and £0.1bn higher. The data-year gap is then £12.2bn in both the table and the text. - Replace the unsourced explanation that an "SRP" figure caps SERPS in the FRS: state_pension_reported comes from the FRS benefits table (and is imputed on Survey of Personal Incomes donor rows), and the FRS's mean State Pension matches DWP's. - Point the open items at their tracking issues: the enhanced FRS shortfall (policyengine-uk-data#493) and the projected pension-age population and new-cohort awards (#1929). - Drop an unsourced reference note and a reference the page no longer uses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator
|
Reviewed: approve with changes. Removing the double count is correct and well tested. The new tests pass locally (8/8), and restoring #1899's formula fails exactly the 5 tests claimed, with the 3 controls passing.
Verified against the sources
I didn't check the £131.2bn or the FRS £130.6bn / £135.1bn sources. |
…rating Address review on #1922: - Keep the is_SP_age factor in additional_state_pension as a guard. A computed state_pension_type is already NONE below State Pension age, so results are unchanged (checked with a full microsimulation rerun), and it also holds when the type is an input. - The tests now check that the components partition the reported amount: the flat-rate part is the amount up to the full rate uprated by the full rate, and the part above it is uprated by add_on_uprating, which is the model's flat-rate ratio for now. In law additional pensions and protected payments rise with CPI (SI 2026/148 arts 4(3), 6(3)); #1941 tracks that, and fixing it only needs that helper changed. - Docs: say that the model uprates add-ons by the full rate while the law uses CPI (#1941), and that records moving to a new State Pension cohort keep their reported amounts, one reason award growth lags DWP's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator
Author
|
Thanks @vahid-ahmadi. Changes are in dce3fbc (and eca6134 for point 3).
|
Open
18 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.
Fixes #1921
Stacked on #1899 (base
state-pension-age-67-phase-in); GitHub retargets it tomainwhen #1899 merges.Summary
basic_state_pensionandnew_state_pensionsplit the data year'sstate_pension_reportedbystate_pension_typein the period simulated, butadditional_state_pensionsplit it by the type in the data year. Survey ages are held fixed when a dataset is projected (extend_single_year_datasetcopies the data year), so a record's birth cohort moves one year later for each year projected and its type can change. A man aged 75 in the 2024-25 data reached State Pension age in 2014 (basic); held at 75 in 2030-31, the record stands for a man who reached it in 2021 (new). For such recordsnew_state_pensionpaid the reported amount up to the new State Pension's full rate andadditional_state_pensionpaid everything above the basic State Pension's full rate, so the band between the two flat rates was paid twice.additional_state_pensionnow readsstate_pension_typefor the period, like the other two components. Basic, new and additional State Pension then add up to the reported amount uprated by the period type's flat rate.is_SP_agefactor Set State Pension age from date of birth, including the rise to 67 #1899 added stays as a guard (review). A computed type is alreadyNONEbelow State Pension age, so it changes nothing there (a rerun of the full data gives identical results); it also holds if a dataset or reform setsstate_pension_typedirectly.docs/book/programs/gov/dwp/state-pension.mdexplains the split across years. It replaces the stale aggregate-gap section (it quoted ~£127.5bn against ~£140bn, and an unsourced mechanism for it) with a sourced comparison to DWP's and the OBR's forecasts and links to the two tracking issues.The reverse case, new State Pension in the data year and basic in the period, would leave the band unpaid. With ages held fixed, cohorts only move later, so it never occurs in projections from the FRS (0 people in every year below); the tests cover it with datasets that carry other ages.
Invariants (stated and tested)
For every person and every year, with T the person's
state_pension_typein that year, F_T the full weekly rate for T, r the weekly amount reported in the data year d, and U_T the add-on's uprating:add_on_uprating), so fixing Uprate additional State Pension and protected payments by CPI, not the flat rate #1941 changes only that helper.NONEexactly whenis_SP_ageis false, and all three components are 0.BASIC, and new is 0 unless T isNEW. Basic ≤ 52 × F_BASIC(year), new ≤ 52 × F_NEW(year), and additional ≥ 0.test_state_pension_components.pybuilds simulations from in-memory data:extend_single_year_datasetwith ages held fixed, and checks every year from 2024-25 to 2030-31. The other gives the period arbitrary ages, so every pair of data-year and period types occurs.is_SP_ageguard also covers) pass either way. The minimal counterexamples are a woman aged 72 reporting £170 a week (basic in 2024-25, new later) and a woman below State Pension age in the data year reporting £170 a week who is on the basic State Pension in the period.Why pytest, not YAML: the YAML runner builds a simulation without a dataset (
policyengine_core/tools/test_runner.pyusesSimulationBuilder.build_from_dict, and a test has no dataset key). Theresimulation.datasetisNone, so the data year is the period and the old and new code agree. Settingstate_pension_reportedat an earlier year in a YAML test doesn't change that; the value just carries forward to the period. The existing YAML tests intests/policy/baseline/gov/dwp/(121) pass unchanged.Microsimulation impact
Enhanced FRS 2024-25 (policyengine-uk-data-private 1.57.4, snapshot ace89433; data year 2024-25). Every figure comes from a full microsimulation run of #1899's head (2466f44), without and with this change. Nothing is scaled; every figure is read from the model's own outputs. The "before" run matches #1899's recorded run of c46893e on all 96 State Pension, Pension Credit and State Pension age aggregates. The others differ because 2466f44 also merges #1901 (Housing Benefit) from
main.Change from the fix (after minus before)
Guarantee Credit and Savings Credit are gated by
is_pension_credit_eligible. Theguarantee_creditvariable on its own has no pension-age condition and sums to £104-126bn a year over 2024-25 to 2030-31, so don't read it as an entitlement.Where the overlap came from (before the fix)
The fix widens the gap to the OBR's forecast
The model was already below the OBR's State Pension forecast. Removing the double count moves it further below: by £0.8bn in 2025-26, rising to £6.8bn in 2030-31. The table compares with DWP's Spring Forecast 2026, which is consistent with the OBR's March 2026 forecast: the two differ by about £2m a year from 2026-27. The OBR's figures are £1.4bn higher in 2024-25, which the OBR records as outturn, and £0.1bn higher in 2025-26.
Against the OBR and DWP State Pension forecast
Growth from 2025-26 to 2030-31: model recipients -3.0% vs DWP caseload less paid outside the UK +4.8%; model State Pension per recipient +21.2% before and +16.4% after, vs DWP +18.3%; State Pension +17.5% before and +12.9% after, vs DWP less paid outside the UK +23.9%.
What the comparison covers:
state_pension_reportedis the FRS benefits-table weekly amount for State Pension × 52 (policyengine-uk-datadatasets/frs.py, 1.57.4 = b45c373, lines 1151-1182). On the Survey of Personal Incomes donor rows the enhanced FRS adds, a second-stage QRF re-imputes it (datasets/imputations/frs_only.py:85, called fromimputations/income.py).SRPis an HMRC Survey of Personal Incomes field (policyengine-uk-datadatasets/spi.py), not the source of the FRS amount, and the FRS's mean amount matches DWP's.Why the gap widens in projected years (#1929), computed from the runs and the DWP tables:
household_weightgrows with total population (policyengine_uk/data/uprating_indices.yaml), so the pension-age population keeps the 2024-25 age structure. From 2025-26 to 2030-31, model recipients fall 3.0% as State Pension age rises to 67. DWP's caseload less those paid abroad rises 4.8%.So the widening after the data year comes from the model not ageing the pension-age population and not giving new cohorts new awards (#1929). The data-year shortfall is in the enhanced FRS (policyengine-uk-data#493). Neither is a reason to keep an overlap that pays part of one amount twice.
Out of scope, found in review. A State Pension rate reform without a date, such as
{"gov.dwp.state_pension.new_state_pension.amount": 300}, also rewrites the data year's rate. The uprating ratio is then 1, so a new State Pension record gets its un-uprated reported amount. For a man aged 70 reporting £240 a week in 2024-25, that is £12,480 in 2027 against £14,077 at baseline. With{"2026-01-01.2100-12-31": 300}it is £16,926. The same split does this onmainand on the #1899 base; it is not changed here and is being split off as a follow-up.Tests run
policyengine-core test policyengine_uk/tests/policy/baseline/gov/dwp/ -c policyengine_uk: 121 passed.pytest policyengine_uk/tests/test_state_pension_components.py policyengine_uk/tests/test_state_pension_age.py policyengine_uk/tests/test_triple_lock_outturn.py: 43 passed (13 min) at 0ca9bec. After the review changes,test_state_pension_components.pyagain: 8 passed.ruff format --check .andruff checkon the changed files: clean.make test(limited to targeted tests while the host's memory is in use elsewhere; CI runs the suite) andtests/microsimulation/(needs the private dataset; the before/after runs above cover it).axiom: n/a: microsimulation data-handling. This changes how the survey-reported State Pension is split across PolicyEngine's components in projected years; no statutory rule changes. rulespec-uk's
uk/policies/govuk/state-pension.yamlcomputes entitlement from qualifying years and never reads a reported amount.🤖 Generated with Claude Code