Skip to content

Commit ce1d3f5

Browse files
committed
Refine UC rebalancing protection modeling
1 parent 9bfcb4d commit ce1d3f5

6 files changed

Lines changed: 103 additions & 96 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +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.
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: 1 addition & 1 deletion
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) 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.
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

docs/book/policy/uc-rebalancing.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,18 @@ The reforms combine a higher standard allowance, protected awards for existing h
1414

1515
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.
1616

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.
17+
3. **Standard allowance uplifts**: The standard allowance receives additional uplifts beyond the annual inflationary increase from 2026-2029.
1818

1919
## Health element changes
2020

2121
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.
2222

2323
Existing recipients are treated differently. Their LCWRA amount is uprated so that the combined value of:
2424

25-
- the single-over-25 standard allowance, and
26-
- the health element
25+
- their standard allowance, and
26+
- their health element
2727

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.
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.
2929

3030
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:
3131

@@ -52,7 +52,7 @@ The reforms are implemented through parameters, scenario modifiers, and scenario
5252
```
5353

5454
- **Parameters**: Three YAML files define the reform's activation status, health element amount for new claimants, and standard allowance uplift rates.
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.
55+
- **Scenario modifier**: The `add_universal_credit_reform` function applies the protected existing-claimant health-element path during microsimulation.
5656
- **Scenario**: The `universal_credit_july_2025_reform` scenario enables the reforms in policy analysis.
5757

5858
## Examples

docs/book/usage/scenarios.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ def modify_uc_for_new_claimants(sim: Microsimulation):
436436

437437
# General standard allowance uplifts are already handled in the
438438
# uc_standard_allowance formula. Scenario modifiers only need to add
439-
# claimant-specific overrides such as protected top-ups.
439+
# claimant-specific overrides such as protected health elements.
440440

441441
# Create the UC rebalancing scenario
442442
uc_rebalancing = Scenario(simulation_modifier=modify_uc_for_new_claimants)

policyengine_uk/scenarios/uc_reform.py

Lines changed: 30 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,13 @@
99
BASELINE_UC_REBALANCING_YEAR = 2025
1010

1111

12-
def _cpi_protected_uc_award_monthly(
13-
sim: Microsimulation, year: int, claimant_type: str
14-
) -> float:
12+
def _benefit_uprating_ratio(sim: Microsimulation, year: int) -> float:
1513
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
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
2617
)
27-
return (baseline_standard_allowance + baseline_health_element) * cpi_factor
18+
return current_index / baseline_index
2819

2920

3021
def _rebalanced_standard_allowance_monthly(
@@ -43,21 +34,28 @@ def _rebalanced_standard_allowance_monthly(
4334
def _protected_existing_health_element_monthly(
4435
sim: Microsimulation, year: int
4536
) -> float:
46-
return _cpi_protected_uc_award_monthly(
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(
4743
sim, year, "SINGLE_OLD"
48-
) - _rebalanced_standard_allowance_monthly(sim, year, "SINGLE_OLD")
44+
)
4945

5046

51-
def _single_young_standard_allowance_topup_monthly(
47+
def _protected_single_young_health_element_monthly(
5248
sim: Microsimulation, year: int
5349
) -> 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"
50+
baseline = sim.tax_benefit_system.parameters(str(BASELINE_UC_REBALANCING_YEAR))
51+
protected_combined_award = _benefit_uprating_ratio(sim, year) * (
52+
float(
53+
baseline.gov.dwp.universal_credit.standard_allowance.amount.SINGLE_YOUNG
54+
)
55+
+ float(baseline.gov.dwp.universal_credit.elements.disabled.amount)
5756
)
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
57+
return protected_combined_award - _rebalanced_standard_allowance_monthly(
58+
sim, year, "SINGLE_YOUNG"
6159
)
6260

6361

@@ -79,24 +77,19 @@ def add_universal_credit_reform(sim: Microsimulation):
7977
if not rebalancing.active(year):
8078
continue
8179
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-
9380
current_health_element = sim.calculate("uc_LCWRA_element", year)
81+
claimant_type = sim.calculate("uc_standard_allowance_claimant_type", year)
9482
has_health_element = current_health_element > 0
95-
protected_health_element = (
96-
_protected_existing_health_element_monthly(sim, year) * 12
83+
protected_health_element = np.full(
84+
current_health_element.shape,
85+
_protected_existing_health_element_monthly(sim, year) * 12,
86+
dtype=current_health_element.dtype,
9787
)
88+
protected_health_element[
89+
claimant_type == UCClaimantType.SINGLE_YOUNG.name
90+
] = _protected_single_young_health_element_monthly(sim, year) * 12
9891
current_health_element[has_health_element & ~is_post_2025_claimant] = (
99-
protected_health_element
92+
protected_health_element[has_health_element & ~is_post_2025_claimant]
10093
)
10194
# Set post-April 2026 claimants to £217.26/month.
10295
# https://bills.parliament.uk/publications/62123/documents/6889#page=16

policyengine_uk/tests/test_uc_rebalancing.py

Lines changed: 65 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,16 @@
44
import policyengine_uk.scenarios.uc_reform as uc_reform
55
from policyengine_uk import Simulation
66

7-
BASELINE_YEAR = 2025
7+
YEARS = range(2025, 2030)
88

99

10-
def _uc_claimant(age: int) -> dict:
10+
def _uc_claimant(age_2025: int) -> dict:
1111
return {
1212
"people": {
1313
"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-
},
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},
1917
}
2018
},
2119
"benunits": {"benunit": {"members": ["person"]}},
@@ -38,66 +36,62 @@ def _force_uc_seed(monkeypatch, values):
3836
)
3937

4038

41-
def _cpi_protected_uc_award_monthly(
42-
sim: Simulation, year: int, claimant_type: str
43-
) -> float:
39+
def _benefit_uprating_factor(sim: Simulation, year: int) -> float:
4440
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
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
5744

5845

5946
def _rebalanced_standard_allowance_monthly(
6047
sim: Simulation, year: int, claimant_type: str
6148
) -> float:
6249
current = sim.tax_benefit_system.parameters(str(year))
63-
return float(
50+
standard_allowance = float(
6451
current.gov.dwp.universal_credit.standard_allowance.amount[claimant_type]
65-
) * (
66-
1
67-
+ float(current.gov.dwp.universal_credit.rebalancing.standard_allowance_uplift)
6852
)
53+
uplift = float(
54+
current.gov.dwp.universal_credit.rebalancing.standard_allowance_uplift
55+
)
56+
return standard_allowance * (1 + uplift)
6957

7058

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)
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+
)
8570

8671

87-
def test_existing_single_under_25_claimant_gets_extra_topup(monkeypatch):
72+
@pytest.mark.parametrize("age_2025", [20, 30])
73+
def test_existing_claimants_keep_combined_award_cpi_protected(
74+
monkeypatch, age_2025
75+
):
8876
_force_uc_seed(monkeypatch, [0.99])
89-
sim = Simulation(situation=_uc_claimant(22))
77+
sim = Simulation(situation=_uc_claimant(age_2025))
78+
claimant_type = "SINGLE_YOUNG" if age_2025 < 25 else "SINGLE_OLD"
9079

9180
for year in range(2026, 2030):
9281
standard_allowance = sim.calculate("uc_standard_allowance", year)[0] / 12
9382
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"
83+
new_claimant_health_element = (
84+
float(
85+
sim.tax_benefit_system.parameters(
86+
str(year)
87+
).gov.dwp.universal_credit.rebalancing.new_claimant_health_element
88+
)
9789
)
9890

99-
assert standard_allowance + health_element == pytest.approx(expected_total)
100-
assert standard_allowance > baseline_rebalanced_standard_allowance
91+
assert health_element > new_claimant_health_element
92+
assert standard_allowance + health_element == pytest.approx(
93+
_cpi_protected_uc_award_monthly(sim, year, claimant_type)
94+
)
10195

10296

10397
def test_new_claimants_use_fixed_health_element(monkeypatch):
@@ -106,10 +100,30 @@ def test_new_claimants_use_fixed_health_element(monkeypatch):
106100

107101
for year in range(2026, 2030):
108102
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
103+
expected_health = (
104+
float(
105+
sim.tax_benefit_system.parameters(
106+
str(year)
107+
).gov.dwp.universal_credit.rebalancing.new_claimant_health_element
108+
)
113109
)
114110

115111
assert health_element == pytest.approx(expected_health)
112+
113+
114+
def test_standard_allowance_reforms_still_change_standard_allowance(monkeypatch):
115+
_force_uc_seed(monkeypatch, [0.99])
116+
baseline = Simulation(situation=_uc_claimant(30))
117+
reformed = Simulation(
118+
situation=_uc_claimant(30),
119+
reform={
120+
"gov.dwp.universal_credit.standard_allowance.amount.SINGLE_OLD": {
121+
"2025-01-01.2100-12-31": 800
122+
}
123+
},
124+
)
125+
126+
baseline_standard_allowance = baseline.calculate("uc_standard_allowance", 2026)[0]
127+
reformed_standard_allowance = reformed.calculate("uc_standard_allowance", 2026)[0]
128+
129+
assert reformed_standard_allowance / baseline_standard_allowance > 1.5

0 commit comments

Comments
 (0)