PolicyEngine / PolicyEngine/policyengine-uk
State pension undershoot: -£12bn vs OBR, ASP/Protected Payment under-captured
Nobody has claimed this yet.
- 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:
-
Formula-side — change
new_state_pensionto usestate_pension_reporteddirectly (with data-year uprating, matching thebasic_state_pension/additional_state_pensionpattern). 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. -
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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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