From 0ca9becda6554c6d4b7c1fda0eaae80c0a3a6974 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 30 Sep 2026 09:05:16 -0400 Subject: [PATCH 1/5] Split additional State Pension by the period's State Pension type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- ...itional-state-pension-period-type.fixed.md | 1 + docs/book/programs/gov/dwp/state-pension.md | 84 +++++- .../tests/test_state_pension_components.py | 266 ++++++++++++++++++ .../gov/dwp/additional_state_pension.py | 15 +- 4 files changed, 354 insertions(+), 12 deletions(-) create mode 100644 changelog.d/additional-state-pension-period-type.fixed.md create mode 100644 policyengine_uk/tests/test_state_pension_components.py diff --git a/changelog.d/additional-state-pension-period-type.fixed.md b/changelog.d/additional-state-pension-period-type.fixed.md new file mode 100644 index 0000000000..8d6871c631 --- /dev/null +++ b/changelog.d/additional-state-pension-period-type.fixed.md @@ -0,0 +1 @@ +Split additional State Pension by each person's State Pension type in the year simulated, as basic and new State Pension already are, instead of their type in the data year. Survey ages are held fixed across projected years, so records that were on the basic State Pension in the data year stand for new State Pension cohorts later, and the part of their reported State Pension between the two flat rates was paid twice. Basic, new and additional State Pension now add up to the reported amount uprated by the flat rate. diff --git a/docs/book/programs/gov/dwp/state-pension.md b/docs/book/programs/gov/dwp/state-pension.md index db743051f2..5cd763c2b5 100644 --- a/docs/book/programs/gov/dwp/state-pension.md +++ b/docs/book/programs/gov/dwp/state-pension.md @@ -28,6 +28,24 @@ Three variables under `gov/dwp/`: - Both are computed as `max(reported - flat_max_for_type, 0)` per week, multiplied by `WEEKS_IN_YEAR`. +All three split the amount a person reported in the data year (the +dataset's first year) by the person's `state_pension_type` in the year +simulated, and uprate it by that type's flat rate: the year's full rate +over the data year's. Together they pay the reported amount uprated by the +flat rate, for anyone over State Pension age. + +Survey ages are held fixed in the years a dataset is projected to, 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, on the basic State Pension, but a man aged 75 in 2030-31 reached it +in 2021, on the new State Pension. Until the fix for +[#1921](https://github.com/PolicyEngine/policyengine-uk/issues/1921), +`additional_state_pension` used the data year's type while the other two +used the year's, so for records like his the part of the reported amount +between the basic and new flat rates was paid twice. The records keep their +reported amounts: a record that moves to a new State Pension cohort is not +given a new State Pension award. + The flag-up split (`state_pension_type`) is settled by [PR #1618](https://github.com/PolicyEngine/policyengine-uk/pull/1618): classification is based on whether the person reaches State Pension age before or on/after 6 April 2016. No component is paid before State @@ -98,10 +116,53 @@ These flags exist so that policy reforms can disable individual limbs ## Known aggregate gap (#1632) -After the BASIC/NEW classification fix in PR #1618 and the pro-rating -+ Protected Payment fixes in [PR #1634](https://github.com/PolicyEngine/policyengine-uk/pull/1634), -the model's State Pension aggregate is **~£127.5 bn** against the OBR -2025 target of **~£140 bn** — a **-£12 bn gap**. +The model's State Pension covers UK private households: the Family +Resources Survey covers private households only, not nursing homes and +other communal establishments. The OBR's State Pension line matches DWP's +forecast of State Pension spending, which covers Great Britain and UK State +Pensions paid to people living abroad, but not Northern Ireland, whose +State Pension the Department for Communities pays. The table compares full +microsimulation runs on the enhanced FRS 2024-25 +(policyengine-uk-data-private 1.57.4) with that line less DWP's payments +abroad. + +| £bn | 2024-25 | 2025-26 | 2026-27 | 2027-28 | 2028-29 | 2029-30 | 2030-31 | +|---|---:|---:|---:|---:|---:|---:|---:| +| Model State Pension | 119.0 | 124.8 | 129.6 | 130.9 | 132.9 | 136.9 | 140.9 | +| OBR March 2026 EFO, State Pension | 138.0 | 146.2 | 154.2 | 158.9 | 164.0 | 172.2 | 180.7 | +| of which paid abroad (DWP) | 5.3 | 5.6 | 5.9 | 6.1 | 6.2 | 6.4 | 6.6 | +| Model minus OBR less paid abroad | -13.6 | -15.8 | -18.6 | -21.9 | -24.8 | -28.9 | -33.2 | + +The model also includes Northern Ireland, which the OBR line does not, so +the like-for-like gap is larger by Northern Ireland's State Pension. In +2024-25, the data year, DWP's outturn for residents of Great Britain is +£131.2bn, £12.2bn above the model. The FRS's own grossed estimate for +the UK that year, with benefit amounts linked to DWP records, is £130.6bn, +against £135.1bn from administrative data, so most of the model's shortfall +arises in building the enhanced FRS rather than in the survey's coverage. + +The gap widens in later years for two reasons: + +- **Caseload.** Survey ages are held fixed in projected years and household + weights grow with total population + (`policyengine_uk/data/uprating_indices.yaml`), so the + pension-age population keeps the data year's age structure. From 2025-26 + to 2030-31 the model's State Pension recipients fall 3.0%, as State + Pension age rises to 67, while DWP's caseload less those paid abroad rises + 4.8%. +- **Awards.** Each record keeps its reported amount, uprated by the flat + rate, including records whose cohort moves from basic to new State + Pension. The model's State Pension per recipient grows 16.4% over those + years. DWP's spending per recipient, less those paid abroad, grows 18.3%, + as its new State Pension spending grows from £56.1bn to £102.4bn and its + basic State Pension spending falls from £66.7bn to £57.8bn. + +Until the fix for [#1921](https://github.com/PolicyEngine/policyengine-uk/issues/1921), +`additional_state_pension` paid the band between the flat rates twice for +records moving from basic to new State Pension cohorts (see Components +above): £0.8bn in 2025-26, rising to £6.8bn in 2030-31. +That made the model's State Pension per recipient grow 21.2%, faster than +DWP's, and hid part of the gap. ### What's been fixed @@ -110,11 +171,12 @@ the model's State Pension aggregate is **~£127.5 bn** against the OBR | BASIC vs NEW classification used `is_SP_age` heuristic | Fixed | #1618 | | `new_state_pension` returned flat max for every NEW retiree, ignoring partial NI records | Fixed | #1634 | | Protected Payment only computed for BASIC-type recipients | Fixed | #1634 (ASP now extends to NEW) | +| `additional_state_pension` split the reported amount by the data year's State Pension type, paying part of it twice for records moving from basic to new cohorts | Fixed | #1921 | ### What's still open -The remaining ~£12 bn gap appears to come from the **data side** rather -than the formula. The FRS records State Pension as a single weekly +The data year's gap appears to come from the **data side** rather than +the formula. The FRS records State Pension as a single weekly benefit value (`state_pension_reported`), which is derived from the DWP-administered single weekly figure (SRP). For BASIC-type retirees who reported exactly the maximum basic rate, the formula assigns ASP = @@ -129,6 +191,10 @@ The proposed data-side fix lives in `policyengine-uk-data` and would: - Source the ASP-by-band distribution from ONS *National Pensioners Survey* breakdowns or DWP administrative caseload by pension type. +Projected years also need the pension-age population to follow its +cohorts, and records that move to a new State Pension cohort to get that +cohort's awards; neither is modelled. + This is tracked under [#1632](https://github.com/PolicyEngine/policyengine-uk/issues/1632) and the broader UK pipeline-alignment tracker [#1621](https://github.com/PolicyEngine/policyengine-uk/issues/1621). @@ -139,5 +205,9 @@ and the broader UK pipeline-alignment tracker - HMRC, [State Pension forecast](https://www.gov.uk/check-state-pension) (the underlying SP1 figure that lands in the FRS). - [Pensions Act 2014](https://www.legislation.gov.uk/ukpga/2014/19/contents) — introduces the New State Pension and the BASIC / NEW boundary. - [Social Security Contributions and Benefits Act 1992, Part 2](https://www.legislation.gov.uk/ukpga/1992/4/part/II) — primary statute for the basic scheme. -- OBR March 2026 EFO — State Pension expenditure target. +- OBR, [Economic and fiscal outlook, March 2026: detailed forecast tables, expenditure](https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-expenditure/), table 4.9, "State pension". +- DWP, [Benefit expenditure and caseload tables 2026: outturn and forecast, Spring Forecast 2026](https://assets.publishing.service.gov.uk/media/69dcdc8c6b695d635c34dcc4/outturn-and-forecast-tables-spring-forecast-2026.xlsx), "State Pension" sheet: total, paid outside the UK, components and caseload. +- DWP, [State Pension expenditure by country of residence, 2024-25](https://assets.publishing.service.gov.uk/media/693ffff9cfacd5e888491fb6/State-Pension-by-country-2024-25.ods), for the 2024-25 outturn for residents of Great Britain. +- DWP, [Family Resources Survey, integrating administrative data for benefits: tables](https://assets.publishing.service.gov.uk/media/69c416acb66ff902f45441f0/family-resources-survey-transformation-benefits.xlsx), sheet 16, State Pension: admin-linked survey and administrative estimates, 2024-25. +- DWP, [Family Resources Survey 2024-25: background information and methodology](https://www.gov.uk/government/statistics/family-resources-survey-financial-year-2024-to-2025/family-resources-survey-background-information-and-methodology), section 4.1, for the survey's coverage of private households. - House of Commons Library, [State Pension triple lock](https://commonslibrary.parliament.uk/research-briefings/cbp-7812/) — context for the triple-lock parameters. diff --git a/policyengine_uk/tests/test_state_pension_components.py b/policyengine_uk/tests/test_state_pension_components.py new file mode 100644 index 0000000000..1a84da2336 --- /dev/null +++ b/policyengine_uk/tests/test_state_pension_components.py @@ -0,0 +1,266 @@ +"""State Pension components when the model year is not the data year. + +basic_state_pension, new_state_pension and additional_state_pension split the +State Pension a person reported in the data year. Survey ages are held fixed +across the years a dataset is projected to, so a record's birth cohort moves +one year later for each year projected, and its State Pension type in the +period can differ from its type in the data year. All three components split +by the period's type, so together they pay the reported amount uprated by that +type's flat rate. + +The YAML runner builds simulations without a dataset, where the data year is +the period by construction, so these cases build simulations from data. +""" + +import numpy as np +import pandas as pd +import pytest +from hypothesis import given, settings +from hypothesis import strategies as st + +from policyengine_uk import Microsimulation +from policyengine_uk.data import UKMultiYearDataset, UKSingleYearDataset + +DATA_YEAR = 2024 +YEARS = range(DATA_YEAR, 2031) +WEEKS_IN_YEAR = 52 +COMPONENTS = ["basic_state_pension", "new_state_pension", "additional_state_pension"] + + +def person_table(people: list, ages=None) -> pd.DataFrame: + ids = np.arange(len(people)) + return pd.DataFrame( + { + "person_id": ids, + "person_benunit_id": ids, + "person_household_id": ids, + "age": [p["age"] for p in people] if ages is None else ages, + "gender": ["MALE" if p["male"] else "FEMALE" for p in people], + "months_since_last_birthday": [p["months"] for p in people], + "state_pension_reported": [p["weekly"] * WEEKS_IN_YEAR for p in people], + } + ) + + +def year_dataset(people: list, year: int, ages=None) -> UKSingleYearDataset: + """One person per household.""" + ids = np.arange(len(people)) + household = pd.DataFrame( + { + "household_id": ids, + "household_weight": np.ones(len(people)), + "region": "LONDON", + "tenure_type": "OWNED_OUTRIGHT", + "rent": 0.0, + "council_tax": 0.0, + } + ) + return UKSingleYearDataset( + person=person_table(people, ages), + benunit=pd.DataFrame({"benunit_id": ids}), + household=household, + fiscal_year=year, + ) + + +def projected_simulation(people: list) -> Microsimulation: + """Data for 2024-25, projected forward as survey data is: ages held fixed.""" + return Microsimulation(dataset=year_dataset(people, DATA_YEAR)) + + +def flat_rates(sim, year: int) -> dict: + p = sim.tax_benefit_system.parameters.gov.dwp.state_pension + return { + "BASIC": p.basic_state_pension.amount(year), + "NEW": p.new_state_pension.amount(year), + } + + +def uprated_reported(sim, year: int, pension_type: str, weekly: float) -> float: + """The data-year amount uprated by the flat rate of the period's type.""" + now, then = flat_rates(sim, year), flat_rates(sim, DATA_YEAR) + return weekly * WEEKS_IN_YEAR * now[pension_type] / then[pension_type] + + +def calculate(sim, variable: str, year: int) -> np.ndarray: + return np.asarray(sim.calculate(variable, year)) + + +def test_basic_in_data_year_and_new_in_period_is_not_paid_twice(): + """A man aged 75 on 6 October 2024, born 6 July 1949, reached State Pension + age at 65 in 2014: basic State Pension. Held at 75, the record in 2030-31 + stands for a man born 6 July 1955, who reached it at 66 in 2021: new State + Pension. His £200 a week, below the new State Pension's full rate, is all + new State Pension; before the fix, the £30.50 above the basic State + Pension's full rate was paid again as additional State Pension.""" + person = {"age": 75, "male": True, "months": 3, "weekly": 200} + sim = projected_simulation([person]) + assert calculate(sim, "state_pension_type", DATA_YEAR)[0] == "BASIC" + for year in (2027, 2030): + assert calculate(sim, "state_pension_type", year)[0] == "NEW" + expected = uprated_reported(sim, year, "NEW", 200) + assert calculate(sim, "new_state_pension", year)[0] == pytest.approx( + expected, abs=0.01 + ) + assert calculate(sim, "basic_state_pension", year)[0] == 0 + assert calculate(sim, "additional_state_pension", year)[0] == 0 + assert calculate(sim, "state_pension", year)[0] == pytest.approx( + expected, abs=0.01 + ) + + +def test_protected_payment_above_new_state_pension_follows_period_type(): + """A woman aged 72 in 2024-25 (born 6 July 1952) reached State Pension age in + 2014 on the basic State Pension; held at 72 she stands, by 2030-31, for a + woman born in 1958, on the new State Pension. Her £240 a week exceeds the + new State Pension's full rate, so the excess is additional State Pension + (a protected payment) at the new State Pension's uprating.""" + person = {"age": 72, "male": False, "months": 3, "weekly": 240} + sim = projected_simulation([person]) + assert calculate(sim, "state_pension_type", DATA_YEAR)[0] == "BASIC" + assert calculate(sim, "state_pension_type", 2030)[0] == "NEW" + rates, data_rates = flat_rates(sim, 2030), flat_rates(sim, DATA_YEAR) + uprating = rates["NEW"] / data_rates["NEW"] + assert calculate(sim, "new_state_pension", 2030)[0] == pytest.approx( + rates["NEW"] * WEEKS_IN_YEAR, abs=0.01 + ) + assert calculate(sim, "additional_state_pension", 2030)[0] == pytest.approx( + (240 - data_rates["NEW"]) * WEEKS_IN_YEAR * uprating, abs=0.01 + ) + + +@pytest.mark.parametrize( + "person, pension_type", + [ + # Aged 90: born 1934 in the data year, 1940 by 2030-31. + ({"age": 90, "male": True, "months": 3, "weekly": 250}, "BASIC"), + # Aged 70: born 1954 in the data year, 1960 by 2030-31; both reached + # State Pension age after 5 April 2016. + ({"age": 70, "male": True, "months": 3, "weekly": 240}, "NEW"), + ], +) +def test_unchanged_type_splits_at_its_own_flat_rate(person, pension_type): + sim = projected_simulation([person]) + for year in YEARS: + assert calculate(sim, "state_pension_type", year)[0] == pension_type + total = sum(calculate(sim, v, year)[0] for v in COMPONENTS) + assert total == pytest.approx( + uprated_reported(sim, year, pension_type, person["weekly"]), abs=0.01 + ) + + +def test_nothing_is_paid_once_the_record_falls_below_state_pension_age(): + """Aged 66 in 2024-25 (born 6 July 1958) is over State Pension age on the new + State Pension; aged 66 in 2027-28 (born 6 July 1961) attains it at 67.""" + person = {"age": 66, "male": True, "months": 3, "weekly": 230} + sim = projected_simulation([person]) + assert calculate(sim, "state_pension_type", DATA_YEAR)[0] == "NEW" + assert calculate(sim, "state_pension_type", 2027)[0] == "NONE" + for variable in COMPONENTS + ["state_pension"]: + assert calculate(sim, variable, 2027)[0] == 0 + + +def test_new_in_data_year_and_basic_in_period_is_paid_in_full(): + """Held-fixed ages only move cohorts later, so a record never goes from new + to basic State Pension when projected. A dataset may still carry other ages + in later years; the band between the two flat rates must still be paid.""" + people = [ + # New State Pension at 70 in 2024-25; 85 in 2030-31 is basic. + {"age": 70, "male": True, "months": 3, "weekly": 240}, + # Below State Pension age at 60 in 2024-25; 70 in 2030-31 is new. + {"age": 60, "male": True, "months": 3, "weekly": 230}, + ] + dataset = UKMultiYearDataset( + datasets=[ + year_dataset(people, DATA_YEAR), + year_dataset(people, 2030, ages=[85, 70]), + ] + ) + sim = Microsimulation(dataset=dataset) + assert list(calculate(sim, "state_pension_type", DATA_YEAR)) == ["NEW", "NONE"] + assert list(calculate(sim, "state_pension_type", 2030)) == ["BASIC", "NEW"] + total = sum(calculate(sim, v, 2030) for v in COMPONENTS) + assert total[0] == pytest.approx( + uprated_reported(sim, 2030, "BASIC", 240), abs=0.01 + ) + assert total[1] == pytest.approx(uprated_reported(sim, 2030, "NEW", 230), abs=0.01) + + +PEOPLE = st.lists( + st.fixed_dictionaries( + { + "age": st.integers(min_value=55, max_value=100), + "male": st.booleans(), + "months": st.floats(min_value=0, max_value=12, exclude_max=True), + # Include each data-year flat rate, where the split changes. + "weekly": st.one_of( + st.floats(min_value=0, max_value=600), + st.sampled_from([0, 169.5, 221.2]), + ), + } + ), + min_size=1, + max_size=30, +) + + +def assert_components_add_up(sim, people: list, years) -> None: + """For each person and year: below State Pension age, no component and type + NONE; over it, only the period type's flat-rate component, and the three + components sum to the reported amount uprated by that type's flat rate.""" + weekly = np.array([p["weekly"] for p in people]) + for year in years: + pension_type = calculate(sim, "state_pension_type", year).astype(str) + over_spa = calculate(sim, "is_SP_age", year).astype(bool) + basic, new, additional = (calculate(sim, v, year) for v in COMPONENTS) + rates, data_rates = flat_rates(sim, year), flat_rates(sim, DATA_YEAR) + uprating = np.select( + [pension_type == "BASIC", pension_type == "NEW"], + [ + rates["BASIC"] / data_rates["BASIC"], + rates["NEW"] / data_rates["NEW"], + ], + 0, + ) + expected = over_spa * weekly * WEEKS_IN_YEAR * uprating + assert np.array_equal(pension_type == "NONE", ~over_spa), year + assert np.allclose(basic + new + additional, expected, rtol=1e-5, atol=0.01) + assert np.all(basic[pension_type != "BASIC"] == 0) + assert np.all(new[pension_type != "NEW"] == 0) + assert np.all(additional >= 0) + assert np.all(basic <= rates["BASIC"] * WEEKS_IN_YEAR * (1 + 1e-6)) + assert np.all(new <= rates["NEW"] * WEEKS_IN_YEAR * (1 + 1e-6)) + + +@settings(max_examples=25, deadline=None) +@given(people=PEOPLE) +def test_components_add_up_to_uprated_reported_amount(people): + """Data projected with ages held fixed, every year from 2024-25 to 2030-31.""" + sim = projected_simulation(people) + assert_components_add_up(sim, people, YEARS) + + +@settings(max_examples=25, deadline=None) +@given( + people=PEOPLE, + period=st.integers(min_value=DATA_YEAR + 1, max_value=2030), + data=st.data(), +) +def test_components_add_up_whatever_ages_the_period_carries(people, period, data): + """Any age in the period, so every pair of data-year and period types occurs, + including new to basic and none to either.""" + ages = data.draw( + st.lists( + st.integers(min_value=55, max_value=100), + min_size=len(people), + max_size=len(people), + ) + ) + dataset = UKMultiYearDataset( + datasets=[ + year_dataset(people, DATA_YEAR), + year_dataset(people, period, ages=ages), + ] + ) + sim = Microsimulation(dataset=dataset) + assert_components_add_up(sim, people, (DATA_YEAR, period)) diff --git a/policyengine_uk/variables/gov/dwp/additional_state_pension.py b/policyengine_uk/variables/gov/dwp/additional_state_pension.py index 0467ec7d3e..3a8c5204f2 100644 --- a/policyengine_uk/variables/gov/dwp/additional_state_pension.py +++ b/policyengine_uk/variables/gov/dwp/additional_state_pension.py @@ -19,7 +19,14 @@ def formula(person, period, parameters): else: data_year = period.start.year reported = person("state_pension_reported", data_year) / WEEKS_IN_YEAR - pension_type = person("state_pension_type", data_year) + # Split by the period's type, as basic_state_pension and + # new_state_pension do, so the three components add up to the + # reported amount. Survey ages are held fixed across years, so a + # record's birth cohort, and with it its type, can differ from the + # data year's. The data year's type would pay the band between the + # two flat-rate ceilings twice for a record that is BASIC in the data + # year and NEW in the period, and leave it unpaid the other way round. + pension_type = person("state_pension_type", period) types = pension_type.possible_values bsp_amount = parameters.gov.dwp.state_pension.basic_state_pension.amount @@ -51,7 +58,5 @@ def formula(person, period, parameters): max_for_type_period / max_for_type_data, 1, ) - # No State Pension is paid before State Pension age; the data-year - # type above only splits the reported amount into its components. - is_sp_age = person("is_SP_age", period) - return is_sp_age * amount_in_data_year * uprating * WEEKS_IN_YEAR + # The type is NONE below State Pension age, so nothing is paid there. + return amount_in_data_year * uprating * WEEKS_IN_YEAR From eca613478ea1ffe27fe079a2cd30c00e773a725a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 30 Sep 2026 11:48:00 -0400 Subject: [PATCH 2/5] Compare State Pension with DWP's forecast and fix the gap explanation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/book/programs/gov/dwp/state-pension.md | 95 ++++++++++----------- 1 file changed, 47 insertions(+), 48 deletions(-) diff --git a/docs/book/programs/gov/dwp/state-pension.md b/docs/book/programs/gov/dwp/state-pension.md index 5cd763c2b5..c061de9a46 100644 --- a/docs/book/programs/gov/dwp/state-pension.md +++ b/docs/book/programs/gov/dwp/state-pension.md @@ -118,38 +118,52 @@ These flags exist so that policy reforms can disable individual limbs The model's State Pension covers UK private households: the Family Resources Survey covers private households only, not nursing homes and -other communal establishments. The OBR's State Pension line matches DWP's -forecast of State Pension spending, which covers Great Britain and UK State -Pensions paid to people living abroad, but not Northern Ireland, whose -State Pension the Department for Communities pays. The table compares full -microsimulation runs on the enhanced FRS 2024-25 -(policyengine-uk-data-private 1.57.4) with that line less DWP's payments +other communal establishments. DWP's State Pension spending covers Great +Britain and UK State Pensions paid to people living abroad, but not +Northern Ireland, whose State Pension the Department for Communities pays. +DWP's Spring Forecast 2026 is consistent with the OBR's March 2026 forecast. +From 2026-27 the two differ by about £2m a year. The OBR's figures are +£1.4bn higher in 2024-25, which the OBR records as outturn, and £0.1bn +higher in 2025-26. + +The table compares full microsimulation runs on the enhanced FRS 2024-25 +(policyengine-uk-data-private 1.57.4) with DWP's spending less its payments abroad. | £bn | 2024-25 | 2025-26 | 2026-27 | 2027-28 | 2028-29 | 2029-30 | 2030-31 | |---|---:|---:|---:|---:|---:|---:|---:| | Model State Pension | 119.0 | 124.8 | 129.6 | 130.9 | 132.9 | 136.9 | 140.9 | | OBR March 2026 EFO, State Pension | 138.0 | 146.2 | 154.2 | 158.9 | 164.0 | 172.2 | 180.7 | -| of which paid abroad (DWP) | 5.3 | 5.6 | 5.9 | 6.1 | 6.2 | 6.4 | 6.6 | -| Model minus OBR less paid abroad | -13.6 | -15.8 | -18.6 | -21.9 | -24.8 | -28.9 | -33.2 | - -The model also includes Northern Ireland, which the OBR line does not, so -the like-for-like gap is larger by Northern Ireland's State Pension. In -2024-25, the data year, DWP's outturn for residents of Great Britain is -£131.2bn, £12.2bn above the model. The FRS's own grossed estimate for -the UK that year, with benefit amounts linked to DWP records, is £130.6bn, -against £135.1bn from administrative data, so most of the model's shortfall -arises in building the enhanced FRS rather than in the survey's coverage. - -The gap widens in later years for two reasons: +| DWP Spring Forecast 2026, State Pension | 136.6 | 146.1 | 154.2 | 158.9 | 164.0 | 172.2 | 180.7 | +| of which paid abroad | 5.3 | 5.6 | 5.9 | 6.1 | 6.2 | 6.4 | 6.6 | +| Model minus DWP less paid abroad | -12.2 | -15.6 | -18.6 | -21.9 | -24.8 | -28.9 | -33.1 | + +The model also includes Northern Ireland, which DWP's figures do not, so the +like-for-like gap is larger by Northern Ireland's State Pension. + +**The data year.** In 2024-25 the model has 11.36m State Pension recipients +averaging £201.6 a week. The FRS's own grossed figures are 11.53m recipients +averaging £212 a week, and DWP's administrative figures are 11.88m and £212 +(FRS methodology tables M.6a and M.6b). The FRS's grossed estimate for the +UK, with benefit amounts linked to DWP records, is £130.6bn, against +£135.1bn from administrative data. So most of the model's £16bn shortfall +against administrative data for the UK arises in building the enhanced FRS, +not in the survey. For FRS respondents, `state_pension_reported` is the +FRS benefits table's weekly State Pension amount times 52 +(policyengine-uk-data's `datasets/frs.py`). On the rows the enhanced FRS +adds from the Survey of Personal Incomes, it is imputed +(`datasets/imputations/frs_only.py`). The shortfall is tracked in +[policyengine-uk-data#493](https://github.com/PolicyEngine/policyengine-uk-data/issues/493). + +**Projected years.** The gap widens after the data year for two reasons, +tracked in [#1929](https://github.com/PolicyEngine/policyengine-uk/issues/1929): - **Caseload.** Survey ages are held fixed in projected years and household weights grow with total population - (`policyengine_uk/data/uprating_indices.yaml`), so the - pension-age population keeps the data year's age structure. From 2025-26 - to 2030-31 the model's State Pension recipients fall 3.0%, as State - Pension age rises to 67, while DWP's caseload less those paid abroad rises - 4.8%. + (`policyengine_uk/data/uprating_indices.yaml`), so the pension-age + population keeps the data year's age structure. From 2025-26 to 2030-31 + the model's State Pension recipients fall 3.0%, as State Pension age rises + to 67, while DWP's caseload less those paid abroad rises 4.8%. - **Awards.** Each record keeps its reported amount, uprated by the flat rate, including records whose cohort moves from basic to new State Pension. The model's State Pension per recipient grows 16.4% over those @@ -160,9 +174,9 @@ The gap widens in later years for two reasons: Until the fix for [#1921](https://github.com/PolicyEngine/policyengine-uk/issues/1921), `additional_state_pension` paid the band between the flat rates twice for records moving from basic to new State Pension cohorts (see Components -above): £0.8bn in 2025-26, rising to £6.8bn in 2030-31. -That made the model's State Pension per recipient grow 21.2%, faster than -DWP's, and hid part of the gap. +above): £0.8bn in 2025-26, rising to £6.8bn in 2030-31. That made the +model's State Pension per recipient grow 21.2%, faster than DWP's, and hid +part of the widening. ### What's been fixed @@ -175,39 +189,24 @@ DWP's, and hid part of the gap. ### What's still open -The data year's gap appears to come from the **data side** rather than -the formula. The FRS records State Pension as a single weekly -benefit value (`state_pension_reported`), which is derived from the -DWP-administered single weekly figure (SRP). For BASIC-type retirees -who reported exactly the maximum basic rate, the formula assigns ASP = -0 — but in reality many of those retirees also received SERPS / S2P -top-ups that the single weekly figure either caps or rounds. - -The proposed data-side fix lives in `policyengine-uk-data` and would: - -- Impute an ASP component on BASIC-type rows whose reported state - pension matches the max basic rate exactly, using the DWP-published - share of SERPS / S2P recipients in that band. -- Source the ASP-by-band distribution from ONS *National Pensioners - Survey* breakdowns or DWP administrative caseload by pension type. - -Projected years also need the pension-age population to follow its -cohorts, and records that move to a new State Pension cohort to get that -cohort's awards; neither is modelled. +- The data year's shortfall in the enhanced FRS: + [policyengine-uk-data#493](https://github.com/PolicyEngine/policyengine-uk-data/issues/493). +- The pension-age population and new-cohort awards in projected years: + [#1929](https://github.com/PolicyEngine/policyengine-uk/issues/1929). -This is tracked under [#1632](https://github.com/PolicyEngine/policyengine-uk/issues/1632) +Both are part of [#1632](https://github.com/PolicyEngine/policyengine-uk/issues/1632) and the broader UK pipeline-alignment tracker [#1621](https://github.com/PolicyEngine/policyengine-uk/issues/1621). ## References - DWP, [New State Pension](https://www.gov.uk/new-state-pension) and [Basic State Pension](https://www.gov.uk/state-pension) user-facing pages. -- HMRC, [State Pension forecast](https://www.gov.uk/check-state-pension) (the underlying SP1 figure that lands in the FRS). +- GOV.UK, [Check your State Pension forecast](https://www.gov.uk/check-state-pension). - [Pensions Act 2014](https://www.legislation.gov.uk/ukpga/2014/19/contents) — introduces the New State Pension and the BASIC / NEW boundary. - [Social Security Contributions and Benefits Act 1992, Part 2](https://www.legislation.gov.uk/ukpga/1992/4/part/II) — primary statute for the basic scheme. - OBR, [Economic and fiscal outlook, March 2026: detailed forecast tables, expenditure](https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-expenditure/), table 4.9, "State pension". - DWP, [Benefit expenditure and caseload tables 2026: outturn and forecast, Spring Forecast 2026](https://assets.publishing.service.gov.uk/media/69dcdc8c6b695d635c34dcc4/outturn-and-forecast-tables-spring-forecast-2026.xlsx), "State Pension" sheet: total, paid outside the UK, components and caseload. -- DWP, [State Pension expenditure by country of residence, 2024-25](https://assets.publishing.service.gov.uk/media/693ffff9cfacd5e888491fb6/State-Pension-by-country-2024-25.ods), for the 2024-25 outturn for residents of Great Britain. - DWP, [Family Resources Survey, integrating administrative data for benefits: tables](https://assets.publishing.service.gov.uk/media/69c416acb66ff902f45441f0/family-resources-survey-transformation-benefits.xlsx), sheet 16, State Pension: admin-linked survey and administrative estimates, 2024-25. +- DWP, [Family Resources Survey 2024-25: methodology and standard error tables](https://assets.publishing.service.gov.uk/media/69c3c0d2471d520038d0f571/ch1_methodology_and_standard_errors.xlsx), tables M.6a and M.6b: State Pension recipients and weekly amounts, FRS against administrative data. - DWP, [Family Resources Survey 2024-25: background information and methodology](https://www.gov.uk/government/statistics/family-resources-survey-financial-year-2024-to-2025/family-resources-survey-background-information-and-methodology), section 4.1, for the survey's coverage of private households. - House of Commons Library, [State Pension triple lock](https://commonslibrary.parliament.uk/research-briefings/cbp-7812/) — context for the triple-lock parameters. From dce3fbc1c5a5b4b76574211995f9004b4747c5f8 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 30 Sep 2026 13:01:32 -0400 Subject: [PATCH 3/5] Keep the State Pension age guard; don't tie the tests to flat-rate uprating 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 --- docs/book/programs/gov/dwp/state-pension.md | 16 +++- .../tests/test_state_pension_components.py | 77 ++++++++++++++----- .../gov/dwp/additional_state_pension.py | 6 +- 3 files changed, 74 insertions(+), 25 deletions(-) diff --git a/docs/book/programs/gov/dwp/state-pension.md b/docs/book/programs/gov/dwp/state-pension.md index c061de9a46..14f881d969 100644 --- a/docs/book/programs/gov/dwp/state-pension.md +++ b/docs/book/programs/gov/dwp/state-pension.md @@ -30,9 +30,15 @@ Three variables under `gov/dwp/`: All three split the amount a person reported in the data year (the dataset's first year) by the person's `state_pension_type` in the year -simulated, and uprate it by that type's flat rate: the year's full rate -over the data year's. Together they pay the reported amount uprated by the -flat rate, for anyone over State Pension age. +simulated. The part up to that type's full rate in the data year is basic +or new State Pension, and the part above it is additional State Pension. +Both parts are uprated by the type's full rate (the year's over the data +year's), so together they pay the reported amount uprated by the flat rate, +for anyone over State Pension age. In law, additional pensions and +protected payments rise with prices instead: the Social Security Benefits +Up-rating Order 2026 raised them by 3.8% in April 2026 (articles 4(3) and +6(3)), and the full rates by 4.8%. This is tracked in +[#1941](https://github.com/PolicyEngine/policyengine-uk/issues/1941). Survey ages are held fixed in the years a dataset is projected to, so a record's birth cohort moves one year later for each year projected, and its @@ -44,7 +50,9 @@ in 2021, on the new State Pension. Until the fix for used the year's, so for records like his the part of the reported amount between the basic and new flat rates was paid twice. The records keep their reported amounts: a record that moves to a new State Pension cohort is not -given a new State Pension award. +given a new State Pension award. That is one reason the model's State +Pension per recipient grows more slowly than DWP's (see Known aggregate gap +below). The flag-up split (`state_pension_type`) is settled by [PR #1618](https://github.com/PolicyEngine/policyengine-uk/pull/1618): classification is based on whether the person reaches State Pension age diff --git a/policyengine_uk/tests/test_state_pension_components.py b/policyengine_uk/tests/test_state_pension_components.py index 1a84da2336..eb3d5b28ce 100644 --- a/policyengine_uk/tests/test_state_pension_components.py +++ b/policyengine_uk/tests/test_state_pension_components.py @@ -5,8 +5,9 @@ across the years a dataset is projected to, so a record's birth cohort moves one year later for each year projected, and its State Pension type in the period can differ from its type in the data year. All three components split -by the period's type, so together they pay the reported amount uprated by that -type's flat rate. +by the period's type, so together they split the reported amount with no +overlap and no gap: the part up to the type's full rate is uprated by the full +rate, and the part above it by the add-on's uprating (see add_on_uprating). The YAML runner builds simulations without a dataset, where the data year is the period by construction, so these cases build simulations from data. @@ -76,10 +77,29 @@ def flat_rates(sim, year: int) -> dict: } +def flat_uprating(sim, year: int, pension_type: str) -> float: + """The type's full rate in the year over its full rate in the data year.""" + return ( + flat_rates(sim, year)[pension_type] / flat_rates(sim, DATA_YEAR)[pension_type] + ) + + +def add_on_uprating(sim, year: int, pension_type: str) -> float: + """Uprating of the part above the full rate (additional pension, protected + payments). The model uprates it by the full rate; in law these rise with + prices (Social Security Benefits Up-rating Order 2026 arts 4(3) and 6(3)), + tracked in PolicyEngine/policyengine-uk#1941. Change it here with the model.""" + return flat_uprating(sim, year, pension_type) + + def uprated_reported(sim, year: int, pension_type: str, weekly: float) -> float: - """The data-year amount uprated by the flat rate of the period's type.""" - now, then = flat_rates(sim, year), flat_rates(sim, DATA_YEAR) - return weekly * WEEKS_IN_YEAR * now[pension_type] / then[pension_type] + """The data-year amount, its part up to the full rate uprated by the full + rate and the part above it by the add-on's uprating.""" + full = flat_rates(sim, DATA_YEAR)[pension_type] + return WEEKS_IN_YEAR * ( + min(weekly, full) * flat_uprating(sim, year, pension_type) + + max(weekly - full, 0) * add_on_uprating(sim, year, pension_type) + ) def calculate(sim, variable: str, year: int) -> np.ndarray: @@ -114,18 +134,18 @@ def test_protected_payment_above_new_state_pension_follows_period_type(): 2014 on the basic State Pension; held at 72 she stands, by 2030-31, for a woman born in 1958, on the new State Pension. Her £240 a week exceeds the new State Pension's full rate, so the excess is additional State Pension - (a protected payment) at the new State Pension's uprating.""" + (a protected payment), uprated as the model uprates add-ons.""" person = {"age": 72, "male": False, "months": 3, "weekly": 240} sim = projected_simulation([person]) assert calculate(sim, "state_pension_type", DATA_YEAR)[0] == "BASIC" assert calculate(sim, "state_pension_type", 2030)[0] == "NEW" rates, data_rates = flat_rates(sim, 2030), flat_rates(sim, DATA_YEAR) - uprating = rates["NEW"] / data_rates["NEW"] assert calculate(sim, "new_state_pension", 2030)[0] == pytest.approx( rates["NEW"] * WEEKS_IN_YEAR, abs=0.01 ) assert calculate(sim, "additional_state_pension", 2030)[0] == pytest.approx( - (240 - data_rates["NEW"]) * WEEKS_IN_YEAR * uprating, abs=0.01 + (240 - data_rates["NEW"]) * WEEKS_IN_YEAR * add_on_uprating(sim, 2030, "NEW"), + abs=0.01, ) @@ -204,27 +224,46 @@ def test_new_in_data_year_and_basic_in_period_is_paid_in_full(): ) +def by_type(pension_type: np.ndarray, basic, new) -> np.ndarray: + return np.select([pension_type == "BASIC", pension_type == "NEW"], [basic, new], 0) + + def assert_components_add_up(sim, people: list, years) -> None: """For each person and year: below State Pension age, no component and type - NONE; over it, only the period type's flat-rate component, and the three - components sum to the reported amount uprated by that type's flat rate.""" + NONE. Over it, only the period type's flat-rate component, which is the + reported amount up to the type's full rate, uprated by the full rate; and, + deflating each component by its own uprating, the three components add up + to exactly the reported amount, with no overlap and no gap.""" weekly = np.array([p["weekly"] for p in people]) for year in years: pension_type = calculate(sim, "state_pension_type", year).astype(str) over_spa = calculate(sim, "is_SP_age", year).astype(bool) basic, new, additional = (calculate(sim, v, year) for v in COMPONENTS) rates, data_rates = flat_rates(sim, year), flat_rates(sim, DATA_YEAR) - uprating = np.select( - [pension_type == "BASIC", pension_type == "NEW"], - [ - rates["BASIC"] / data_rates["BASIC"], - rates["NEW"] / data_rates["NEW"], - ], - 0, + full = by_type(pension_type, data_rates["BASIC"], data_rates["NEW"]) + flat_up = by_type( + pension_type, + flat_uprating(sim, year, "BASIC"), + flat_uprating(sim, year, "NEW"), ) - expected = over_spa * weekly * WEEKS_IN_YEAR * uprating + add_up = by_type( + pension_type, + add_on_uprating(sim, year, "BASIC"), + add_on_uprating(sim, year, "NEW"), + ) + reported = weekly * WEEKS_IN_YEAR assert np.array_equal(pension_type == "NONE", ~over_spa), year - assert np.allclose(basic + new + additional, expected, rtol=1e-5, atol=0.01) + flat = basic + new + expected_flat = over_spa * np.minimum(reported, full * WEEKS_IN_YEAR) * flat_up + assert np.allclose(flat, expected_flat, rtol=1e-5, atol=0.01) + deflated = np.where( + over_spa, + flat / np.where(over_spa, flat_up, 1) + + additional / np.where(over_spa, add_up, 1), + 0, + ) + assert np.allclose(deflated, over_spa * reported, rtol=1e-5, atol=0.01) + assert np.all(additional[~over_spa] == 0) assert np.all(basic[pension_type != "BASIC"] == 0) assert np.all(new[pension_type != "NEW"] == 0) assert np.all(additional >= 0) diff --git a/policyengine_uk/variables/gov/dwp/additional_state_pension.py b/policyengine_uk/variables/gov/dwp/additional_state_pension.py index 3a8c5204f2..9dd31f7c5d 100644 --- a/policyengine_uk/variables/gov/dwp/additional_state_pension.py +++ b/policyengine_uk/variables/gov/dwp/additional_state_pension.py @@ -58,5 +58,7 @@ def formula(person, period, parameters): max_for_type_period / max_for_type_data, 1, ) - # The type is NONE below State Pension age, so nothing is paid there. - return amount_in_data_year * uprating * WEEKS_IN_YEAR + # No State Pension is paid before State Pension age. A computed type + # is already NONE there; this also holds if the type is an input. + is_sp_age = person("is_SP_age", period) + return is_sp_age * amount_in_data_year * uprating * WEEKS_IN_YEAR From 215ef0beac0f8997da4eb10cb822dc17c0057e94 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 3 Oct 2026 16:25:59 -0400 Subject: [PATCH 4/5] Update State Pension docs figures to runs on current main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rerun the before/after microsimulation on main (325d585e) and this branch merged with it, after #1899 merged. Figures from 2027-28 move slightly because main now projects the flat rates from the triple lock's statutory inputs: the gap to DWP's forecast less payments abroad is -£32.5bn in 2030-31 (was -£33.1bn), and State Pension per recipient grows 17.0% from 2025-26 to 2030-31 (was 16.4%; 21.8% before the fix, was 21.2%). Co-Authored-By: Claude Opus 5.5 --- docs/book/programs/gov/dwp/state-pension.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/book/programs/gov/dwp/state-pension.md b/docs/book/programs/gov/dwp/state-pension.md index 2518c0a89b..2f0a2e2178 100644 --- a/docs/book/programs/gov/dwp/state-pension.md +++ b/docs/book/programs/gov/dwp/state-pension.md @@ -288,11 +288,11 @@ abroad. | £bn | 2024-25 | 2025-26 | 2026-27 | 2027-28 | 2028-29 | 2029-30 | 2030-31 | |---|---:|---:|---:|---:|---:|---:|---:| -| Model State Pension | 119.0 | 124.8 | 129.6 | 130.9 | 132.9 | 136.9 | 140.9 | +| Model State Pension | 119.0 | 124.8 | 129.6 | 131.6 | 133.6 | 137.5 | 141.6 | | OBR March 2026 EFO, State Pension | 138.0 | 146.2 | 154.2 | 158.9 | 164.0 | 172.2 | 180.7 | | DWP Spring Forecast 2026, State Pension | 136.6 | 146.1 | 154.2 | 158.9 | 164.0 | 172.2 | 180.7 | | of which paid abroad | 5.3 | 5.6 | 5.9 | 6.1 | 6.2 | 6.4 | 6.6 | -| Model minus DWP less paid abroad | -12.2 | -15.6 | -18.6 | -21.9 | -24.8 | -28.9 | -33.1 | +| Model minus DWP less paid abroad | -12.2 | -15.6 | -18.6 | -21.3 | -24.2 | -28.3 | -32.5 | The model also includes Northern Ireland, which DWP's figures do not, so the like-for-like gap is larger by Northern Ireland's State Pension. @@ -322,7 +322,7 @@ tracked in [#1929](https://github.com/PolicyEngine/policyengine-uk/issues/1929): to 67, while DWP's caseload less those paid abroad rises 4.8%. - **Awards.** Each record keeps its reported amount, uprated by the flat rate, including records whose cohort moves from basic to new State - Pension. The model's State Pension per recipient grows 16.4% over those + Pension. The model's State Pension per recipient grows 17.0% over those years. DWP's spending per recipient, less those paid abroad, grows 18.3%, as its new State Pension spending grows from £56.1bn to £102.4bn and its basic State Pension spending falls from £66.7bn to £57.8bn. @@ -331,7 +331,7 @@ Until the fix for [#1921](https://github.com/PolicyEngine/policyengine-uk/issues `additional_state_pension` paid the band between the flat rates twice for records moving from basic to new State Pension cohorts (see Components above): £0.8bn in 2025-26, rising to £6.8bn in 2030-31. That made the -model's State Pension per recipient grow 21.2%, faster than DWP's, and hid +model's State Pension per recipient grow 21.8%, faster than DWP's, and hid part of the widening. ### What's been fixed From 723524e0815989cfda1e5d1e10bab272b4becfc7 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 4 Oct 2026 00:58:06 -0400 Subject: [PATCH 5/5] Make the State Pension property tests deterministic; tighten docs Address the merge-gate review of 215ef0be: - Use the repo's usual Hypothesis settings (derandomize=True, too_slow health check suppressed) with 10 examples per property test, so the file runs in about 5 minutes. Both property tests still fail on main's formula. - Docs: the additional State Pension bullet now says the excess is uprated; "no component before State Pension age" is stated for a computed type; FRS tables M.6a/M.6b are for Great Britain, the model's 11.36m for the UK. Co-Authored-By: Claude Opus 5.5 --- docs/book/programs/gov/dwp/state-pension.md | 16 +++++++++------- .../tests/test_state_pension_components.py | 15 ++++++++++++--- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/docs/book/programs/gov/dwp/state-pension.md b/docs/book/programs/gov/dwp/state-pension.md index 2f0a2e2178..9f2c1d8b09 100644 --- a/docs/book/programs/gov/dwp/state-pension.md +++ b/docs/book/programs/gov/dwp/state-pension.md @@ -25,8 +25,9 @@ Three variables under `gov/dwp/`: accrual that exceeds the new flat rate, folded into NSP under current law but tracked separately in PolicyEngine so the reform surface stays clean. - - Both are computed as `max(reported - flat_max_for_type, 0)` per week, - multiplied by `WEEKS_IN_YEAR`. + - Both are the reported weekly amount above the type's full rate in the + data year, `max(reported - flat_max_for_type, 0)`, uprated as described + below and multiplied by `WEEKS_IN_YEAR`. All three split the amount a person reported in the data year (the dataset's first year) by the person's `state_pension_type` in the year @@ -56,8 +57,8 @@ below). The flag-up split (`state_pension_type`) is settled by [PR #1618](https://github.com/PolicyEngine/policyengine-uk/pull/1618): classification is based on whether the person reaches State Pension age -before or on/after 6 April 2016. No component is paid before State -Pension age. +before or on/after 6 April 2016. A computed `state_pension_type` is `NONE` +below State Pension age, so no component is paid there. ## State Pension age @@ -298,9 +299,10 @@ The model also includes Northern Ireland, which DWP's figures do not, so the like-for-like gap is larger by Northern Ireland's State Pension. **The data year.** In 2024-25 the model has 11.36m State Pension recipients -averaging £201.6 a week. The FRS's own grossed figures are 11.53m recipients -averaging £212 a week, and DWP's administrative figures are 11.88m and £212 -(FRS methodology tables M.6a and M.6b). The FRS's grossed estimate for the +averaging £201.6 a week, for the UK. The FRS's own grossed figures for Great +Britain are 11.53m recipients averaging £212 a week, and DWP's administrative +figures are 11.88m and £212 (FRS methodology tables M.6a and M.6b, which +cover Great Britain). The FRS's grossed estimate for the UK, with benefit amounts linked to DWP records, is £130.6bn, against £135.1bn from administrative data. So most of the model's £16bn shortfall against administrative data for the UK arises in building the enhanced FRS, diff --git a/policyengine_uk/tests/test_state_pension_components.py b/policyengine_uk/tests/test_state_pension_components.py index eb3d5b28ce..69f7c5d49b 100644 --- a/policyengine_uk/tests/test_state_pension_components.py +++ b/policyengine_uk/tests/test_state_pension_components.py @@ -16,7 +16,7 @@ import numpy as np import pandas as pd import pytest -from hypothesis import given, settings +from hypothesis import HealthCheck, given, settings from hypothesis import strategies as st from policyengine_uk import Microsimulation @@ -206,6 +206,15 @@ def test_new_in_data_year_and_basic_in_period_is_paid_in_full(): assert total[1] == pytest.approx(uprated_reported(sim, 2030, "NEW", 230), abs=0.01) +# Each example builds a microsimulation, so examples are slow to generate. +PROPERTY_SETTINGS = settings( + max_examples=10, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow], +) + + PEOPLE = st.lists( st.fixed_dictionaries( { @@ -271,7 +280,7 @@ def assert_components_add_up(sim, people: list, years) -> None: assert np.all(new <= rates["NEW"] * WEEKS_IN_YEAR * (1 + 1e-6)) -@settings(max_examples=25, deadline=None) +@PROPERTY_SETTINGS @given(people=PEOPLE) def test_components_add_up_to_uprated_reported_amount(people): """Data projected with ages held fixed, every year from 2024-25 to 2030-31.""" @@ -279,7 +288,7 @@ def test_components_add_up_to_uprated_reported_amount(people): assert_components_add_up(sim, people, YEARS) -@settings(max_examples=25, deadline=None) +@PROPERTY_SETTINGS @given( people=PEOPLE, period=st.integers(min_value=DATA_YEAR + 1, max_value=2030),