diff --git a/changelog.d/uc-deductions-floor-reconciliation.fixed.md b/changelog.d/uc-deductions-floor-reconciliation.fixed.md new file mode 100644 index 0000000000..68af97f62e --- /dev/null +++ b/changelog.d/uc-deductions-floor-reconciliation.fixed.md @@ -0,0 +1,5 @@ +- Fixed Universal Credit deductions and benefit cap reductions to report what the award actually loses under a protected minimum floor, so the components sum to the reduction applied instead of over-reporting it. Where the floor binds, deductions are met first and the cap reduction absorbs the rest, however heavily the award is capped. +- Added `uc_benefit_cap_reduction`, the benefit cap reduction applied to a Universal Credit award after the protected floor and bounded by the award, leaving `benefit_cap_reduction` gross for Housing Benefit. +- Fixed a protected floor above 1 adding money to Universal Credit awards, by clamping the permitted reduction at zero. +- Fixed Universal Credit deductions applying to benefit units that would not claim Universal Credit. +- Corrected the effective date of the 25% deductions cap to 12 April 2021, and the one-penny minimum payable citation to Schedule 6, paragraph 3(1)(a) of SI 2013/380. diff --git a/docs/book/validation/uc-deductions.md b/docs/book/validation/uc-deductions.md index 5d7d4fc4a4..8314085a61 100644 --- a/docs/book/validation/uc-deductions.md +++ b/docs/book/validation/uc-deductions.md @@ -12,12 +12,14 @@ The implementation separates **latent demand** from the **statutory cap**. `uc_l Enhanced FRS 2023/24 v1.40.3, against [DWP's Universal Credit deductions statistics](https://www.gov.uk/government/statistics/universal-credit-quarterly-statistics-29-april-2013-to-12-february-2026/universal-credit-deductions-statistics-march-2025-to-february-2026). +Shares below are of **all** UC households, not of those with deductions — the denominator both the model and DWP use. + | Statistic | Model | DWP published | |---|---|---| | Share of UC households with deductions | 47.0–47.1% | 46–47% | -| At-cap share, 25% regime (2024) | 13.4% | 13–14% (Mar–May 2025) | -| At-cap share, 15% regime (2025) | 26.4% | 21% (Jun 2025–Feb 2026) | -| Above-cap share (last resort) | 1.8% | 2% | +| At-cap share of all UC households, 25% regime (2024) | 13.4% | 13–14% (Mar–May 2025) | +| At-cap share of all UC households, 15% regime (2025) | 26.4% | 21% (Jun 2025–Feb 2026) | +| Above-cap share of all UC households (last resort) | 1.8% | 2% | | Mean monthly deduction, 25% regime | £66 | £67–68 | | Mean monthly deduction, 15% regime | £50 | £51–54 | | Mean annual gain, 25%→15% cap | £421 | £420 (gov.uk) | @@ -47,7 +49,13 @@ The latent distribution is observed in 2025 and applied to all model years as a ### The protected floor binds on last resort deductions -The `protected_floor` lever limits combined deductions and benefit cap reductions to (1 − floor) × the standard allowance. It binds on the whole of `uc_deductions`, including the above-cap excess that current law exempts from the deductions cap (last resort and child maintenance deductions). JRF's briefing does not say whether their floor exempts those categories — its worked example involves only cappable deductions and the benefit cap — so this is a modeling choice. +The `protected_floor` lever limits combined deductions and benefit cap reductions to (1 − floor) × the standard allowance. Where the current-law split already fits inside that allowance, nothing changes. Where it does not, deductions are met first from the protected award, up to the allowance, and the benefit cap reduction absorbs the rest. Both `uc_deductions` and `uc_benefit_cap_reduction` come from one allocation (`_reduction_allocation.py`), so they agree by construction. + +JRF specifies the combined limit, not a priority between the two: their [September 2026 briefing](https://www.jrf.org.uk/social-security/a-protected-minimum-floor-in-universal-credit-to-cut-deep-child-poverty) proposes that "the sum of the 'capped amount' and any debt deductions is limited to the difference between a household's standard allowance and the level of the protected minimum floor". Debt first is the modeling choice, for three reasons. Under the Fair Repayment Rate the 15% deductions cap equals the allowance an 85% floor leaves, so cappable deductions never breach the floor on their own, and the policy JRF builds on (extending the Fair Repayment Rate "to all deductions, including the benefit cap") leaves deductions where that rate puts them. JRF's own example (standard allowance £98 a week, a £15 deduction, an £80 cap reduction, total limited to £15) is consistent with it. And it does not depend on how heavily the household is capped: if the deduction were limited by the gross cap reduction, a household capped down to a few pence would have its deduction squeezed to almost nothing even though the floor then removes most of that cap. The award is the same under either priority; only the split differs. + +Where deductions alone do exceed the allowance, the floor binds on the whole of `uc_deductions`, including the above-cap excess that current law exempts from the deductions cap (last resort and child maintenance deductions). JRF's briefing does not say whether their floor exempts those categories — its worked example involves only cappable deductions and the benefit cap — so this is a modeling choice. + +Both components report what the award actually loses, so `uc_deductions` and `uc_benefit_cap_reduction` sum to the entitlement forgone, and the applied cap reduction never exceeds the award (a £1,000 cap reduction on a £500 award applies £500). `benefit_cap_reduction` itself stays gross: it also drives Housing Benefit, which the Universal Credit floor does not protect. Read `uc_benefit_cap_reduction` for the cap reduction applied to a UC award under a floor reform. ### Other assumptions @@ -75,7 +83,7 @@ The `protected_floor` lever limits combined deductions and benefit cap reduction {"gov.dwp.universal_credit.deductions.protected_floor": {"2026-01-01.2030-12-31": 0.85}} ``` -A cap of 1 − *x* is equivalent to a protected minimum floor at *x* of the standard allowance for capped deduction types. The dedicated `protected_floor` lever additionally binds on benefit cap reductions, which is where most of JRF's post-Fair-Repayment-Rate effect comes from. +A cap of 1 − *x* is equivalent to a protected minimum floor at *x* of the standard allowance for capped deduction types. The dedicated `protected_floor` lever additionally binds on benefit cap reductions, which is where most of JRF's post-Fair-Repayment-Rate effect comes from — and, because deductions are met first, where nearly all of the modeled effect lands. ## Where the assignment lives diff --git a/policyengine_uk/parameters/gov/dwp/universal_credit/deductions/cap.yaml b/policyengine_uk/parameters/gov/dwp/universal_credit/deductions/cap.yaml index 9a62d98a81..bfe577cc36 100644 --- a/policyengine_uk/parameters/gov/dwp/universal_credit/deductions/cap.yaml +++ b/policyengine_uk/parameters/gov/dwp/universal_credit/deductions/cap.yaml @@ -1,8 +1,8 @@ -description: Maximum total deductions from a Universal Credit award, as a fraction of the standard allowance. Schedule 6 paragraph 4(1) of SI 2013/380 sets a statutory ceiling of 40%; within it, DWP sets the operational cap administratively - 30% from October 2019 (Budget 2018), 25% from April 2021 (announced for October 2021 at Budget 2020, brought forward at Budget 2021), and 15% (the Fair Repayment Rate) from 30 April 2025, which required no legislative change. Last resort deductions (housing and fuel arrears hardship deductions) and child maintenance deductions - separate categories in DWP guidance - may each exceed the cap (Explanatory Memorandum to SI 2025/387, paragraphs 5.10 to 5.15). +description: Maximum total deductions from a Universal Credit award, as a fraction of the standard allowance. Schedule 6 paragraph 4(1) of SI 2013/380 sets a statutory ceiling of 40%; within it, DWP sets the operational cap administratively - 30% from October 2019 (Budget 2018), 25% from 12 April 2021 (announced for October 2021 at Budget 2020, brought forward at Budget 2021; DWP Annual Report and Accounts 2020-21), and 15% (the Fair Repayment Rate) from 30 April 2025, which required no legislative change. Last resort deductions (housing and fuel arrears hardship deductions) and child maintenance deductions - separate categories in DWP guidance - may each exceed the cap (Explanatory Memorandum to SI 2025/387, paragraphs 5.10 to 5.15). values: 2015-04-01: 0.4 2019-10-01: 0.3 - 2021-04-01: 0.25 + 2021-04-12: 0.25 2025-04-30: 0.15 metadata: label: UC deductions cap @@ -23,5 +23,7 @@ metadata: href: https://assets.publishing.service.gov.uk/media/5e68d0dce90e070ac74f08ea/Budget_2020_Web_Accessible_Complete.pdf#page=73 - title: HM Treasury, Budget 2021 (25% cap brought forward to April 2021) href: https://www.gov.uk/government/publications/budget-2021-documents/budget-2021-html + - title: DWP Annual Report and Accounts 2020-21, p.57 (cap reduced from 30% to 25% from 12 April 2021) + href: https://assets.publishing.service.gov.uk/media/60fa97498fa8f5043411d83d/dwp-annual-report-and-accounts-2020-2021-web-ready.pdf#page=58 - title: Universal Credit change brings £420 boost to over a million households (Fair Repayment Rate, 30 April 2025) href: https://www.gov.uk/government/news/universal-credit-change-brings-420-boost-to-over-a-million-households diff --git a/policyengine_uk/parameters/gov/dwp/universal_credit/deductions/minimum_payable.yaml b/policyengine_uk/parameters/gov/dwp/universal_credit/deductions/minimum_payable.yaml index 075b067fe6..47e5da327b 100644 --- a/policyengine_uk/parameters/gov/dwp/universal_credit/deductions/minimum_payable.yaml +++ b/policyengine_uk/parameters/gov/dwp/universal_credit/deductions/minimum_payable.yaml @@ -1,4 +1,4 @@ -description: Minimum Universal Credit payable after deductions, per assessment period. Schedule 6 of SI 2013/380 provides that deductions must not reduce the amount payable to the claimant below one penny. +description: Minimum Universal Credit payable after deductions, per assessment period. Schedule 6, paragraph 3(1)(a) of SI 2013/380 provides that deductions must not reduce the amount payable to the claimant below one penny. values: 2015-04-01: 0.01 metadata: @@ -6,5 +6,5 @@ metadata: unit: currency-GBP period: month reference: - - title: SI 2013/380, Schedule 6 (deductions from benefit and direct payment to third parties) - href: https://www.legislation.gov.uk/uksi/2013/380/schedule/6 + - title: SI 2013/380, Schedule 6, paragraph 3(1)(a) (deductions must leave at least one penny payable) + href: https://www.legislation.gov.uk/uksi/2013/380/schedule/6/paragraph/3 diff --git a/policyengine_uk/parameters/gov/dwp/universal_credit/deductions/protected_floor.yaml b/policyengine_uk/parameters/gov/dwp/universal_credit/deductions/protected_floor.yaml index 50fbf3aca3..50f754caa9 100644 --- a/policyengine_uk/parameters/gov/dwp/universal_credit/deductions/protected_floor.yaml +++ b/policyengine_uk/parameters/gov/dwp/universal_credit/deductions/protected_floor.yaml @@ -1,4 +1,4 @@ -description: Protected minimum floor in Universal Credit, as a fraction of the standard allowance. When above zero, combined deductions and benefit cap reductions are limited to (1 - floor) times the standard allowance, following JRF's proposal design (their worked example limits the reduction itself to 15% of the standard allowance). Zero - the value under current law, which has no such floor - leaves reductions unlimited. Setting 0.85 models JRF's proposal; this reform-lever parameter encodes no current-law value other than zero. The modeled floor binds on all deductions, including the above-cap excess that current law exempts from the deductions cap (last resort and child maintenance deductions) - JRF's briefing does not state whether their floor exempts those categories, and its worked example involves only cappable deductions and the benefit cap, so treating the floor as binding on everything is a modeling choice rather than a stated feature of the proposal. +description: Protected minimum floor in Universal Credit, as a fraction of the standard allowance. When above zero, combined deductions and benefit cap reductions are limited to (1 - floor) times the standard allowance, following JRF's proposal design (their worked example limits the reduction itself to 15% of the standard allowance). Zero - the value under current law, which has no such floor - leaves reductions unlimited. Setting 0.85 models JRF's proposal; this reform-lever parameter encodes no current-law value other than zero. Where the current-law split already fits inside the allowance nothing changes; where it does not, deductions are met first, up to the allowance, and the benefit cap reduction absorbs the rest (uc_benefit_cap_reduction). JRF fixes the combined limit, not the priority between the two, so this ordering is a modeling choice; it reproduces JRF's September 2026 example, where a 15% deduction survives and the cap reduction goes. Where deductions do exceed it, the modeled floor binds on all of them, including the above-cap excess that current law exempts from the deductions cap (last resort and child maintenance deductions) - JRF's briefing does not state whether their floor exempts those categories, and its example involves only cappable deductions and the benefit cap, so treating the floor as binding on everything is a modeling choice rather than a stated feature of the proposal. values: 2015-04-01: 0 metadata: @@ -6,5 +6,5 @@ metadata: unit: /1 period: year reference: - - title: JRF, A protected minimum floor in Universal Credit (October 2025 briefing; proposal design source, not current law) - href: https://www.jrf.org.uk/social-security/protected-minimum-floor-in-universal-credit-new-policy-targeting-hardship + - title: JRF, A protected minimum floor in Universal Credit to cut deep child poverty (11 September 2026, replacing the 9 October 2025 briefing; proposal design source, not current law) + href: https://www.jrf.org.uk/social-security/a-protected-minimum-floor-in-universal-credit-to-cut-deep-child-poverty diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_deductions.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_deductions.yaml index 1840c13910..23e9f370d2 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_deductions.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_deductions.yaml @@ -107,7 +107,8 @@ household: members: person output: - # Schedule 6 of SI 2013/380: the award cannot be deducted below one + # Schedule 6, paragraph 3(1)(a) of SI 2013/380: the award cannot be + # deducted below one # penny per assessment period (12p per year). uc_deductions: 499.88 universal_credit: 0.12 diff --git a/policyengine_uk/tests/test_uc_deductions.py b/policyengine_uk/tests/test_uc_deductions.py index ffab86dfe2..a012fb2c8b 100644 --- a/policyengine_uk/tests/test_uc_deductions.py +++ b/policyengine_uk/tests/test_uc_deductions.py @@ -9,6 +9,12 @@ YEAR = 2025 +FLOOR_85 = { + "gov.dwp.universal_credit.deductions.protected_floor": { + "2025-01-01.2025-12-31": 0.85 + } +} + def make_situation(**benunit_overrides): benunit = { @@ -161,9 +167,9 @@ def test_protected_floor_inactive_when_reductions_stay_above_it(self): ) def test_protected_floor_binds_on_the_above_cap_excess(self): - # Latent demand of 30% is 15% cappable plus a 15% last resort excess - # that current law exempts from the cap, giving a 20% deduction rate - # under the 15% cap. The modeled floor limits combined reductions + # Latent demand of 30% is 25% cappable (cut to 15% by the cap) plus a + # 5% last resort excess that current law exempts from the cap, giving + # a 20% deduction rate. The modeled floor limits combined reductions # regardless of category, so it cuts into that excess. JRF's briefing # does not say whether their floor exempts last resort and child # maintenance deductions; this pins the modeling choice. @@ -186,6 +192,174 @@ def test_protected_floor_binds_on_the_above_cap_excess(self): 6_000 - 0.15 * sa ) + def test_floor_components_reconcile_with_the_reduction_applied(self): + # uc_deductions and uc_benefit_cap_reduction are what the award + # actually loses, so they must sum to the entitlement forgone. Before + # the floor was pushed into the components, they reported £960 and + # £1,000 against a £720 reduction actually applied. + situation = make_situation( + benefit_cap_reduction={YEAR: 1_000}, + uc_latent_deduction_rate={YEAR: 0.30}, + uc_deduction_combination={YEAR: "ADVANCE_ONLY"}, + ) + reform = Simulation( + situation=situation, + reform={ + "gov.dwp.universal_credit.deductions.protected_floor": { + "2025-01-01.2025-12-31": 0.85 + } + }, + ) + sa = reform.calculate("uc_standard_allowance", YEAR)[0] + deductions = reform.calculate("uc_deductions", YEAR)[0] + cap_reduction = reform.calculate("uc_benefit_cap_reduction", YEAR)[0] + universal_credit = reform.calculate("universal_credit", YEAR)[0] + assert deductions + cap_reduction == pytest.approx(0.15 * sa) + assert deductions + cap_reduction == pytest.approx(6_000 - universal_credit) + # The gross benefit cap reduction is unchanged: it also drives + # Housing Benefit, which the Universal Credit floor does not protect. + assert reform.calculate("benefit_cap_reduction", YEAR)[0] == 1_000 + + def test_the_benefit_cap_reduction_absorbs_the_floor_first(self): + # Deductions of 10% of the standard allowance fit inside a 15% floor + # allowance, so they survive intact and the cap reduction keeps only + # the 5% left. test_jrf_2026_example_keeps_a_full_rate_deduction + # covers JRF's own example, where a 15% deduction fills the allowance. + situation = make_situation( + benefit_cap_reduction={YEAR: 2_500}, + uc_latent_deduction_rate={YEAR: 0.10}, + uc_deduction_combination={YEAR: "ADVANCE_ONLY"}, + ) + reform = Simulation( + situation=situation, + reform={ + "gov.dwp.universal_credit.deductions.protected_floor": { + "2025-01-01.2025-12-31": 0.85 + } + }, + ) + sa = reform.calculate("uc_standard_allowance", YEAR)[0] + assert reform.calculate("uc_deductions", YEAR)[0] == pytest.approx(0.10 * sa) + assert reform.calculate("uc_benefit_cap_reduction", YEAR)[0] == pytest.approx( + 0.05 * sa + ) + + def test_jrf_2026_example_keeps_a_full_rate_deduction(self): + # JRF's September 2026 briefing: standard allowance £98 a week, a + # Fair Repayment Rate deduction of £15 and a benefit cap reduction of + # £80; an 85% floor limits the total reduction to £15 and the + # household gains £80. Latent demand of 25% is cut to the 15% cap, + # which exactly fills the floor allowance, so the deduction survives + # whole and the cap reduction falls to zero. + situation = make_situation( + uc_latent_deduction_rate={YEAR: 0.25}, + uc_deduction_combination={YEAR: "ADVANCE_ONLY"}, + ) + sa = Simulation(situation=situation).calculate("uc_standard_allowance", YEAR)[0] + situation["benunits"]["benunit"]["benefit_cap_reduction"] = {YEAR: 80 / 98 * sa} + reform = Simulation(situation=situation, reform=FLOOR_85) + assert reform.calculate("uc_deductions", YEAR)[0] == pytest.approx(0.15 * sa) + assert reform.calculate("uc_benefit_cap_reduction", YEAR)[0] == pytest.approx( + 0, abs=0.01 + ) + assert reform.calculate("universal_credit", YEAR)[0] == pytest.approx( + 6_000 - 0.15 * sa + ) + + @pytest.mark.parametrize("gross_cap_reduction", [5_500, 5_900]) + def test_heavy_capping_does_not_squeeze_the_deduction(self, gross_cap_reduction): + # Under the gross cap the penny rule leaves room for only £499.88 or + # £99.88 of deductions. The floor removes all but the allowance, so + # the deduction is met in full from the protected award and the cap + # reduction absorbs the floor: £720.25 and £0, not £499.88/£220.37 + # or £99.88/£620.37. + situation = make_situation( + benefit_cap_reduction={YEAR: gross_cap_reduction}, + uc_latent_deduction_rate={YEAR: 0.30}, + uc_deduction_combination={YEAR: "ADVANCE_ONLY"}, + ) + reform = Simulation(situation=situation, reform=FLOOR_85) + sa = reform.calculate("uc_standard_allowance", YEAR)[0] + assert reform.calculate("uc_deductions", YEAR)[0] == pytest.approx(0.15 * sa) + assert reform.calculate("uc_benefit_cap_reduction", YEAR)[0] == 0 + assert reform.calculate("universal_credit", YEAR)[0] == pytest.approx( + 6_000 - 0.15 * sa + ) + assert reform.calculate("benefit_cap_reduction", YEAR)[0] == gross_cap_reduction + + @pytest.mark.parametrize("floor", [0, 0.85]) + def test_applied_cap_never_exceeds_the_award_lost(self, floor): + # A £1,000 gross cap reduction on a £500 award takes £500, not £1,000 + # (no floor) or the £720.25 floor allowance. The gross figure, which + # Housing Benefit reads, is unchanged. + situation = make_situation( + universal_credit_pre_benefit_cap={YEAR: 500}, + benefit_cap_reduction={YEAR: 1_000}, + ) + reform = Simulation( + situation=situation, + reform={ + "gov.dwp.universal_credit.deductions.protected_floor": { + "2025-01-01.2025-12-31": floor + } + }, + ) + assert reform.calculate("uc_deductions", YEAR)[0] == 0 + assert reform.calculate("uc_benefit_cap_reduction", YEAR)[0] == 500 + assert reform.calculate("universal_credit", YEAR)[0] == 0 + assert reform.calculate("benefit_cap_reduction", YEAR)[0] == 1_000 + + def test_floor_changes_nothing_where_it_does_not_bind(self): + # A £700 award, a £1,000 gross cap and deduction demand above the + # floor allowance: the cap already takes the whole award, which is + # less than the £720.25 allowance, so the floor leaves the current-law + # split alone rather than moving the loss from the cap to deductions. + situation = make_situation( + universal_credit_pre_benefit_cap={YEAR: 700}, + benefit_cap_reduction={YEAR: 1_000}, + uc_latent_deduction_rate={YEAR: 0.30}, + uc_deduction_combination={YEAR: "ADVANCE_ONLY"}, + ) + reform = Simulation(situation=situation, reform=FLOOR_85) + assert reform.calculate("uc_deductions", YEAR)[0] == 0 + assert reform.calculate("uc_benefit_cap_reduction", YEAR)[0] == 700 + assert reform.calculate("universal_credit", YEAR)[0] == 0 + + def test_a_floor_above_the_standard_allowance_adds_nothing(self): + # (1 - floor) x standard allowance goes negative above a floor of 1; + # subtracting it added £960 to a £6,000 award. The allowance clamps + # at zero, where the floor protects the standard allowance entirely. + situation = make_situation( + benefit_cap_reduction={YEAR: 1_000}, + uc_latent_deduction_rate={YEAR: 0.25}, + uc_deduction_combination={YEAR: "ADVANCE_ONLY"}, + ) + reform = Simulation( + situation=situation, + reform={ + "gov.dwp.universal_credit.deductions.protected_floor": { + "2025-01-01.2025-12-31": 1.2 + } + }, + ) + assert reform.calculate("uc_deductions", YEAR)[0] == 0 + assert reform.calculate("uc_benefit_cap_reduction", YEAR)[0] == 0 + assert reform.calculate("universal_credit", YEAR)[0] == pytest.approx(6_000) + + def test_no_deductions_without_a_universal_credit_claim(self): + # Datasets are meant to impute uc_latent_deduction_rate directly, at + # which point nothing else gates deductions on the claim. + sim = Simulation( + situation=make_situation( + would_claim_uc={YEAR: False}, + uc_latent_deduction_rate={YEAR: 0.30}, + uc_deduction_combination={YEAR: "ADVANCE_ONLY"}, + ) + ) + assert sim.calculate("universal_credit", YEAR)[0] == 0 + assert sim.calculate("uc_deductions", YEAR)[0] == 0 + assert sim.calculate("uc_benefit_cap_reduction", YEAR)[0] == 0 + def test_zeroing_every_type_share_leaves_a_valid_enum_array(self): situation = make_situation( uc_deduction_random_draw={YEAR: 0.0}, diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/deductions/_reduction_allocation.py b/policyengine_uk/variables/gov/dwp/universal_credit/deductions/_reduction_allocation.py new file mode 100644 index 0000000000..b22e6ff0f0 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/deductions/_reduction_allocation.py @@ -0,0 +1,61 @@ +"""Split a Universal Credit award's reductions between debt deductions and +the benefit cap. + +uc_deductions and uc_benefit_cap_reduction both call this, so they agree by +construction, and neither reads the other, which keeps the graph acyclic. +""" + +from policyengine_uk.model_api import * + + +def allocate_uc_reductions( + pre_cap_award, gross_cap_reduction, deduction_demand, max_reduction, minimum_payable +): + """Return (deductions, applied cap reduction) for each benefit unit. + + pre_cap_award: universal_credit_pre_benefit_cap. + gross_cap_reduction: benefit_cap_reduction, the regulation 81 amount of + SI 2013/376. Housing Benefit reads it too, so it is never floored. + deduction_demand: the deduction rate times the standard allowance, + before the one-penny rule. + max_reduction: uc_maximum_reduction, infinite when no protected floor is + in force (current law). + minimum_payable: the one-penny rule of Schedule 6 paragraph 3(1)(a) of + SI 2013/380, annualised. + + Current law: regulation 81 reduces the award by the cap first, and + deductions come out of what is left, leaving at least a penny payable + (main's operations, in main's order). The applied cap reduction is the + award actually lost, so it is bounded by the award; the gross reduction + can exceed it. + + Under a protected floor nothing changes unless that split takes more + than the floor allowance. Where it does, deductions are met first from + the protected award, up to the allowance, and the cap reduction takes + whatever allowance is left. Working from the deduction demand rather + than the current-law deduction matters for heavily capped awards: there + the penny rule had already squeezed the deduction under the gross cap, + but the floor removes most of that cap. + """ + floor_active = np.isfinite(max_reduction) + + # Current law. + award = max_(pre_cap_award - gross_cap_reduction, 0) + deductions = min_(deduction_demand, max_(award - minimum_payable, 0)) + cap_reduction = min_(gross_cap_reduction, max_(pre_cap_award - deductions, 0)) + + # Protected floor binding: deductions first, the cap reduction absorbs. + binds = floor_active & (cap_reduction + deductions > max_reduction) + floor_deductions = min_( + min_(deduction_demand, max_reduction), + max_(pre_cap_award - minimum_payable, 0), + ) + penny = where(floor_deductions > 0, minimum_payable, 0) + floor_cap_reduction = min_( + min_(gross_cap_reduction, max_(max_reduction - floor_deductions, 0)), + max_(pre_cap_award - floor_deductions - penny, 0), + ) + return ( + where(binds, floor_deductions, deductions), + where(binds, floor_cap_reduction, cap_reduction), + ) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/deductions/uc_deductions.py b/policyengine_uk/variables/gov/dwp/universal_credit/deductions/uc_deductions.py index 0ef750fec5..eb569c6cb4 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/deductions/uc_deductions.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/deductions/uc_deductions.py @@ -1,4 +1,7 @@ from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp.universal_credit.deductions._reduction_allocation import ( + allocate_uc_reductions, +) class uc_deductions(Variable): @@ -7,27 +10,38 @@ class uc_deductions(Variable): "Money deducted from this benefit unit's Universal Credit award to " "repay debts: advance repayments, government debt (benefit and tax " "credit overpayments) and third party deductions. Deductions leave " - "at least one penny per assessment period payable (Schedule 6 of " - "SI 2013/380). Per-household statistics are validated against the " - "DWP deductions statistics; weighted aggregates are not, because " - "they scale with the model's UC caseload, which falls short of the " - "administrative count. Annualized from the surveyed monthly rate: " - "for deduction types with spells shorter than a year (e.g. advance " - "recovery), annual per-household amounts are upper bounds." + "at least one penny per assessment period payable (Schedule 6, " + "paragraph 3(1)(a) of SI 2013/380). Per-household statistics are " + "validated against the DWP deductions statistics; weighted " + "aggregates are not, because they scale with the model's UC " + "caseload, which falls short of the administrative count. " + "Annualized from the surveyed monthly rate: for deduction types " + "with spells shorter than a year (e.g. advance recovery), annual " + "per-household amounts are upper bounds." ) entity = BenUnit definition_period = YEAR value_type = float unit = GBP + defined_for = "would_claim_uc" def formula(benunit, period, parameters): p = parameters(period).gov.dwp.universal_credit.deductions rate = benunit("uc_deduction_rate", period) standard_allowance = benunit("uc_standard_allowance", period) - award = max_( - benunit("universal_credit_pre_benefit_cap", period) - - benunit("benefit_cap_reduction", period), - 0, + # Shared with uc_benefit_cap_reduction. Under current law the cap + # reduces the award first and deductions leave a penny of what is + # left. Where a protected minimum floor binds, deductions are met + # first, up to the floor allowance, and the cap reduction absorbs the + # rest. The floor then also binds on the above-cap excess that + # current law exempts from the deductions cap (last resort and child + # maintenance deductions); JRF's briefing does not say whether their + # floor exempts those categories, so that is a modeling choice. + deductions, _ = allocate_uc_reductions( + benunit("universal_credit_pre_benefit_cap", period), + benunit("benefit_cap_reduction", period), + rate * standard_allowance, + benunit("uc_maximum_reduction", period), + p.minimum_payable * MONTHS_IN_YEAR, ) - minimum_payable = p.minimum_payable * MONTHS_IN_YEAR - return min_(rate * standard_allowance, max_(award - minimum_payable, 0)) + return deductions diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/deductions/uc_maximum_reduction.py b/policyengine_uk/variables/gov/dwp/universal_credit/deductions/uc_maximum_reduction.py new file mode 100644 index 0000000000..82e37fa76d --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/deductions/uc_maximum_reduction.py @@ -0,0 +1,35 @@ +from policyengine_uk.model_api import * + + +class uc_maximum_reduction(Variable): + label = "UC maximum combined reduction" + documentation = ( + "Most that benefit cap reductions and deductions combined may take " + "off a Universal Credit award under the protected minimum floor: " + "(1 - floor) x the standard allowance. Infinite when the floor is " + "zero - the value under current law, which has no such floor - so " + "reductions are unlimited." + ) + entity = BenUnit + definition_period = YEAR + value_type = float + unit = GBP + + def formula(benunit, period, parameters): + # A protected minimum floor (zero under current law) caps combined + # benefit cap reductions and deductions at (1 - floor) x the standard + # allowance, per JRF's protected minimum floor design (their worked + # example limits the reduction itself to 15% of the standard + # allowance). Zero means reductions are unlimited. + floor_rate = parameters( + period + ).gov.dwp.universal_credit.deductions.protected_floor + standard_allowance = benunit("uc_standard_allowance", period) + # A floor above 1 would make the allowance negative; subtracting a + # negative reduction would add money to the award. Clamp at zero, + # where the floor protects the whole standard allowance. + return where( + floor_rate > 0, + max_((1 - floor_rate) * standard_allowance, 0), + np.inf, + ) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/uc_benefit_cap_reduction.py b/policyengine_uk/variables/gov/dwp/universal_credit/uc_benefit_cap_reduction.py new file mode 100644 index 0000000000..af45f75bc5 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/uc_benefit_cap_reduction.py @@ -0,0 +1,40 @@ +from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp.universal_credit.deductions._reduction_allocation import ( + allocate_uc_reductions, +) + + +class uc_benefit_cap_reduction(Variable): + label = "UC benefit cap reduction applied" + documentation = ( + "Benefit cap reduction actually taken off this benefit unit's " + "Universal Credit award. Under current law this is " + "benefit_cap_reduction, bounded by the award: a cap reduction larger " + "than the award takes the award to zero and no further. Where a " + "protected minimum floor binds, deductions are met first, up to the " + "floor allowance, and this takes whatever allowance is left. " + "benefit_cap_reduction itself stays gross, because Housing Benefit " + "also reads it and the Universal Credit floor does not protect " + "Housing Benefit." + ) + entity = BenUnit + definition_period = YEAR + value_type = float + unit = GBP + defined_for = "would_claim_uc" + + def formula(benunit, period, parameters): + p = parameters(period).gov.dwp.universal_credit.deductions + # JRF's September 2026 example (standard allowance £98 a week, a £15 + # Fair Repayment Rate deduction, an £80 cap reduction, an 85% floor) + # limits the total to £15. Meeting the deduction first, as here, + # removes the whole cap reduction. + _, cap_reduction = allocate_uc_reductions( + benunit("universal_credit_pre_benefit_cap", period), + benunit("benefit_cap_reduction", period), + benunit("uc_deduction_rate", period) + * benunit("uc_standard_allowance", period), + benunit("uc_maximum_reduction", period), + p.minimum_payable * MONTHS_IN_YEAR, + ) + return cap_reduction diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/universal_credit.py b/policyengine_uk/variables/gov/dwp/universal_credit/universal_credit.py index f30621334a..31bf7b86f6 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/universal_credit.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/universal_credit.py @@ -11,26 +11,11 @@ class universal_credit(Variable): def formula(benunit, period, parameters): uc_max_entitlement = benunit("universal_credit_pre_benefit_cap", period) - benefit_cap_reduction = benunit("benefit_cap_reduction", period) + # Both components already reflect the protected minimum floor (zero, + # and so inactive, under current law), so the reported components sum + # to the reduction actually applied. Summing them before subtracting + # keeps main's float arithmetic, so current-law awards are unchanged + # to the bit. + benefit_cap_reduction = benunit("uc_benefit_cap_reduction", period) deductions = benunit("uc_deductions", period) - # A protected minimum floor (zero under current law) caps combined - # benefit cap reductions and deductions at (1 - floor) x the standard - # allowance, per JRF's protected minimum floor design (their worked - # example limits the reduction itself to 15% of the standard - # allowance). Zero means reductions are unlimited. - # The floor binds on the whole of uc_deductions, including the above- - # cap excess that current law exempts from the deductions cap (last - # resort and child maintenance deductions). JRF's briefing does not - # say whether their floor exempts those categories - its worked - # example involves only cappable deductions and the benefit cap - so - # this is a modeling choice, tracked for follow-up. - floor_rate = parameters( - period - ).gov.dwp.universal_credit.deductions.protected_floor - total_reductions = benefit_cap_reduction + deductions - max_reductions = where( - floor_rate > 0, - (1 - floor_rate) * benunit("uc_standard_allowance", period), - np.inf, - ) - return max_(uc_max_entitlement - min_(total_reductions, max_reductions), 0) + return max_(uc_max_entitlement - (benefit_cap_reduction + deductions), 0)