Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/fix-state-pension-reform.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix state pension parameter reforms having no budget impact.
2 changes: 1 addition & 1 deletion policyengine_uk/tests/microsimulation/reforms_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ reforms:
parameters:
gov.hmrc.child_benefit.amount.additional: 25
- name: Reduce Universal Credit taper rate to 20%
expected_impact: -43.2
expected_impact: -42.0
parameters:
gov.dwp.universal_credit.means_test.reduction_rate: 0.2
- name: Raise Class 1 main employee NICs rate to 10%
Expand Down
53 changes: 53 additions & 0 deletions policyengine_uk/tests/microsimulation/test_state_pension_reform.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""
Test that reforms to basic State Pension parameters affect
microsimulation results.

Issue #1178: basic_state_pension reads parameters(data_year) instead of
parameters(period), so reforms to the parameter for the simulation year
have no budget impact.
"""

import pytest
from policyengine_uk import Microsimulation

YEAR = 2025


@pytest.fixture(scope="module")
def baseline():
"""Baseline microsimulation with current law."""
return Microsimulation()


@pytest.fixture(scope="module")
def reform_halved():
"""Reform that halves the basic State Pension amount for 2025."""
return Microsimulation(
reform={
"gov.dwp.state_pension.basic_state_pension.amount": {
"2025-01-01": 84.75,
}
}
)


@pytest.mark.microsimulation
def test_basic_state_pension_responds_to_reform(baseline, reform_halved):
"""
Halving the basic State Pension parameter should significantly
reduce the total basic_state_pension output.
"""
baseline_total = baseline.calculate("basic_state_pension", YEAR).sum()
reform_total = reform_halved.calculate("basic_state_pension", YEAR).sum()

assert (
baseline_total > 0
), "Baseline basic_state_pension should be positive"

# Halving the parameter should reduce total by at least 10%
assert reform_total < baseline_total * 0.9, (
f"Halving the basic SP parameter should significantly reduce "
f"total basic_state_pension. "
f"Baseline: {baseline_total / 1e9:.3f}bn, "
f"Reform: {reform_total / 1e9:.3f}bn"
)
22 changes: 16 additions & 6 deletions policyengine_uk/variables/gov/dwp/additional_state_pension.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,22 @@ def formula(person, period, parameters):
data_year = period.start.year
reported = person("state_pension_reported", data_year) / WEEKS_IN_YEAR
type = person("state_pension_type", data_year)
maximum_basic_sp = parameters(
data_year
).gov.dwp.state_pension.basic_state_pension.amount
amount_in_data_year = where(
# Use leaf-level access so parameter reforms are visible
sp_amount = parameters.gov.dwp.state_pension.basic_state_pension.amount
max_sp_data_year = sp_amount(data_year)
max_sp_period = sp_amount(period)

# Additional SP is the excess above the basic SP maximum
additional_data_year = where(
type == type.possible_values.BASIC,
max_(reported - maximum_basic_sp, 0),
max_(reported - max_sp_data_year, 0),
0,
)
return amount_in_data_year * WEEKS_IN_YEAR

# Uprate using the ratio of basic SP parameters (responds to reforms)
uprating = where(
max_sp_data_year > 0,
max_sp_period / max_sp_data_year,
1,
)
return additional_data_year * uprating * WEEKS_IN_YEAR
37 changes: 19 additions & 18 deletions policyengine_uk/variables/gov/dwp/basic_state_pension.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,25 +23,26 @@ def formula(person, period, parameters):

reported = person("state_pension_reported", data_year) / WEEKS_IN_YEAR
pension_type = person("state_pension_type", period)
maximum_basic_sp = parameters(
data_year
).gov.dwp.state_pension.basic_state_pension.amount

# For BASIC pension type, cap at the maximum; otherwise return 0
amount_in_data_year = where(
pension_type == pension_type.possible_values.BASIC,
min_(reported, maximum_basic_sp),
# Use leaf-level access (not node-level) so parameter reforms
# are visible. sp_amount(instant) reflects reforms, while
# sp(instant).amount does not.
sp_amount = parameters.gov.dwp.state_pension.basic_state_pension.amount
max_sp_data_year = sp_amount(data_year)
max_sp_period = sp_amount(period)

# Compute each person's share of the maximum basic SP at data year
share = where(
max_sp_data_year > 0,
min_(reported, max_sp_data_year) / max_sp_data_year,
0,
)

# Apply triple lock uprating only when using dataset
# (i.e., when data year differs from simulation period)
if has_dataset:
triple_lock = (
parameters.gov.economic_assumptions.indices.triple_lock
# Apply share to current period's maximum (responds to reforms)
return (
where(
pension_type == pension_type.possible_values.BASIC,
share * max_sp_period,
0,
)
uprating = triple_lock(period) / triple_lock(data_year)
else:
uprating = 1

return amount_in_data_year * uprating * WEEKS_IN_YEAR
* WEEKS_IN_YEAR
)