PolicyEngine / PolicyEngine/policyengine-uk

State pension undershoot: -£12bn vs OBR, ASP/Protected Payment under-captured

Open
#1,632 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
Python
Stars
50
Forks
33
Avg merge
20h 58m
Merged PRs (30d)
14

Description

Background

After PR #1618 fixed state_pension_type (BASIC vs NEW classification), the model's state pension aggregate is £127.5 bn vs the OBR 2025 target of ~£140 bn — still a -£12 bn gap, identified in tracking issue #1621 as the largest residual benefit aggregate misalignment.

Diagnosis

Two related pipeline limitations combine to understate the total:

1. new_state_pension pays flat max amount to every NEW-type retiree

Current formula in policyengine_uk/variables/gov/dwp/new_state_pension.py:

def formula(person, period, parameters):
    type = person("state_pension_type", period)
    eligible = type == type.possible_values.NEW
    p = parameters(period).gov.dwp.state_pension.new_state_pension
    return eligible * p.amount * WEEKS_IN_YEAR  # full amount for everyone

This ignores the reported amount entirely. In reality:

  • Under 35 NI years → pro-rated full amount
  • Over full rate (Protected Payment from pre-2016 SERPS/S2P) → higher than flat amount

The flat-max assumption over-states for partial-NI-record retirees and under-states Protected Payment for those with substantial pre-2016 SERPS contributions. The net effect on aggregate is ambiguous without data.

2. additional_state_pension only captures reported amounts exceeding max basic, for BASIC type

Current ASP formula in policyengine_uk/variables/gov/dwp/additional_state_pension.py:

reported = person("state_pension_reported", data_year) / WEEKS_IN_YEAR
type = person("state_pension_type", data_year)
# ...
amount_in_data_year = where(
    type == type.possible_values.BASIC,
    max_(reported - max_sp_data_year, 0),  # only BASIC
    0,
)

For BASIC-type retirees reporting exactly the max basic amount (£169/wk in 2024), this yields ASP = 0 — but many of those retirees in reality also received SERPS/S2P top-ups. FRS state_pension_reported is derived from SRP (the single weekly benefit value), which aggregates basic + ASP; if SRP is capped or rounded at the max basic, ASP is under-imputed.

Additionally, there's no Protected Payment component for NEW-type retirees anywhere in the pipeline.

Proposed fixes

Two options, both non-trivial:

  1. Formula-side — change new_state_pension to use state_pension_reported directly (with data-year uprating, matching the basic_state_pension / additional_state_pension pattern). This captures Protected Payment organically for NEW-type retirees who reported more than the flat max. Risk: reduces aggregate for partial-NI-record retirees whose reported is < max.

  2. Data-side — impute an ASP component on BASIC-type rows whose reported matches the max basic exactly. This requires breaking out SERPS/S2P coverage from an external source (ONS National Pensioners Survey or DWP administrative caseload by pension type).

Either approach needs empirical validation against OBR/DWP outturn for FY 2024-25. Filing for follow-up; too scoped to land in the current PR sweep.

References

  • OBR March 2026 EFO — state pension total ~£140 bn
  • Tracking issue: #1621
  • Prior fix: #1618 (state_pension_type BASIC vs NEW classification)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading policyengine_uk/variables/gov/dwp/new_state_pension.py and policyengine_uk/variables/gov/dwp/additional_state_pension.py, then trace state_pension_reported and the data-year uprating used by related formulas. Compare the candidate approaches against OBR/DWP FY 2024-25 outturn data. Done means an empirically validated change that addresses the aggregate gap without introducing unexplained partial-record or Protected Payment errors.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend, data
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.