diff --git a/changelog.d/uc-rebalancing-protection.fixed.md b/changelog.d/uc-rebalancing-protection.fixed.md new file mode 100644 index 000000000..7db1d28c9 --- /dev/null +++ b/changelog.d/uc-rebalancing-protection.fixed.md @@ -0,0 +1 @@ +Corrected Universal Credit rebalancing so existing health-element claimants keep their combined standard allowance and health element award CPI-protected. diff --git a/docs/book/policy/model-baseline.md b/docs/book/policy/model-baseline.md index 18ccded4a..6c42fd8dd 100644 --- a/docs/book/policy/model-baseline.md +++ b/docs/book/policy/model-baseline.md @@ -12,7 +12,7 @@ The government increased the [employer National Insurance rate from 13.8% to 15% ### Universal Credit rebalancing -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. +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. ### Benefit uprating @@ -113,4 +113,4 @@ Notable exclusions: - **Non-UK resident stamp duty surcharge**: 2% additional rate from fiscal year 2021-22 is not modelled - **Some devolved tax policies**: Beyond property transaction taxes, other devolved policies may have limited coverage -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). \ No newline at end of file +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). diff --git a/docs/book/policy/uc-rebalancing.md b/docs/book/policy/uc-rebalancing.md index cfc280c69..0101818aa 100644 --- a/docs/book/policy/uc-rebalancing.md +++ b/docs/book/policy/uc-rebalancing.md @@ -7,16 +7,25 @@ The Universal Credit rebalancing reforms represent changes to Universal Credit p ## Overview ```{important} -The reforms consist of two main components: health element changes for new claimants and standard allowance uplifts. +The reforms combine a higher standard allowance, protected awards for existing health-element recipients, and a lower fixed health element for most new claimants. ``` -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. +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. -2. **Standard allowance uplifts**: The standard allowance receives additional uplifts beyond the annual inflationary increase from 2026-2029. +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. + +3. **Standard allowance uplifts**: The standard allowance receives additional uplifts beyond the annual inflationary increase from 2026-2029. ## Health element changes -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. +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. + +Existing recipients are treated differently. Their LCWRA amount is uprated so that the combined value of: + +- their standard allowance, and +- their health element + +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. 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: @@ -34,7 +43,7 @@ The standard allowance receives additional percentage uplifts beyond the normal - 2028: 4.0% additional uplift (cumulative) - 2029: 4.8% additional uplift (cumulative) -These uplifts are applied to the previous year's standard allowance amount and compound over time. +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. ## Implementation @@ -43,7 +52,7 @@ The reforms are implemented through parameters, scenario modifiers, and scenario ``` - **Parameters**: Three YAML files define the reform's activation status, health element amount for new claimants, and standard allowance uplift rates. -- **Scenario modifier**: The `add_universal_credit_reform` function applies the changes to Universal Credit calculations during microsimulation. +- **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. ## Examples @@ -98,4 +107,8 @@ sim = Simulation(scenario=scenario) ## Legislative reference -The reforms are based on provisions in the Universal Credit Bill, available at: https://bills.parliament.uk/publications/62123/documents/6889. \ No newline at end of file +The reforms are based on the Universal Credit Bill and its impact assessment: + +- 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 diff --git a/docs/book/usage/scenarios.md b/docs/book/usage/scenarios.md index 04b71efb9..e99a5008b 100644 --- a/docs/book/usage/scenarios.md +++ b/docs/book/usage/scenarios.md @@ -391,14 +391,14 @@ for year in [2025, 2027, 2029]: ### Building Universal Credit scenarios with dynamic changes -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: +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: ```python from policyengine_uk import Scenario, Microsimulation import numpy as np def modify_uc_for_new_claimants(sim: Microsimulation): - """Reduce health elements for new UC claimants while increasing standard allowances""" + """Reduce health elements for new UC claimants while preserving claimant protections""" # Access the parameter system to check if reforms are active rebalancing_params = sim.tax_benefit_system.parameters.gov.dwp.universal_credit.rebalancing @@ -434,11 +434,9 @@ def modify_uc_for_new_claimants(sim: Microsimulation): sim.set_input("uc_LCWRA_element", year, current_health_element) - # Increase standard allowances for everyone - uplift_rate = rebalancing_params.standard_allowance_uplift(year) - previous_allowance = sim.calculate("uc_standard_allowance", year - 1) - new_allowance = previous_allowance * (1 + uplift_rate) - sim.set_input("uc_standard_allowance", year, new_allowance) + # General standard allowance uplifts are already handled in the + # uc_standard_allowance formula. Scenario modifiers only need to add + # claimant-specific overrides such as protected health elements. # Create the UC rebalancing scenario uc_rebalancing = Scenario(simulation_modifier=modify_uc_for_new_claimants) diff --git a/policyengine_uk/scenarios/uc_reform.py b/policyengine_uk/scenarios/uc_reform.py index 1de17dcf9..524d6e1d7 100644 --- a/policyengine_uk/scenarios/uc_reform.py +++ b/policyengine_uk/scenarios/uc_reform.py @@ -1,15 +1,69 @@ from policyengine_uk.model_api import Scenario from policyengine_uk import Microsimulation +from policyengine_uk.variables.gov.dwp.universal_credit.standard_allowance.uc_standard_allowance_claimant_type import ( + UCClaimantType, +) import numpy as np +BASELINE_UC_REBALANCING_YEAR = 2025 + + +def _benefit_uprating_ratio(sim: Microsimulation, year: int) -> float: + parameters = sim.tax_benefit_system.parameters + current_index = float(parameters(str(year)).gov.benefit_uprating_cpi) + baseline_index = float( + parameters(str(BASELINE_UC_REBALANCING_YEAR)).gov.benefit_uprating_cpi + ) + return current_index / baseline_index + + +def _rebalanced_standard_allowance_monthly( + sim: Microsimulation, year: int, claimant_type: str +) -> float: + current = sim.tax_benefit_system.parameters(str(year)) + standard_allowance = float( + current.gov.dwp.universal_credit.standard_allowance.amount[claimant_type] + ) + uplift = float( + current.gov.dwp.universal_credit.rebalancing.standard_allowance_uplift + ) + return standard_allowance * (1 + uplift) + + +def _protected_existing_health_element_monthly( + sim: Microsimulation, year: int +) -> float: + baseline = sim.tax_benefit_system.parameters(str(BASELINE_UC_REBALANCING_YEAR)) + protected_combined_award = _benefit_uprating_ratio(sim, year) * ( + float(baseline.gov.dwp.universal_credit.standard_allowance.amount.SINGLE_OLD) + + float(baseline.gov.dwp.universal_credit.elements.disabled.amount) + ) + return protected_combined_award - _rebalanced_standard_allowance_monthly( + sim, year, "SINGLE_OLD" + ) + + +def _protected_single_young_health_element_monthly( + sim: Microsimulation, year: int +) -> float: + baseline = sim.tax_benefit_system.parameters(str(BASELINE_UC_REBALANCING_YEAR)) + protected_combined_award = _benefit_uprating_ratio(sim, year) * ( + float(baseline.gov.dwp.universal_credit.standard_allowance.amount.SINGLE_YOUNG) + + float(baseline.gov.dwp.universal_credit.elements.disabled.amount) + ) + return protected_combined_award - _rebalanced_standard_allowance_monthly( + sim, year, "SINGLE_YOUNG" + ) + + def add_universal_credit_reform(sim: Microsimulation): rebalancing = sim.tax_benefit_system.parameters.gov.dwp.universal_credit.rebalancing generator = np.random.default_rng(43) uc_seed = generator.random(len(sim.calculate("benunit_id"))) - p_uc_post_2026_status = { + post_2025_claimant_share = { 2025: 0, 2026: 0.11, 2027: 0.13, @@ -20,11 +74,24 @@ def add_universal_credit_reform(sim: Microsimulation): for year in range(2026, 2030): if not rebalancing.active(year): continue - is_post_25_claimant = uc_seed < p_uc_post_2026_status[year] + is_post_2025_claimant = uc_seed < post_2025_claimant_share[year] current_health_element = sim.calculate("uc_LCWRA_element", year) - # Set new claimants to £217.26/month from April 2026 (pre-2026 claimaints keep inflation-linked increases) + claimant_type = sim.calculate("uc_standard_allowance_claimant_type", year) + has_health_element = current_health_element > 0 + protected_health_element = np.full( + current_health_element.shape, + _protected_existing_health_element_monthly(sim, year) * 12, + dtype=current_health_element.dtype, + ) + protected_health_element[claimant_type == UCClaimantType.SINGLE_YOUNG.name] = ( + _protected_single_young_health_element_monthly(sim, year) * 12 + ) + current_health_element[has_health_element & ~is_post_2025_claimant] = ( + protected_health_element[has_health_element & ~is_post_2025_claimant] + ) + # Set post-April 2026 claimants to £217.26/month. # https://bills.parliament.uk/publications/62123/documents/6889#page=16 - current_health_element[(current_health_element > 0) & is_post_25_claimant] = ( + current_health_element[has_health_element & is_post_2025_claimant] = ( new_claimant_health_element(year) * 12 ) # Monthly amount * 12 sim.set_input("uc_LCWRA_element", year, current_health_element) diff --git a/policyengine_uk/tests/test_uc_rebalancing.py b/policyengine_uk/tests/test_uc_rebalancing.py new file mode 100644 index 000000000..b7d294976 --- /dev/null +++ b/policyengine_uk/tests/test_uc_rebalancing.py @@ -0,0 +1,123 @@ +import numpy as np +import pytest + +import policyengine_uk.scenarios.uc_reform as uc_reform +from policyengine_uk import Simulation + +YEARS = range(2025, 2030) + + +def _uc_claimant(age_2025: int) -> dict: + return { + "people": { + "person": { + "age": {year: age_2025 + year - 2025 for year in YEARS}, + "employment_income": {year: 0 for year in YEARS}, + "uc_limited_capability_for_WRA": {year: True for year in YEARS}, + } + }, + "benunits": {"benunit": {"members": ["person"]}}, + "households": {"household": {"members": ["person"]}}, + } + + +class _FixedRng: + def __init__(self, values): + self.values = np.array(values, dtype=float) + + def random(self, size): + assert size == len(self.values) + return self.values + + +def _force_uc_seed(monkeypatch, values): + monkeypatch.setattr( + uc_reform.np.random, "default_rng", lambda seed: _FixedRng(values) + ) + + +def _benefit_uprating_factor(sim: Simulation, year: int) -> float: + parameters = sim.tax_benefit_system.parameters + current_index = float(parameters(str(year)).gov.benefit_uprating_cpi) + baseline_index = float(parameters("2025").gov.benefit_uprating_cpi) + return current_index / baseline_index + + +def _rebalanced_standard_allowance_monthly( + sim: Simulation, year: int, claimant_type: str +) -> float: + current = sim.tax_benefit_system.parameters(str(year)) + standard_allowance = float( + current.gov.dwp.universal_credit.standard_allowance.amount[claimant_type] + ) + uplift = float( + current.gov.dwp.universal_credit.rebalancing.standard_allowance_uplift + ) + return standard_allowance * (1 + uplift) + + +def _cpi_protected_uc_award_monthly( + sim: Simulation, year: int, claimant_type: str +) -> float: + baseline = sim.tax_benefit_system.parameters("2025").gov.dwp.universal_credit + baseline_standard_allowance = float( + baseline.standard_allowance.amount[claimant_type] + ) + baseline_health_element = float(baseline.elements.disabled.amount) + return _benefit_uprating_factor(sim, year) * ( + baseline_standard_allowance + baseline_health_element + ) + + +@pytest.mark.parametrize("age_2025", [20, 30]) +def test_existing_claimants_keep_combined_award_cpi_protected(monkeypatch, age_2025): + _force_uc_seed(monkeypatch, [0.99]) + sim = Simulation(situation=_uc_claimant(age_2025)) + claimant_type = "SINGLE_YOUNG" if age_2025 < 25 else "SINGLE_OLD" + + for year in range(2026, 2030): + standard_allowance = sim.calculate("uc_standard_allowance", year)[0] / 12 + health_element = sim.calculate("uc_LCWRA_element", year)[0] / 12 + new_claimant_health_element = float( + sim.tax_benefit_system.parameters( + str(year) + ).gov.dwp.universal_credit.rebalancing.new_claimant_health_element + ) + + assert health_element > new_claimant_health_element + assert standard_allowance + health_element == pytest.approx( + _cpi_protected_uc_award_monthly(sim, year, claimant_type) + ) + + +def test_new_claimants_use_fixed_health_element(monkeypatch): + _force_uc_seed(monkeypatch, [0.0]) + sim = Simulation(situation=_uc_claimant(30)) + + for year in range(2026, 2030): + health_element = sim.calculate("uc_LCWRA_element", year)[0] / 12 + expected_health = float( + sim.tax_benefit_system.parameters( + str(year) + ).gov.dwp.universal_credit.rebalancing.new_claimant_health_element + ) + + assert health_element == pytest.approx(expected_health) + + +def test_standard_allowance_reforms_still_change_standard_allowance(monkeypatch): + _force_uc_seed(monkeypatch, [0.99]) + baseline = Simulation(situation=_uc_claimant(30)) + reformed = Simulation( + situation=_uc_claimant(30), + reform={ + "gov.dwp.universal_credit.standard_allowance.amount.SINGLE_OLD": { + "2025-01-01.2100-12-31": 800 + } + }, + ) + + baseline_standard_allowance = baseline.calculate("uc_standard_allowance", 2026)[0] + reformed_standard_allowance = reformed.calculate("uc_standard_allowance", 2026)[0] + + assert reformed_standard_allowance / baseline_standard_allowance > 1.5