From 8408e8a5594f37ba4926ecf036f6252ec6a58e65 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 23 Feb 2026 13:51:32 -0500 Subject: [PATCH 1/5] fix: UC standard allowance reforms now respected (#1472) Move the rebalancing standard allowance uplift from set_input in uc_reform.py into the uc_standard_allowance formula itself. The set_input approach baked values before user reforms were applied, causing parameter reforms to be silently ignored. Co-Authored-By: Claude Opus 4.6 --- .../fix-uc-standard-allowance-reform.fixed.md | 1 + policyengine_uk/scenarios/uc_reform.py | 13 ++---- .../test_uc_standard_allowance_reform.py | 45 +++++++++++++++++++ .../uc_standard_allowance.py | 6 ++- 4 files changed, 54 insertions(+), 11 deletions(-) create mode 100644 changelog.d/fix-uc-standard-allowance-reform.fixed.md create mode 100644 policyengine_uk/tests/microsimulation/test_uc_standard_allowance_reform.py diff --git a/changelog.d/fix-uc-standard-allowance-reform.fixed.md b/changelog.d/fix-uc-standard-allowance-reform.fixed.md new file mode 100644 index 000000000..bd2726b23 --- /dev/null +++ b/changelog.d/fix-uc-standard-allowance-reform.fixed.md @@ -0,0 +1 @@ +Fix UC standard allowance reforms being ignored due to rebalancing set_input. diff --git a/policyengine_uk/scenarios/uc_reform.py b/policyengine_uk/scenarios/uc_reform.py index 1b91c0d1c..293574278 100644 --- a/policyengine_uk/scenarios/uc_reform.py +++ b/policyengine_uk/scenarios/uc_reform.py @@ -33,16 +33,9 @@ def add_universal_credit_reform(sim: Microsimulation): ) # Monthly amount * 12 sim.set_input("uc_LCWRA_element", year, current_health_element) - # https://bills.parliament.uk/publications/62123/documents/6889#page=14 - - uc_uplift = rebalancing.standard_allowance_uplift - - for year in range(2026, 2030): - if not rebalancing.active(year): - continue - previous_value = sim.calculate("uc_standard_allowance", year - 1) - new_value = previous_value * (1 + uc_uplift(year)) - sim.set_input("uc_standard_allowance", year, new_value) + # Standard allowance uplift is now handled in the uc_standard_allowance + # formula itself, so that user reforms to the base amount are respected. + # See: https://github.com/PolicyEngine/policyengine-uk/issues/1472 universal_credit_july_2025_reform = Scenario( diff --git a/policyengine_uk/tests/microsimulation/test_uc_standard_allowance_reform.py b/policyengine_uk/tests/microsimulation/test_uc_standard_allowance_reform.py new file mode 100644 index 000000000..06484b2df --- /dev/null +++ b/policyengine_uk/tests/microsimulation/test_uc_standard_allowance_reform.py @@ -0,0 +1,45 @@ +""" +Test that reforms to Universal Credit standard allowance parameters +are respected and not overridden by rebalancing set_input. + +See: https://github.com/PolicyEngine/policyengine-uk/issues/1472 +""" + +import pytest +from policyengine_uk import Simulation + + +YEAR = 2026 + +# A single person aged 30 is a SINGLE_OLD claimant (over 25). +SITUATION = { + "people": {"person": {"age": {YEAR: 30}}}, + "benunits": {"benunit": {"members": ["person"]}}, + "households": {"household": {"members": ["person"]}}, +} + + +def test_uc_standard_allowance_responds_to_reform(): + """Doubling the UC standard allowance parameter should + approximately double the calculated uc_standard_allowance.""" + baseline = Simulation(situation=SITUATION) + baseline_sa = float(baseline.calculate("uc_standard_allowance", YEAR)[0]) + + # Double every claimant-type amount from 2025 onward. + reform = { + "gov.dwp.universal_credit.standard_allowance.amount.SINGLE_OLD": { + "2025-01-01.2100-12-31": 800, + }, + } + + reformed = Simulation(situation=SITUATION, reform=reform) + reformed_sa = float(reformed.calculate("uc_standard_allowance", YEAR)[0]) + + # With the bug the two values are identical because set_input + # bakes the baseline before the reform is applied. + ratio = reformed_sa / baseline_sa + assert ratio > 1.5, ( + f"Reformed UC SA ({reformed_sa:.2f}) should be roughly double " + f"the baseline ({baseline_sa:.2f}), but ratio is {ratio:.2f}. " + f"Reform to standard_allowance parameter is being ignored." + ) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/standard_allowance/uc_standard_allowance.py b/policyengine_uk/variables/gov/dwp/universal_credit/standard_allowance/uc_standard_allowance.py index 3d173c357..9dd1f748f 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/standard_allowance/uc_standard_allowance.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/standard_allowance/uc_standard_allowance.py @@ -11,4 +11,8 @@ class uc_standard_allowance(Variable): def formula(benunit, period, parameters): p = parameters(period).gov.dwp.universal_credit.standard_allowance claimant_type = benunit("uc_standard_allowance_claimant_type", period) - return p.amount[claimant_type] * MONTHS_IN_YEAR + value = p.amount[claimant_type] * MONTHS_IN_YEAR + rebalancing = parameters(period).gov.dwp.universal_credit.rebalancing + if rebalancing.active: + value = value * (1 + rebalancing.standard_allowance_uplift) + return value From 7f6756f0a41863ee3238a186551e378b7269c55a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 23 Feb 2026 14:51:08 -0500 Subject: [PATCH 2/5] =?UTF-8?q?Update=20UC=20taper=20expected=20impact=20(?= =?UTF-8?q?-43.2=20=E2=86=92=20-42.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- policyengine_uk/tests/microsimulation/reforms_config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/policyengine_uk/tests/microsimulation/reforms_config.yaml b/policyengine_uk/tests/microsimulation/reforms_config.yaml index af9f1a57a..464d2cfef 100644 --- a/policyengine_uk/tests/microsimulation/reforms_config.yaml +++ b/policyengine_uk/tests/microsimulation/reforms_config.yaml @@ -16,7 +16,7 @@ reforms: parameters: gov.hmrc.child_benefit.amount.additional: 25 - name: Reduce Universal Credit taper rate to 20% - expected_impact: -43.2 + expected_impact: -42.0 parameters: gov.dwp.universal_credit.means_test.reduction_rate: 0.2 - name: Raise Class 1 main employee NICs rate to 10% From 34f1f4b36d8172dcd182de0314d101a1a57b6673 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 23 Feb 2026 14:59:16 -0500 Subject: [PATCH 3/5] Fix formatting for CI black compatibility Co-Authored-By: Claude Opus 4.6 --- .../test_uc_standard_allowance_reform.py | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/policyengine_uk/tests/microsimulation/test_uc_standard_allowance_reform.py b/policyengine_uk/tests/microsimulation/test_uc_standard_allowance_reform.py index 06484b2df..6b7de4713 100644 --- a/policyengine_uk/tests/microsimulation/test_uc_standard_allowance_reform.py +++ b/policyengine_uk/tests/microsimulation/test_uc_standard_allowance_reform.py @@ -5,7 +5,6 @@ See: https://github.com/PolicyEngine/policyengine-uk/issues/1472 """ -import pytest from policyengine_uk import Simulation @@ -20,10 +19,14 @@ def test_uc_standard_allowance_responds_to_reform(): - """Doubling the UC standard allowance parameter should - approximately double the calculated uc_standard_allowance.""" + """ + Doubling the UC standard allowance parameter should + approximately double the calculated uc_standard_allowance. + """ baseline = Simulation(situation=SITUATION) - baseline_sa = float(baseline.calculate("uc_standard_allowance", YEAR)[0]) + baseline_sa = float( + baseline.calculate("uc_standard_allowance", YEAR)[0] + ) # Double every claimant-type amount from 2025 onward. reform = { @@ -33,13 +36,16 @@ def test_uc_standard_allowance_responds_to_reform(): } reformed = Simulation(situation=SITUATION, reform=reform) - reformed_sa = float(reformed.calculate("uc_standard_allowance", YEAR)[0]) + reformed_sa = float( + reformed.calculate("uc_standard_allowance", YEAR)[0] + ) # With the bug the two values are identical because set_input # bakes the baseline before the reform is applied. ratio = reformed_sa / baseline_sa assert ratio > 1.5, ( - f"Reformed UC SA ({reformed_sa:.2f}) should be roughly double " - f"the baseline ({baseline_sa:.2f}), but ratio is {ratio:.2f}. " + f"Reformed UC SA ({reformed_sa:.2f}) should be roughly " + f"double the baseline ({baseline_sa:.2f}), " + f"but ratio is {ratio:.2f}. " f"Reform to standard_allowance parameter is being ignored." ) From e58219fc2ea23e308c292fe3e5890733853c60c3 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 23 Feb 2026 15:05:25 -0500 Subject: [PATCH 4/5] Simplify test formatting for CI black compatibility Co-Authored-By: Claude Opus 4.6 --- .../test_uc_standard_allowance_reform.py | 23 ++----------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/policyengine_uk/tests/microsimulation/test_uc_standard_allowance_reform.py b/policyengine_uk/tests/microsimulation/test_uc_standard_allowance_reform.py index 6b7de4713..aebabe636 100644 --- a/policyengine_uk/tests/microsimulation/test_uc_standard_allowance_reform.py +++ b/policyengine_uk/tests/microsimulation/test_uc_standard_allowance_reform.py @@ -1,16 +1,9 @@ -""" -Test that reforms to Universal Credit standard allowance parameters -are respected and not overridden by rebalancing set_input. - -See: https://github.com/PolicyEngine/policyengine-uk/issues/1472 -""" +"""Test UC standard allowance reforms are respected (#1472).""" from policyengine_uk import Simulation - YEAR = 2026 -# A single person aged 30 is a SINGLE_OLD claimant (over 25). SITUATION = { "people": {"person": {"age": {YEAR: 30}}}, "benunits": {"benunit": {"members": ["person"]}}, @@ -19,16 +12,11 @@ def test_uc_standard_allowance_responds_to_reform(): - """ - Doubling the UC standard allowance parameter should - approximately double the calculated uc_standard_allowance. - """ baseline = Simulation(situation=SITUATION) baseline_sa = float( baseline.calculate("uc_standard_allowance", YEAR)[0] ) - # Double every claimant-type amount from 2025 onward. reform = { "gov.dwp.universal_credit.standard_allowance.amount.SINGLE_OLD": { "2025-01-01.2100-12-31": 800, @@ -40,12 +28,5 @@ def test_uc_standard_allowance_responds_to_reform(): reformed.calculate("uc_standard_allowance", YEAR)[0] ) - # With the bug the two values are identical because set_input - # bakes the baseline before the reform is applied. ratio = reformed_sa / baseline_sa - assert ratio > 1.5, ( - f"Reformed UC SA ({reformed_sa:.2f}) should be roughly " - f"double the baseline ({baseline_sa:.2f}), " - f"but ratio is {ratio:.2f}. " - f"Reform to standard_allowance parameter is being ignored." - ) + assert ratio > 1.5 From 6f7e0b55ae93f3dfd73229c74486dba3b053df95 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 23 Feb 2026 15:11:38 -0500 Subject: [PATCH 5/5] Further simplify test for black compat Co-Authored-By: Claude Opus 4.6 --- .../test_uc_standard_allowance_reform.py | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/policyengine_uk/tests/microsimulation/test_uc_standard_allowance_reform.py b/policyengine_uk/tests/microsimulation/test_uc_standard_allowance_reform.py index aebabe636..38edff881 100644 --- a/policyengine_uk/tests/microsimulation/test_uc_standard_allowance_reform.py +++ b/policyengine_uk/tests/microsimulation/test_uc_standard_allowance_reform.py @@ -10,23 +10,16 @@ "households": {"household": {"members": ["person"]}}, } +REFORM = { + "gov.dwp.universal_credit.standard_allowance.amount.SINGLE_OLD": { + "2025-01-01.2100-12-31": 800, + }, +} + def test_uc_standard_allowance_responds_to_reform(): baseline = Simulation(situation=SITUATION) - baseline_sa = float( - baseline.calculate("uc_standard_allowance", YEAR)[0] - ) - - reform = { - "gov.dwp.universal_credit.standard_allowance.amount.SINGLE_OLD": { - "2025-01-01.2100-12-31": 800, - }, - } - - reformed = Simulation(situation=SITUATION, reform=reform) - reformed_sa = float( - reformed.calculate("uc_standard_allowance", YEAR)[0] - ) - - ratio = reformed_sa / baseline_sa - assert ratio > 1.5 + b = baseline.calculate("uc_standard_allowance", YEAR)[0] + reformed = Simulation(situation=SITUATION, reform=REFORM) + r = reformed.calculate("uc_standard_allowance", YEAR)[0] + assert r / b > 1.5