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/2239.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Apply the Universal Credit Act 2025 s. 1 uplifts to the Universal Credit standard allowance for 2027-28 to 2029-30 (3.1%, 4.0% and 4.8% above the CPI-only path from 2025-26). The allowance is now uprated by a dedicated index, benefit uprating CPI times one plus the uplift, so it grows by CPI times the change in uplift each year after the legislated 2026-27 rates, and by CPI alone after 2029-30. Switching the rebalancing reforms off, or changing a later year's uplift, now moves those years. `standard_allowance_uplift` was previously not read.
33 changes: 20 additions & 13 deletions docs/book/policy/uc-rebalancing.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,24 @@ Whether a benefit unit gets the new-claimant rate is the benefit unit's `uc_rece

## Standard allowance uplifts

The standard allowance receives additional percentage uplifts beyond the normal inflationary increase:
Universal Credit Act 2025 s. 1 sets the minimum standard allowance for 2026-27 to 2029-30. Each year's minimum is the previous year's CPI-uprated amount (the 2025-26 amount for 2026-27), increased by September CPI (never below 0%), and then by that year's uplift:

- 2026: 2.3% additional uplift
- 2027: 3.1% additional uplift (cumulative)
- 2028: 4.0% additional uplift (cumulative)
- 2029: 4.8% additional uplift (cumulative)
- 2026-27: 2.3%
- 2027-28: 3.1%
- 2028-29: 4.0%
- 2029-30: 4.8%

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.
Each uplift is measured against the CPI-only path from 2025-26, not compounded on the previous one, so from one year to the next the allowance grows by CPI times (1 + this year's uplift) / (1 + last year's uplift). After 2029-30 the uplift stays at 4.8% and the allowance grows with CPI alone.

The 2026-27 amounts are the legislated rates in `standard_allowance/amount.yaml`, used as they are. Later years are uprated by `gov.dwp.universal_credit.standard_allowance.uprating`, which `rebalancing/create_standard_allowance_uprating.py` builds when parameters are processed: the benefit uprating CPI index times one plus the uplift. For a legislated year the uplift is the one its rates include (2.3% for 2026-27); for later years it is the uplift in force. So switching `rebalancing.active` off, or changing a later year's uplift, moves every year after 2026-27, which stays at its legislated rates. To change 2026-27 itself, set `gov.dwp.universal_credit.standard_allowance.amount`.

## Implementation

```{tip}
The reforms are implemented through parameters, scenario modifiers, and scenarios that work together to enable policy analysis.
```

- **Parameters**: YAML files define the reform's activation status, the health element amounts for new claimants and for protected (pre-2026) claimants, and the standard allowance uplift rates.
- **Parameters**: YAML files define the reform's activation status, the health element amounts for new claimants and for protected (pre-2026) claimants, and the standard allowance uplift rates. The uplift reaches the standard allowance through its uprating index.
- **Scenario modifier**: The `add_universal_credit_reform` function applies the protected existing-claimant health-element path during microsimulation.
- **Scenario**: The `universal_credit_july_2025_reform` scenario enables the reforms in policy analysis.

Expand All @@ -71,26 +73,30 @@ scenario = Scenario(
sim = Simulation(scenario=scenario)
```

The 2026-27 standard allowance stays at its legislated rates, which include the 2.3% uplift; from 2027-28 it follows CPI alone from the 2026-27 rates less that uplift.

### Changing the standard allowance uplift parameters

```python
from policyengine_uk import Simulation, Scenario

# Set different uplift rates - e.g. 5% in 2026, 7% in 2027
# Set different uplift rates from 2027-28 (2026-27 is legislated), e.g. 7%.
# A bare year names the fiscal year from 6 April.
scenario = Scenario(
parameter_changes={
"gov.dwp.universal_credit.rebalancing.standard_allowance_uplift": {
"2026-01-01": 0.05,
"2027-01-01": 0.07,
"2028-01-01": 0.07,
"2029-01-01": 0.07,
"2027": 0.07,
"2028": 0.07,
"2029": 0.07,
}
}
)

sim = Simulation(scenario=scenario)
```

The uplift reaches the allowance through parameter processing, which `Scenario(parameter_changes=...)` re-runs. A `reform=` dictionary is applied after processing, so to change the allowance that way set `gov.dwp.universal_credit.standard_allowance.amount` directly.

### Changing the health element amount for new claimants

```python
Expand All @@ -110,8 +116,9 @@ sim = Simulation(scenario=scenario)

## Legislative reference

The reforms are based on the Universal Credit Bill and its impact assessment:
The reforms are based on the Universal Credit Act 2025, the Bill and its impact assessment:

- https://www.legislation.gov.uk/ukpga/2025/22/section/1
- https://bills.parliament.uk/publications/62123/documents/6889
- https://bills.parliament.uk/publications/62124/documents/6892
- https://assets.publishing.service.gov.uk/media/689ca49e1c63de6de5bb1298/withdrawn-universal-credit-bill-uc-rebalancing-impact-assessment.pdf
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
"""Build the uprating index for the Universal Credit standard allowance.

Universal Credit Act 2025 s. 1 sets minimum standard allowance amounts for
tax years 2026-27 to 2029-30 in three steps:

1. Take the 2025-26 amounts for 2026-27, and the previous year's Step 2
amounts for each later year.
2. Increase them by the relevant CPI percentage, never below 0% (s. 1(3)).
3. Increase the result by the relevant uplift percentage for the year: 2.3%,
3.1%, 4.0% and 4.8% (s. 1(4)).

Each uplift is measured against the CPI-only path from 2025-26, not compounded
on the previous uplift, because Step 1 takes the previous year's amount before
its Step 3. From one year to the next the allowance therefore grows by CPI
times (1 + this year's uplift) / (1 + last year's uplift).

The legislated amounts (2026-27 so far) are written into
``standard_allowance/amount.yaml`` and used as they are. Later years are
uprated by the index built here, ``gov.dwp.universal_credit.standard_allowance
.uprating``: the benefit uprating index ``gov.benefit_uprating_cpi``, with
s. 1(3)'s 0% floor in 2026-27 to 2029-30, times one plus the uplift. For a
year with legislated amounts the uplift is the one those amounts include, from
the s. 1(4) table; for later years it is the uplift in force (nil with
``rebalancing.active`` false). Uprating the legislated amounts by the index
gives each later year the CPI growth and the change in uplift, so switching
rebalancing off, or changing a later year's uplift, reaches every year after
the legislated ones; changing a legislated year needs its amount set. After
2029-30 the uplift stays at 4.8%, so the allowance grows with CPI alone.

The benefit index is the model's measure of CPI for benefit uprating; s. 1(3)
names September CPI. Because it is the same index, the difference from a
CPI-only path is exactly the uplift. It is read on 30 April of each year, the
date fiscal-year conversion samples, as uprating will extend it, so a reform to
the benefit index reaches the allowance in the fiscal year it covers.

This runs in ``process_parameters`` before uprating, so a parameter change
made through ``Scenario(parameter_changes=...)``, which re-runs that pipeline,
reaches the allowance. A ``reform=`` dict applied after processing does not
re-run uprating; set ``standard_allowance.amount`` directly instead.
"""

from functools import lru_cache
from pathlib import Path

import yaml
from policyengine_core.parameters import Parameter, ParameterNode, get_parameter

INDEX_NAME = "gov.dwp.universal_credit.standard_allowance.uprating"
AMOUNT_FILE = Path(__file__).parents[1] / "standard_allowance" / "amount.yaml"
# Universal Credit Act 2025 s. 1(1): tax years 2026-27 to 2029-30.
FIRST_ACT_YEAR = 2026
LAST_ACT_YEAR = 2029
# s. 1(4): the uplift included in a legislated amount for each tax year.
LEGISLATED_UPLIFT = {2026: 0.023, 2027: 0.031, 2028: 0.040, 2029: 0.048}


def tax_year_instant(year: int) -> str:
"""Date at which policy parameters are read for the tax year from April
``year``: 30 April, the date fiscal-year conversion samples."""
return f"{year}-04-30"


def tax_year(instant_str: str) -> int:
"""Tax year of a dated amount. Rates are dated from the start of April,
before the 6 April start of the tax year they apply to."""
year, month = int(instant_str[:4]), int(instant_str[5:7])
return year if month >= 4 else year - 1


def uplift_in_force(rebalancing: ParameterNode, year: int) -> float:
instant = tax_year_instant(year)
if not rebalancing.active(instant):
return 0.0
uplift = rebalancing.standard_allowance_uplift(instant)
# Nil before the uplift parameter starts (2025-26).
return 0.0 if uplift is None else uplift


@lru_cache(maxsize=1)
def last_legislated_year() -> int:
"""The latest tax year with a legislated amount in amount.yaml for every
claimant type. Read from the file, so a reform that replaces an amount
does not change which years are legislated."""
data = yaml.safe_load(AMOUNT_FILE.read_text())
return min(
max(
tax_year(str(instant))
for instant, value in node["values"].items()
if value is not None
)
for node in data.values()
if isinstance(node, dict) and "values" in node
)


def benefit_index_values(parameters: ParameterNode) -> dict:
"""``gov.benefit_uprating_cpi`` for each year of its uprating index, as
uprating will extend it: its own values up to its latest one, then that
value moved with its uprating index. Each year is read on 30 April and
keyed by 1 January, as the index is."""
benefit_index = parameters.gov.benefit_uprating_cpi
cpi_index = get_parameter(parameters, benefit_index.metadata["uprating"])
last_instant = benefit_index.values_list[0].instant_str
value_at_last = benefit_index(last_instant)
cpi_at_last = cpi_index(last_instant)
values = {}
for entry in sorted(cpi_index.values_list, key=lambda e: e.instant_str):
instant = tax_year_instant(int(entry.instant_str[:4]))
if instant <= last_instant or cpi_at_last is None:
values[entry.instant_str] = benefit_index(instant)
else:
values[entry.instant_str] = value_at_last * cpi_index(instant) / cpi_at_last
return values


def add_uc_standard_allowance_uprating(parameters: ParameterNode) -> ParameterNode:
universal_credit = parameters.gov.dwp.universal_credit
rebalancing = universal_credit.rebalancing
standard_allowance = universal_credit.standard_allowance
legislated_through = last_legislated_year()

values = {}
cpi_path = 1.0
previous_cpi = None
for instant, cpi in benefit_index_values(parameters).items():
year = int(instant[:4])
if cpi is None:
continue
if previous_cpi is not None:
growth = cpi / previous_cpi
if FIRST_ACT_YEAR <= year <= LAST_ACT_YEAR:
growth = max(1.0, growth)
cpi_path *= growth
previous_cpi = cpi
if year <= legislated_through and year in LEGISLATED_UPLIFT:
# The uplift the legislated amounts for this year include.
uplift = LEGISLATED_UPLIFT[year]
else:
uplift = uplift_in_force(rebalancing, year)
values[instant] = cpi_path * (1 + uplift)

standard_allowance.add_child(
"uprating",
Parameter(
name=INDEX_NAME,
data={
"description": (
"Uprating index for the Universal Credit standard "
"allowance: benefit uprating CPI times one plus the "
"Universal Credit Act 2025 uplift."
),
"values": values,
"metadata": {
"unit": "/1",
"label": "Universal Credit standard allowance uprating index",
"reference": [
{
"title": "Universal Credit Act 2025 s. 1",
"href": "https://www.legislation.gov.uk/ukpga/2025/22/section/1",
}
],
},
},
),
)
return parameters
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
description: Uplift in the Standard Allowance for Universal Credit, in addition to the annual inflationary increase, compared to 2025-26 levels.
description: Uplift to the Universal Credit standard allowance above CPI-only uprating from its 2025-26 level (the relevant uplift percentage). It is not compounded on the previous year's uplift.
values:
2025-01-01: 0.000
2026-01-01: 0.023
2027-01-01: 0.031
2028-01-01: 0.040
2029-01-01: 0.048
2025-04-06: 0.000
2026-04-06: 0.023
2027-04-06: 0.031
2028-04-06: 0.040
2029-04-06: 0.048
metadata:
unit: /1
label: Universal Credit rebalancing standard allowance uplift
reference:
- title: Universal Credit Bill
href: https://bills.parliament.uk/publications/62123/documents/6889
- title: Universal Credit Act 2025 s. 1(4)
href: https://www.legislation.gov.uk/ukpga/2025/22/section/1
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ SINGLE_YOUNG:
2026-04-01: 338.58
metadata:
label: Universal Credit single amount (under 25)
uprating: gov.benefit_uprating_cpi
uprating: gov.dwp.universal_credit.standard_allowance.uprating
unit: currency-GBP
period: month
SINGLE_OLD:
Expand All @@ -42,7 +42,7 @@ SINGLE_OLD:
2026-04-01: 424.9
metadata:
label: Universal Credit single amount (over 25)
uprating: gov.benefit_uprating_cpi
uprating: gov.dwp.universal_credit.standard_allowance.uprating
unit: currency-GBP
period: month
COUPLE_YOUNG:
Expand All @@ -65,7 +65,7 @@ COUPLE_YOUNG:
2026-04-01: 528.34
metadata:
label: Universal Credit couple amount (both under 25)
uprating: gov.benefit_uprating_cpi
uprating: gov.dwp.universal_credit.standard_allowance.uprating
unit: currency-GBP
period: month
COUPLE_OLD:
Expand Down Expand Up @@ -93,7 +93,7 @@ COUPLE_OLD:
period: month
metadata:
unit: currency-GBP
uprating: gov.benefit_uprating_cpi
uprating: gov.dwp.universal_credit.standard_allowance.uprating
propagate_metadata_to_children: true
reference:
- title: Benefit and pension rates 2020 to 2021 (DWP, gov.uk)
Expand All @@ -111,3 +111,5 @@ metadata:
# As-of date.
- https://www.legislation.gov.uk/uksi/2013/376/regulation/1
- https://www.gov.uk/universal-credit/what-youll-get
# Uprating from 2027-28: CPI and the uplifts to 2029-30.
- https://www.legislation.gov.uk/ukpga/2025/22/section/1
5 changes: 3 additions & 2 deletions policyengine_uk/scenarios/uc_reform.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,9 @@ def add_universal_credit_reform(sim: Microsimulation):
) # Monthly amount * 12
sim.set_input("uc_LCWRA_element", year, current_health_element)

# Standard allowance uplift is handled in the formula itself so user
# reforms to the base amount are applied before the uplift.
# The standard allowance uplift is applied through the allowance's
# uprating index (rebalancing/create_standard_allowance_uprating.py), so a
# reform to the amount itself is used as given.


universal_credit_july_2025_reform = Scenario(
Expand Down
5 changes: 5 additions & 0 deletions policyengine_uk/tax_benefit_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
from policyengine_uk.parameters.gov.dwp.state_pension.triple_lock.create_triple_lock import (
add_triple_lock,
)
from policyengine_uk.parameters.gov.dwp.universal_credit.rebalancing.create_standard_allowance_uprating import (
add_uc_standard_allowance_uprating,
)
from policyengine_uk.parameters.gov.economic_assumptions.create_september_cpi_uprating import (
add_september_cpi_uprating,
)
Expand Down Expand Up @@ -100,6 +103,7 @@ def process_parameters(self) -> None:
- September CPI uprating
- Economic assumption indices
- Income tax and NICs threshold indexation after the freeze
- Universal Credit standard allowance uprating (UC Act 2025 s. 1)
- Parameter uprating and backdating
- Conversion to fiscal year parameters
"""
Expand All @@ -113,6 +117,7 @@ def process_parameters(self) -> None:
self.parameters = add_september_cpi_uprating(self.parameters)
self.parameters = create_economic_assumption_indices(self.parameters)
self.parameters = add_threshold_indexation(self.parameters)
self.parameters = add_uc_standard_allowance_uprating(self.parameters)
self.parameters = add_lsr_deprecation_aliases(self.parameters)

# Create baseline parameters for reform comparisons
Expand Down
21 changes: 9 additions & 12 deletions policyengine_uk/tests/test_dwp_published_rates.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@
3. Projections continue from the last published rate: for a series published
through 2026-27, an uprated parameter in 2027-28 equals the 2026-27 rate
times the change in its uprating index, so there is no jump when
projections begin. The UC standard allowances and LCWRA element are left
out: the Universal Credit Act 2025 sets a minimum for the standard
allowance above CPI uprating (s.1) and freezes the LCWRA element (s.3) for
2026-27 to 2029-30, which the model does not yet follow.
projections begin. The UC standard allowances are uprated by their own
index, CPI times the change in the Universal Credit Act 2025 s.1 uplift,
and are checked against it like any other series. The LCWRA element is
left out: s.3 freezes it for 2026-27 to 2029-30, which the model does not
yet follow.
4. Housing Benefit non-dependant deductions: each year's bands and deductions
are the published ones, and the scale, read with lower edges inclusive as
reg 74(2) words them ("not less than"), never falls as income rises.
Expand Down Expand Up @@ -73,14 +74,10 @@ def test_no_start_instant_overrides_published_rates(name):
assert not (isinstance(uprating, dict) and "start_instant" in uprating)


# Universal Credit Act 2025 (ukpga/2025/22) s.1 (standard allowance minimum:
# the previous Step 2 amount, increased by CPI and then by the uplift in its
# table) and s.3 (LCWRA element frozen for 2026-27 to 2029-30).
# Universal Credit Act 2025 (ukpga/2025/22) s.3: the LCWRA element is frozen
# for 2026-27 to 2029-30. The standard allowances follow s.1 through their own
# uprating index, so they are not exceptions.
UC_ACT_2025_EXCEPTIONS = {
"gov.dwp.universal_credit.standard_allowance.amount.SINGLE_YOUNG",
"gov.dwp.universal_credit.standard_allowance.amount.SINGLE_OLD",
"gov.dwp.universal_credit.standard_allowance.amount.COUPLE_YOUNG",
"gov.dwp.universal_credit.standard_allowance.amount.COUPLE_OLD",
"gov.dwp.universal_credit.elements.disabled.amount",
}

Expand All @@ -96,7 +93,7 @@ def test_projection_continues_from_last_published_rate(name):
if last < 2026:
pytest.skip("series ends before 2026-27")
if name in UC_ACT_2025_EXCEPTIONS:
pytest.skip("Universal Credit Act 2025 ss.1 and 3 depart from CPI uprating")
pytest.skip("Universal Credit Act 2025 s.3 departs from CPI uprating")
index = node(uprating if isinstance(uprating, str) else uprating["parameter"])
ratio = index(f"{last + 1}-01-01") / index(f"{last}-01-01")
expected = PARAMETERS[name]["values"][last] * ratio
Expand Down
Loading
Loading