Skip to content
1 change: 1 addition & 0 deletions changelog.d/uc-lcwra-post-2029.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- From 2030-31, keep paying pre-2026 (protected) Universal Credit LCWRA claimants the protected amount, uprated from its 2029-30 level by the LCWRA element's uprating index, and new claimants the new-claimant amount. The rebalancing scenario used to stop in 2029-30, so every claimant fell to the new-claimant amount (a protected claimant from £457.15 to £235.86 a month). The new-claimant share is now the parameter `gov.dwp.universal_credit.rebalancing.new_claimant_share`, held at 22% after 2029-30.
12 changes: 8 additions & 4 deletions docs/book/policy/uc-rebalancing.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,28 @@ Universal Credit Act 2025 s. 3 freezes the element for 2026-27 to 2029-30: it di

Two parameters hold this amount, and both are uprated by `gov.economic_assumptions.indices.uc_health_element`, so they follow the same path:

- `gov.dwp.universal_credit.elements.disabled.amount` is the LCWRA element the `uc_LCWRA_element` formula pays. A simulation uses it from 2030-31, and whenever the rebalancing reforms are inactive.
- `gov.dwp.universal_credit.rebalancing.new_claimant_health_element` is the amount the rebalancing scenario pays new claimants for 2026-27 to 2029-30.
- `gov.dwp.universal_credit.elements.disabled.amount` is the LCWRA element the `uc_LCWRA_element` formula pays. A simulation uses it whenever the rebalancing reforms are inactive, and from 2033-34, after the data's projection ends, when the rebalancing scenario no longer sets the element.
- `gov.dwp.universal_credit.rebalancing.new_claimant_health_element` is the amount the rebalancing scenario pays new claimants in the years it sets the element, `uc_reform.REBALANCING_YEARS` (2026-27 to 2032-33).

A change to the freeze or to the benefit uprating index moves both. A reform to either amount itself does not change the other.
A change to the freeze or to the benefit uprating index moves both, and so does a reform that sets the index, `gov.economic_assumptions.indices.uc_health_element`, directly (it replaces the freeze and the no-fall floor for the years it names). A reform to either amount itself does not change the other.

`rebalancing/create_health_element_uprating.py` builds the index when parameters are processed: the benefit uprating index, held flat in each year in which `rebalancing.health_element_freeze` and `rebalancing.active` are both true.

Existing recipients are treated differently. Universal Credit Regulations 2013 reg. 36 gives one amount, the protected LCWRA amount, to every pre-2026 claimant, severe conditions criteria claimant or terminally ill claimant, whatever their age or couple status. For 2026-27 it is £429.80 a month ([SI 2026/113](https://www.legislation.gov.uk/uksi/2026/113/made) reg. 3(3)(b)), stored in `gov.dwp.universal_credit.rebalancing.protected_health_element`.

For a tax year without a legislated amount, the model uses the lowest amount the Universal Credit Act 2025 s. 4 duty allows. For every standard allowance amount, the protected amount plus that allowance must be at least the previous year's sum, increased by the relevant CPI percentage: the CPI 12-month rate in the September before the tax year, never below 0% (s. 4(4)(a)). The model reads that rate from `gov.economic_assumptions.statutory_uprating_inputs.cpi_september`. Section 3 switches off the element's ordinary uprating, so the amount otherwise stays where it was. Run from the 2025-26 amounts at the 3.8% September 2025 CPI rate, this rule gives the legislated £429.80.

Whether a benefit unit gets the new-claimant rate is the benefit unit's `uc_receives_new_claimant_health_element` for the year. A household situation can set it, for example `{"uc_receives_new_claimant_health_element": {"2026": True}}` on the benefit unit; otherwise it is false, which pays the protected amount. A dataset can supply it too. A simulation built from data that does not supply it assigns it by a seeded draw, using 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:
The s. 4 duty covers 2026-27 to 2029-30 only, and s. 3 disapplies the annual up-rating only in the tax years ending 5 April 2026 to 5 April 2029. The protected row stays in reg. 36: s. 2 only relabels it. So from 2030-31 the protected amount is the year before's amount uprated by the same index as the new-claimant amount, `gov.economic_assumptions.indices.uc_health_element`, and a protected claimant keeps the higher amount.

Whether a benefit unit gets the new-claimant rate is the benefit unit's `uc_receives_new_claimant_health_element` for the year. A household situation can set it, for example `{"uc_receives_new_claimant_health_element": {"2026": True}}` on the benefit unit; otherwise it is false, which pays the protected amount. A dataset can supply it too. A simulation built from data that does not supply it assigns it by a seeded draw against `gov.dwp.universal_credit.rebalancing.new_claimant_share`, from 2026-27 to the end of the data's projection. The shares to 2029-30 are WPI Economics' estimates for the Trussell Trust, derived from administrative Personal Independence Payment data:

- 2026: 11%
- 2027: 13%
- 2028: 16%
- 2029: 22%

Later years hold 22%. That is an assumption, not a source: the share keeps rising as the caseload turns over, so holding it understates new claimants from 2030-31.

## Standard allowance uplifts

The standard allowance receives additional percentage uplifts beyond the normal inflationary increase:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,17 @@
by a year or a ``start.stop`` range. So a change to the freeze, to
``rebalancing.active``, to the benefit index or the forecasts behind it, or to
an amount itself reaches the element, which is then held or uprated from the
changed value. A ``reform=`` dict key naming a single day still edits the
annualised parameter on that day only and does not re-run uprating.
changed value. That is the default, generated path.

A reform can also name the index itself. Processing writes such a change just
after this step and before uprating, so it replaces the generated value, the
freeze and the no-fall floor included, for the years it names. The index is a
calendar-year series (it inherits ``calendar_year_series`` from
``gov.economic_assumptions.indices``), so on it a bare year or a 1 January
key names that calendar year and re-runs processing. On parameters that are
not calendar-year series, a ``reform=`` dict key naming a single day still
edits the annualised parameter on that day only and does not re-run
uprating.
"""

from typing import Dict
Expand Down Expand Up @@ -187,9 +196,10 @@ def add_uc_health_element_uprating(parameters: ParameterNode) -> ParameterNode:
"the benefit uprating index, held flat in the tax years "
"for which the Universal Credit Act 2025 freezes the "
"element and never falling. It is built when parameters "
"are processed; to change it, change "
"gov.dwp.universal_credit.rebalancing."
"health_element_freeze or the benefit uprating index."
"are processed from gov.dwp.universal_credit.rebalancing."
"health_element_freeze and the benefit uprating index; a "
"reform that sets it directly replaces that path, freeze "
"and floor included, for the years it names."
),
"values": health_element_index_values(parameters),
"metadata": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
description: Monthly value of the Universal Credit health element (the LCWRA element) for new claimants from April 2026. The rebalancing scenario pays it to new claimants for 2026-27 to 2029-30. It follows the same path as the LCWRA element amount, frozen for 2026-27 to 2029-30 by Universal Credit Act 2025 s. 3 and uprated afterwards.
description: Monthly value of the Universal Credit health element (the LCWRA element) for new claimants from April 2026. The rebalancing scenario pays it to new claimants in the years it sets the element (2026-27 to 2032-33, the end of the data's projection). It follows the same path as the LCWRA element amount, frozen for 2026-27 to 2029-30 by Universal Credit Act 2025 s. 3 and uprated afterwards.
values:
2026-01-01: 217.26
metadata:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
description: >-
Share of benefit units with the Universal Credit LCWRA element that are new claimants, paid the new-claimant amount rather than the protected (pre-2026) amount. The rebalancing scenario uses it to assign the status by a seeded draw when the data do not supply it; a household situation without the status gets the protected amount. The shares for 2026-27 to 2029-30 are WPI Economics' estimates for the Trussell Trust from administrative Personal Independence Payment data (2025). Later years hold the 2029-30 share. That is an assumption, not a source: the share keeps rising as the caseload turns over, so holding it understates new claimants and overstates protected amounts from 2030-31.
values:
2025-04-06: 0
2026-04-06: 0.11
2027-04-06: 0.13
2028-04-06: 0.16
2029-04-06: 0.22
metadata:
unit: /1
label: Universal Credit LCWRA new-claimant share
reference:
- title: Universal Credit Act 2025 s. 2 (new-claimant and protected LCWRA amounts)
href: https://www.legislation.gov.uk/ukpga/2025/22/section/2
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
description: Monthly LCWRA element for a pre-2026 claimant, a severe conditions criteria claimant or a claimant who is terminally ill (the protected LCWRA amount). Each dated value is the legislated amount for the tax year it falls in; a later tax year without one takes the lowest amount the Universal Credit Act 2025 s. 4 duty allows.
description: Monthly LCWRA element for a pre-2026 claimant, a severe conditions criteria claimant or a claimant who is terminally ill (the protected LCWRA amount). Each dated value is the legislated amount for the tax year it falls in. A later tax year without one takes, to 2029-30, the lowest amount the Universal Credit Act 2025 s. 4 duty allows, and from 2030-31 the year before's amount uprated by the LCWRA element's uprating index (s. 4 ends with 2029-30 and s. 3 no longer disapplies the annual up-rating).
values:
2026-04-01:
value: 429.80
Expand Down
56 changes: 41 additions & 15 deletions policyengine_uk/scenarios/uc_reform.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
from policyengine_uk.model_api import Scenario
from policyengine_uk import Microsimulation
from policyengine_uk.data.economic_assumptions import PROJECTION_END_YEAR
from policyengine_uk.utils.supplied_inputs import set_model_value
import numpy as np


# Universal Credit Act 2025: the first tax year of the rebalancing (2026-27,
# ss. 2 to 4) and the last the s. 4 duty covers (2029-30).
FIRST_REBALANCING_YEAR = 2026
SECTION_4_LAST_YEAR = 2029
# The tax years in which the scenario sets the element: from the first year of
# the rebalancing to the end of the data's projection. Other code reads this
# (for example to warn that a reform to elements.disabled.amount does not reach
# these years' awards). Later years fall to the uc_LCWRA_element formula, which
# pays every claimant the new-claimant amount.
REBALANCING_YEARS = range(FIRST_REBALANCING_YEAR, PROJECTION_END_YEAR + 1)


# The standard allowance amounts the protected LCWRA amount is paired with in
# the Universal Credit Act 2025 s. 4(2) duty: every amount of the allowance.
STANDARD_ALLOWANCE_TYPES = (
Expand Down Expand Up @@ -71,9 +84,19 @@ def _protected_existing_health_element_monthly(

UC Regs 2013 reg. 36 gives one amount to every pre-2026, severe conditions
criteria or terminally ill claimant, whatever their age or couple status
(£429.80 for 2026-27, SI 2026/113 reg. 3(3)(b)). A tax year whose amount is
null (not yet legislated) takes the s. 4 floor from the year before; any
other amount, legislated or set by a reform, is used as given.
(£429.80 for 2026-27, SI 2026/113 reg. 3(3)(b)). Any non-null amount,
legislated or set by a reform, is used as given. A tax year whose amount
is null (not yet legislated):

- to 2029-30 takes the s. 4 floor from the year before;
- from 2030-31 takes the year before's amount uprated by the element's
uprating index. The row stays in reg. 36 (s. 2 only relabels it), s. 4
covers only 2026-27 to 2029-30, and s. 3 disapplies the SSAA 1992
s. 150 up-rating only in the tax years ending 5 April 2026 to 5 April
2029, so the review in 2029-30 governs the April 2030 rate again. The
index is the one the new-claimant amount follows
(``rebalancing/create_health_element_uprating.py``): the benefit
uprating index, held in any year the freeze is in force, never falling.
"""
parameter = _protected_amount_parameter(sim)
amount = parameter(f"{year}-{FISCAL_YEAR_SAMPLE_DATE}")
Expand All @@ -88,8 +111,15 @@ def _protected_existing_health_element_monthly(
raise ValueError(
f"No protected LCWRA amount is set for {year} or any earlier year."
)
previous = _protected_existing_health_element_monthly(sim, year - 1)
if year > SECTION_4_LAST_YEAR:
index = sim.tax_benefit_system.parameters.gov.economic_assumptions.indices.uc_health_element
return previous * (
index(f"{year}-{FISCAL_YEAR_SAMPLE_DATE}")
/ index(f"{year - 1}-{FISCAL_YEAR_SAMPLE_DATE}")
)
return protected_lcwra_floor(
_protected_existing_health_element_monthly(sim, year - 1),
previous,
_standard_allowances_monthly(sim, year - 1),
_standard_allowances_monthly(sim, year),
_relevant_cpi_factor(sim, year),
Expand Down Expand Up @@ -129,20 +159,16 @@ def add_universal_credit_reform(sim: Microsimulation):
generator = np.random.default_rng(43)

uc_seed = generator.random(len(sim.calculate("benunit_id")))
post_2025_claimant_share = {
2025: 0,
2026: 0.11,
2027: 0.13,
2028: 0.16,
2029: 0.22,
} # WPI Economics for Trussell Trust based on admin PIP data, 2025
# rebalancing.new_claimant_share: WPI Economics' estimates for 2026-27 to
# 2029-30, held after that (an assumption; see the parameter).
new_claimant_share = rebalancing.new_claimant_share
new_claimant_health_element = rebalancing.new_claimant_health_element
built_from_data = getattr(sim, "built_from_dataset", False)
for year in range(2026, 2030):
for year in REBALANCING_YEARS:
if not rebalancing.active(year):
continue
is_post_2025_claimant = _new_claimant_health_element_status(
sim, year, uc_seed < post_2025_claimant_share[year], built_from_data
sim, year, uc_seed < new_claimant_share(year), built_from_data
)
# Copy: calculate returns the simulation's cached array, and the writes
# below must reach the cache only through set_input.
Expand All @@ -152,8 +178,8 @@ def add_universal_credit_reform(sim: Microsimulation):
current_health_element[has_health_element & ~is_post_2025_claimant] = (
_protected_existing_health_element_monthly(sim, year) * 12
)
# Set post-April 2026 claimants to £217.26/month.
# https://bills.parliament.uk/publications/62123/documents/6889#page=16
# New claimants: £217.26 a month from April 2026 (UC Act 2025 s. 2),
# frozen to 2029-30 (s. 3) and uprated after.
current_health_element[has_health_element & is_post_2025_claimant] = (
new_claimant_health_element(year) * 12
) # Monthly amount * 12
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
from policyengine_core.reforms import Reform
from policyengine_uk import Simulation
from policyengine_uk.model_api import BenUnit, Variable
from policyengine_uk.scenarios.uc_reform import REBALANCING_YEARS
from policyengine_uk.utils.supplied_inputs import set_model_value, supplied_input

YEAR = 2025
Expand Down Expand Up @@ -675,7 +676,7 @@ def rebalanced():
return simulation, float(simulation.calculate("uc_LCWRA_element", year)[0])


REBALANCED_YEARS = [2026, 2027, 2028, 2029]
REBALANCED_YEARS = list(REBALANCING_YEARS)


@pytest.mark.parametrize(
Expand All @@ -699,7 +700,7 @@ def rebalanced():
def test_an_input_makes_the_element_an_entry_only_in_years_it_stores(
rebalanced, input_period, amount, entered_years
):
"""The rebalancing stores the model's element for 2026-2029. An input
"""The rebalancing stores the model's element for each rebalancing year. An input
for another period that leaves a year's stored value in place leaves it
the model's, so the non-claimant's limited capability still does not
lift the cap that year; an input stored for the year is an entry, even
Expand Down
26 changes: 15 additions & 11 deletions policyengine_uk/tests/test_dataframe_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
This holds for frames that name the identifiers and give values for
different years, and for simulations given inputs for other years once
built, as the Universal Credit rebalancing gives each benefit unit's
health element status for 2026 to 2029. The export gives every
health element status for each rebalancing year. The export gives every
identifier for each whole year a column names, which is where a
simulation built from it holds them, and person_id and the benefit unit
and household identifiers exactly.
Expand Down Expand Up @@ -87,6 +87,7 @@
with_identifiers_in_each_year,
)
from policyengine_uk.scenarios import no_economic_assumptions
from policyengine_uk.scenarios.uc_reform import REBALANCING_YEARS

pytestmark = pytest.mark.usefixtures("cloned_uk_tax_benefit_system")

Expand Down Expand Up @@ -1207,11 +1208,12 @@ def test_members_may_all_leave_a_group_value_missing(tax_benefit_system):
)
ROUND_TRIP_SETTINGS = settings(SIMULATION_SETTINGS, max_examples=15)
# The smallest example of the round trip failing once the Universal Credit
# rebalancing recorded each benefit unit's health element status for 2026 to
# 2029 (#2251). The export gave that status for those years but the person
# and their groups for 2025 alone. Loaded back, the frame was a dataset with a
# year for each of 2025 to 2029, which holds the identifiers in each, so the
# second export gave them for 2026 to 2029 as well: 38 columns, not 18.
# rebalancing recorded each benefit unit's health element status for its
# rebalancing years (2026 to 2029 when #2251 landed). The export gave that
# status for those years but the person and their groups for 2025 alone.
# Loaded back, the frame was a dataset with a year for each of 2025 to 2029,
# which holds the identifiers in each, so the second export gave them for
# those years as well: 38 columns, not 18.
ONE_PERSON = Population(
person_ids=np.array([1]),
groups={"benunit": np.array([1]), "household": np.array([1])},
Expand Down Expand Up @@ -1283,16 +1285,18 @@ def columns_of(frame_: pd.DataFrame, variable: str) -> list:

def test_an_export_names_the_people_in_each_year_the_rebalancing_records():
# The rebalancing records the status of a simulation built from data for
# 2025 for 2026 to 2029, where it holds its people and groups by carrying
# them forward from 2025.
# 2025 for each rebalancing year, where it holds its people and groups by
# carrying them forward from 2025.
exported = Simulation(dataset=frame(ONE_PERSON)).to_input_dataframe()

status = "uc_receives_new_claimant_health_element"
years = range(YEAR, 2030)
assert columns_of(exported, status) == [f"{status}__{year}" for year in years[1:]]
identifier_years = range(YEAR, REBALANCING_YEARS[-1] + 1)
assert columns_of(exported, status) == [
f"{status}__{year}" for year in REBALANCING_YEARS
]
for identifier in IDENTIFIER_VARIABLES:
columns = columns_of(exported, identifier)
assert columns == [f"{identifier}__{year}" for year in years]
assert columns == [f"{identifier}__{year}" for year in identifier_years]
# Together, where the exporter puts an input set for each year.
start = exported.columns.get_loc(columns[0])
assert exported.columns[start : start + len(columns)].tolist() == columns
Expand Down
Loading
Loading