Skip to content

Commit 9bfcb4d

Browse files
committed
Fix UC rebalancing protection for existing claimants
1 parent 6d46ff4 commit 9bfcb4d

6 files changed

Lines changed: 223 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 award CPI-protected and single claimants under 25 receive the matching standard allowance top-up.

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. Single claimants under 25 receive an additional standard allowance top-up to preserve that protection.
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. Single claimants under 25 receive a further top-up so their protected combined award also keeps pace with inflation.
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+
- the single-over-25 standard allowance, and
26+
- the health element
27+
28+
rises at least in line with CPI inflation. Because that flat protected health element is calibrated against the single-over-25 rate, single claimants under 25 receive an additional standard allowance top-up to preserve the same real-terms protection.
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 and the single-under-25 top-up 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 top-ups.
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: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,73 @@
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 _cpi_protected_uc_award_monthly(
13+
sim: Microsimulation, year: int, claimant_type: str
14+
) -> float:
15+
parameters = sim.tax_benefit_system.parameters
16+
baseline = parameters(str(BASELINE_UC_REBALANCING_YEAR))
17+
current = parameters(str(year))
18+
cpi_factor = float(current.gov.benefit_uprating_cpi) / float(
19+
baseline.gov.benefit_uprating_cpi
20+
)
21+
baseline_standard_allowance = float(
22+
baseline.gov.dwp.universal_credit.standard_allowance.amount[claimant_type]
23+
)
24+
baseline_health_element = float(
25+
baseline.gov.dwp.universal_credit.elements.disabled.amount
26+
)
27+
return (baseline_standard_allowance + baseline_health_element) * cpi_factor
28+
29+
30+
def _rebalanced_standard_allowance_monthly(
31+
sim: Microsimulation, year: int, claimant_type: str
32+
) -> float:
33+
current = sim.tax_benefit_system.parameters(str(year))
34+
standard_allowance = float(
35+
current.gov.dwp.universal_credit.standard_allowance.amount[claimant_type]
36+
)
37+
uplift = float(
38+
current.gov.dwp.universal_credit.rebalancing.standard_allowance_uplift
39+
)
40+
return standard_allowance * (1 + uplift)
41+
42+
43+
def _protected_existing_health_element_monthly(
44+
sim: Microsimulation, year: int
45+
) -> float:
46+
return _cpi_protected_uc_award_monthly(
47+
sim, year, "SINGLE_OLD"
48+
) - _rebalanced_standard_allowance_monthly(sim, year, "SINGLE_OLD")
49+
50+
51+
def _single_young_standard_allowance_topup_monthly(
52+
sim: Microsimulation, year: int
53+
) -> float:
54+
protected_young_award = _cpi_protected_uc_award_monthly(sim, year, "SINGLE_YOUNG")
55+
standard_allowance = _rebalanced_standard_allowance_monthly(
56+
sim, year, "SINGLE_YOUNG"
57+
)
58+
protected_health_element = _protected_existing_health_element_monthly(sim, year)
59+
return max(
60+
0.0, protected_young_award - standard_allowance - protected_health_element
61+
)
62+
63+
664
def add_universal_credit_reform(sim: Microsimulation):
765
rebalancing = sim.tax_benefit_system.parameters.gov.dwp.universal_credit.rebalancing
866

967
generator = np.random.default_rng(43)
1068

1169
uc_seed = generator.random(len(sim.calculate("benunit_id")))
12-
p_uc_post_2026_status = {
70+
post_2025_claimant_share = {
1371
2025: 0,
1472
2026: 0.11,
1573
2027: 0.13,
@@ -20,11 +78,29 @@ def add_universal_credit_reform(sim: Microsimulation):
2078
for year in range(2026, 2030):
2179
if not rebalancing.active(year):
2280
continue
23-
is_post_25_claimant = uc_seed < p_uc_post_2026_status[year]
81+
is_post_2025_claimant = uc_seed < post_2025_claimant_share[year]
82+
83+
claimant_type = sim.calculate("uc_standard_allowance_claimant_type", year)
84+
current_standard_allowance = sim.calculate("uc_standard_allowance", year)
85+
single_young_topup = (
86+
_single_young_standard_allowance_topup_monthly(sim, year) * 12
87+
)
88+
current_standard_allowance[
89+
claimant_type == UCClaimantType.SINGLE_YOUNG.name
90+
] += single_young_topup
91+
sim.set_input("uc_standard_allowance", year, current_standard_allowance)
92+
2493
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)
94+
has_health_element = current_health_element > 0
95+
protected_health_element = (
96+
_protected_existing_health_element_monthly(sim, year) * 12
97+
)
98+
current_health_element[has_health_element & ~is_post_2025_claimant] = (
99+
protected_health_element
100+
)
101+
# Set post-April 2026 claimants to £217.26/month.
26102
# https://bills.parliament.uk/publications/62123/documents/6889#page=16
27-
current_health_element[(current_health_element > 0) & is_post_25_claimant] = (
103+
current_health_element[has_health_element & is_post_2025_claimant] = (
28104
new_claimant_health_element(year) * 12
29105
) # Monthly amount * 12
30106
sim.set_input("uc_LCWRA_element", year, current_health_element)
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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+
BASELINE_YEAR = 2025
8+
9+
10+
def _uc_claimant(age: int) -> dict:
11+
return {
12+
"people": {
13+
"person": {
14+
"age": {year: age for year in range(2025, 2030)},
15+
"employment_income": {year: 0 for year in range(2025, 2030)},
16+
"uc_limited_capability_for_WRA": {
17+
year: True for year in range(2025, 2030)
18+
},
19+
}
20+
},
21+
"benunits": {"benunit": {"members": ["person"]}},
22+
"households": {"household": {"members": ["person"]}},
23+
}
24+
25+
26+
class _FixedRng:
27+
def __init__(self, values):
28+
self.values = np.array(values, dtype=float)
29+
30+
def random(self, size):
31+
assert size == len(self.values)
32+
return self.values
33+
34+
35+
def _force_uc_seed(monkeypatch, values):
36+
monkeypatch.setattr(
37+
uc_reform.np.random, "default_rng", lambda seed: _FixedRng(values)
38+
)
39+
40+
41+
def _cpi_protected_uc_award_monthly(
42+
sim: Simulation, year: int, claimant_type: str
43+
) -> float:
44+
parameters = sim.tax_benefit_system.parameters
45+
baseline = parameters(str(BASELINE_YEAR))
46+
current = parameters(str(year))
47+
cpi_factor = float(current.gov.benefit_uprating_cpi) / float(
48+
baseline.gov.benefit_uprating_cpi
49+
)
50+
baseline_standard_allowance = float(
51+
baseline.gov.dwp.universal_credit.standard_allowance.amount[claimant_type]
52+
)
53+
baseline_health_element = float(
54+
baseline.gov.dwp.universal_credit.elements.disabled.amount
55+
)
56+
return (baseline_standard_allowance + baseline_health_element) * cpi_factor
57+
58+
59+
def _rebalanced_standard_allowance_monthly(
60+
sim: Simulation, year: int, claimant_type: str
61+
) -> float:
62+
current = sim.tax_benefit_system.parameters(str(year))
63+
return float(
64+
current.gov.dwp.universal_credit.standard_allowance.amount[claimant_type]
65+
) * (
66+
1
67+
+ float(current.gov.dwp.universal_credit.rebalancing.standard_allowance_uplift)
68+
)
69+
70+
71+
def test_existing_single_over_25_claimant_combined_award_tracks_cpi(monkeypatch):
72+
_force_uc_seed(monkeypatch, [0.99])
73+
sim = Simulation(situation=_uc_claimant(30))
74+
75+
for year in range(2026, 2030):
76+
standard_allowance = sim.calculate("uc_standard_allowance", year)[0] / 12
77+
health_element = sim.calculate("uc_LCWRA_element", year)[0] / 12
78+
expected_total = _cpi_protected_uc_award_monthly(sim, year, "SINGLE_OLD")
79+
expected_health = expected_total - _rebalanced_standard_allowance_monthly(
80+
sim, year, "SINGLE_OLD"
81+
)
82+
83+
assert standard_allowance + health_element == pytest.approx(expected_total)
84+
assert health_element == pytest.approx(expected_health)
85+
86+
87+
def test_existing_single_under_25_claimant_gets_extra_topup(monkeypatch):
88+
_force_uc_seed(monkeypatch, [0.99])
89+
sim = Simulation(situation=_uc_claimant(22))
90+
91+
for year in range(2026, 2030):
92+
standard_allowance = sim.calculate("uc_standard_allowance", year)[0] / 12
93+
health_element = sim.calculate("uc_LCWRA_element", year)[0] / 12
94+
expected_total = _cpi_protected_uc_award_monthly(sim, year, "SINGLE_YOUNG")
95+
baseline_rebalanced_standard_allowance = _rebalanced_standard_allowance_monthly(
96+
sim, year, "SINGLE_YOUNG"
97+
)
98+
99+
assert standard_allowance + health_element == pytest.approx(expected_total)
100+
assert standard_allowance > baseline_rebalanced_standard_allowance
101+
102+
103+
def test_new_claimants_use_fixed_health_element(monkeypatch):
104+
_force_uc_seed(monkeypatch, [0.0])
105+
sim = Simulation(situation=_uc_claimant(30))
106+
107+
for year in range(2026, 2030):
108+
health_element = sim.calculate("uc_LCWRA_element", year)[0] / 12
109+
expected_health = float(
110+
sim.tax_benefit_system.parameters(
111+
str(year)
112+
).gov.dwp.universal_credit.rebalancing.new_claimant_health_element
113+
)
114+
115+
assert health_element == pytest.approx(expected_health)

0 commit comments

Comments
 (0)