Skip to content

Commit e8572e3

Browse files
authored
Merge pull request #1567 from PolicyEngine/codex/fix-1389
Fix UC rebalancing protection for existing claimants
2 parents 6d46ff4 + b26d4af commit e8572e3

6 files changed

Lines changed: 222 additions & 20 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Corrected Universal Credit rebalancing so existing health-element claimants keep their combined standard allowance and health element award CPI-protected.

docs/book/policy/model-baseline.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ The government increased the [employer National Insurance rate from 13.8% to 15%
1212

1313
### Universal Credit rebalancing
1414

15-
Parliament passed legislation to implement Universal Credit rebalancing reforms, with the [rebalancing switch activated in fiscal year 2025-26](https://github.com/PolicyEngine/policyengine-uk/blob/master/policyengine_uk/parameters/gov/dwp/universal_credit/rebalancing/active.yaml#L3). The reforms include [graduated standard allowance uplifts](https://github.com/PolicyEngine/policyengine-uk/blob/master/policyengine_uk/parameters/gov/dwp/universal_credit/rebalancing/standard_allowance_uplift.yaml) above inflation: 2.3% in 2026-27, 3.1% in 2027-28, 4.0% in 2028-29, and 4.8% in 2029-30. A [new health element of £217.26](https://github.com/PolicyEngine/policyengine-uk/blob/master/policyengine_uk/parameters/gov/dwp/universal_credit/rebalancing/new_claimant_health_element.yaml#L3) will be introduced for new claimants in fiscal year 2026-27.
15+
Parliament passed legislation to implement Universal Credit rebalancing reforms, with the [rebalancing switch activated in fiscal year 2025-26](https://github.com/PolicyEngine/policyengine-uk/blob/master/policyengine_uk/parameters/gov/dwp/universal_credit/rebalancing/active.yaml#L3). The reforms include [graduated standard allowance uplifts](https://github.com/PolicyEngine/policyengine-uk/blob/master/policyengine_uk/parameters/gov/dwp/universal_credit/rebalancing/standard_allowance_uplift.yaml) above inflation: 2.3% in 2026-27, 3.1% in 2027-28, 4.0% in 2028-29, and 4.8% in 2029-30. A [new health element of £217.26](https://github.com/PolicyEngine/policyengine-uk/blob/master/policyengine_uk/parameters/gov/dwp/universal_credit/rebalancing/new_claimant_health_element.yaml#L3) applies to most new claimants in fiscal year 2026-27, while existing health-element claimants keep the combined value of their standard allowance and health element at least in line with CPI.
1616

1717
### Benefit uprating
1818

@@ -113,4 +113,4 @@ Notable exclusions:
113113
- **Non-UK resident stamp duty surcharge**: 2% additional rate from fiscal year 2021-22 is not modelled
114114
- **Some devolved tax policies**: Beyond property transaction taxes, other devolved policies may have limited coverage
115115

116-
All parameter values include references to primary legislation and can be found in the [PolicyEngine UK parameters directory](https://github.com/PolicyEngine/policyengine-uk/tree/master/policyengine_uk/parameters).
116+
All parameter values include references to primary legislation and can be found in the [PolicyEngine UK parameters directory](https://github.com/PolicyEngine/policyengine-uk/tree/master/policyengine_uk/parameters).

docs/book/policy/uc-rebalancing.md

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,25 @@ The Universal Credit rebalancing reforms represent changes to Universal Credit p
77
## Overview
88

99
```{important}
10-
The reforms consist of two main components: health element changes for new claimants and standard allowance uplifts.
10+
The reforms combine a higher standard allowance, protected awards for existing health-element recipients, and a lower fixed health element for most new claimants.
1111
```
1212

13-
1. **Health element changes for new claimants**: New Universal Credit claimants from April 2026 onwards receive a fixed health element amount, while existing claimants continue to receive inflation-linked increases.
13+
1. **Protected awards for existing claimants**: Existing recipients of the health element keep the combined value of their standard allowance and health element at least in line with CPI inflation through 2029-30.
1414

15-
2. **Standard allowance uplifts**: The standard allowance receives additional uplifts beyond the annual inflationary increase from 2026-2029.
15+
2. **Health element changes for new claimants**: New Universal Credit claimants from April 2026 onwards receive a fixed monthly health element amount of £217.26, rather than the protected existing-claimant amount.
16+
17+
3. **Standard allowance uplifts**: The standard allowance receives additional uplifts beyond the annual inflationary increase from 2026-2029.
1618

1719
## Health element changes
1820

19-
From April 2026, new Universal Credit claimants who qualify for the Limited Capacity for Work-Related Activity (LCWRA) element receive a fixed monthly amount of £217.26, rather than the inflation-adjusted amount that pre-2026 claimants continue to receive.
21+
From April 2026, new Universal Credit claimants who qualify for the Limited Capability for Work-Related Activity (LCWRA) element receive a fixed monthly amount of £217.26.
22+
23+
Existing recipients are treated differently. Their LCWRA amount is uprated so that the combined value of:
24+
25+
- their standard allowance, and
26+
- their health element
27+
28+
rises at least in line with CPI inflation. The model implements that protection through the health element itself, preserving the combined award outcome without separately modelling the small administrative split between protected LCWRA amounts and any under-25 standard allowance supplement.
2029

2130
The implementation uses transition probabilities based on WPI Economics analysis for the Trussell Trust, derived from administrative Personal Independence Payment data. The probability of being a new claimant varies by year:
2231

@@ -34,7 +43,7 @@ The standard allowance receives additional percentage uplifts beyond the normal
3443
- 2028: 4.0% additional uplift (cumulative)
3544
- 2029: 4.8% additional uplift (cumulative)
3645

37-
These uplifts are applied to the previous year's standard allowance amount and compound over time.
46+
These uplifts are applied to the CPI-uprated standard allowance for each year. In other words, the model first applies the usual CPI uprating and then applies the rebalancing uplift on top.
3847

3948
## Implementation
4049

@@ -43,7 +52,7 @@ The reforms are implemented through parameters, scenario modifiers, and scenario
4352
```
4453

4554
- **Parameters**: Three YAML files define the reform's activation status, health element amount for new claimants, and standard allowance uplift rates.
46-
- **Scenario modifier**: The `add_universal_credit_reform` function applies the changes to Universal Credit calculations during microsimulation.
55+
- **Scenario modifier**: The `add_universal_credit_reform` function applies the protected existing-claimant health-element path during microsimulation.
4756
- **Scenario**: The `universal_credit_july_2025_reform` scenario enables the reforms in policy analysis.
4857

4958
## Examples
@@ -98,4 +107,8 @@ sim = Simulation(scenario=scenario)
98107

99108
## Legislative reference
100109

101-
The reforms are based on provisions in the Universal Credit Bill, available at: https://bills.parliament.uk/publications/62123/documents/6889.
110+
The reforms are based on the Universal Credit Bill and its impact assessment:
111+
112+
- https://bills.parliament.uk/publications/62123/documents/6889
113+
- https://bills.parliament.uk/publications/62124/documents/6892
114+
- https://assets.publishing.service.gov.uk/media/689ca49e1c63de6de5bb1298/withdrawn-universal-credit-bill-uc-rebalancing-impact-assessment.pdf

docs/book/usage/scenarios.md

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -391,14 +391,14 @@ for year in [2025, 2027, 2029]:
391391

392392
### Building Universal Credit scenarios with dynamic changes
393393

394-
Some scenarios need to make changes that depend on the simulation's own data. Here's how to create a UC scenario that adjusts payments based on claimant characteristics:
394+
Some scenarios need to make changes that depend on the simulation's own data. Here's how to create a UC scenario that adjusts health-element payments based on claimant characteristics:
395395

396396
```python
397397
from policyengine_uk import Scenario, Microsimulation
398398
import numpy as np
399399

400400
def modify_uc_for_new_claimants(sim: Microsimulation):
401-
"""Reduce health elements for new UC claimants while increasing standard allowances"""
401+
"""Reduce health elements for new UC claimants while preserving claimant protections"""
402402
# Access the parameter system to check if reforms are active
403403
rebalancing_params = sim.tax_benefit_system.parameters.gov.dwp.universal_credit.rebalancing
404404

@@ -434,11 +434,9 @@ def modify_uc_for_new_claimants(sim: Microsimulation):
434434

435435
sim.set_input("uc_LCWRA_element", year, current_health_element)
436436

437-
# Increase standard allowances for everyone
438-
uplift_rate = rebalancing_params.standard_allowance_uplift(year)
439-
previous_allowance = sim.calculate("uc_standard_allowance", year - 1)
440-
new_allowance = previous_allowance * (1 + uplift_rate)
441-
sim.set_input("uc_standard_allowance", year, new_allowance)
437+
# General standard allowance uplifts are already handled in the
438+
# uc_standard_allowance formula. Scenario modifiers only need to add
439+
# claimant-specific overrides such as protected health elements.
442440

443441
# Create the UC rebalancing scenario
444442
uc_rebalancing = Scenario(simulation_modifier=modify_uc_for_new_claimants)

policyengine_uk/scenarios/uc_reform.py

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,69 @@
11
from policyengine_uk.model_api import Scenario
22
from policyengine_uk import Microsimulation
3+
from policyengine_uk.variables.gov.dwp.universal_credit.standard_allowance.uc_standard_allowance_claimant_type import (
4+
UCClaimantType,
5+
)
36
import numpy as np
47

58

9+
BASELINE_UC_REBALANCING_YEAR = 2025
10+
11+
12+
def _benefit_uprating_ratio(sim: Microsimulation, year: int) -> float:
13+
parameters = sim.tax_benefit_system.parameters
14+
current_index = float(parameters(str(year)).gov.benefit_uprating_cpi)
15+
baseline_index = float(
16+
parameters(str(BASELINE_UC_REBALANCING_YEAR)).gov.benefit_uprating_cpi
17+
)
18+
return current_index / baseline_index
19+
20+
21+
def _rebalanced_standard_allowance_monthly(
22+
sim: Microsimulation, year: int, claimant_type: str
23+
) -> float:
24+
current = sim.tax_benefit_system.parameters(str(year))
25+
standard_allowance = float(
26+
current.gov.dwp.universal_credit.standard_allowance.amount[claimant_type]
27+
)
28+
uplift = float(
29+
current.gov.dwp.universal_credit.rebalancing.standard_allowance_uplift
30+
)
31+
return standard_allowance * (1 + uplift)
32+
33+
34+
def _protected_existing_health_element_monthly(
35+
sim: Microsimulation, year: int
36+
) -> float:
37+
baseline = sim.tax_benefit_system.parameters(str(BASELINE_UC_REBALANCING_YEAR))
38+
protected_combined_award = _benefit_uprating_ratio(sim, year) * (
39+
float(baseline.gov.dwp.universal_credit.standard_allowance.amount.SINGLE_OLD)
40+
+ float(baseline.gov.dwp.universal_credit.elements.disabled.amount)
41+
)
42+
return protected_combined_award - _rebalanced_standard_allowance_monthly(
43+
sim, year, "SINGLE_OLD"
44+
)
45+
46+
47+
def _protected_single_young_health_element_monthly(
48+
sim: Microsimulation, year: int
49+
) -> float:
50+
baseline = sim.tax_benefit_system.parameters(str(BASELINE_UC_REBALANCING_YEAR))
51+
protected_combined_award = _benefit_uprating_ratio(sim, year) * (
52+
float(baseline.gov.dwp.universal_credit.standard_allowance.amount.SINGLE_YOUNG)
53+
+ float(baseline.gov.dwp.universal_credit.elements.disabled.amount)
54+
)
55+
return protected_combined_award - _rebalanced_standard_allowance_monthly(
56+
sim, year, "SINGLE_YOUNG"
57+
)
58+
59+
660
def add_universal_credit_reform(sim: Microsimulation):
761
rebalancing = sim.tax_benefit_system.parameters.gov.dwp.universal_credit.rebalancing
862

963
generator = np.random.default_rng(43)
1064

1165
uc_seed = generator.random(len(sim.calculate("benunit_id")))
12-
p_uc_post_2026_status = {
66+
post_2025_claimant_share = {
1367
2025: 0,
1468
2026: 0.11,
1569
2027: 0.13,
@@ -20,11 +74,24 @@ def add_universal_credit_reform(sim: Microsimulation):
2074
for year in range(2026, 2030):
2175
if not rebalancing.active(year):
2276
continue
23-
is_post_25_claimant = uc_seed < p_uc_post_2026_status[year]
77+
is_post_2025_claimant = uc_seed < post_2025_claimant_share[year]
2478
current_health_element = sim.calculate("uc_LCWRA_element", year)
25-
# Set new claimants to £217.26/month from April 2026 (pre-2026 claimaints keep inflation-linked increases)
79+
claimant_type = sim.calculate("uc_standard_allowance_claimant_type", year)
80+
has_health_element = current_health_element > 0
81+
protected_health_element = np.full(
82+
current_health_element.shape,
83+
_protected_existing_health_element_monthly(sim, year) * 12,
84+
dtype=current_health_element.dtype,
85+
)
86+
protected_health_element[claimant_type == UCClaimantType.SINGLE_YOUNG.name] = (
87+
_protected_single_young_health_element_monthly(sim, year) * 12
88+
)
89+
current_health_element[has_health_element & ~is_post_2025_claimant] = (
90+
protected_health_element[has_health_element & ~is_post_2025_claimant]
91+
)
92+
# Set post-April 2026 claimants to £217.26/month.
2693
# https://bills.parliament.uk/publications/62123/documents/6889#page=16
27-
current_health_element[(current_health_element > 0) & is_post_25_claimant] = (
94+
current_health_element[has_health_element & is_post_2025_claimant] = (
2895
new_claimant_health_element(year) * 12
2996
) # Monthly amount * 12
3097
sim.set_input("uc_LCWRA_element", year, current_health_element)
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import numpy as np
2+
import pytest
3+
4+
import policyengine_uk.scenarios.uc_reform as uc_reform
5+
from policyengine_uk import Simulation
6+
7+
YEARS = range(2025, 2030)
8+
9+
10+
def _uc_claimant(age_2025: int) -> dict:
11+
return {
12+
"people": {
13+
"person": {
14+
"age": {year: age_2025 + year - 2025 for year in YEARS},
15+
"employment_income": {year: 0 for year in YEARS},
16+
"uc_limited_capability_for_WRA": {year: True for year in YEARS},
17+
}
18+
},
19+
"benunits": {"benunit": {"members": ["person"]}},
20+
"households": {"household": {"members": ["person"]}},
21+
}
22+
23+
24+
class _FixedRng:
25+
def __init__(self, values):
26+
self.values = np.array(values, dtype=float)
27+
28+
def random(self, size):
29+
assert size == len(self.values)
30+
return self.values
31+
32+
33+
def _force_uc_seed(monkeypatch, values):
34+
monkeypatch.setattr(
35+
uc_reform.np.random, "default_rng", lambda seed: _FixedRng(values)
36+
)
37+
38+
39+
def _benefit_uprating_factor(sim: Simulation, year: int) -> float:
40+
parameters = sim.tax_benefit_system.parameters
41+
current_index = float(parameters(str(year)).gov.benefit_uprating_cpi)
42+
baseline_index = float(parameters("2025").gov.benefit_uprating_cpi)
43+
return current_index / baseline_index
44+
45+
46+
def _rebalanced_standard_allowance_monthly(
47+
sim: Simulation, year: int, claimant_type: str
48+
) -> float:
49+
current = sim.tax_benefit_system.parameters(str(year))
50+
standard_allowance = float(
51+
current.gov.dwp.universal_credit.standard_allowance.amount[claimant_type]
52+
)
53+
uplift = float(
54+
current.gov.dwp.universal_credit.rebalancing.standard_allowance_uplift
55+
)
56+
return standard_allowance * (1 + uplift)
57+
58+
59+
def _cpi_protected_uc_award_monthly(
60+
sim: Simulation, year: int, claimant_type: str
61+
) -> float:
62+
baseline = sim.tax_benefit_system.parameters("2025").gov.dwp.universal_credit
63+
baseline_standard_allowance = float(
64+
baseline.standard_allowance.amount[claimant_type]
65+
)
66+
baseline_health_element = float(baseline.elements.disabled.amount)
67+
return _benefit_uprating_factor(sim, year) * (
68+
baseline_standard_allowance + baseline_health_element
69+
)
70+
71+
72+
@pytest.mark.parametrize("age_2025", [20, 30])
73+
def test_existing_claimants_keep_combined_award_cpi_protected(monkeypatch, age_2025):
74+
_force_uc_seed(monkeypatch, [0.99])
75+
sim = Simulation(situation=_uc_claimant(age_2025))
76+
claimant_type = "SINGLE_YOUNG" if age_2025 < 25 else "SINGLE_OLD"
77+
78+
for year in range(2026, 2030):
79+
standard_allowance = sim.calculate("uc_standard_allowance", year)[0] / 12
80+
health_element = sim.calculate("uc_LCWRA_element", year)[0] / 12
81+
new_claimant_health_element = float(
82+
sim.tax_benefit_system.parameters(
83+
str(year)
84+
).gov.dwp.universal_credit.rebalancing.new_claimant_health_element
85+
)
86+
87+
assert health_element > new_claimant_health_element
88+
assert standard_allowance + health_element == pytest.approx(
89+
_cpi_protected_uc_award_monthly(sim, year, claimant_type)
90+
)
91+
92+
93+
def test_new_claimants_use_fixed_health_element(monkeypatch):
94+
_force_uc_seed(monkeypatch, [0.0])
95+
sim = Simulation(situation=_uc_claimant(30))
96+
97+
for year in range(2026, 2030):
98+
health_element = sim.calculate("uc_LCWRA_element", year)[0] / 12
99+
expected_health = float(
100+
sim.tax_benefit_system.parameters(
101+
str(year)
102+
).gov.dwp.universal_credit.rebalancing.new_claimant_health_element
103+
)
104+
105+
assert health_element == pytest.approx(expected_health)
106+
107+
108+
def test_standard_allowance_reforms_still_change_standard_allowance(monkeypatch):
109+
_force_uc_seed(monkeypatch, [0.99])
110+
baseline = Simulation(situation=_uc_claimant(30))
111+
reformed = Simulation(
112+
situation=_uc_claimant(30),
113+
reform={
114+
"gov.dwp.universal_credit.standard_allowance.amount.SINGLE_OLD": {
115+
"2025-01-01.2100-12-31": 800
116+
}
117+
},
118+
)
119+
120+
baseline_standard_allowance = baseline.calculate("uc_standard_allowance", 2026)[0]
121+
reformed_standard_allowance = reformed.calculate("uc_standard_allowance", 2026)[0]
122+
123+
assert reformed_standard_allowance / baseline_standard_allowance > 1.5

0 commit comments

Comments
 (0)