From aa043f96e758575c8f9d58ce889db1ec922a7977 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 1 Oct 2026 10:04:10 -0400 Subject: [PATCH 1/9] Placeholder: boarder and lodger rent inputs from the companion branch Stand-ins for rent_paid_as_boarder, rent_paid_as_lodger and pays_rent_to_householder, which the householder-side branch (boarder-lodger-rent) adds. This commit is dropped when that branch is merged in. Co-Authored-By: Claude Opus 5.5 --- .../consumption/rent/pays_rent_to_householder.py | 16 ++++++++++++++++ .../consumption/rent/rent_paid_as_boarder.py | 13 +++++++++++++ .../consumption/rent/rent_paid_as_lodger.py | 13 +++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 policyengine_uk/variables/household/consumption/rent/pays_rent_to_householder.py create mode 100644 policyengine_uk/variables/household/consumption/rent/rent_paid_as_boarder.py create mode 100644 policyengine_uk/variables/household/consumption/rent/rent_paid_as_lodger.py diff --git a/policyengine_uk/variables/household/consumption/rent/pays_rent_to_householder.py b/policyengine_uk/variables/household/consumption/rent/pays_rent_to_householder.py new file mode 100644 index 000000000..7a510c402 --- /dev/null +++ b/policyengine_uk/variables/household/consumption/rent/pays_rent_to_householder.py @@ -0,0 +1,16 @@ +from policyengine_uk.model_api import * + + +class pays_rent_to_householder(Variable): + value_type = bool + entity = Person + label = "Family pays rent to the householder" + documentation = ( + "Whether any member of this person's benefit unit pays the " + "householder rent as a boarder or lodger." + ) + definition_period = YEAR + + def formula(person, period, parameters): + payments = add(person, period, ["rent_paid_as_boarder", "rent_paid_as_lodger"]) + return person.benunit.any(payments > 0) diff --git a/policyengine_uk/variables/household/consumption/rent/rent_paid_as_boarder.py b/policyengine_uk/variables/household/consumption/rent/rent_paid_as_boarder.py new file mode 100644 index 000000000..47d6d220d --- /dev/null +++ b/policyengine_uk/variables/household/consumption/rent/rent_paid_as_boarder.py @@ -0,0 +1,13 @@ +from policyengine_uk.model_api import * + + +class rent_paid_as_boarder(Variable): + value_type = float + entity = Person + label = "Rent paid as a boarder" + documentation = ( + "Rent this person pays the householder for board and lodging " + "(accommodation with at least some meals) in the householder's home." + ) + definition_period = YEAR + unit = GBP diff --git a/policyengine_uk/variables/household/consumption/rent/rent_paid_as_lodger.py b/policyengine_uk/variables/household/consumption/rent/rent_paid_as_lodger.py new file mode 100644 index 000000000..e53796d73 --- /dev/null +++ b/policyengine_uk/variables/household/consumption/rent/rent_paid_as_lodger.py @@ -0,0 +1,13 @@ +from policyengine_uk.model_api import * + + +class rent_paid_as_lodger(Variable): + value_type = float + entity = Person + label = "Rent paid as a lodger" + documentation = ( + "Rent this person pays the householder for lodging only in the " + "householder's home." + ) + definition_period = YEAR + unit = GBP From 358c72060d16781ac2841c4eb07a4b3b526e4455 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 1 Oct 2026 10:16:26 -0400 Subject: [PATCH 2/9] Give sharers, boarders and lodgers their own rent, tenure and LHA category - personal_rent / benunit_rent: each family's own liability. The household's rent is shared equally among the people liable for it (the household head's family plus any family liable_for_share_of_household_rent: UC Regs 2013 Sch 4 para 24(4); HB Regs 2006 reg 12B(4)), plus what boarders and lodgers pay the householder (UC Sch 1 para 2(b); HB reg 12(1)). - benunit_tenure_type: a boarder or lodger rents privately whatever the household's tenure (UC Sch 4 para 20; HB reg 13C(5)(a)); in_social_housing and uc_housing_costs_element follow the family's tenure. - Non-dependants: only the household head's family has them, and families liable for rent are never non-dependants (UC Sch 4 para 9(2)(d)-(f); HB reg 3(2)(d)-(e), 3(4)). Applied to UC and HB deductions, UC bedrooms and the specified-renter test. - LHA category: Universal Credit by entitlement only (Sch 4 para 25(2)(b)); a new Housing Benefit category (reg 13D(2)-(3)) with its own size criteria and rate, including the shared rate for a claimant entitled to one bedroom who lacks exclusive use, used by LHA_cap. - Housing Benefit meals: fixed weekly amounts (Sch 1 para 2, 2008-2026) and a meals_included_in_rent input; deducted from HB eligible rent (reg 12B(2)(b), 13(13)). Co-Authored-By: Claude Opus 5.5 --- .../at_least_three_a_day/aged_16_or_over.yaml | 31 +++++ .../meals/at_least_three_a_day/under_16.yaml | 31 +++++ .../meals/breakfast_only.yaml | 31 +++++ .../aged_16_or_over.yaml | 31 +++++ .../fewer_than_three_a_day/under_16.yaml | 31 +++++ .../family/LHA_claimant_composition.yaml | 47 ++++--- .../housing_benefit_pension_age_claims.yaml | 4 +- .../housing_benefit_non_dep_deductions.yaml | 46 ++++++- .../uc_non_dep_deductions.yaml | 46 ++++++- .../variables/gov/dwp/LHA_allowed_bedrooms.py | 83 +++++++----- policyengine_uk/variables/gov/dwp/LHA_cap.py | 28 +++- .../variables/gov/dwp/LHA_category.py | 30 ++++- .../housing_benefit_entitlement.py | 8 +- .../housing_benefit_meals_deduction.py | 48 +++++++ .../housing_benefit_non_dep_deductions.py | 22 ++- .../housing_benefit_LHA_allowed_bedrooms.py | 37 ++++++ .../gov/dwp/housing_benefit_LHA_category.py | 58 ++++++++ .../gov/dwp/housing_benefit_LHA_rate.py | 28 ++++ .../housing_benefit_shares_accommodation.py | 36 +++++ .../is_housing_benefit_young_individual.py | 23 ++++ .../dwp/is_non_dependant_of_household_head.py | 33 +++++ .../gov/dwp/lha_renter_has_non_dependant.py | 28 ++-- .../gov/dwp/uncapped_BRMA_LHA_rate.py | 125 ++++++++++-------- .../uc_non_dep_deductions.py | 27 ++-- .../uc_housing_costs_element.py | 6 +- .../household/consumption/benunit_rent.py | 4 +- .../household/consumption/personal_rent.py | 43 +++++- .../rent/is_liable_for_household_rent.py | 22 +++ .../liable_for_share_of_household_rent.py | 21 +++ .../rent/meals_included_in_rent.py | 22 +++ .../demographic/benunit_tenure_type.py | 26 +++- .../demographic/in_social_housing.py | 8 +- uv.lock | 2 +- 33 files changed, 894 insertions(+), 172 deletions(-) create mode 100644 policyengine_uk/parameters/gov/dwp/housing_benefit/ineligible_charges/meals/at_least_three_a_day/aged_16_or_over.yaml create mode 100644 policyengine_uk/parameters/gov/dwp/housing_benefit/ineligible_charges/meals/at_least_three_a_day/under_16.yaml create mode 100644 policyengine_uk/parameters/gov/dwp/housing_benefit/ineligible_charges/meals/breakfast_only.yaml create mode 100644 policyengine_uk/parameters/gov/dwp/housing_benefit/ineligible_charges/meals/fewer_than_three_a_day/aged_16_or_over.yaml create mode 100644 policyengine_uk/parameters/gov/dwp/housing_benefit/ineligible_charges/meals/fewer_than_three_a_day/under_16.yaml create mode 100644 policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_meals_deduction.py create mode 100644 policyengine_uk/variables/gov/dwp/housing_benefit_LHA_allowed_bedrooms.py create mode 100644 policyengine_uk/variables/gov/dwp/housing_benefit_LHA_category.py create mode 100644 policyengine_uk/variables/gov/dwp/housing_benefit_LHA_rate.py create mode 100644 policyengine_uk/variables/gov/dwp/housing_benefit_shares_accommodation.py create mode 100644 policyengine_uk/variables/gov/dwp/is_housing_benefit_young_individual.py create mode 100644 policyengine_uk/variables/gov/dwp/is_non_dependant_of_household_head.py create mode 100644 policyengine_uk/variables/household/consumption/rent/is_liable_for_household_rent.py create mode 100644 policyengine_uk/variables/household/consumption/rent/liable_for_share_of_household_rent.py create mode 100644 policyengine_uk/variables/household/consumption/rent/meals_included_in_rent.py diff --git a/policyengine_uk/parameters/gov/dwp/housing_benefit/ineligible_charges/meals/at_least_three_a_day/aged_16_or_over.yaml b/policyengine_uk/parameters/gov/dwp/housing_benefit/ineligible_charges/meals/at_least_three_a_day/aged_16_or_over.yaml new file mode 100644 index 000000000..0baccceb3 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/ineligible_charges/meals/at_least_three_a_day/aged_16_or_over.yaml @@ -0,0 +1,31 @@ +description: Weekly amount ineligible for meals, at least three meals a day, for the claimant and each family member aged 16 or over. +values: + 2008-04-01: 21.60 + 2009-04-01: 22.95 + 2010-04-01: 23.35 + 2011-04-01: 24.05 + 2012-04-01: 25.30 + 2013-04-01: 25.85 + 2014-04-01: 26.55 + 2015-04-01: 26.85 + 2017-04-01: 27.10 + 2019-04-01: 28.55 + 2020-04-01: 29.05 + 2021-04-01: 29.20 + 2022-04-01: 30.10 + 2023-04-01: 33.15 + 2024-04-01: 35.35 + 2025-04-01: 35.95 + 2026-04-01: 37.30 +metadata: + unit: currency-GBP + period: week + label: housing benefit meals deduction, three or more meals a day, aged 16 or over + reference: + - title: Housing Benefit Regulations 2006 Sch 1 para 2(2) + href: https://www.legislation.gov.uk/uksi/2006/213/schedule/1/paragraph/2 + - title: Housing Benefit (Persons who have attained the qualifying age for state pension credit) Regulations 2006 Sch 1 para 2(2) + href: https://www.legislation.gov.uk/uksi/2006/214/schedule/1/paragraph/2 + - title: Social Security Benefits Up-rating Order 2026 (S.I. 2026/148) art. 23(4) + href: https://www.legislation.gov.uk/uksi/2026/148/article/23 + note: Amounts from 2008 to 2025 are the point-in-time texts of Sch 1 para 2 on legislation.gov.uk, set by each year's up-rating order (S.I. 2008/632, S.I. 2009/497, S.I. 2010/793, S.I. 2011/821, S.I. 2012/780, S.I. 2013/574, S.I. 2014/516, S.I. 2015/457, S.I. 2017/260, S.I. 2019/480, S.I. 2020/234, S.I. 2021/162, S.I. 2022/292, S.I. 2023/316, S.I. 2024/242, S.I. 2025/295, S.I. 2026/148). diff --git a/policyengine_uk/parameters/gov/dwp/housing_benefit/ineligible_charges/meals/at_least_three_a_day/under_16.yaml b/policyengine_uk/parameters/gov/dwp/housing_benefit/ineligible_charges/meals/at_least_three_a_day/under_16.yaml new file mode 100644 index 000000000..dd7fd5e79 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/ineligible_charges/meals/at_least_three_a_day/under_16.yaml @@ -0,0 +1,31 @@ +description: Weekly amount ineligible for meals, at least three meals a day, for each family member under 16. +values: + 2008-04-01: 10.90 + 2009-04-01: 11.60 + 2010-04-01: 11.80 + 2011-04-01: 12.15 + 2012-04-01: 12.80 + 2013-04-01: 13.10 + 2014-04-01: 13.45 + 2015-04-01: 13.60 + 2017-04-01: 13.75 + 2019-04-01: 14.50 + 2020-04-01: 14.75 + 2021-04-01: 14.80 + 2022-04-01: 15.25 + 2023-04-01: 16.80 + 2024-04-01: 17.95 + 2025-04-01: 18.25 + 2026-04-01: 18.95 +metadata: + unit: currency-GBP + period: week + label: housing benefit meals deduction, three or more meals a day, under 16 + reference: + - title: Housing Benefit Regulations 2006 Sch 1 para 2(2)(b)(ii) + href: https://www.legislation.gov.uk/uksi/2006/213/schedule/1/paragraph/2 + - title: Housing Benefit (Persons who have attained the qualifying age for state pension credit) Regulations 2006 Sch 1 para 2(2)(b)(ii) + href: https://www.legislation.gov.uk/uksi/2006/214/schedule/1/paragraph/2 + - title: Social Security Benefits Up-rating Order 2026 (S.I. 2026/148) art. 23(4) + href: https://www.legislation.gov.uk/uksi/2026/148/article/23 + note: Amounts from 2008 to 2025 are the point-in-time texts of Sch 1 para 2 on legislation.gov.uk, set by each year's up-rating order (S.I. 2008/632, S.I. 2009/497, S.I. 2010/793, S.I. 2011/821, S.I. 2012/780, S.I. 2013/574, S.I. 2014/516, S.I. 2015/457, S.I. 2017/260, S.I. 2019/480, S.I. 2020/234, S.I. 2021/162, S.I. 2022/292, S.I. 2023/316, S.I. 2024/242, S.I. 2025/295, S.I. 2026/148). diff --git a/policyengine_uk/parameters/gov/dwp/housing_benefit/ineligible_charges/meals/breakfast_only.yaml b/policyengine_uk/parameters/gov/dwp/housing_benefit/ineligible_charges/meals/breakfast_only.yaml new file mode 100644 index 000000000..92f8b0712 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/ineligible_charges/meals/breakfast_only.yaml @@ -0,0 +1,31 @@ +description: Weekly amount ineligible for meals, breakfast only, for the claimant and each family member. +values: + 2008-04-01: 2.65 + 2009-04-01: 2.80 + 2010-04-01: 2.85 + 2011-04-01: 2.95 + 2012-04-01: 3.10 + 2013-04-01: 3.15 + 2014-04-01: 3.25 + 2015-04-01: 3.30 + 2017-04-01: 3.35 + 2019-04-01: 3.55 + 2020-04-01: 3.60 + 2021-04-01: 3.60 + 2022-04-01: 3.70 + 2023-04-01: 4.05 + 2024-04-01: 4.30 + 2025-04-01: 4.35 + 2026-04-01: 4.50 +metadata: + unit: currency-GBP + period: week + label: housing benefit meals deduction, breakfast only + reference: + - title: Housing Benefit Regulations 2006 Sch 1 para 2(5) + href: https://www.legislation.gov.uk/uksi/2006/213/schedule/1/paragraph/2 + - title: Housing Benefit (Persons who have attained the qualifying age for state pension credit) Regulations 2006 Sch 1 para 2(5) + href: https://www.legislation.gov.uk/uksi/2006/214/schedule/1/paragraph/2 + - title: Social Security Benefits Up-rating Order 2026 (S.I. 2026/148) art. 23(4) + href: https://www.legislation.gov.uk/uksi/2026/148/article/23 + note: Amounts from 2008 to 2025 are the point-in-time texts of Sch 1 para 2 on legislation.gov.uk, set by each year's up-rating order (S.I. 2008/632, S.I. 2009/497, S.I. 2010/793, S.I. 2011/821, S.I. 2012/780, S.I. 2013/574, S.I. 2014/516, S.I. 2015/457, S.I. 2017/260, S.I. 2019/480, S.I. 2020/234, S.I. 2021/162, S.I. 2022/292, S.I. 2023/316, S.I. 2024/242, S.I. 2025/295, S.I. 2026/148). diff --git a/policyengine_uk/parameters/gov/dwp/housing_benefit/ineligible_charges/meals/fewer_than_three_a_day/aged_16_or_over.yaml b/policyengine_uk/parameters/gov/dwp/housing_benefit/ineligible_charges/meals/fewer_than_three_a_day/aged_16_or_over.yaml new file mode 100644 index 000000000..324209dc6 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/ineligible_charges/meals/fewer_than_three_a_day/aged_16_or_over.yaml @@ -0,0 +1,31 @@ +description: Weekly amount ineligible for meals, fewer than three meals a day, for the claimant and each family member aged 16 or over. +values: + 2008-04-01: 14.35 + 2009-04-01: 15.25 + 2010-04-01: 15.50 + 2011-04-01: 16.00 + 2012-04-01: 16.85 + 2013-04-01: 17.20 + 2014-04-01: 17.65 + 2015-04-01: 17.85 + 2017-04-01: 18.05 + 2019-04-01: 19.05 + 2020-04-01: 19.35 + 2021-04-01: 19.45 + 2022-04-01: 20.05 + 2023-04-01: 22.10 + 2024-04-01: 23.60 + 2025-04-01: 24.00 + 2026-04-01: 24.90 +metadata: + unit: currency-GBP + period: week + label: housing benefit meals deduction, fewer than three meals a day, aged 16 or over + reference: + - title: Housing Benefit Regulations 2006 Sch 1 para 2(3) + href: https://www.legislation.gov.uk/uksi/2006/213/schedule/1/paragraph/2 + - title: Housing Benefit (Persons who have attained the qualifying age for state pension credit) Regulations 2006 Sch 1 para 2(3) + href: https://www.legislation.gov.uk/uksi/2006/214/schedule/1/paragraph/2 + - title: Social Security Benefits Up-rating Order 2026 (S.I. 2026/148) art. 23(4) + href: https://www.legislation.gov.uk/uksi/2026/148/article/23 + note: Amounts from 2008 to 2025 are the point-in-time texts of Sch 1 para 2 on legislation.gov.uk, set by each year's up-rating order (S.I. 2008/632, S.I. 2009/497, S.I. 2010/793, S.I. 2011/821, S.I. 2012/780, S.I. 2013/574, S.I. 2014/516, S.I. 2015/457, S.I. 2017/260, S.I. 2019/480, S.I. 2020/234, S.I. 2021/162, S.I. 2022/292, S.I. 2023/316, S.I. 2024/242, S.I. 2025/295, S.I. 2026/148). diff --git a/policyengine_uk/parameters/gov/dwp/housing_benefit/ineligible_charges/meals/fewer_than_three_a_day/under_16.yaml b/policyengine_uk/parameters/gov/dwp/housing_benefit/ineligible_charges/meals/fewer_than_three_a_day/under_16.yaml new file mode 100644 index 000000000..12c50c948 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/ineligible_charges/meals/fewer_than_three_a_day/under_16.yaml @@ -0,0 +1,31 @@ +description: Weekly amount ineligible for meals, fewer than three meals a day, for each family member under 16. +values: + 2008-04-01: 7.20 + 2009-04-01: 7.65 + 2010-04-01: 7.80 + 2011-04-01: 8.05 + 2012-04-01: 8.45 + 2013-04-01: 8.65 + 2014-04-01: 8.90 + 2015-04-01: 9.00 + 2017-04-01: 9.10 + 2019-04-01: 9.55 + 2020-04-01: 9.70 + 2021-04-01: 9.75 + 2022-04-01: 10.05 + 2023-04-01: 11.05 + 2024-04-01: 11.80 + 2025-04-01: 12.00 + 2026-04-01: 12.45 +metadata: + unit: currency-GBP + period: week + label: housing benefit meals deduction, fewer than three meals a day, under 16 + reference: + - title: Housing Benefit Regulations 2006 Sch 1 para 2(3)(b)(ii) + href: https://www.legislation.gov.uk/uksi/2006/213/schedule/1/paragraph/2 + - title: Housing Benefit (Persons who have attained the qualifying age for state pension credit) Regulations 2006 Sch 1 para 2(3)(b)(ii) + href: https://www.legislation.gov.uk/uksi/2006/214/schedule/1/paragraph/2 + - title: Social Security Benefits Up-rating Order 2026 (S.I. 2026/148) art. 23(4) + href: https://www.legislation.gov.uk/uksi/2026/148/article/23 + note: Amounts from 2008 to 2025 are the point-in-time texts of Sch 1 para 2 on legislation.gov.uk, set by each year's up-rating order (S.I. 2008/632, S.I. 2009/497, S.I. 2010/793, S.I. 2011/821, S.I. 2012/780, S.I. 2013/574, S.I. 2014/516, S.I. 2015/457, S.I. 2017/260, S.I. 2019/480, S.I. 2020/234, S.I. 2021/162, S.I. 2022/292, S.I. 2023/316, S.I. 2024/242, S.I. 2025/295, S.I. 2026/148). diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_claimant_composition.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_claimant_composition.yaml index b1435b45d..2038e2346 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_claimant_composition.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_claimant_composition.yaml @@ -115,7 +115,7 @@ # couple room = 1: B under the existing bedroom calculation. LHA_category: B -- name: A 17-year-old claimant in another benefit unit blocks the shared rate +- name: A 17-year-old in another family is a non-dependant of the household head only period: 2025 input: people: @@ -134,13 +134,16 @@ home: members: [renter, apprentice] output: - lha_renter_has_non_dependant: [true, true] - is_lha_shared_accommodation_rate_specified_renter: [false, false] - # Other unit's claimant blocks A; 1 + 1 occupant aged 16+ outside = 2: C. - # Renter previously A because the other claimant was under 18. - LHA_category: [C, C] + lha_renter_has_non_dependant: [true, false] + is_lha_shared_accommodation_rate_specified_renter: [false, true] + # The renter (the household head, the elder) has the apprentice as a + # non-dependant: 1 + 1 non-dependant aged 16+ = 2: C. Only the household + # head's family has non-dependants from other families (UC Sch 4 para + # 9(2)(e)-(f)), so the apprentice's own family has none: single, 17 < 35, + # no dependants: A. + LHA_category: [C, A] -- name: A 17-year-old student claimant in another unit also blocks the shared rate +- name: A 17-year-old student in another family is also the household head's non-dependant period: 2025 input: people: @@ -158,12 +161,13 @@ home: members: [renter, student_claimant] output: - lha_renter_has_non_dependant: [true, true] - # Other claimant blocks A even if their age/education meets the QYP test. - # 1 + 1 occupant aged 16+ outside = 2: C (renter previously A). - LHA_category: [C, C] + lha_renter_has_non_dependant: [true, false] + # The other family's claimant is the household head's non-dependant even + # if their age and education meet the QYP test: 1 + 1 = 2: C. The + # student's own family has no non-dependant: A. + LHA_category: [C, A] -- name: Rent liability of another benefit unit does not exempt its claimant +- name: A family liable for rent is not a non-dependant period: 2025 input: people: @@ -183,9 +187,12 @@ members: [renter, other_renter] output: eldest_claimant_or_partner_age: [30, 40] - lha_renter_has_non_dependant: [true, true] - # Conservative proxy retained: 1 + 1 occupant aged 16+ outside = 2: C. - LHA_category: [C, C] + lha_renter_has_non_dependant: [false, false] + # A person liable on a commercial basis for their occupation is not a + # non-dependant (UC Sch 4 para 9(2)(d)), so neither family adds a bedroom + # to the other. Renter: single, 30 < 35, no non-dependant: A. Household + # head (40): 1 bedroom: B. + LHA_category: [A, B] - name: Occupants in a different household do not block the shared rate period: 2025 @@ -211,15 +218,19 @@ # Other renter 40 >= 35; 1 bedroom: B. LHA_category: [A, B] -- name: Actual shared accommodation retains its separate route to category A +- name: Actual shared accommodation gives the shared rate for Housing Benefit only period: 2025 input: age: 40 is_shared_accommodation: true output: is_lha_shared_accommodation_rate_specified_renter: false - # 40 >= 35, but actual shared accommodation independently gives A. - LHA_category: A + # Universal Credit sets the category by entitlement alone (UC Sch 4 para + # 25(2)(b)): 40 >= 35 and 1 bedroom: B, whether or not the accommodation + # is shared. Housing Benefit gives the shared rate to a claimant entitled + # to one bedroom who lacks exclusive use (HB Regs 2006 reg 13D(2)(a)(ii)). + LHA_category: B + housing_benefit_LHA_category: A - name: A disabled 17-year-old renter is excepted from the shared accommodation rate # UC Sch 4 para 29(5)(c): a renter under 35 receiving the PIP daily living diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_pension_age_claims.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_pension_age_claims.yaml index 394d7ba16..80544aada 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_pension_age_claims.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_pension_age_claims.yaml @@ -40,7 +40,7 @@ benunits: benunit: members: [older, younger] - BRMA_LHA_rate: 8_000 + housing_benefit_LHA_rate: 8_000 households: household: members: [older, younger] @@ -62,7 +62,7 @@ benunits: benunit: members: [person] - BRMA_LHA_rate: 12_000 + housing_benefit_LHA_rate: 12_000 housing_benefit_applicable_income: 20_000 households: household: diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml index 46c2b6ac4..81d1b71bc 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml @@ -1,9 +1,15 @@ +# Non-dependant deductions fall on the household head's family only: joint +# tenants and other sharers of the rent, boarders and lodgers are not +# non-dependants, and a sharer, boarder or lodger has no non-dependants from +# the household head's family (UC Regs 2013 Sch 4 para 9(2)(d)-(f); HB Regs +# 2006 reg 3(2)(d)-(e) and 3(4)). - name: Single family, no deductions period: 2025 absolute_error_margin: 0 input: people: person_1: + is_household_head: true household_benefits_individual_non_dep_deduction: 1 person_2: household_benefits_individual_non_dep_deduction: 2 @@ -16,12 +22,13 @@ output: housing_benefit_non_dep_deductions: 0 -- name: Two families, each affecting only the other +- name: Two families, the household head's family bears the other's deduction period: 2025 absolute_error_margin: 0 input: people: person_1: + is_household_head: true household_benefits_individual_non_dep_deduction: 1 person_2: household_benefits_individual_non_dep_deduction: 2 @@ -34,9 +41,10 @@ household: members: [person_1, person_2] output: - housing_benefit_non_dep_deductions: [2, 1] + # Only the head's family has non-dependants: 2; the other family has none. + housing_benefit_non_dep_deductions: [2, 0] -- name: Three families, each affecting only the other +- name: Three families, the household head's family bears both others period: 2025 absolute_error_margin: 0 input: @@ -44,6 +52,7 @@ person_1: household_benefits_individual_non_dep_deduction: 1 person_2: + is_household_head: true household_benefits_individual_non_dep_deduction: 2 person_3: household_benefits_individual_non_dep_deduction: 3 @@ -58,4 +67,33 @@ household: members: [person_1, person_2, person_3] output: - housing_benefit_non_dep_deductions: [5, 4, 3] + # The head's family: 1 + 3 = 4. + housing_benefit_non_dep_deductions: [0, 4, 0] + +- name: A family liable for rent is not a non-dependant + period: 2025 + absolute_error_margin: 0 + input: + people: + person_1: + is_household_head: true + household_benefits_individual_non_dep_deduction: 1 + person_2: + household_benefits_individual_non_dep_deduction: 2 + person_3: + household_benefits_individual_non_dep_deduction: 3 + benunits: + benunit_1: + members: person_1 + benunit_2: + members: person_2 + # A lodger or a sharer of the rent. + benunit_is_rent_liable: true + benunit_3: + members: person_3 + households: + household: + members: [person_1, person_2, person_3] + output: + # Only person 3 is a non-dependant of the head's family: 3. + housing_benefit_non_dep_deductions: [3, 0, 0] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deductions.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deductions.yaml index bcb7998b0..16852634d 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deductions.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deductions.yaml @@ -1,9 +1,15 @@ +# Non-dependant deductions fall on the household head's family only: joint +# tenants and other sharers of the rent, boarders and lodgers are not +# non-dependants, and a sharer, boarder or lodger has no non-dependants from +# the household head's family (UC Regs 2013 Sch 4 para 9(2)(d)-(f); HB Regs +# 2006 reg 3(2)(d)-(e) and 3(4)). - name: Single family, no deductions period: 2022 absolute_error_margin: 0 input: people: person_1: + is_household_head: true uc_individual_non_dep_deduction: 1 person_2: uc_individual_non_dep_deduction: 2 @@ -16,12 +22,13 @@ output: uc_non_dep_deductions: 0 -- name: Two families, each affecting only the other +- name: Two families, the household head's family bears the other's deduction period: 2022 absolute_error_margin: 0 input: people: person_1: + is_household_head: true uc_individual_non_dep_deduction: 1 person_2: uc_individual_non_dep_deduction: 2 @@ -34,9 +41,10 @@ household: members: [person_1, person_2] output: - uc_non_dep_deductions: [2, 1] + # Only the head's family has non-dependants: 2; the other family has none. + uc_non_dep_deductions: [2, 0] -- name: Three families, each affecting only the other +- name: Three families, the household head's family bears both others period: 2022 absolute_error_margin: 0 input: @@ -44,6 +52,7 @@ person_1: uc_individual_non_dep_deduction: 1 person_2: + is_household_head: true uc_individual_non_dep_deduction: 2 person_3: uc_individual_non_dep_deduction: 3 @@ -58,4 +67,33 @@ household: members: [person_1, person_2, person_3] output: - uc_non_dep_deductions: [5, 4, 3] + # The head's family: 1 + 3 = 4. + uc_non_dep_deductions: [0, 4, 0] + +- name: A family liable for rent is not a non-dependant + period: 2022 + absolute_error_margin: 0 + input: + people: + person_1: + is_household_head: true + uc_individual_non_dep_deduction: 1 + person_2: + uc_individual_non_dep_deduction: 2 + person_3: + uc_individual_non_dep_deduction: 3 + benunits: + benunit_1: + members: person_1 + benunit_2: + members: person_2 + # A lodger or a sharer of the rent. + benunit_is_rent_liable: true + benunit_3: + members: person_3 + households: + household: + members: [person_1, person_2, person_3] + output: + # Only person 3 is a non-dependant of the head's family: 3. + uc_non_dep_deductions: [3, 0, 0] diff --git a/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py b/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py index 9dff0c03c..14d4d0b3e 100644 --- a/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py +++ b/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py @@ -6,46 +6,61 @@ warnings.filterwarnings("ignore") +def bedrooms_for_children(benunit, period): + """Bedrooms for the family's children under 16. + + Children must share rooms in pairs unless they are opposite-sex and one + is 10 or over. This is the minimum number of bedrooms that allocates them + under those rules. + """ + person = benunit.members + age = person("age", period) + male = person("is_male", period) + under_16 = age < 16 + under_10 = age < 10 + child_over_10 = ~under_10 & under_16 + boys_under_10 = benunit.sum(under_10 & male) + boys_over_10 = benunit.sum(child_over_10 & male) + girls_under_10 = benunit.sum(under_10 & ~male) + girls_over_10 = benunit.sum(child_over_10 & ~male) + # First, have over-10s share where possible + over_10_rooms = (boys_over_10 + 1) // 2 + (girls_over_10 + 1) // 2 + # There may children over 10 still not sharing + space_for_boy_under_10 = boys_over_10 % 2 + space_for_girl_under_10 = girls_over_10 % 2 + # Have those spaces filled where possible by children under 10 + left_over_boys_under_10 = max_(boys_under_10 - space_for_boy_under_10, 0) + left_over_girls_under_10 = max_(girls_under_10 - space_for_girl_under_10, 0) + # The remaining children must share in pairs + under_10_rooms = (left_over_boys_under_10 + left_over_girls_under_10 + 1) // 2 + return over_10_rooms + under_10_rooms + + class LHA_allowed_bedrooms(Variable): value_type = float entity = BenUnit label = "The number of bedrooms covered by LHA for the benefit unit" + documentation = ( + "Universal Credit size criteria: one bedroom for the renter or " + "couple, one for each non-dependant aged 16 or over, and the " + "children's bedrooms. Joint tenants and other sharers of the rent, " + "boarders and lodgers are not non-dependants, so they add no bedroom " + "to anyone's entitlement; only the household head's family has " + "non-dependants (see is_non_dependant_of_household_head)." + ) definition_period = YEAR - reference = "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/10/2021-04-06" + reference = ( + "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/9", + "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/10", + ) def formula(benunit, period, parameters): - """ - LHA allows for one room for: - a) The benefit unit adult(s) - b) Each person over 16 outside the benefit unit - but within the household - Children must share rooms in pairs unless they are - opposite-sex and one is over 10. The number of bedrooms - allowed under LHA rules is the minimum number of bedrooms - required to allocate people satisfying these rules. - """ person = benunit.members - age = person("age", period) - male = person("is_male", period) - under_16 = age < 16 - under_10 = age < 10 - child_over_10 = ~under_10 & under_16 - # One room each for over-16s outside the benefit unit - non_dependants = benunit.max(person.household.sum(~under_16)) - benunit.sum( - ~under_16 + aged_16_or_over = person("age", period) >= 16 + # UC Regs 2013 Sch 4 para 10(1)(c): a non-dependant who is not a child. + non_dependant = aged_16_or_over & person( + "is_non_dependant_of_household_head", period ) - boys_under_10 = benunit.sum(under_10 & male) - boys_over_10 = benunit.sum(child_over_10 & male) - girls_under_10 = benunit.sum(under_10 & ~male) - girls_over_10 = benunit.sum(child_over_10 & ~male) - # First, have over-10s share where possible - over_10_rooms = (boys_over_10 + 1) // 2 + (girls_over_10 + 1) // 2 - # There may children over 10 still not sharing - space_for_boy_under_10 = boys_over_10 % 2 - space_for_girl_under_10 = girls_over_10 % 2 - # Have those spaces filled where possible by children under 10 - left_over_boys_under_10 = max_(boys_under_10 - space_for_boy_under_10, 0) - left_over_girls_under_10 = max_(girls_under_10 - space_for_girl_under_10, 0) - # The remaining children must share in pairs - under_10_rooms = (left_over_boys_under_10 + left_over_girls_under_10 + 1) // 2 - return 1 + non_dependants + over_10_rooms + under_10_rooms + head_family = benunit.any(person("is_household_head", period)) + non_dependants = head_family * benunit.max(person.household.sum(non_dependant)) + return 1 + non_dependants + bedrooms_for_children(benunit, period) diff --git a/policyengine_uk/variables/gov/dwp/LHA_cap.py b/policyengine_uk/variables/gov/dwp/LHA_cap.py index d7139422c..dedaa40a3 100644 --- a/policyengine_uk/variables/gov/dwp/LHA_cap.py +++ b/policyengine_uk/variables/gov/dwp/LHA_cap.py @@ -10,11 +10,33 @@ class LHA_cap(Variable): value_type = float entity = BenUnit label = "Applicable amount for LHA" - documentation = "Applicable amount for Local Housing Allowance" + documentation = ( + "Rent eligible for Housing Benefit where the Local Housing Allowance " + "applies: the lower of the rent and the Housing Benefit LHA rate. " + "Where the rent pays for meals, the rent officer route applies " + "instead and the fixed meals amount is deducted from the maximum " + "rent; the model takes the rent less that amount, capped at the LHA " + "rate, which stands in for the rent officer's determination." + ) definition_period = YEAR unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/12D", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/13", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/13C", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/13D", + ) def formula(benunit, period, parameters): - rent = benunit("benunit_rent", period) - cap = benunit("BRMA_LHA_rate", period) + # HB Regs 2006 regs 12D(2)(a) and 13D(5): the maximum rent (LHA) is + # the LHA rate or, if lower, the cap rent. A rent officer finding + # that a substantial part of the rent is for board and attendance + # takes the case off the LHA (reg 13C(5)(e)); the maximum rent then + # has the Sch 1 para 2 amount for meals deducted (reg 13(13)). + rent = max_( + 0, + benunit("benunit_rent", period) + - benunit("housing_benefit_meals_deduction", period), + ) + cap = benunit("housing_benefit_LHA_rate", period) return min_(rent, cap) diff --git a/policyengine_uk/variables/gov/dwp/LHA_category.py b/policyengine_uk/variables/gov/dwp/LHA_category.py index f0f2d4eb7..ecd771d7d 100644 --- a/policyengine_uk/variables/gov/dwp/LHA_category.py +++ b/policyengine_uk/variables/gov/dwp/LHA_category.py @@ -18,21 +18,35 @@ class LHA_category(Variable): value_type = Enum entity = BenUnit label = "LHA category for the benefit unit, taking into account LHA rules on the number of LHA-covered bedrooms" + documentation = ( + "The Universal Credit category of accommodation, which depends only " + "on what the renter is entitled to: the shared accommodation rate " + "for a specified renter, otherwise the category for the number of " + "bedrooms in the size criteria. Whether the accommodation the renter " + "actually occupies is shared does not matter, so a single renter " + "aged 35 or over in a room gets the one-bedroom rate. Housing Benefit " + "has its own category: see housing_benefit_LHA_category." + ) + reference = ( + "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/25", + "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/27", + "https://www.legislation.gov.uk/uksi/2013/382/schedule/1/paragraph/1", + ) definition_period = YEAR possible_values = LHACategory default_value = LHACategory.C def formula(benunit, period, parameters): + # UC Regs 2013 Sch 4 para 25(1) step 1 and para 25(2)(b): the + # category to which the renter is entitled under paras 8-12 and + # 26-29. num_rooms = benunit("LHA_allowed_bedrooms", period.this_year) - person = benunit.members - household = person.household - is_shared = benunit.any(household("is_shared_accommodation", period.this_year)) can_only_claim_shared = benunit( "is_lha_shared_accommodation_rate_specified_renter", period ) return select( [ - is_shared | can_only_claim_shared, + can_only_claim_shared, num_rooms == 1, num_rooms == 2, num_rooms == 3, @@ -123,11 +137,15 @@ def find_freeze_anchor(freeze_parameter: Parameter, period: str) -> str: MONTHLY_MAXIMUM_FIRST_YEAR = 2020 -def category_maximum(benunit, period, node_name: str): +def category_maximum( + benunit, period, node_name: str, category_variable: str = "LHA_category" +): """Per-category national maximum, read at the determination year. Frozen rates are held at the level last determined, so the maximum in force then is the one that binds, not the current year's. + ``category_variable`` names the category to look up: the Universal + Credit one by default, or the Housing Benefit one. """ lha = benunit.simulation.tax_benefit_system.parameters.gov.dwp.LHA @@ -137,6 +155,6 @@ def category_maximum(benunit, period, node_name: str): determination_period = str(period.start.year) node = getattr(lha, node_name) - category = benunit("LHA_category", period).decode_to_str() + category = benunit(category_variable, period).decode_to_str() caps = {cat: node.children[cat](determination_period) for cat in node.children} return pd.Series(category).map(caps).to_numpy(dtype=float) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/entitlement/housing_benefit_entitlement.py b/policyengine_uk/variables/gov/dwp/housing_benefit/entitlement/housing_benefit_entitlement.py index f44c35d13..84e19c27c 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/entitlement/housing_benefit_entitlement.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/entitlement/housing_benefit_entitlement.py @@ -9,7 +9,13 @@ class housing_benefit_entitlement(Variable): unit = GBP def formula(benunit, period, parameters): - rent = benunit("benunit_rent", period) + # Charges for meals are not eligible to be met by Housing Benefit + # (HB Regs 2006 reg 12B(2)(b) and Sch 1 paras 1(a)(i) and 2). + rent = max_( + 0, + benunit("benunit_rent", period) + - benunit("housing_benefit_meals_deduction", period), + ) applicable_amount = benunit("housing_benefit_applicable_amount", period) income = benunit("housing_benefit_applicable_income", period) withdrawal_rate = parameters( diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_meals_deduction.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_meals_deduction.py new file mode 100644 index 000000000..faa79a30b --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_meals_deduction.py @@ -0,0 +1,48 @@ +from policyengine_uk.model_api import * + + +class housing_benefit_meals_deduction(Variable): + value_type = float + entity = BenUnit + label = "Housing Benefit deduction for meals in the rent" + documentation = ( + "The amount of the rent that is ineligible for Housing Benefit " + "because it pays for meals: a fixed weekly amount for the claimant " + "and each family member, by the meals provided and whether the " + "person is 16 or over." + ) + definition_period = YEAR + unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/schedule/1/paragraph/2", + "https://www.legislation.gov.uk/uksi/2006/214/schedule/1/paragraph/2", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/13", + ) + + def formula(benunit, period, parameters): + p = parameters(period).gov.dwp.housing_benefit.ineligible_charges.meals + meals = benunit("meals_included_in_rent", period) + provision = meals.possible_values + person = benunit.members + # Sch 1 para 2(4): a person attains 16 on the first Monday in + # September after their 16th birthday; annual ages cannot place that + # date, so the age is used. + aged_16_or_over = benunit.sum(person("age", period) >= 16) + under_16 = benunit.sum(person("age", period) < 16) + members = aged_16_or_over + under_16 + weekly = select( + [ + meals == provision.AT_LEAST_THREE_A_DAY, + meals == provision.FEWER_THAN_THREE_A_DAY, + meals == provision.BREAKFAST_ONLY, + ], + [ + aged_16_or_over * p.at_least_three_a_day.aged_16_or_over + + under_16 * p.at_least_three_a_day.under_16, + aged_16_or_over * p.fewer_than_three_a_day.aged_16_or_over + + under_16 * p.fewer_than_three_a_day.under_16, + members * p.breakfast_only, + ], + default=0, + ) + return weekly * WEEKS_IN_YEAR diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py index 3c35be82d..2cda43875 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py @@ -7,13 +7,21 @@ class housing_benefit_non_dep_deductions(Variable): label = "non-dependent deductions" definition_period = YEAR unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/3", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/74", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/55", + ) def formula(benunit, period, parameters): - deductions = benunit.members( + # Deductions are made for non-dependants residing with the claimant + # (HB Regs 2006 reg 74; HB (SPC) Regs 2006 reg 55). Joint occupiers, + # boarders, lodgers and the landlord's household are not + # non-dependants (reg 3(2)(d)-(e), 3(4)), so only the household + # head's family has them. + person = benunit.members + deductions = person( "household_benefits_individual_non_dep_deduction", period - ) - non_dep_deductions_in_hh = benunit.max( - benunit.members.household.sum(deductions) - ) - non_dep_deductions_in_bu = benunit.sum(deductions) - return non_dep_deductions_in_hh - non_dep_deductions_in_bu + ) * person("is_non_dependant_of_household_head", period) + head_family = benunit.any(person("is_household_head", period)) + return head_family * benunit.max(person.household.sum(deductions)) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_allowed_bedrooms.py b/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_allowed_bedrooms.py new file mode 100644 index 000000000..005d00c65 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_allowed_bedrooms.py @@ -0,0 +1,37 @@ +from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp.LHA_allowed_bedrooms import ( + bedrooms_for_children, +) + + +class housing_benefit_LHA_allowed_bedrooms(Variable): + value_type = float + entity = BenUnit + label = "Bedrooms in the Housing Benefit size criteria" + documentation = ( + "Housing Benefit size criteria: one bedroom for the claimant or " + "couple, one for each other occupier aged 16 or over, and the " + "children's bedrooms. Occupiers are everyone who lives in the " + "dwelling as their home except a joint tenant outside the claimant's " + "household, so a householder's boarder or lodger adds a bedroom, as " + "does a non-dependant, but a sharer of the rent does not. A sharer's, " + "boarder's or lodger's own claim counts only their own family." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/13D", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/13D", + ) + + def formula(benunit, period, parameters): + person = benunit.members + aged_16_or_over = person("age", period) >= 16 + head_family = person.benunit.any(person("is_household_head", period)) + sharer = person.benunit("liable_for_share_of_household_rent", period) + # HB Regs 2006 reg 13D(3), with "occupiers" as defined in 13D(12). + other_occupier = aged_16_or_over & ~head_family & ~sharer + is_head_family = benunit.any(person("is_household_head", period)) + other_occupiers = is_head_family * benunit.max( + person.household.sum(other_occupier) + ) + return 1 + other_occupiers + bedrooms_for_children(benunit, period) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_category.py b/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_category.py new file mode 100644 index 000000000..305cf304b --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_category.py @@ -0,0 +1,58 @@ +from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp.LHA_category import LHACategory + + +class housing_benefit_LHA_category(Variable): + value_type = Enum + entity = BenUnit + label = "LHA category of dwelling (Housing Benefit)" + documentation = ( + "The Housing Benefit category of dwelling. The shared accommodation " + "rate applies to a young individual with no non-dependant, and to a " + "claimant entitled to one bedroom who lacks exclusive use of " + "self-contained accommodation, unless the severe disability premium " + "applies. Otherwise the category follows the number of bedrooms in " + "the Housing Benefit size criteria, up to four." + ) + definition_period = YEAR + possible_values = LHACategory + default_value = LHACategory.C + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/13D", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/13D", + "https://www.legislation.gov.uk/uksi/1997/1984/schedule/3B", + ) + + def formula(benunit, period, parameters): + rooms = benunit("housing_benefit_LHA_allowed_bedrooms", period.this_year) + # Schedule 3 paragraph 14 (severe disability premium) applies. + severe_disability = benunit("severe_disability_premium", period) > 0 + # HB Regs 2006 reg 13D(2)(a)(i). + young_individual = ( + benunit("is_housing_benefit_young_individual", period) + & ~benunit("lha_renter_has_non_dependant", period) + & ~severe_disability + ) + # Reg 13D(2)(a)(ii): entitled to one bedroom but neither condition in + # 13D(2)(b) is met. + one_bedroom_shared = ( + (rooms == 1) + & benunit("housing_benefit_shares_accommodation", period) + & ~severe_disability + ) + return select( + [ + young_individual | one_bedroom_shared, + rooms == 1, + rooms == 2, + rooms == 3, + rooms > 3, + ], + [ + LHACategory.A, + LHACategory.B, + LHACategory.C, + LHACategory.D, + LHACategory.E, + ], + ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_rate.py b/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_rate.py new file mode 100644 index 000000000..b21e8c6e4 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_rate.py @@ -0,0 +1,28 @@ +from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp.LHA_category import category_maximum +from policyengine_uk.variables.gov.dwp.uncapped_BRMA_LHA_rate import ( + lha_rate_for_category, +) + + +class housing_benefit_LHA_rate(Variable): + value_type = float + entity = BenUnit + label = "LHA rate (Housing Benefit)" + documentation = ( + "The Local Housing Allowance for the Housing Benefit category of " + "dwelling: the Broad Rental Market Area rate, capped at the weekly " + "national maximum for the category." + ) + definition_period = YEAR + unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/13D", + "https://www.legislation.gov.uk/uksi/1997/1984/schedule/3B", + ) + + def formula(benunit, period, parameters): + category = "housing_benefit_LHA_category" + rate = lha_rate_for_category(benunit, period, category) + maximum = category_maximum(benunit, period, "maximum", category) + return min_(rate, maximum * 52) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit_shares_accommodation.py b/policyengine_uk/variables/gov/dwp/housing_benefit_shares_accommodation.py new file mode 100644 index 000000000..fd4031f38 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit_shares_accommodation.py @@ -0,0 +1,36 @@ +from policyengine_uk.model_api import * + + +class housing_benefit_shares_accommodation(Variable): + value_type = bool + entity = BenUnit + label = "Lacks exclusive use of self-contained accommodation (Housing Benefit)" + documentation = ( + "Whether the family lacks exclusive use of two or more rooms, or of " + "one room with a bathroom, toilet and kitchen. Rooms shared only with " + "the family's own household, its non-dependants or people who pay it " + "rent still count as exclusive. A boarder or lodger shares the " + "householder's rooms, and in a household whose rent is shared every " + "liable family shares the others' rooms. The household input " + "is_shared_accommodation marks any other case." + ) + definition_period = YEAR + reference = "https://www.legislation.gov.uk/uksi/2006/213/regulation/13D" + + def formula(benunit, period, parameters): + # HB Regs 2006 reg 13D(2)(b): exclusive use of two or more rooms, or + # of one room, a bathroom and toilet and a kitchen or facilities for + # cooking, excluding rooms shared with anyone other than a member of + # the household, a non-dependant or a person who pays rent. + person = benunit.members + household_input = benunit.any( + person.household("is_shared_accommodation", period) + ) + boarder_or_lodger = benunit.any(person("pays_rent_to_householder", period)) + rent_is_shared = benunit.any( + person.household.any( + person.benunit("liable_for_share_of_household_rent", period) + ) + ) + liable = benunit("benunit_is_rent_liable", period) + return household_input | boarder_or_lodger | (rent_is_shared & liable) diff --git a/policyengine_uk/variables/gov/dwp/is_housing_benefit_young_individual.py b/policyengine_uk/variables/gov/dwp/is_housing_benefit_young_individual.py new file mode 100644 index 000000000..f66e7af8e --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/is_housing_benefit_young_individual.py @@ -0,0 +1,23 @@ +from policyengine_uk.model_api import * + + +class is_housing_benefit_young_individual(Variable): + value_type = bool + entity = BenUnit + label = "Young individual (Housing Benefit)" + documentation = ( + "A single claimant (no partner and not a lone parent) under the " + "shared accommodation age threshold. The exceptions for a housing " + "association landlord, care leavers, former hostel residents, " + "offenders under multi-agency management, people who need overnight " + "care, qualifying parents or carers, victims of domestic violence " + "and of modern slavery are not identified." + ) + definition_period = YEAR + reference = "https://www.legislation.gov.uk/uksi/2006/213/regulation/2" + + def formula(benunit, period, parameters): + threshold = parameters(period).gov.dwp.LHA.shared_accommodation_age_threshold + return benunit("is_single_person", period) & ( + benunit("eldest_claimant_or_partner_age", period) < threshold + ) diff --git a/policyengine_uk/variables/gov/dwp/is_non_dependant_of_household_head.py b/policyengine_uk/variables/gov/dwp/is_non_dependant_of_household_head.py new file mode 100644 index 000000000..ebaf567b5 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/is_non_dependant_of_household_head.py @@ -0,0 +1,33 @@ +from policyengine_uk.model_api import * + + +class is_non_dependant_of_household_head(Variable): + value_type = bool + entity = Person + label = "Non-dependant of the household head's family" + documentation = ( + "Whether this person lives in the household outside the household " + "head's family and is not liable for rent. A joint tenant or other " + "sharer of the rent, a boarder and a lodger are liable on a " + "commercial basis for their occupation, so none is a non-dependant " + "of anyone. The household head and their household are not " + "non-dependants of a boarder or lodger who pays them, and someone who " + "is already a non-dependant of one claimant is not one of another " + "claimant liable for the same accommodation, so in this model only " + "the household head's family has non-dependants from other families. " + "Foster children and carers engaged through a charity are not " + "identified." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/9", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/3", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/3", + ) + + def formula(person, period, parameters): + # UC Regs 2013 Sch 4 para 9(2)(d)-(f); HB Regs 2006 reg 3(2)(d)-(e) + # and 3(4); HB (SPC) Regs 2006 reg 3. + head_family = person.benunit.any(person("is_household_head", period)) + liable_for_rent = person.benunit("benunit_is_rent_liable", period) + return ~head_family & ~liable_for_rent diff --git a/policyengine_uk/variables/gov/dwp/lha_renter_has_non_dependant.py b/policyengine_uk/variables/gov/dwp/lha_renter_has_non_dependant.py index 347788ab6..31f652b56 100644 --- a/policyengine_uk/variables/gov/dwp/lha_renter_has_non_dependant.py +++ b/policyengine_uk/variables/gov/dwp/lha_renter_has_non_dependant.py @@ -4,14 +4,15 @@ class lha_renter_has_non_dependant(Variable): value_type = bool entity = BenUnit - label = "LHA renter has a non-dependant (household composition proxy)" + label = "LHA renter has a non-dependant" documentation = ( - "A conservative proxy: a claimant or partner of another benefit unit " - "in the household, or someone in this benefit unit who is neither a " - "claimant/partner nor a UC child or qualifying young person. The model " - "does not identify the statutory exclusions for joint renters, " - "commercial lodgers, landlords or foster children here. Rent liability " - "alone does not exclude another benefit unit's claimant." + "Someone in this benefit unit who is neither a claimant or partner " + "nor a child or young person, or, for the household head's family, a " + "claimant or partner of another family who is a non-dependant of the " + "household head (see is_non_dependant_of_household_head): joint " + "tenants and other sharers of the rent, boarders and lodgers are not " + "non-dependants, and a sharer, boarder or lodger has none from the " + "household head's family. Foster children are not identified." ) definition_period = YEAR reference = ( @@ -31,7 +32,12 @@ def formula(benunit, period, parameters): | ((age >= 16) & (age < 17)) ) within_benefit_unit = benunit.any(~claimant_or_partner & ~child_or_qyp) - other_benefit_unit_claimants = benunit.max( - person.household.sum(claimant_or_partner) - ) - benunit.sum(claimant_or_partner) - return within_benefit_unit | (other_benefit_unit_claimants > 0) + # UC Regs 2013 Sch 4 para 9(2)(d)-(f); HB Regs 2006 reg 3(2)(d)-(e). + non_dependant_claimants = claimant_or_partner & person( + "is_non_dependant_of_household_head", period + ) + head_family = benunit.any(person("is_household_head", period)) + other_family_non_dependants = head_family * benunit.max( + person.household.sum(non_dependant_claimants) + ) + return within_benefit_unit | (other_family_non_dependants > 0) diff --git a/policyengine_uk/variables/gov/dwp/uncapped_BRMA_LHA_rate.py b/policyengine_uk/variables/gov/dwp/uncapped_BRMA_LHA_rate.py index 6ccef7167..27658fd46 100644 --- a/policyengine_uk/variables/gov/dwp/uncapped_BRMA_LHA_rate.py +++ b/policyengine_uk/variables/gov/dwp/uncapped_BRMA_LHA_rate.py @@ -10,6 +10,72 @@ warnings.filterwarnings("ignore") +def lha_rate_for_category(benunit, period, category_variable="LHA_category"): + """The Broad Rental Market Area LHA rate (annual) for a category. + + ``category_variable`` names the category: the Universal Credit one by + default, or the Housing Benefit one. + """ + brma = benunit.value_from_first_person( + benunit.members.household("brma", period).decode_to_str() + ) + category = benunit(category_variable, period).decode_to_str() + + from policyengine_uk.parameters.gov.dwp.LHA import lha_list_of_rents + + parameters = benunit.simulation.tax_benefit_system.parameters + lha = parameters.gov.dwp.LHA + + # We first need to know what time period to collect rents from. If LHA is frozen, we need to look earlier + # than the current time period. + + frozen = lha.freeze(period) + if frozen: + # Rates are held at the level last determined, so every input to + # the determination is read at that year, not the current one. + freeze_anchor = find_freeze_anchor(lha.freeze, period.start) + lha_period = int(freeze_anchor[:4]) # Get year + else: + lha_period = int(period.start.year) + + determination_period = str(lha_period) + + private_rent_index = parameters.gov.indices.private_rent_index + lha_list_of_rents = time_shift_dataset( + lha_list_of_rents.copy(), lha_period, private_rent_index + ) + + percentile = lha.percentile(determination_period) + + lha_rates = lha_list_of_rents.groupby( + ["brma", "lha_category"] + ).weekly_rent.quantile(percentile) + + # Convert MultiIndex Series to DataFrame for merge + lha_rates_df = lha_rates.reset_index() + lha_rates_df.columns = ["brma", "lha_category", "weekly_rent"] + + # Determined rates are rounded to the nearest penny, half up + # (Schedule 3B paragraph 2(10)); np.round is half-even. Pence are + # snapped to 6dp first, because an exact half such as 298.835 is + # held as 29883.499999999996 once scaled and would round down. + lha_rates_df.weekly_rent = ( + np.floor(np.round(lha_rates_df.weekly_rent * 100, 6) + 0.5) / 100 + ) + + lha_lookup_table = pd.DataFrame( + { + "brma": brma, + "lha_category": category, + } + ) + # Use merge instead of row-by-row apply for vectorised lookup + lha_lookup_table = lha_lookup_table.merge( + lha_rates_df, on=["brma", "lha_category"], how="left" + ) + return lha_lookup_table.weekly_rent.values * 52 + + class uncapped_BRMA_LHA_rate(Variable): value_type = float entity = BenUnit @@ -20,61 +86,4 @@ class uncapped_BRMA_LHA_rate(Variable): reference = "https://www.legislation.gov.uk/uksi/1997/1984/schedule/3B" def formula(benunit, period, parameters): - brma = benunit.value_from_first_person( - benunit.members.household("brma", period).decode_to_str() - ) - category = benunit("LHA_category", period).decode_to_str() - - from policyengine_uk.parameters.gov.dwp.LHA import lha_list_of_rents - - parameters = benunit.simulation.tax_benefit_system.parameters - lha = parameters.gov.dwp.LHA - - # We first need to know what time period to collect rents from. If LHA is frozen, we need to look earlier - # than the current time period. - - frozen = lha.freeze(period) - if frozen: - # Rates are held at the level last determined, so every input to - # the determination is read at that year, not the current one. - freeze_anchor = find_freeze_anchor(lha.freeze, period.start) - lha_period = int(freeze_anchor[:4]) # Get year - else: - lha_period = int(period.start.year) - - determination_period = str(lha_period) - - private_rent_index = parameters.gov.indices.private_rent_index - lha_list_of_rents = time_shift_dataset( - lha_list_of_rents.copy(), lha_period, private_rent_index - ) - - percentile = lha.percentile(determination_period) - - lha_rates = lha_list_of_rents.groupby( - ["brma", "lha_category"] - ).weekly_rent.quantile(percentile) - - # Convert MultiIndex Series to DataFrame for merge - lha_rates_df = lha_rates.reset_index() - lha_rates_df.columns = ["brma", "lha_category", "weekly_rent"] - - # Determined rates are rounded to the nearest penny, half up - # (Schedule 3B paragraph 2(10)); np.round is half-even. Pence are - # snapped to 6dp first, because an exact half such as 298.835 is - # held as 29883.499999999996 once scaled and would round down. - lha_rates_df.weekly_rent = ( - np.floor(np.round(lha_rates_df.weekly_rent * 100, 6) + 0.5) / 100 - ) - - lha_lookup_table = pd.DataFrame( - { - "brma": brma, - "lha_category": category, - } - ) - # Use merge instead of row-by-row apply for vectorised lookup - lha_lookup_table = lha_lookup_table.merge( - lha_rates_df, on=["brma", "lha_category"], how="left" - ) - return lha_lookup_table.weekly_rent.values * 52 + return lha_rate_for_category(benunit, period) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deductions.py b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deductions.py index 59afb020d..eca4abcd2 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deductions.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deductions.py @@ -7,18 +7,21 @@ class uc_non_dep_deductions(Variable): label = "Universal Credit non-dependent deductions" definition_period = YEAR unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/9", + "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/13", + ) def formula(benunit, period, parameters): - # Deductions are made for non-dependents outside the benefit unit, - # but within the household, who meet certain conditions. To do this, - # we first calculate the non-dependent deduction for each person (from - # the perspective of a different benefit unit). Then, to calculate - # the deduction for non-dependents outside the benefit unit, we subtract - # the total non-dependent deductions for the benefit unit members from - # the deductions for household members. - deductions = benunit.members("uc_individual_non_dep_deduction", period) - non_dep_deductions_in_hh = benunit.max( - benunit.members.household.sum(deductions) + # A housing cost contribution is deducted for each non-dependant in + # the renter's extended benefit unit (UC Regs 2013 Sch 4 para 13). + # Non-dependants live outside the household head's family and are + # not liable for rent, and only the household head's family has them + # (para 9(2)(d)-(f)): a sharer, boarder or lodger has none from the + # household head's family. + person = benunit.members + deductions = person("uc_individual_non_dep_deduction", period) * person( + "is_non_dependant_of_household_head", period ) - non_dep_deductions_in_bu = benunit.sum(deductions) - return non_dep_deductions_in_hh - non_dep_deductions_in_bu + head_family = benunit.any(person("is_household_head", period)) + return head_family * benunit.max(person.household.sum(deductions)) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/uc_housing_costs_element.py b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/uc_housing_costs_element.py index ac1fbaf11..62f7c4d10 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/uc_housing_costs_element.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/uc_housing_costs_element.py @@ -9,9 +9,9 @@ class uc_housing_costs_element(Variable): unit = GBP def formula(benunit, period, parameters): - tenure_type = benunit.value_from_first_person( - benunit.members.household("tenure_type", period) - ) + # The family's own tenure: a boarder or lodger is a private renter + # whatever the household's tenure (UC Regs 2013 Sch 4 para 20). + tenure_type = benunit("benunit_tenure_type", period) tenure_types = tenure_type.possible_values rent = benunit("benunit_rent", period) # Universal Credit has its own monthly national maximum, which is diff --git a/policyengine_uk/variables/household/consumption/benunit_rent.py b/policyengine_uk/variables/household/consumption/benunit_rent.py index 50ebc8cdb..29d88d079 100644 --- a/policyengine_uk/variables/household/consumption/benunit_rent.py +++ b/policyengine_uk/variables/household/consumption/benunit_rent.py @@ -6,7 +6,9 @@ class benunit_rent(Variable): entity = BenUnit label = "Rent" documentation = ( - "Gross rent that members of this family are liable for (social housing only)" + "Rent that members of this family are liable for: the family's share " + "of the household's rent, plus anything its members pay the " + "householder as boarders or lodgers." ) definition_period = YEAR unit = GBP diff --git a/policyengine_uk/variables/household/consumption/personal_rent.py b/policyengine_uk/variables/household/consumption/personal_rent.py index 8dbd3dd76..f121bee0f 100644 --- a/policyengine_uk/variables/household/consumption/personal_rent.py +++ b/policyengine_uk/variables/household/consumption/personal_rent.py @@ -5,8 +5,45 @@ class personal_rent(Variable): value_type = float entity = Person label = "Rent liable" - documentation = "The gross rent this person is liable for" + documentation = ( + "The rent this person is liable for: an equal share of the " + "household's rent among the people liable for it, plus anything this " + "person pays the householder as a boarder or lodger. Where the " + "household head's family is the only one liable, its claimant and " + "partner share the whole rent, so the family's total is the " + "household's rent." + ) definition_period = YEAR unit = GBP - defined_for = "is_household_head" - adds = ["rent"] + reference = ( + "https://www.legislation.gov.uk/uksi/2013/376/schedule/1/paragraph/2", + "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/24", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/12", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/12B", + ) + + def formula(person, period, parameters): + # Where people in more than one family are liable for the same rent, + # each person's core rent is the total divided by the number of + # people liable (UC Regs 2013 Sch 4 para 24(4): A / B x C), which also + # apportions Housing Benefit eligible rent by the number of people + # liable (HB Regs 2006 reg 12B(4)). + rent = person.household("rent", period) + liable = person("is_liable_for_household_rent", period) + liable_people = person.household.sum(liable) + # A household with nobody marked liable (for example a head who is + # not a claimant or partner) leaves the rent with the household head. + head = person("is_household_head", period) + heads = person.household.sum(head) + share = where( + liable_people > 0, + liable / max_(liable_people, 1), + head / max_(heads, 1), + ) + # A licence or other permission to occupy is a rent payment (UC Regs + # 2013 Sch 1 para 2(b); HB Regs 2006 reg 12(1)): boarders and lodgers + # are liable for what they pay the householder. + paid_to_householder = add( + person, period, ["rent_paid_as_boarder", "rent_paid_as_lodger"] + ) + return rent * share + paid_to_householder diff --git a/policyengine_uk/variables/household/consumption/rent/is_liable_for_household_rent.py b/policyengine_uk/variables/household/consumption/rent/is_liable_for_household_rent.py new file mode 100644 index 000000000..9ddfc87b3 --- /dev/null +++ b/policyengine_uk/variables/household/consumption/rent/is_liable_for_household_rent.py @@ -0,0 +1,22 @@ +from policyengine_uk.model_api import * + + +class is_liable_for_household_rent(Variable): + value_type = bool + entity = Person + label = "Liable for the household's rent" + documentation = ( + "Whether this person is one of the people liable for the rent of the " + "household's accommodation: the claimant or partner of the household " + "head's family, or of a family liable for a share of that rent." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/24", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/12B", + ) + + def formula(person, period, parameters): + head_family = person.benunit.any(person("is_household_head", period)) + sharer = person.benunit("liable_for_share_of_household_rent", period) + return person("is_claimant_or_partner", period) & (head_family | sharer) diff --git a/policyengine_uk/variables/household/consumption/rent/liable_for_share_of_household_rent.py b/policyengine_uk/variables/household/consumption/rent/liable_for_share_of_household_rent.py new file mode 100644 index 000000000..e3cb0a395 --- /dev/null +++ b/policyengine_uk/variables/household/consumption/rent/liable_for_share_of_household_rent.py @@ -0,0 +1,21 @@ +from policyengine_uk.model_api import * + + +class liable_for_share_of_household_rent(Variable): + value_type = bool + entity = BenUnit + label = "Liable for a share of the household's rent" + documentation = ( + "Whether this family shares liability for the household's rent with " + "the household head's family: a joint tenant, or a sharer with its " + "own agreement for part of the same accommodation (the Family " + "Resources Survey's shared household). The household head's family " + "is always liable, so this matters only for other families. Boarders " + "and lodgers, who pay the householder, are not sharers: see " + "rent_paid_as_boarder and rent_paid_as_lodger." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/24", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/12B", + ) diff --git a/policyengine_uk/variables/household/consumption/rent/meals_included_in_rent.py b/policyengine_uk/variables/household/consumption/rent/meals_included_in_rent.py new file mode 100644 index 000000000..cdcf2135c --- /dev/null +++ b/policyengine_uk/variables/household/consumption/rent/meals_included_in_rent.py @@ -0,0 +1,22 @@ +from policyengine_uk.model_api import * + + +class MealsIncludedInRent(Enum): + NONE = "No meals" + BREAKFAST_ONLY = "Breakfast only" + FEWER_THAN_THREE_A_DAY = "Fewer than three meals a day" + AT_LEAST_THREE_A_DAY = "At least three meals a day" + + +class meals_included_in_rent(Variable): + value_type = Enum + possible_values = MealsIncludedInRent + default_value = MealsIncludedInRent.NONE + entity = BenUnit + label = "Meals included in the family's rent" + documentation = ( + "Meals the family's rent pays for, as for a boarder. The Family " + "Resources Survey records whether someone is a boarder but not how " + "many meals they get." + ) + definition_period = YEAR diff --git a/policyengine_uk/variables/household/demographic/benunit_tenure_type.py b/policyengine_uk/variables/household/demographic/benunit_tenure_type.py index 8496db50c..45be66c94 100644 --- a/policyengine_uk/variables/household/demographic/benunit_tenure_type.py +++ b/policyengine_uk/variables/household/demographic/benunit_tenure_type.py @@ -9,10 +9,32 @@ class benunit_tenure_type(Variable): possible_values = TenureType default_value = TenureType.RENT_PRIVATELY entity = BenUnit - label = "Tenure type of the family's household" + label = "Tenure type of the family" + documentation = ( + "The family's own tenure. A boarder or lodger rents privately from " + "the householder whatever the household's tenure, so a lodger in a " + "council or housing association home is a private renter. Other " + "families take the household's tenure." + ) definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/20", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/13C", + ) def formula(benunit, period, parameters): - return benunit.value_from_first_person( + household_tenure = benunit.value_from_first_person( benunit.members.household("tenure_type", period) ) + # UC private rented sector rules apply to renters liable to anyone + # other than a provider of social housing (UC Regs 2013 Sch 4 para + # 20); a householder is not a social landlord (HB Regs 2006 reg + # 13C(5)(a)). + pays_householder = benunit.any( + benunit.members("pays_rent_to_householder", period) + ) + return where( + pays_householder, + TenureType.RENT_PRIVATELY, + household_tenure.decode(), + ) diff --git a/policyengine_uk/variables/household/demographic/in_social_housing.py b/policyengine_uk/variables/household/demographic/in_social_housing.py index f33c2e768..84a03bcdd 100644 --- a/policyengine_uk/variables/household/demographic/in_social_housing.py +++ b/policyengine_uk/variables/household/demographic/in_social_housing.py @@ -1,14 +1,18 @@ from policyengine_uk.model_api import * -import pandas as pd class in_social_housing(Variable): value_type = bool entity = Person label = "Whether this person lives in social housing" + documentation = ( + "Whether this person's family rents from a council or housing " + "association. A boarder or lodger in a social tenant's home rents " + "from the householder, not from the social landlord." + ) definition_period = YEAR def formula(person, period, parameters): - tenure = person.household("tenure_type", period.this_year) + tenure = person.benunit("benunit_tenure_type", period.this_year) tenures = tenure.possible_values return is_in(tenure, tenures.RENT_FROM_COUNCIL, tenures.RENT_FROM_HA) diff --git a/uv.lock b/uv.lock index 47b04e013..73aa2719b 100644 --- a/uv.lock +++ b/uv.lock @@ -1471,7 +1471,7 @@ wheels = [ [[package]] name = "policyengine-uk" -version = "2.102.3" +version = "2.102.6" source = { editable = "." } dependencies = [ { name = "microdf-python" }, From 613ff63e310329eaed7e0eb7735c492b6b808958 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 1 Oct 2026 11:23:09 -0400 Subject: [PATCH 3/9] Split council tax among jointly liable sharers and apportion shared non-dependants - Council Tax Reduction: where the household's rent is shared, every family liable for it is jointly and severally liable for the council tax and claims on the council tax divided by the number of people jointly liable (SI 2012/2885 Sch 1 para 7(3)-(4); Wales and Scotland the same), with a shared non-dependant apportioned equally (para 8(5)). Joint tenants and lodgers are not non-dependants (reg 9(2)(d)-(e)). National schemes and the five local schemes. - Housing Benefit: a non-dependant of several joint occupiers is apportioned by their shares of the rent (reg 74(5); SPC reg 55(5)). - Cite the current HB reg 13(7) (as substituted by SI 2007/2868) for the meals deduction on the rent officer route. - Tests: meals amounts, sharers, boarders and lodgers, CTR joint liability. Co-Authored-By: Claude Opus 5.5 --- .../housing_benefit_meals_deduction.yaml | 128 +++++++ .../rent_of_sharers_boarders_and_lodgers.yaml | 326 ++++++++++++++++++ .../joint_liability.yaml | 156 +++++++++ policyengine_uk/variables/gov/dwp/LHA_cap.py | 2 +- .../housing_benefit_non_dep_deductions.py | 11 +- .../dwp/is_non_dependant_of_household_head.py | 12 +- .../council_tax_reduction/_legacy.py | 11 +- .../council_tax_reduction_claimant_benunit.py | 33 ++ ...n_individual_non_dep_deduction_eligible.py | 19 +- ...cil_tax_reduction_joint_liability_share.py | 38 ++ ...ouncil_tax_reduction_non_dep_deductions.py | 5 +- ...simulated_council_tax_reduction_benunit.py | 6 +- .../oxford_council_tax_reduction.py | 6 +- .../rent/share_of_household_rent.py | 30 ++ 14 files changed, 762 insertions(+), 21 deletions(-) create mode 100644 policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_meals_deduction.yaml create mode 100644 policyengine_uk/tests/policy/baseline/finance/benefit/family/rent_of_sharers_boarders_and_lodgers.yaml create mode 100644 policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/joint_liability.yaml create mode 100644 policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_claimant_benunit.py create mode 100644 policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_joint_liability_share.py create mode 100644 policyengine_uk/variables/household/consumption/rent/share_of_household_rent.py diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_meals_deduction.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_meals_deduction.yaml new file mode 100644 index 000000000..d86d6feaa --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_meals_deduction.yaml @@ -0,0 +1,128 @@ +# Housing Benefit Regulations 2006 Sch 1 para 2 (amounts ineligible for meals) +# and reg 13(7); the same amounts are in the pension-age regulations +# (SI 2006/214 Sch 1 para 2). Weekly amounts x 52. + +- name: No meals, no deduction + period: 2026 + absolute_error_margin: 0.01 + input: + age: 40 + output: + housing_benefit_meals_deduction: 0 + +- name: Single boarder with at least three meals a day, 2026-27 + period: 2026 + absolute_error_margin: 0.01 + input: + people: + boarder: + age: 40 + benunits: + family: + members: [boarder] + meals_included_in_rent: AT_LEAST_THREE_A_DAY + households: + home: + members: [boarder] + output: + # Para 2(2)(a): £37.30 a week (SI 2026/148). 37.30 x 52 = 1,939.60. + housing_benefit_meals_deduction: 1_939.60 + +- name: Couple with a child under 16, fewer than three meals a day, 2025-26 + period: 2025 + absolute_error_margin: 0.01 + input: + people: + adult_1: + age: 40 + adult_2: + age: 38 + child: + age: 10 + benunits: + family: + members: [adult_1, adult_2, child] + meals_included_in_rent: FEWER_THAN_THREE_A_DAY + households: + home: + members: [adult_1, adult_2, child] + output: + # Para 2(3)(b): £24.00 for each person aged 16 or over and £12.00 for each + # under 16 (SI 2025/295). (2 x 24.00 + 12.00) x 52 = 3,120. + housing_benefit_meals_deduction: 3_120 + +- name: Breakfast only, a flat amount per person + period: 2026 + absolute_error_margin: 0.01 + input: + people: + adult: + age: 30 + child: + age: 5 + benunits: + family: + members: [adult, child] + meals_included_in_rent: BREAKFAST_ONLY + households: + home: + members: [adult, child] + output: + # Para 2(5): £4.50 for the claimant and each family member. + # 2 x 4.50 x 52 = 468. + housing_benefit_meals_deduction: 468 + +- name: Meals come off the rent before the LHA cap + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 60 + is_household_head: true + boarder: + age: 70 + rent_paid_as_boarder: 10_000 + benunits: + householder_family: + members: [householder] + boarder_family: + members: [boarder] + meals_included_in_rent: AT_LEAST_THREE_A_DAY + housing_benefit_LHA_rate: 9_000 + households: + home: + members: [householder, boarder] + tenure_type: OWNED_OUTRIGHT + output: + # Reg 13(7): rent 10,000 less 37.30 x 52 = 1,939.60 is 8,060.40, below + # the 9,000 rate standing in for the rent officer's figure. + LHA_cap: [0, 8_060.40] + +- name: Meals do not reduce the Universal Credit housing costs element + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 60 + is_household_head: true + boarder: + age: 40 + rent_paid_as_boarder: 6_000 + benunits: + householder_family: + members: [householder] + boarder_family: + members: [boarder] + meals_included_in_rent: AT_LEAST_THREE_A_DAY + uncapped_BRMA_LHA_rate: 9_000 + households: + home: + members: [householder, boarder] + tenure_type: OWNED_OUTRIGHT + output: + # UC excludes food from service charges only in the social rented + # sector and for owner-occupiers (UC Sch 1 para 8(1), (6)(c)); a private + # renter's whole payment is core rent: min(6,000, 9,000) = 6,000. + uc_housing_costs_element: [0, 6_000] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/rent_of_sharers_boarders_and_lodgers.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/rent_of_sharers_boarders_and_lodgers.yaml new file mode 100644 index 000000000..60db0c179 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/rent_of_sharers_boarders_and_lodgers.yaml @@ -0,0 +1,326 @@ +# Rent liability, tenure and LHA category of families other than the +# household head's: joint tenants and other sharers of the rent, boarders and +# lodgers. +# UC Regs 2013 (SI 2013/376) Sch 1 para 2, Sch 4 paras 9, 10, 20, 24, 25, 27-29. +# HB Regs 2006 (SI 2006/213) regs 3, 12, 12B, 13, 13C, 13D, Sch 1 para 2. + +- name: The household head's couple shares the household's rent equally + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + is_household_head: true + partner: + age: 38 + benunits: + family: + members: [head, partner] + households: + home: + members: [head, partner] + rent: 12_000 + output: + # Two people liable: 12,000 / 2 each; the family's total is the rent. + personal_rent: [6_000, 6_000] + benunit_rent: 12_000 + +- name: Sharers split the household's rent per person liable + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + is_household_head: true + sharer_1: + age: 30 + sharer_1_partner: + age: 31 + sharer_2: + age: 45 + benunits: + head_family: + members: [head] + couple: + members: [sharer_1, sharer_1_partner] + liable_for_share_of_household_rent: true + single: + members: [sharer_2] + liable_for_share_of_household_rent: true + households: + home: + members: [head, sharer_1, sharer_1_partner, sharer_2] + rent: 20_000 + tenure_type: RENT_PRIVATELY + output: + # UC Sch 4 para 24(4): A / B x C with A = 20,000 and B = 4 people liable, + # so 5,000 for a single person and 10,000 for the couple (C = 2). + is_liable_for_household_rent: [true, true, true, true] + benunit_rent: [5_000, 10_000, 5_000] + benunit_tenure_type: [RENT_PRIVATELY, RENT_PRIVATELY, RENT_PRIVATELY] + # Joint tenants are not non-dependants (para 9(2)(d)): no deductions. + uc_non_dep_deductions: [0, 0, 0] + housing_benefit_non_dep_deductions: [0, 0, 0] + +- name: A family not marked as a sharer has no share of the rent + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 50 + is_household_head: true + adult_son: + age: 25 + benunits: + parent: + members: [head] + son: + members: [adult_son] + households: + home: + members: [head, adult_son] + rent: 9_000 + output: + benunit_rent: [9_000, 0] + benunit_is_rent_liable: [true, false] + is_non_dependant_of_household_head: [false, true] + +- name: A lodger in a council home rents privately from the householder + period: 2025 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 50 + is_household_head: true + lodger: + age: 40 + rent_paid_as_lodger: 5_200 + benunits: + householder_family: + members: [householder] + lodger_family: + members: [lodger] + households: + home: + members: [householder, lodger] + rent: 6_000 + tenure_type: RENT_FROM_COUNCIL + output: + # The householder is liable to the council for the whole rent and the + # lodger to the householder for 5,200 (a licence: UC Sch 1 para 2(b)). + benunit_rent: [6_000, 5_200] + # A householder is not a provider of social housing (UC Sch 4 para 20). + benunit_tenure_type: [RENT_FROM_COUNCIL, RENT_PRIVATELY] + in_social_housing: [true, false] + LHA_eligible: [false, true] + +- name: A lodger in an owner-occupied home is a private renter + period: 2025 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 60 + is_household_head: true + lodger: + age: 40 + rent_paid_as_lodger: 5_200 + benunits: + householder_family: + members: [householder] + lodger_family: + members: [lodger] + uncapped_BRMA_LHA_rate: 6_000 + households: + home: + members: [householder, lodger] + tenure_type: OWNED_OUTRIGHT + output: + benunit_rent: [0, 5_200] + benunit_tenure_type: [OWNED_OUTRIGHT, RENT_PRIVATELY] + # UC: single, 40 >= 35, 1 bedroom: B. Cap = min(rent 5,200, rate 6,000). + LHA_category: B + uc_LHA_cap: [0, 5_200] + uc_housing_costs_element: [0, 5_200] + +- name: The householder's non-dependant is not deducted from a lodger's UC + period: 2025 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 55 + is_household_head: true + adult_daughter: + age: 30 + lodger: + age: 40 + rent_paid_as_lodger: 5_200 + benunits: + householder_family: + members: [householder] + uncapped_BRMA_LHA_rate: 12_000 + daughter: + members: [adult_daughter] + lodger_family: + members: [lodger] + uncapped_BRMA_LHA_rate: 6_000 + households: + home: + members: [householder, adult_daughter, lodger] + rent: 10_000 + tenure_type: RENT_PRIVATELY + output: + # The daughter is the householder's non-dependant; the lodger is no one's + # (UC Sch 4 para 9(2)(d)), and the householder's household is not the + # lodger's (para 9(2)(e)). + is_non_dependant_of_household_head: [false, true, false] + # One housing cost contribution, £93.02 a month for 2025-26 (Sch 4 para + # 14): 93.02 x 12 = 1,116.24. + uc_non_dep_deductions: [1_116.24, 0, 0] + # Householder: min(10,000, 12,000) - 1,116.24 = 8,883.76. The lodger's + # element is not reduced: min(5,200, 6,000) = 5,200. + uc_housing_costs_element: [8_883.76, 0, 5_200] + # UC bedrooms: householder 1 + daughter 1 = 2 (the lodger adds none). + LHA_allowed_bedrooms: [2, 1, 1] + +- name: A lodger adds a bedroom to the householder's HB size criteria but not UC + period: 2025 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 40 + is_household_head: true + lodger: + age: 40 + rent_paid_as_lodger: 5_200 + benunits: + householder_family: + members: [householder] + lodger_family: + members: [lodger] + households: + home: + members: [householder, lodger] + rent: 12_000 + tenure_type: RENT_PRIVATELY + output: + # UC: the lodger is not a non-dependant (Sch 4 para 9(2)(d)), so the + # householder is entitled to 1 bedroom: B. + LHA_allowed_bedrooms: [1, 1] + LHA_category: [B, B] + # HB: the lodger occupies the dwelling and is not a joint tenant, so is + # an "occupier" (reg 13D(12)): 1 + 1 = 2 bedrooms: C. The lodger's own + # claim counts only the lodger: 1 bedroom, shares the householder's rooms + # (reg 13D(2)(a)(ii)): A. + housing_benefit_LHA_allowed_bedrooms: [2, 1] + housing_benefit_shares_accommodation: [false, true] + housing_benefit_LHA_category: [C, A] + +- name: A lodger does not stop a householder under 35 being a specified renter + period: 2025 + input: + people: + householder: + age: 30 + is_household_head: true + lodger: + age: 45 + rent_paid_as_lodger: 5_200 + benunits: + householder_family: + members: [householder] + lodger_family: + members: [lodger] + households: + home: + members: [householder, lodger] + rent: 9_000 + tenure_type: RENT_PRIVATELY + output: + # UC Sch 4 para 28(4): no person is a non-dependant of the householder. + lha_renter_has_non_dependant: [false, false] + is_lha_shared_accommodation_rate_specified_renter: [true, false] + LHA_category: [A, B] + # HB reg 13D(2)(a)(i): a young individual with no non-dependant: A. + housing_benefit_LHA_category: [A, A] + +- name: A sharer aged 40 gets the UC one-bedroom rate and the HB shared rate + period: 2025 + input: + people: + head: + age: 40 + is_household_head: true + sharer: + age: 40 + benunits: + head_family: + members: [head] + sharer_family: + members: [sharer] + liable_for_share_of_household_rent: true + households: + home: + members: [head, sharer] + rent: 18_000 + tenure_type: RENT_PRIVATELY + output: + # UC: entitlement only (Sch 4 para 25(2)(b)): 1 bedroom, not a specified + # renter (40 >= 35): B for both. + LHA_category: [B, B] + # HB: each is entitled to one bedroom and shares rooms with someone who + # is neither household, non-dependant nor paying rent to them (reg + # 13D(2)(b)), so the shared rate (13D(2)(a)(ii)): A for both. + housing_benefit_shares_accommodation: [true, true] + housing_benefit_LHA_category: [A, A] + +- name: The severe disability premium keeps a sharer off the HB shared rate + period: 2025 + input: + people: + head: + age: 40 + is_household_head: true + sharer: + age: 40 + benunits: + head_family: + members: [head] + sharer_family: + members: [sharer] + liable_for_share_of_household_rent: true + severe_disability_premium: 3_000 + households: + home: + members: [head, sharer] + rent: 18_000 + tenure_type: RENT_PRIVATELY + output: + # Reg 13D(2)(a)(ii) excludes a claimant to whom Sch 3 para 14 applies, + # so 13D(2)(c): 1 bedroom: B. + housing_benefit_LHA_category: [A, B] + +- name: The household input for shared accommodation affects Housing Benefit only + period: 2025 + input: + people: + person: + age: 50 + benunits: + family: + members: [person] + households: + home: + members: [person] + rent: 9_000 + tenure_type: RENT_PRIVATELY + is_shared_accommodation: true + output: + LHA_category: B + housing_benefit_LHA_category: A diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/joint_liability.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/joint_liability.yaml new file mode 100644 index 000000000..47b722480 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/joint_liability.yaml @@ -0,0 +1,156 @@ +# Council Tax Reduction where the household's rent, and so its council tax, +# is shared: each jointly and severally liable family claims on the council +# tax divided by the number of people jointly liable, and a non-dependant of +# two or more of them is apportioned equally between them. Joint tenants are +# not non-dependants. +# England pensioners SI 2012/2885 reg 9(2)(d), Sch 1 paras 7(3)-(4) and 8(5); +# Wales SI 2013/3029 reg 9(2)(d), Sch 1 para 2(3)-(5), Sch 6 para 4(3)-(5). + +- name: Three sharers and a non-dependant in Wales + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + is_household_head: true + sharer_1: + age: 30 + sharer_2: + age: 45 + adult_son: + age: 22 + council_tax_reduction_individual_non_dep_deduction: 300 + benunits: + head_family: + members: [head] + council_tax_reduction_applicable_income: 0 + sharer_1_family: + members: [sharer_1] + liable_for_share_of_household_rent: true + council_tax_reduction_applicable_income: 0 + sharer_2_family: + members: [sharer_2] + liable_for_share_of_household_rent: true + council_tax_reduction_applicable_income: 0 + son_family: + members: [adult_son] + households: + home: + members: [head, sharer_1, sharer_2, adult_son] + rent: 18_000 + tenure_type: RENT_PRIVATELY + council_tax: 1_800 + country: WALES + region: WALES + output: + council_tax_reduction_claimant_benunit: [true, true, true, false] + council_tax_reduction_joint_liability_share: [0.333333, 0.333333, 0.333333, 1] + # Joint tenants are not non-dependants; the adult son is. + council_tax_reduction_individual_non_dep_deduction_eligible: [false, false, false, true] + # The son's 300 is apportioned equally: 100 each. + council_tax_reduction_non_dep_deductions: [100, 100, 100, 0] + # 1,800 / 3 x 100% - 100 = 500 each; the son is not liable and cannot claim. + simulated_council_tax_reduction_benunit: [500, 500, 500, 0] + council_tax_reduction: 1_500 + +- name: A couple sharing with one other person claims a third of the council tax + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + is_household_head: true + partner: + age: 40 + sharer: + age: 30 + benunits: + couple: + members: [head, partner] + council_tax_reduction_applicable_income: 0 + sharer_family: + members: [sharer] + liable_for_share_of_household_rent: true + council_tax_reduction_applicable_income: 0 + households: + home: + members: [head, partner, sharer] + rent: 18_000 + tenure_type: RENT_PRIVATELY + council_tax: 1_800 + country: WALES + region: WALES + output: + # The regulations divide by every person jointly liable (three), with no + # multiplier for the claimant's partner; the partner-only exception does + # not apply because a third person is liable. + council_tax_reduction_joint_liability_share: [0.333333, 0.333333] + simulated_council_tax_reduction_benunit: [600, 600] + +- name: A couple liable only with each other claims the whole council tax + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + is_household_head: true + partner: + age: 40 + adult_son: + age: 22 + council_tax_reduction_individual_non_dep_deduction: 300 + benunits: + couple: + members: [head, partner] + council_tax_reduction_applicable_income: 0 + son_family: + members: [adult_son] + households: + home: + members: [head, partner, adult_son] + rent: 9_000 + tenure_type: RENT_PRIVATELY + council_tax: 1_800 + country: WALES + region: WALES + output: + # No one outside the couple is jointly liable: no division (para (4)). + council_tax_reduction_joint_liability_share: [1, 1] + council_tax_reduction_claimant_benunit: [true, false] + council_tax_reduction_non_dep_deductions: [300, 0] + simulated_council_tax_reduction_benunit: [1_500, 0] + +- name: A lodger is not a non-dependant of the householder + period: 2025 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 50 + is_household_head: true + lodger: + age: 30 + rent_paid_as_lodger: 5_200 + benunits: + householder_family: + members: [householder] + council_tax_reduction_applicable_income: 0 + lodger_family: + members: [lodger] + households: + home: + members: [householder, lodger] + tenure_type: OWNED_OUTRIGHT + council_tax: 1_800 + country: WALES + region: WALES + output: + # SI 2013/3029 reg 9(2)(e): liable to make payments on a commercial basis + # to the applicant. A lodger is not jointly liable for council tax, so + # the householder claims the whole bill. + council_tax_reduction_individual_non_dep_deduction_eligible: [false, false] + council_tax_reduction_joint_liability_share: [1, 1] + simulated_council_tax_reduction_benunit: [1_800, 0] diff --git a/policyengine_uk/variables/gov/dwp/LHA_cap.py b/policyengine_uk/variables/gov/dwp/LHA_cap.py index dedaa40a3..0d26ce517 100644 --- a/policyengine_uk/variables/gov/dwp/LHA_cap.py +++ b/policyengine_uk/variables/gov/dwp/LHA_cap.py @@ -32,7 +32,7 @@ def formula(benunit, period, parameters): # the LHA rate or, if lower, the cap rent. A rent officer finding # that a substantial part of the rent is for board and attendance # takes the case off the LHA (reg 13C(5)(e)); the maximum rent then - # has the Sch 1 para 2 amount for meals deducted (reg 13(13)). + # has the Sch 1 para 2 amount for meals deducted (reg 13(7)). rent = max_( 0, benunit("benunit_rent", period) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py index 2cda43875..e5e9e3a93 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py @@ -17,11 +17,14 @@ def formula(benunit, period, parameters): # Deductions are made for non-dependants residing with the claimant # (HB Regs 2006 reg 74; HB (SPC) Regs 2006 reg 55). Joint occupiers, # boarders, lodgers and the landlord's household are not - # non-dependants (reg 3(2)(d)-(e), 3(4)), so only the household - # head's family has them. + # non-dependants (reg 3(2)(d)-(e), 3(4)). A non-dependant of more + # than one joint occupier is apportioned between them by their shares + # of the payments (reg 74(5); SPC reg 55(5)), so each family liable + # for the household's rent bears its share; a boarder or lodger bears + # none. person = benunit.members deductions = person( "household_benefits_individual_non_dep_deduction", period ) * person("is_non_dependant_of_household_head", period) - head_family = benunit.any(person("is_household_head", period)) - return head_family * benunit.max(person.household.sum(deductions)) + share = benunit("share_of_household_rent", period) + return share * benunit.max(person.household.sum(deductions)) diff --git a/policyengine_uk/variables/gov/dwp/is_non_dependant_of_household_head.py b/policyengine_uk/variables/gov/dwp/is_non_dependant_of_household_head.py index ebaf567b5..2c80db627 100644 --- a/policyengine_uk/variables/gov/dwp/is_non_dependant_of_household_head.py +++ b/policyengine_uk/variables/gov/dwp/is_non_dependant_of_household_head.py @@ -11,12 +11,12 @@ class is_non_dependant_of_household_head(Variable): "sharer of the rent, a boarder and a lodger are liable on a " "commercial basis for their occupation, so none is a non-dependant " "of anyone. The household head and their household are not " - "non-dependants of a boarder or lodger who pays them, and someone who " - "is already a non-dependant of one claimant is not one of another " - "claimant liable for the same accommodation, so in this model only " - "the household head's family has non-dependants from other families. " - "Foster children and carers engaged through a charity are not " - "identified." + "non-dependants of a boarder or lodger who pays them. Universal " + "Credit counts a non-dependant in one claim only, which the model " + "gives to the household head's family; Housing Benefit and Council " + "Tax Reduction apportion a non-dependant of several joint occupiers " + "between them. Foster children and carers engaged through a charity " + "are not identified." ) definition_period = YEAR reference = ( diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py index 54883e9b4..966d230ff 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py @@ -18,7 +18,9 @@ def legacy_council_tax_reduction( non_dep_deductions_variable, additional_applicable_income=0, ): - is_household_head_benunit = benunit("benunit_contains_household_head", period) + is_household_head_benunit = benunit( + "council_tax_reduction_claimant_benunit", period + ) would_claim = benunit("would_claim_council_tax_reduction", period) applicable_amount = benunit("council_tax_reduction_applicable_amount", period) applicable_income = benunit("council_tax_reduction_applicable_income", period) @@ -39,7 +41,7 @@ def legacy_council_tax_reduction( ) liability = benunit.household( "council_tax_reduction_maximum_eligible_liability", period - ) + ) * benunit("council_tax_reduction_joint_liability_share", period) non_dep_deductions = benunit(non_dep_deductions_variable, period) excess_income = max_(0, applicable_income - applicable_amount) excess_income = where( @@ -86,7 +88,10 @@ def local_non_dep_deductions( deductions_in_household = benunit.max( benunit.members.household.sum(deductions_to_count) ) - return deductions_in_household - deduction_for_benunit + # A non-dependant of two or more jointly liable people is apportioned + # equally between them (SI 2012/2885 Sch 1 para 8(5)). + share = benunit("council_tax_reduction_joint_liability_share", period) + return (deductions_in_household - deduction_for_benunit) * share def normal_gross_income_non_dep_deduction( diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_claimant_benunit.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_claimant_benunit.py new file mode 100644 index 000000000..449b9d080 --- /dev/null +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_claimant_benunit.py @@ -0,0 +1,33 @@ +from policyengine_uk.model_api import * + + +class council_tax_reduction_claimant_benunit(Variable): + value_type = bool + entity = BenUnit + label = "Family claims Council Tax Reduction for the household" + documentation = ( + "Whether this family can claim Council Tax Reduction on the " + "household's council tax. Where the household's rent is shared, every " + "family liable for it is jointly and severally liable for the council " + "tax and claims on its part. Otherwise the family of the household's " + "oldest adult claims, as before." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/ukpga/1992/14/section/6", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/7", + ) + + def formula(benunit, period, parameters): + person = benunit.members + rent_is_shared = benunit.any( + person.household.any( + person.benunit("liable_for_share_of_household_rent", period) + ) + ) + liable_family = benunit.any(person("is_liable_for_household_rent", period)) + return where( + rent_is_shared, + liable_family, + benunit("benunit_contains_household_head", period), + ) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py index 36cb046b4..459598f8d 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py @@ -5,9 +5,24 @@ class council_tax_reduction_individual_non_dep_deduction_eligible(Variable): value_type = bool entity = Person label = "eligible person for CTR non-dependent deduction" + documentation = ( + "An adult outside the claimant's family who is not liable for rent. " + "Someone jointly and severally liable for the council tax with the " + "claimant (a joint tenant or other sharer of the rent) and someone " + "liable to the claimant on a commercial basis (a boarder or lodger) " + "is not a non-dependant." + ) definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2012/2885/regulation/9", + "https://www.legislation.gov.uk/wsi/2013/3029", + "https://www.legislation.gov.uk/ssi/2021/249", + ) def formula(person, period, parameters): - return (person("age", period) >= 18) & ~person.benunit( - "benunit_contains_household_head", period + # SI 2012/2885 reg 9(2)(d)-(e) and the Welsh and Scottish equivalents. + return ( + (person("age", period) >= 18) + & ~person.benunit("benunit_contains_household_head", period) + & ~person.benunit("benunit_is_rent_liable", period) ) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_joint_liability_share.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_joint_liability_share.py new file mode 100644 index 000000000..215992982 --- /dev/null +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_joint_liability_share.py @@ -0,0 +1,38 @@ +from policyengine_uk.model_api import * + + +class council_tax_reduction_joint_liability_share(Variable): + value_type = float + entity = BenUnit + label = "Share of the council tax for a jointly liable Council Tax Reduction claim" + documentation = ( + "Where the claimant is jointly and severally liable for the council " + "tax with people other than their partner, the council tax used for " + "the maximum reduction is divided by the number of people jointly and " + "severally liable, and a deduction for a non-dependant of two or more " + "of them is apportioned equally between them. The model takes the " + "people liable for the household's rent as those jointly liable for " + "its council tax (residents with the same interest, Local Government " + "Finance Act 1992 s.6) and follows the regulations' wording, dividing " + "by every person liable, the claimant's partner included. Students " + "are not excluded from the count. Otherwise the share is one." + ) + definition_period = YEAR + unit = "/1" + reference = ( + "https://www.legislation.gov.uk/ukpga/1992/14/section/6", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/7", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/8", + "https://www.legislation.gov.uk/wsi/2013/3029", + "https://www.legislation.gov.uk/ssi/2021/249", + ) + + def formula(benunit, period, parameters): + # SI 2012/2885 Sch 1 para 7(3)-(4) and para 8(5); the Welsh and + # Scottish schemes have the same wording. + person = benunit.members + liable = person("is_liable_for_household_rent", period) + liable_people = benunit.max(person.household.sum(liable)) + in_family = benunit.sum(liable) + jointly_with_others = (in_family > 0) & (liable_people > in_family) + return where(jointly_with_others, 1 / max_(liable_people, 1), 1) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py index c6b5fc1d8..6241abf4c 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py @@ -14,4 +14,7 @@ def formula(benunit, period, parameters): ) deductions_in_household = benunit.max(benunit.members.household.sum(deductions)) deductions_in_benunit = benunit.sum(deductions) - return deductions_in_household - deductions_in_benunit + # A non-dependant of two or more jointly liable people is apportioned + # equally between them (SI 2012/2885 Sch 1 para 8(5)). + share = benunit("council_tax_reduction_joint_liability_share", period) + return (deductions_in_household - deductions_in_benunit) * share diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/simulated_council_tax_reduction_benunit.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/simulated_council_tax_reduction_benunit.py index ce3eeb7ee..6f3714fee 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/simulated_council_tax_reduction_benunit.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/simulated_council_tax_reduction_benunit.py @@ -38,11 +38,13 @@ def formula(benunit, period, parameters): wales = is_wales_scheme(country) national_scheme = england_pensioners | scotland | wales - is_household_head_benunit = benunit("benunit_contains_household_head", period) + is_household_head_benunit = benunit( + "council_tax_reduction_claimant_benunit", period + ) would_claim = benunit("would_claim_council_tax_reduction", period) liability = benunit.household( "council_tax_reduction_maximum_eligible_liability", period - ) + ) * benunit("council_tax_reduction_joint_liability_share", period) applicable_amount = benunit("council_tax_reduction_applicable_amount", period) applicable_income = benunit("council_tax_reduction_applicable_income", period) non_dep_deductions = benunit("council_tax_reduction_non_dep_deductions", period) diff --git a/policyengine_uk/variables/gov/local_authorities/oxford/council_tax_reduction/oxford_council_tax_reduction.py b/policyengine_uk/variables/gov/local_authorities/oxford/council_tax_reduction/oxford_council_tax_reduction.py index 332b9781c..abeea551d 100644 --- a/policyengine_uk/variables/gov/local_authorities/oxford/council_tax_reduction/oxford_council_tax_reduction.py +++ b/policyengine_uk/variables/gov/local_authorities/oxford/council_tax_reduction/oxford_council_tax_reduction.py @@ -20,7 +20,9 @@ def formula(benunit, period, parameters): household("country", period), household("council_tax_reduction_household_has_pensioner", period), ) - is_household_head_benunit = benunit("benunit_contains_household_head", period) + is_household_head_benunit = benunit( + "council_tax_reduction_claimant_benunit", period + ) would_claim = benunit("would_claim_council_tax_reduction", period) universal_credit = benunit("universal_credit", period) has_uc_award = universal_credit > 0 @@ -47,7 +49,7 @@ def formula(benunit, period, parameters): support_rate = ctr.income_band.maximum_support_rate.calc(weekly_income) liability = household( "council_tax_reduction_maximum_eligible_liability", period - ) + ) * benunit("council_tax_reduction_joint_liability_share", period) non_dep_deductions = benunit( "oxford_council_tax_reduction_non_dep_deductions", period ) diff --git a/policyengine_uk/variables/household/consumption/rent/share_of_household_rent.py b/policyengine_uk/variables/household/consumption/rent/share_of_household_rent.py new file mode 100644 index 000000000..9588c5184 --- /dev/null +++ b/policyengine_uk/variables/household/consumption/rent/share_of_household_rent.py @@ -0,0 +1,30 @@ +from policyengine_uk.model_api import * + + +class share_of_household_rent(Variable): + value_type = float + entity = BenUnit + label = "Share of the household's rent" + documentation = ( + "The share of the household's rent this family is liable for: the " + "people liable for it in this family over all the people liable for " + "it. The household head's family has the whole rent unless other " + "families are liable for a share." + ) + definition_period = YEAR + unit = "/1" + reference = ( + "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/24", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/12B", + ) + + def formula(benunit, period, parameters): + person = benunit.members + liable = person("is_liable_for_household_rent", period) + liable_people = benunit.max(person.household.sum(liable)) + head_family = benunit.any(person("is_household_head", period)) + return where( + liable_people > 0, + benunit.sum(liable) / max_(liable_people, 1), + head_family, + ) From 24a88dac5f36bca6624193bcd1da8a6814014cce Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 1 Oct 2026 11:54:17 -0400 Subject: [PATCH 4/9] Property tests for sharers', boarders' and lodgers' rent, tenure and housing support Hypothesis invariants over generated households: rent conservation and bounds, boarder and lodger tenure, non-dependant exclusion, UC category independent of the shared-accommodation input, HB shared rate where exclusive use is lacking, meals never raising HB eligible rent, CTR claim shares in shared households, monotonicity in household rent, and a no-op check against the previous rent and deduction formulas. Co-Authored-By: Claude Opus 5.5 --- ...sharers_boarders_and_lodgers_properties.py | 274 ++++++++++++++++++ .../demographic/benunit_tenure_type.py | 12 +- 2 files changed, 282 insertions(+), 4 deletions(-) create mode 100644 policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py diff --git a/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py b/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py new file mode 100644 index 000000000..c3b3efca5 --- /dev/null +++ b/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py @@ -0,0 +1,274 @@ +"""Property-based tests for the rent, tenure and housing support of families +other than the household head's: sharers of the rent, boarders and lodgers. + +Invariants, for any generated population of households: + +1. Conservation: the families' shares of the household's rent sum to one + wherever anyone is liable for it, and the families' rents sum to the + household's rent plus what boarders and lodgers pay the householder. +2. Bounds: each share is in [0, 1]; each family's rent is non-negative; a + family that is neither the household head's, nor a sharer, nor a boarder + or lodger has no rent. +3. Tenure: a boarder's or lodger's family rents privately, is never in social + housing, and is LHA-eligible. +4. Non-dependants: a family liable for rent is never a non-dependant; only + the household head's family has Universal Credit non-dependant + deductions; a boarder's or lodger's family has no Housing Benefit or + Council Tax Reduction non-dependant deductions. +5. Category: the Universal Credit LHA category does not depend on the + household input is_shared_accommodation; the Housing Benefit category is + the shared rate for any family entitled to one bedroom that lacks + exclusive use and has no severe disability premium. +6. Meals: the Housing Benefit meals deduction is non-negative, and adding + meals never raises the Housing Benefit eligible rent (LHA_cap). +7. Council tax: in a household whose rent is shared, the jointly liable + claim shares never exceed one in total. +8. Monotonicity: a family's rent is non-decreasing in the household's rent. +9. No-op: in a household with no sharers, boarders or lodgers, the household + head's family has the whole rent and everyone else none, as before, and + the head's Universal Credit non-dependant deductions equal the previous + formula (every other family's deductions). +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +YEAR = 2025 +PROPERTY_SETTINGS = settings( + max_examples=8, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +TENURES = [ + "RENT_FROM_COUNCIL", + "RENT_FROM_HA", + "RENT_PRIVATELY", + "OWNED_OUTRIGHT", + "OWNED_WITH_MORTGAGE", +] +ROLES = ["sharer", "boarder", "lodger", "non_dependant"] +MEALS = ["NONE", "BREAKFAST_ONLY", "FEWER_THAN_THREE_A_DAY", "AT_LEAST_THREE_A_DAY"] +money = st.floats(0, 30_000, allow_nan=False, allow_infinity=False) + + +@st.composite +def other_family(draw): + return dict( + role=draw(st.sampled_from(ROLES)), + ages=draw(st.lists(st.integers(18, 85), min_size=1, max_size=2)), + child_age=draw(st.one_of(st.none(), st.integers(0, 15))), + payment=draw(st.floats(1, 15_000, allow_nan=False)), + meals=draw(st.sampled_from(MEALS)), + sdp=draw(st.booleans()), + ) + + +@st.composite +def households(draw): + return dict( + head_ages=draw(st.lists(st.integers(18, 85), min_size=1, max_size=2)), + head_child_age=draw(st.one_of(st.none(), st.integers(0, 15))), + tenure=draw(st.sampled_from(TENURES)), + rent=draw(money), + council_tax=draw(st.floats(0, 3_000, allow_nan=False)), + others=draw(st.lists(other_family(), min_size=0, max_size=3)), + shared=draw(st.booleans()), + ) + + +population = st.lists(households(), min_size=1, max_size=6) + + +def build(population, shared_override=None, meals_override=None, rent_bump=0.0): + """One situation for the whole population; returns it and family roles.""" + people, benunits, homes, roles = {}, {}, {}, [] + for h, house in enumerate(population): + members = [] + + def add_family(name, ages, child_age, role, extra): + ids = [] + for i, age in enumerate(ages): + pid = f"{name}_adult_{i}" + people[pid] = {"age": age, "is_claimant_or_partner": True} + ids.append(pid) + if child_age is not None: + pid = f"{name}_child" + people[pid] = {"age": child_age, "is_claimant_or_partner": False} + ids.append(pid) + benunits[name] = {"members": ids, **extra} + members.extend(ids) + roles.append(role) + return ids + + head_ids = add_family( + f"h{h}_head", house["head_ages"], house["head_child_age"], "head", {} + ) + for pid in head_ids: + people[pid]["is_household_head"] = False + people[head_ids[0]]["is_household_head"] = True + for f, other in enumerate(house["others"]): + name = f"h{h}_f{f}" + extra = { + "liable_for_share_of_household_rent": other["role"] == "sharer", + "meals_included_in_rent": ( + meals_override or other["meals"] + if other["role"] == "boarder" + else "NONE" + ), + } + if other["sdp"]: + extra["severe_disability_premium"] = 3_000 + ids = add_family( + name, other["ages"], other["child_age"], other["role"], extra + ) + for pid in ids: + people[pid]["is_household_head"] = False + if other["role"] in ("boarder", "lodger"): + variable = f"rent_paid_as_{other['role']}" + people[ids[0]][variable] = other["payment"] + homes[f"h{h}"] = { + "members": members, + "tenure_type": house["tenure"], + "rent": house["rent"] + rent_bump, + "council_tax": house["council_tax"], + "is_shared_accommodation": ( + house["shared"] if shared_override is None else shared_override + ), + "brma": "MAIDSTONE", + } + situation = {"people": people, "benunits": benunits, "households": homes} + return situation, np.array(roles) + + +def calc(simulation, variable, map_to=None): + """Values as a plain array (enumerations come back decoded).""" + result = simulation.calculate(variable, YEAR, map_to=map_to) + return np.asarray(result) + + +@PROPERTY_SETTINGS +@given(population) +def test_conservation_bounds_and_tenure(population): + situation, roles = build(population) + sim = Simulation(situation=situation) + share = calc(sim, "share_of_household_rent") + rent = calc(sim, "benunit_rent") + household_rent = calc(sim, "rent") + paid = calc(sim, "rent_paid_as_boarder", "household") + calc( + sim, "rent_paid_as_lodger", "household" + ) + # 1. Conservation. + share_total = sim.map_result(share, "benunit", "household") + assert np.allclose(share_total, 1, atol=1e-6) + rent_total = sim.map_result(rent, "benunit", "household") + assert np.allclose(rent_total, household_rent + paid, rtol=1e-6, atol=0.01) + # 2. Bounds. + assert np.all((share >= -1e-9) & (share <= 1 + 1e-9)) + assert np.all(rent >= 0) + assert np.all(rent[roles == "non_dependant"] == 0) + assert np.all(share[np.isin(roles, ["boarder", "lodger", "non_dependant"])] == 0) + # 3. Tenure. + payer = np.isin(roles, ["boarder", "lodger"]) + tenure = sim.calculate("benunit_tenure_type", YEAR) + assert np.all(tenure[payer] == "RENT_PRIVATELY") + social = calc(sim, "in_social_housing", "benunit") > 0 + assert not np.any(social[payer]) + assert np.all(calc(sim, "LHA_eligible")[payer]) + + +@PROPERTY_SETTINGS +@given(population) +def test_non_dependants(population): + situation, roles = build(population) + sim = Simulation(situation=situation) + liable = calc(sim, "benunit_is_rent_liable") + non_dependant = calc(sim, "is_non_dependant_of_household_head", "benunit") > 0 + assert not np.any(non_dependant & liable) + uc = calc(sim, "uc_non_dep_deductions") + assert np.all(uc[roles != "head"] == 0) + payer = np.isin(roles, ["boarder", "lodger"]) + assert np.all(calc(sim, "housing_benefit_non_dep_deductions")[payer] == 0) + assert np.all(calc(sim, "council_tax_reduction_non_dep_deductions")[payer] == 0) + + +@PROPERTY_SETTINGS +@given(population) +def test_lha_categories(population): + with_input, _ = build(population, shared_override=True) + without_input, _ = build(population, shared_override=False) + a = Simulation(situation=with_input) + b = Simulation(situation=without_input) + # 5. The UC category is set by entitlement alone. + assert np.array_equal( + a.calculate("LHA_category", YEAR), + b.calculate("LHA_category", YEAR), + ) + for sim in (a, b): + rooms = calc(sim, "housing_benefit_LHA_allowed_bedrooms") + shares = calc(sim, "housing_benefit_shares_accommodation") + sdp = calc(sim, "severe_disability_premium") > 0 + category = sim.calculate("housing_benefit_LHA_category", YEAR) + assert np.all(category[(rooms == 1) & shares & ~sdp] == "A") + + +@PROPERTY_SETTINGS +@given(population) +def test_meals_and_council_tax(population): + with_meals, roles = build(population, meals_override="AT_LEAST_THREE_A_DAY") + no_meals, _ = build(population, meals_override="NONE") + a = Simulation(situation=with_meals) + b = Simulation(situation=no_meals) + # 6. Meals. + assert np.all(calc(a, "housing_benefit_meals_deduction") >= 0) + assert np.all(calc(a, "LHA_cap") <= calc(b, "LHA_cap") + 1e-6) + # 7. Council tax shares, where the rent is shared. (Elsewhere the family + # of the household's oldest adult claims, and families tied for oldest + # both do: a separate, existing issue.) + claimant = calc(a, "council_tax_reduction_claimant_benunit") + share = calc(a, "council_tax_reduction_joint_liability_share") + total = a.map_result(claimant * share, "benunit", "household") + sharer = calc(a, "liable_for_share_of_household_rent") + shared = a.map_result(sharer, "benunit", "household") > 0 + assert np.all(total[shared] <= 1 + 1e-6) + + +@PROPERTY_SETTINGS +@given(population, st.floats(1, 10_000, allow_nan=False)) +def test_rent_is_monotone_in_household_rent(population, bump): + base, _ = build(population) + raised, _ = build(population, rent_bump=bump) + before = calc(Simulation(situation=base), "benunit_rent") + after = calc(Simulation(situation=raised), "benunit_rent") + assert np.all(after >= before - 1e-6) + + +@PROPERTY_SETTINGS +@given(population) +def test_no_op_without_sharers_boarders_or_lodgers(population): + for house in population: + for other in house["others"]: + other["role"] = "non_dependant" + situation, roles = build(population) + sim = Simulation(situation=situation) + # 9. The head's family has the whole rent; everyone else none. + rent = calc(sim, "benunit_rent") + head = roles == "head" + assert np.all(rent[~head] == 0) + rent_total = sim.map_result(rent, "benunit", "household") + assert np.allclose(rent_total, calc(sim, "rent"), atol=0.01) + # The previous formula: the head's family was charged for everyone + # outside it (and every other family likewise). + individual = calc(sim, "uc_individual_non_dep_deduction") + person_head_family = sim.map_result(head, "benunit", "person") > 0 + everyone = sim.map_result(individual, "person", "household") + head_family_own = sim.map_result( + individual * person_head_family, "person", "household" + ) + head_deductions = sim.map_result( + calc(sim, "uc_non_dep_deductions") * head, "benunit", "household" + ) + assert np.allclose(head_deductions, everyone - head_family_own, atol=0.01) diff --git a/policyengine_uk/variables/household/demographic/benunit_tenure_type.py b/policyengine_uk/variables/household/demographic/benunit_tenure_type.py index 45be66c94..c244b584a 100644 --- a/policyengine_uk/variables/household/demographic/benunit_tenure_type.py +++ b/policyengine_uk/variables/household/demographic/benunit_tenure_type.py @@ -1,4 +1,5 @@ from policyengine_uk.model_api import * +from policyengine_core.enums import EnumArray from policyengine_uk.variables.household.demographic.tenure_type import ( TenureType, ) @@ -33,8 +34,11 @@ def formula(benunit, period, parameters): pays_householder = benunit.any( benunit.members("pays_rent_to_householder", period) ) - return where( - pays_householder, - TenureType.RENT_PRIVATELY, - household_tenure.decode(), + return EnumArray( + where( + pays_householder, + TenureType.RENT_PRIVATELY.index, + household_tenure, + ).astype(np.int16), + TenureType, ) From 53c327d362df73774bc73594c44fa7df53c3173a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 1 Oct 2026 11:54:55 -0400 Subject: [PATCH 5/9] Use the companion branch's boarder and lodger input definitions verbatim rent_paid_as_boarder, rent_paid_as_lodger and pays_rent_to_householder are copied byte for byte from boarder-lodger-rent (PolicyEngine/policyengine-uk#2002) at the same paths, so the two branches merge without conflict in either order. Co-Authored-By: Claude Opus 5.5 --- .../rent/pays_rent_to_householder.py | 16 ------------ .../consumption/rent/rent_paid_as_boarder.py | 13 ---------- .../consumption/rent/rent_paid_as_lodger.py | 13 ---------- .../income/pays_rent_to_householder.py | 25 +++++++++++++++++++ .../variables/input/rent_paid_as_boarder.py | 17 +++++++++++++ .../variables/input/rent_paid_as_lodger.py | 17 +++++++++++++ 6 files changed, 59 insertions(+), 42 deletions(-) delete mode 100644 policyengine_uk/variables/household/consumption/rent/pays_rent_to_householder.py delete mode 100644 policyengine_uk/variables/household/consumption/rent/rent_paid_as_boarder.py delete mode 100644 policyengine_uk/variables/household/consumption/rent/rent_paid_as_lodger.py create mode 100644 policyengine_uk/variables/household/income/pays_rent_to_householder.py create mode 100644 policyengine_uk/variables/input/rent_paid_as_boarder.py create mode 100644 policyengine_uk/variables/input/rent_paid_as_lodger.py diff --git a/policyengine_uk/variables/household/consumption/rent/pays_rent_to_householder.py b/policyengine_uk/variables/household/consumption/rent/pays_rent_to_householder.py deleted file mode 100644 index 7a510c402..000000000 --- a/policyengine_uk/variables/household/consumption/rent/pays_rent_to_householder.py +++ /dev/null @@ -1,16 +0,0 @@ -from policyengine_uk.model_api import * - - -class pays_rent_to_householder(Variable): - value_type = bool - entity = Person - label = "Family pays rent to the householder" - documentation = ( - "Whether any member of this person's benefit unit pays the " - "householder rent as a boarder or lodger." - ) - definition_period = YEAR - - def formula(person, period, parameters): - payments = add(person, period, ["rent_paid_as_boarder", "rent_paid_as_lodger"]) - return person.benunit.any(payments > 0) diff --git a/policyengine_uk/variables/household/consumption/rent/rent_paid_as_boarder.py b/policyengine_uk/variables/household/consumption/rent/rent_paid_as_boarder.py deleted file mode 100644 index 47d6d220d..000000000 --- a/policyengine_uk/variables/household/consumption/rent/rent_paid_as_boarder.py +++ /dev/null @@ -1,13 +0,0 @@ -from policyengine_uk.model_api import * - - -class rent_paid_as_boarder(Variable): - value_type = float - entity = Person - label = "Rent paid as a boarder" - documentation = ( - "Rent this person pays the householder for board and lodging " - "(accommodation with at least some meals) in the householder's home." - ) - definition_period = YEAR - unit = GBP diff --git a/policyengine_uk/variables/household/consumption/rent/rent_paid_as_lodger.py b/policyengine_uk/variables/household/consumption/rent/rent_paid_as_lodger.py deleted file mode 100644 index e53796d73..000000000 --- a/policyengine_uk/variables/household/consumption/rent/rent_paid_as_lodger.py +++ /dev/null @@ -1,13 +0,0 @@ -from policyengine_uk.model_api import * - - -class rent_paid_as_lodger(Variable): - value_type = float - entity = Person - label = "Rent paid as a lodger" - documentation = ( - "Rent this person pays the householder for lodging only in the " - "householder's home." - ) - definition_period = YEAR - unit = GBP diff --git a/policyengine_uk/variables/household/income/pays_rent_to_householder.py b/policyengine_uk/variables/household/income/pays_rent_to_householder.py new file mode 100644 index 000000000..2cc695e85 --- /dev/null +++ b/policyengine_uk/variables/household/income/pays_rent_to_householder.py @@ -0,0 +1,25 @@ +from policyengine_uk.model_api import * + + +class pays_rent_to_householder(Variable): + value_type = bool + entity = Person + label = "in a family that pays the householder rent as boarders or lodgers" + documentation = ( + "Whether this person's benefit unit, which does not contain the " + "household head, pays the household head rent for board and lodging " + "or for lodging. Such a person is liable to make payments on a " + "commercial basis for their occupation, so is not a non-dependant of " + "the householder." + ) + definition_period = YEAR + reference = [ + "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/9", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/3", + "https://www.legislation.gov.uk/uksi/2012/2885/regulation/9", + ] + + def formula(person, period, parameters): + paid = add(person, period, ["rent_paid_as_boarder", "rent_paid_as_lodger"]) + in_head_benunit = person.benunit.any(person("is_household_head", period)) + return person.benunit.any(paid > 0) & ~in_head_benunit diff --git a/policyengine_uk/variables/input/rent_paid_as_boarder.py b/policyengine_uk/variables/input/rent_paid_as_boarder.py new file mode 100644 index 000000000..ba168a0ce --- /dev/null +++ b/policyengine_uk/variables/input/rent_paid_as_boarder.py @@ -0,0 +1,17 @@ +from policyengine_uk.model_api import * + + +class rent_paid_as_boarder(Variable): + value_type = float + entity = Person + label = "rent paid to the householder as a boarder" + documentation = ( + "Rent this person pays the householder for board and lodging (a room " + "and at least some meals) in the householder's home, where this " + "person lives as a member of the household outside the householder's " + "benefit unit. The Family Resources Survey records it on the payer " + "(CVPAY where CONVBL is 1)." + ) + definition_period = YEAR + unit = GBP + uprating = "gov.economic_assumptions.indices.obr.per_capita.gdp" diff --git a/policyengine_uk/variables/input/rent_paid_as_lodger.py b/policyengine_uk/variables/input/rent_paid_as_lodger.py new file mode 100644 index 000000000..f788d9441 --- /dev/null +++ b/policyengine_uk/variables/input/rent_paid_as_lodger.py @@ -0,0 +1,17 @@ +from policyengine_uk.model_api import * + + +class rent_paid_as_lodger(Variable): + value_type = float + entity = Person + label = "rent paid to the householder as a lodger" + documentation = ( + "Rent this person pays the householder for lodging without meals in " + "the householder's home, where this person lives as a member of the " + "household outside the householder's benefit unit. The Family " + "Resources Survey records it on the payer (CVPAY where CONVBL is not " + "1)." + ) + definition_period = YEAR + unit = GBP + uprating = "gov.economic_assumptions.indices.obr.per_capita.gdp" From 190482e33a9629fa88d141c04b1dbd0bd38893da Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 1 Oct 2026 12:02:28 -0400 Subject: [PATCH 6/9] Changelog for #2005 Co-Authored-By: Claude Opus 5.5 --- changelog.d/2005.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/2005.added.md diff --git a/changelog.d/2005.added.md b/changelog.d/2005.added.md new file mode 100644 index 000000000..9680d4e64 --- /dev/null +++ b/changelog.d/2005.added.md @@ -0,0 +1 @@ +- Give joint tenants and other sharers their share of the household's rent, boarders and lodgers their own rent and private tenure, a Housing Benefit LHA category (with its own size criteria and shared-accommodation rule) beside the Universal Credit one, statutory non-dependant exclusions and apportionment, the Housing Benefit meals deduction, and Council Tax Reduction on the council tax divided among jointly liable people. From b065a48d2ccedc7f90e9e86a9c18195b89072538 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 1 Oct 2026 14:03:13 -0400 Subject: [PATCH 7/9] Keep each family's share of the household's rent on one person personal_rent puts the family's share (share_of_household_rent, per person liable) on the household head for the head's family and on the family's head otherwise, so the household head still carries the whole rent when no one else is liable (test_deterministic_variables TestHeadOrdering). Family totals and the UC Sch 4 para 24(4) split are unchanged. Co-Authored-By: Claude Opus 5.5 --- .../rent_of_sharers_boarders_and_lodgers.yaml | 7 ++-- .../household/consumption/personal_rent.py | 34 ++++++------------- .../rent/share_of_household_rent.py | 3 +- 3 files changed, 17 insertions(+), 27 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/rent_of_sharers_boarders_and_lodgers.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/rent_of_sharers_boarders_and_lodgers.yaml index 60db0c179..e1ee4a617 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/rent_of_sharers_boarders_and_lodgers.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/rent_of_sharers_boarders_and_lodgers.yaml @@ -4,7 +4,7 @@ # UC Regs 2013 (SI 2013/376) Sch 1 para 2, Sch 4 paras 9, 10, 20, 24, 25, 27-29. # HB Regs 2006 (SI 2006/213) regs 3, 12, 12B, 13, 13C, 13D, Sch 1 para 2. -- name: The household head's couple shares the household's rent equally +- name: The household head carries the rent of a couple liable for it period: 2025 absolute_error_margin: 0.01 input: @@ -22,8 +22,9 @@ members: [head, partner] rent: 12_000 output: - # Two people liable: 12,000 / 2 each; the family's total is the rent. - personal_rent: [6_000, 6_000] + # The family's share (the whole rent) sits on the household head. + share_of_household_rent: 1 + personal_rent: [12_000, 0] benunit_rent: 12_000 - name: Sharers split the household's rent per person liable diff --git a/policyengine_uk/variables/household/consumption/personal_rent.py b/policyengine_uk/variables/household/consumption/personal_rent.py index f121bee0f..8166a552a 100644 --- a/policyengine_uk/variables/household/consumption/personal_rent.py +++ b/policyengine_uk/variables/household/consumption/personal_rent.py @@ -6,12 +6,12 @@ class personal_rent(Variable): entity = Person label = "Rent liable" documentation = ( - "The rent this person is liable for: an equal share of the " - "household's rent among the people liable for it, plus anything this " - "person pays the householder as a boarder or lodger. Where the " - "household head's family is the only one liable, its claimant and " - "partner share the whole rent, so the family's total is the " - "household's rent." + "The rent this person is liable for. Each family's share of the " + "household's rent (see share_of_household_rent) sits on one person: " + "the household head for the household head's family, otherwise the " + "family's head. Anything a person pays the householder as a boarder " + "or lodger is their own. Where the household head's family is the " + "only one liable, the household head carries the whole rent." ) definition_period = YEAR unit = GBP @@ -23,27 +23,15 @@ class personal_rent(Variable): ) def formula(person, period, parameters): - # Where people in more than one family are liable for the same rent, - # each person's core rent is the total divided by the number of - # people liable (UC Regs 2013 Sch 4 para 24(4): A / B x C), which also - # apportions Housing Benefit eligible rent by the number of people - # liable (HB Regs 2006 reg 12B(4)). rent = person.household("rent", period) - liable = person("is_liable_for_household_rent", period) - liable_people = person.household.sum(liable) - # A household with nobody marked liable (for example a head who is - # not a claimant or partner) leaves the rent with the household head. - head = person("is_household_head", period) - heads = person.household.sum(head) - share = where( - liable_people > 0, - liable / max_(liable_people, 1), - head / max_(heads, 1), - ) + share = person.benunit("share_of_household_rent", period) + household_head = person("is_household_head", period) + head_family = person.benunit.any(household_head) + holder = where(head_family, household_head, person("is_benunit_head", period)) # A licence or other permission to occupy is a rent payment (UC Regs # 2013 Sch 1 para 2(b); HB Regs 2006 reg 12(1)): boarders and lodgers # are liable for what they pay the householder. paid_to_householder = add( person, period, ["rent_paid_as_boarder", "rent_paid_as_lodger"] ) - return rent * share + paid_to_householder + return rent * share * holder + paid_to_householder diff --git a/policyengine_uk/variables/household/consumption/rent/share_of_household_rent.py b/policyengine_uk/variables/household/consumption/rent/share_of_household_rent.py index 9588c5184..4b6337437 100644 --- a/policyengine_uk/variables/household/consumption/rent/share_of_household_rent.py +++ b/policyengine_uk/variables/household/consumption/rent/share_of_household_rent.py @@ -8,7 +8,8 @@ class share_of_household_rent(Variable): documentation = ( "The share of the household's rent this family is liable for: the " "people liable for it in this family over all the people liable for " - "it. The household head's family has the whole rent unless other " + "it (UC Regs 2013 Sch 4 para 24(4): A / B x C; HB Regs 2006 reg " + "12B(4)). The household head's family has the whole rent unless other " "families are liable for a share." ) definition_period = YEAR From 01d140e7742bde0d25496d497d78c3744fdf022b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 08:36:02 -0400 Subject: [PATCH 8/9] Address the independent review of #2006 - UC non-dependants count in one claim: the first family liable for the rent that is eligible for and claims UC, not always the household head's (Sch 4 para 9(2)(f)). New uc_non_dependants_counted, used by deductions, bedrooms and the specified-renter test. - HB meals are deducted only where a rent officer has found a substantial part of the rent is board and attendance (reg 13C(5)(e), 13(7)); the LHA cap rent is gross (regs 12D, 13D(12)). New input housing_benefit_board_and_attendance_determination. - HB severe disability premium conditions (Sch 3 para 14; SPC Sch 3 para 6) replace the tax credit severe disability test in the HB LHA category, so a PIP daily living recipient living alone keeps the one-bedroom rate. - HB size criteria over the claim's occupiers: one room per couple, one per other occupier aged 16 or over, children paired across occupiers (reg 13D(3), (12)). - CTR non-dependants exclude claimant families, sharers and people paying the householder, not any rent-liable family; excluded students are left out of the joint-liability count and do not claim as sharers (SI 2012/2885 Sch 1 para 7(5); Default Scheme Sch para 75(1)). - Property tests: generated earnings, a fixed household with every role, the single-claim and apportionment invariants, and bedroom and deduction no-op checks against the previous formulas. Co-Authored-By: Claude Opus 5.5 --- .../qualifying_benefits.yaml | 16 ++ .../housing_benefit_meals_deduction.yaml | 61 +++- .../rent_of_sharers_boarders_and_lodgers.yaml | 132 ++++++++- .../joint_liability.yaml | 105 +++++++ ...sharers_boarders_and_lodgers_properties.py | 264 +++++++++++++----- .../variables/gov/dwp/LHA_allowed_bedrooms.py | 44 +-- policyengine_uk/variables/gov/dwp/LHA_cap.py | 41 +-- .../housing_benefit_entitlement.py | 19 +- .../housing_benefit_has_non_dependant.py | 37 +++ ...nefit_severe_disability_premium_applies.py | 50 ++++ .../housing_benefit_LHA_allowed_bedrooms.py | 66 +++-- .../gov/dwp/housing_benefit_LHA_category.py | 6 +- .../housing_benefit_shares_accommodation.py | 10 +- .../dwp/is_non_dependant_of_household_head.py | 16 +- .../gov/dwp/lha_renter_has_non_dependant.py | 20 +- .../uc_non_dep_deductions.py | 11 +- .../uc_non_dependants_counted.py | 40 +++ .../council_tax_reduction_claimant_benunit.py | 11 +- ...n_individual_non_dep_deduction_eligible.py | 15 +- ...cil_tax_reduction_joint_liability_share.py | 25 +- ...efit_board_and_attendance_determination.py | 20 ++ 21 files changed, 824 insertions(+), 185 deletions(-) create mode 100644 policyengine_uk/parameters/gov/dwp/housing_benefit/severe_disability_premium/qualifying_benefits.yaml create mode 100644 policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_has_non_dependant.py create mode 100644 policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_severe_disability_premium_applies.py create mode 100644 policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dependants_counted.py create mode 100644 policyengine_uk/variables/household/consumption/rent/housing_benefit_board_and_attendance_determination.py diff --git a/policyengine_uk/parameters/gov/dwp/housing_benefit/severe_disability_premium/qualifying_benefits.yaml b/policyengine_uk/parameters/gov/dwp/housing_benefit/severe_disability_premium/qualifying_benefits.yaml new file mode 100644 index 000000000..82d79f1db --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/severe_disability_premium/qualifying_benefits.yaml @@ -0,0 +1,16 @@ +description: Benefits whose receipt meets the qualifying-benefit condition of the Housing Benefit severe disability premium. +values: + 2013-04-08: + - attendance_allowance + - dla_sc_middle_plus + - pip_dl + - armed_forces_independence_payment +metadata: + unit: list + label: housing benefit severe disability premium qualifying benefits + reference: + - title: Housing Benefit Regulations 2006 Sch 3 para 14(2) + href: https://www.legislation.gov.uk/uksi/2006/213/schedule/3/paragraph/14 + - title: Housing Benefit (State Pension Credit) Regulations 2006 Sch 3 para 6 + href: https://www.legislation.gov.uk/uksi/2006/214/schedule/3/paragraph/6 + note: The Scottish benefits in the same list (adult disability payment, pension age disability payment, child disability payment and Scottish adult disability living allowance care components) have no separate variables in the model. diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_meals_deduction.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_meals_deduction.yaml index d86d6feaa..9bd91118a 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_meals_deduction.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_meals_deduction.yaml @@ -72,7 +72,7 @@ # 2 x 4.50 x 52 = 468. housing_benefit_meals_deduction: 468 -- name: Meals come off the rent before the LHA cap +- name: On the LHA route the cap rent is the gross rent, meals included period: 2026 absolute_error_margin: 0.01 input: @@ -95,10 +95,65 @@ members: [householder, boarder] tenure_type: OWNED_OUTRIGHT output: - # Reg 13(7): rent 10,000 less 37.30 x 52 = 1,939.60 is 8,060.40, below - # the 9,000 rate standing in for the rent officer's figure. + # Regs 12D(2)(a), 13D(5) and (12): without a rent officer's board and + # attendance finding, min(gross rent 10,000, LHA 9,000) = 9,000. + LHA_cap: [0, 9_000] + +- name: A rent officer's board finding takes meals off the rent instead + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 60 + is_household_head: true + boarder: + age: 70 + rent_paid_as_boarder: 10_000 + benunits: + householder_family: + members: [householder] + boarder_family: + members: [boarder] + meals_included_in_rent: AT_LEAST_THREE_A_DAY + housing_benefit_board_and_attendance_determination: true + housing_benefit_LHA_rate: 9_000 + households: + home: + members: [householder, boarder] + tenure_type: OWNED_OUTRIGHT + output: + # Reg 13C(5)(e): off the LHA. Reg 13(7): rent 10,000 less + # 37.30 x 52 = 1,939.60 is 8,060.40. LHA_cap: [0, 8_060.40] +- name: Breakfast without a board finding leaves Housing Benefit on the gross rent + period: 2025 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 60 + is_household_head: true + boarder: + age: 70 + rent_paid_as_boarder: 6_000 + benunits: + householder_family: + members: [householder] + boarder_family: + members: [boarder] + meals_included_in_rent: BREAKFAST_ONLY + housing_benefit_LHA_rate: 9_000 + housing_benefit_applicable_income: 0 + households: + home: + members: [householder, boarder] + tenure_type: OWNED_OUTRIGHT + output: + LHA_cap: [0, 6_000] + housing_benefit_entitlement: [0, 6_000] + - name: Meals do not reduce the Universal Credit housing costs element period: 2026 absolute_error_margin: 0.01 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/rent_of_sharers_boarders_and_lodgers.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/rent_of_sharers_boarders_and_lodgers.yaml index e1ee4a617..990077e67 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/rent_of_sharers_boarders_and_lodgers.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/rent_of_sharers_boarders_and_lodgers.yaml @@ -290,21 +290,23 @@ is_household_head: true sharer: age: 40 + pip_dl: 3_000 benunits: head_family: members: [head] sharer_family: members: [sharer] liable_for_share_of_household_rent: true - severe_disability_premium: 3_000 households: home: members: [head, sharer] rent: 18_000 tenure_type: RENT_PRIVATELY output: - # Reg 13D(2)(a)(ii) excludes a claimant to whom Sch 3 para 14 applies, - # so 13D(2)(c): 1 bedroom: B. + # The sharer receives the PIP daily living component and has no + # non-dependant, so Sch 3 para 14(2)(a) applies; reg 13D(2)(a)(ii) + # excludes them, so 13D(2)(c): 1 bedroom: B. + housing_benefit_severe_disability_premium_applies: [false, true] housing_benefit_LHA_category: [A, B] - name: The household input for shared accommodation affects Housing Benefit only @@ -325,3 +327,127 @@ output: LHA_category: B housing_benefit_LHA_category: A + +- name: A non-dependant counts in a sharer's UC claim when the head cannot claim UC + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 70 + is_household_head: true + cotenant: + age: 30 + non_dependant: + age: 24 + employment_income: 20_000 + benunits: + head_family: + members: [head] + cotenant_family: + members: [cotenant] + liable_for_share_of_household_rent: true + non_dependant_family: + members: [non_dependant] + households: + home: + members: [head, cotenant, non_dependant] + rent: 18_000 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + # The pensioner head is not eligible for UC, so no other UC claim has + # treated the non-dependant as one (UC Sch 4 para 9(2)(f)): they count + # in the co-tenant's claim. Bedrooms 1 + 1 = 2: C. Core rent 18,000 / 2 + # = 9,000, below the Maidstone two-bedroom rate (11,892.40); less one + # contribution of 93.02 x 12 = 1,116.24: 7,883.76. + uc_non_dependants_counted: [false, true, false] + LHA_allowed_bedrooms: [1, 2, 1] + LHA_category: [B, C, A] + uc_non_dep_deductions: [0, 1_116.24, 0] + uc_housing_costs_element: [9_000, 7_883.76, 0] + +- name: A single PIP recipient living alone keeps the HB one-bedroom rate + period: 2025 + input: + people: + claimant: + age: 28 + pip_dl: 3_988.40 + benunits: + family: + members: [claimant] + households: + home: + members: [claimant] + rent: 9_000 + tenure_type: RENT_PRIVATELY + output: + # Sch 3 para 14(2)(a): PIP daily living and no non-dependant, so the + # young-individual shared rate does not apply (reg 13D(2)(a)(i)); not + # sharing: one bedroom self-contained (13D(2)(b)): B. UC: excepted under + # Sch 4 para 29(5): B. + housing_benefit_severe_disability_premium_applies: true + housing_benefit_LHA_category: B + LHA_category: B + +- name: A lodger couple adds one HB bedroom, a non-dependant's child another + period: 2025 + input: + people: + head: + age: 72 + is_household_head: true + lodger_1: + age: 40 + rent_paid_as_lodger: 3_000 + lodger_2: + age: 42 + daughter: + age: 40 + grandchild: + age: 5 + benunits: + head_family: + members: [head] + lodgers: + members: [lodger_1, lodger_2] + daughter_family: + members: [daughter, grandchild] + households: + home: + members: [head, lodger_1, lodger_2, daughter, grandchild] + rent: 15_000 + tenure_type: RENT_PRIVATELY + output: + # Reg 13D(3): head 1; the lodger couple 1 (a couple); the daughter 1; + # the grandchild 1 (a child): 4 bedrooms for the head: E. The lodgers' + # own claim counts only themselves: 1 bedroom, shared: A. + housing_benefit_LHA_allowed_bedrooms: [4, 1, 2] + housing_benefit_LHA_category: [E, A, C] + +- name: A lodger with exclusive use of two rooms can rebut the shared presumption + period: 2025 + input: + people: + householder: + age: 60 + is_household_head: true + lodger: + age: 70 + rent_paid_as_lodger: 5_200 + benunits: + householder_family: + members: [householder] + lodger_family: + members: [lodger] + housing_benefit_shares_accommodation: false + households: + home: + members: [householder, lodger] + tenure_type: OWNED_OUTRIGHT + output: + # Reg 13D(2)(b)(i): the lodger has exclusive use of two rooms: one + # bedroom self-contained: B. The householder's size criteria count the + # lodger as an occupier (13D(12)): 2 bedrooms: C. + housing_benefit_LHA_category: [C, B] diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/joint_liability.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/joint_liability.yaml index 47b722480..31efa46d3 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/joint_liability.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/joint_liability.yaml @@ -154,3 +154,108 @@ council_tax_reduction_individual_non_dep_deduction_eligible: [false, false] council_tax_reduction_joint_liability_share: [1, 1] simulated_council_tax_reduction_benunit: [1_800, 0] + +- name: An older parent who is not liable stays the claimant where no rent is shared + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + is_household_head: true + employment_income: 30_000 + father: + age: 72 + state_pension: 6_000 + benunits: + head_family: + members: [head] + father_family: + members: [father] + households: + home: + members: [head, father] + rent: 9_000 + tenure_type: RENT_PRIVATELY + council_tax: 2_000 + country: ENGLAND + region: LONDON + output: + # No sharer: the oldest adult's family claims, as before, and the + # rent-paying head is the father's non-dependant, as before. + council_tax_reduction_claimant_benunit: [false, true] + council_tax_reduction_individual_non_dep_deduction_eligible: [true, false] + simulated_council_tax_reduction_benunit: [0, 0] + +- name: A non-liable resident is a non-dependant of the sharers however old + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + is_household_head: true + cotenant: + age: 35 + father: + age: 72 + council_tax_reduction_individual_non_dep_deduction: 300 + benunits: + head_family: + members: [head] + council_tax_reduction_applicable_income: 0 + cotenant_family: + members: [cotenant] + liable_for_share_of_household_rent: true + council_tax_reduction_applicable_income: 0 + father_family: + members: [father] + households: + home: + members: [head, cotenant, father] + rent: 9_000 + tenure_type: RENT_PRIVATELY + council_tax: 1_800 + country: WALES + region: WALES + output: + council_tax_reduction_claimant_benunit: [true, true, false] + council_tax_reduction_individual_non_dep_deduction_eligible: [false, false, true] + # 300 apportioned equally between the two liable persons: 150 each. + council_tax_reduction_non_dep_deductions: [150, 150, 0] + # 1,800 / 2 - 150 = 750 each. + simulated_council_tax_reduction_benunit: [750, 750, 0] + +- name: An excluded student sharer is not counted and does not claim + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 72 + is_household_head: true + student: + age: 25 + in_HE: true + benunits: + head_family: + members: [head] + council_tax_reduction_applicable_income: 0 + student_family: + members: [student] + liable_for_share_of_household_rent: true + households: + home: + members: [head, student] + rent: 9_000 + tenure_type: RENT_PRIVATELY + council_tax: 1_800 + country: WALES + region: WALES + output: + # Sch 1 para 7(5) (Wales Sch 1 para 2(5)): the student is left out of + # the count, so the pensioner's share is 1; a full-time student is + # excluded from entitlement (Default Scheme Sch para 75(1)). + council_tax_reduction_joint_liability_share: [1, 0.5] + council_tax_reduction_claimant_benunit: [true, false] + simulated_council_tax_reduction_benunit: [1_800, 0] diff --git a/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py b/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py index c3b3efca5..aedadee76 100644 --- a/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py +++ b/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py @@ -1,6 +1,10 @@ """Property-based tests for the rent, tenure and housing support of families other than the household head's: sharers of the rent, boarders and lodgers. +Every generated population also contains one fixed household with a sharer, +a boarder, a lodger and a non-dependant, so no selection below is empty. +Adults have generated earnings, so non-dependant deductions are positive. + Invariants, for any generated population of households: 1. Conservation: the families' shares of the household's rent sum to one @@ -11,23 +15,31 @@ or lodger has no rent. 3. Tenure: a boarder's or lodger's family rents privately, is never in social housing, and is LHA-eligible. -4. Non-dependants: a family liable for rent is never a non-dependant; only - the household head's family has Universal Credit non-dependant - deductions; a boarder's or lodger's family has no Housing Benefit or - Council Tax Reduction non-dependant deductions. +4. Non-dependants: nobody in a family liable for rent is a non-dependant; at + most one family per household counts the household's non-dependants for + Universal Credit, it is liable for the household's rent, and only it has + Universal Credit deductions; Housing Benefit deductions over the families + liable for the rent sum to the household's non-dependants' deductions; a + boarder's or lodger's family has no Universal Credit or Housing Benefit + deductions; no sharer, boarder or lodger is a Council Tax Reduction + non-dependant. 5. Category: the Universal Credit LHA category does not depend on the household input is_shared_accommodation; the Housing Benefit category is the shared rate for any family entitled to one bedroom that lacks - exclusive use and has no severe disability premium. -6. Meals: the Housing Benefit meals deduction is non-negative, and adding - meals never raises the Housing Benefit eligible rent (LHA_cap). + exclusive use and does not meet the severe disability premium conditions. +6. Meals: the Housing Benefit meals deduction is non-negative; on the LHA + route (no rent officer board finding) the eligible rent does not depend + on the meals in the rent; with a board finding it is the rent less the + deduction, floored at zero. 7. Council tax: in a household whose rent is shared, the jointly liable claim shares never exceed one in total. 8. Monotonicity: a family's rent is non-decreasing in the household's rent. 9. No-op: in a household with no sharers, boarders or lodgers, the household - head's family has the whole rent and everyone else none, as before, and - the head's Universal Credit non-dependant deductions equal the previous - formula (every other family's deductions). + head's family has the whole rent and everyone else none, as before; and + where the head's family claims Universal Credit, its non-dependant + deductions and bedrooms equal the previous formulas (a deduction for + everyone outside the family and a bedroom for each of them aged 16 or + over). """ import numpy as np @@ -53,24 +65,45 @@ ROLES = ["sharer", "boarder", "lodger", "non_dependant"] MEALS = ["NONE", "BREAKFAST_ONLY", "FEWER_THAN_THREE_A_DAY", "AT_LEAST_THREE_A_DAY"] money = st.floats(0, 30_000, allow_nan=False, allow_infinity=False) +earnings = st.one_of(st.just(0.0), st.floats(1_000, 60_000, allow_nan=False)) +adult = st.tuples(st.integers(18, 85), earnings) +# One household with every role, so that no selection is empty. +SENTINEL = dict( + head_adults=[(45, 0.0)], + head_child_age=None, + tenure="RENT_PRIVATELY", + rent=18_000.0, + council_tax=1_800.0, + others=[ + dict(role=role, adults=[(age, pay)], child_age=None, payment=5_200.0) + | dict(meals="AT_LEAST_THREE_A_DAY", pip=False) + for role, age, pay in [ + ("sharer", 40, 0.0), + ("boarder", 50, 0.0), + ("lodger", 30, 0.0), + ("non_dependant", 25, 25_000.0), + ] + ], + shared=False, +) @st.composite def other_family(draw): return dict( role=draw(st.sampled_from(ROLES)), - ages=draw(st.lists(st.integers(18, 85), min_size=1, max_size=2)), + adults=draw(st.lists(adult, min_size=1, max_size=2)), child_age=draw(st.one_of(st.none(), st.integers(0, 15))), payment=draw(st.floats(1, 15_000, allow_nan=False)), meals=draw(st.sampled_from(MEALS)), - sdp=draw(st.booleans()), + pip=draw(st.booleans()), ) @st.composite def households(draw): return dict( - head_ages=draw(st.lists(st.integers(18, 85), min_size=1, max_size=2)), + head_adults=draw(st.lists(adult, min_size=1, max_size=2)), head_child_age=draw(st.one_of(st.none(), st.integers(0, 15))), tenure=draw(st.sampled_from(TENURES)), rent=draw(money), @@ -80,24 +113,44 @@ def households(draw): ) -population = st.lists(households(), min_size=1, max_size=6) +population = st.lists(households(), min_size=0, max_size=5).map( + lambda generated: [SENTINEL] + generated +) -def build(population, shared_override=None, meals_override=None, rent_bump=0.0): +def build( + population, + shared_override=None, + meals_override=None, + board_finding=False, + rent_bump=0.0, + roles_override=None, +): """One situation for the whole population; returns it and family roles.""" people, benunits, homes, roles = {}, {}, {}, [] for h, house in enumerate(population): members = [] - def add_family(name, ages, child_age, role, extra): + def add_family(name, adults, child_age, role, extra, pip=False): ids = [] - for i, age in enumerate(ages): + for i, (age, pay) in enumerate(adults): pid = f"{name}_adult_{i}" - people[pid] = {"age": age, "is_claimant_or_partner": True} + people[pid] = { + "age": age, + "employment_income": pay, + "is_claimant_or_partner": True, + "is_household_head": False, + } + if pip: + people[pid]["pip_dl"] = 3_988.40 ids.append(pid) if child_age is not None: pid = f"{name}_child" - people[pid] = {"age": child_age, "is_claimant_or_partner": False} + people[pid] = { + "age": child_age, + "is_claimant_or_partner": False, + "is_household_head": False, + } ids.append(pid) benunits[name] = {"members": ids, **extra} members.extend(ids) @@ -105,31 +158,31 @@ def add_family(name, ages, child_age, role, extra): return ids head_ids = add_family( - f"h{h}_head", house["head_ages"], house["head_child_age"], "head", {} + f"h{h}_head", house["head_adults"], house["head_child_age"], "head", {} ) - for pid in head_ids: - people[pid]["is_household_head"] = False people[head_ids[0]]["is_household_head"] = True for f, other in enumerate(house["others"]): - name = f"h{h}_f{f}" + role = roles_override or other["role"] + boarder = role == "boarder" extra = { - "liable_for_share_of_household_rent": other["role"] == "sharer", + "liable_for_share_of_household_rent": role == "sharer", "meals_included_in_rent": ( - meals_override or other["meals"] - if other["role"] == "boarder" - else "NONE" + (meals_override or other["meals"]) if boarder else "NONE" + ), + "housing_benefit_board_and_attendance_determination": ( + board_finding and boarder ), } - if other["sdp"]: - extra["severe_disability_premium"] = 3_000 ids = add_family( - name, other["ages"], other["child_age"], other["role"], extra + f"h{h}_f{f}", + other["adults"], + other["child_age"], + role, + extra, + pip=other["pip"], ) - for pid in ids: - people[pid]["is_household_head"] = False - if other["role"] in ("boarder", "lodger"): - variable = f"rent_paid_as_{other['role']}" - people[ids[0]][variable] = other["payment"] + if role in ("boarder", "lodger"): + people[ids[0]][f"rent_paid_as_{role}"] = other["payment"] homes[f"h{h}"] = { "members": members, "tenure_type": house["tenure"], @@ -150,6 +203,10 @@ def calc(simulation, variable, map_to=None): return np.asarray(result) +def per_household(simulation, benunit_values): + return simulation.map_result(benunit_values, "benunit", "household") + + @PROPERTY_SETTINGS @given(population) def test_conservation_bounds_and_tenure(population): @@ -162,10 +219,10 @@ def test_conservation_bounds_and_tenure(population): sim, "rent_paid_as_lodger", "household" ) # 1. Conservation. - share_total = sim.map_result(share, "benunit", "household") - assert np.allclose(share_total, 1, atol=1e-6) - rent_total = sim.map_result(rent, "benunit", "household") - assert np.allclose(rent_total, household_rent + paid, rtol=1e-6, atol=0.01) + assert np.allclose(per_household(sim, share), 1, atol=1e-6) + assert np.allclose( + per_household(sim, rent), household_rent + paid, rtol=1e-6, atol=0.01 + ) # 2. Bounds. assert np.all((share >= -1e-9) & (share <= 1 + 1e-9)) assert np.all(rent >= 0) @@ -173,7 +230,8 @@ def test_conservation_bounds_and_tenure(population): assert np.all(share[np.isin(roles, ["boarder", "lodger", "non_dependant"])] == 0) # 3. Tenure. payer = np.isin(roles, ["boarder", "lodger"]) - tenure = sim.calculate("benunit_tenure_type", YEAR) + assert payer.any() + tenure = calc(sim, "benunit_tenure_type") assert np.all(tenure[payer] == "RENT_PRIVATELY") social = calc(sim, "in_social_housing", "benunit") > 0 assert not np.any(social[payer]) @@ -185,14 +243,46 @@ def test_conservation_bounds_and_tenure(population): def test_non_dependants(population): situation, roles = build(population) sim = Simulation(situation=situation) - liable = calc(sim, "benunit_is_rent_liable") - non_dependant = calc(sim, "is_non_dependant_of_household_head", "benunit") > 0 - assert not np.any(non_dependant & liable) - uc = calc(sim, "uc_non_dep_deductions") - assert np.all(uc[roles != "head"] == 0) payer = np.isin(roles, ["boarder", "lodger"]) - assert np.all(calc(sim, "housing_benefit_non_dep_deductions")[payer] == 0) - assert np.all(calc(sim, "council_tax_reduction_non_dep_deductions")[payer] == 0) + liable_for_rent = calc(sim, "benunit_is_rent_liable") + non_dependant = calc(sim, "is_non_dependant_of_household_head") + person_liable = sim.map_result(liable_for_rent, "benunit", "person") > 0 + assert not np.any(non_dependant & person_liable) + assert non_dependant.any() + # Universal Credit: one claim per household counts the non-dependants. + counted = calc(sim, "uc_non_dependants_counted") + assert np.all(per_household(sim, counted) <= 1) + household_liable = ( + sim.map_result(calc(sim, "is_liable_for_household_rent"), "person", "benunit") + > 0 + ) + assert not np.any(counted & ~household_liable) + uc = calc(sim, "uc_non_dep_deductions") + assert np.all(uc[~counted] == 0) + individual = calc(sim, "uc_individual_non_dep_deduction") * non_dependant + total = sim.map_result(individual, "person", "household") + assert np.all(per_household(sim, uc) <= total + 0.01) + # Housing Benefit: apportioned over the families liable for the rent. + hb = calc(sim, "housing_benefit_non_dep_deductions") + hb_individual = ( + calc(sim, "household_benefits_individual_non_dep_deduction") * non_dependant + ) + hb_total = sim.map_result(hb_individual, "person", "household") + assert hb_total.max() > 0 + assert np.allclose(per_household(sim, hb), hb_total, rtol=1e-5, atol=0.01) + assert np.all(hb[payer] == 0) + assert np.all(uc[payer] == 0) + # Council Tax Reduction: sharers, boarders and lodgers are not + # non-dependants. + ctr_eligible = ( + sim.map_result( + calc(sim, "council_tax_reduction_individual_non_dep_deduction_eligible"), + "person", + "benunit", + ) + > 0 + ) + assert not np.any(ctr_eligible[np.isin(roles, ["sharer", "boarder", "lodger"])]) @PROPERTY_SETTINGS @@ -203,16 +293,18 @@ def test_lha_categories(population): a = Simulation(situation=with_input) b = Simulation(situation=without_input) # 5. The UC category is set by entitlement alone. - assert np.array_equal( - a.calculate("LHA_category", YEAR), - b.calculate("LHA_category", YEAR), - ) + assert np.array_equal(calc(a, "LHA_category"), calc(b, "LHA_category")) for sim in (a, b): rooms = calc(sim, "housing_benefit_LHA_allowed_bedrooms") shares = calc(sim, "housing_benefit_shares_accommodation") - sdp = calc(sim, "severe_disability_premium") > 0 - category = sim.calculate("housing_benefit_LHA_category", YEAR) - assert np.all(category[(rooms == 1) & shares & ~sdp] == "A") + sdp = calc(sim, "housing_benefit_severe_disability_premium_applies") + category = calc(sim, "housing_benefit_LHA_category") + selected = (rooms == 1) & shares & ~sdp + assert selected.any() + assert np.all(category[selected] == "A") + # The severe disability premium conditions keep a claimant off the + # shared rate (reg 13D(2)(a)). + assert not np.any(category[sdp] == "A") @PROPERTY_SETTINGS @@ -220,19 +312,33 @@ def test_lha_categories(population): def test_meals_and_council_tax(population): with_meals, roles = build(population, meals_override="AT_LEAST_THREE_A_DAY") no_meals, _ = build(population, meals_override="NONE") + finding, _ = build( + population, meals_override="AT_LEAST_THREE_A_DAY", board_finding=True + ) a = Simulation(situation=with_meals) b = Simulation(situation=no_meals) + c = Simulation(situation=finding) + boarder = roles == "boarder" # 6. Meals. - assert np.all(calc(a, "housing_benefit_meals_deduction") >= 0) - assert np.all(calc(a, "LHA_cap") <= calc(b, "LHA_cap") + 1e-6) + deduction = calc(a, "housing_benefit_meals_deduction") + assert np.all(deduction >= 0) + assert np.all(deduction[boarder] > 0) + assert np.allclose(calc(a, "LHA_cap"), calc(b, "LHA_cap"), atol=0.01) + rent = calc(c, "benunit_rent") + assert np.allclose( + calc(c, "LHA_cap")[boarder], + np.maximum(0, rent - deduction)[boarder], + atol=0.01, + ) # 7. Council tax shares, where the rent is shared. (Elsewhere the family # of the household's oldest adult claims, and families tied for oldest # both do: a separate, existing issue.) claimant = calc(a, "council_tax_reduction_claimant_benunit") share = calc(a, "council_tax_reduction_joint_liability_share") - total = a.map_result(claimant * share, "benunit", "household") + total = per_household(a, claimant * share) sharer = calc(a, "liable_for_share_of_household_rent") - shared = a.map_result(sharer, "benunit", "household") > 0 + shared = per_household(a, sharer) > 0 + assert shared.any() assert np.all(total[shared] <= 1 + 1e-6) @@ -249,26 +355,38 @@ def test_rent_is_monotone_in_household_rent(population, bump): @PROPERTY_SETTINGS @given(population) def test_no_op_without_sharers_boarders_or_lodgers(population): - for house in population: - for other in house["others"]: - other["role"] = "non_dependant" - situation, roles = build(population) + situation, roles = build(population, roles_override="non_dependant") sim = Simulation(situation=situation) # 9. The head's family has the whole rent; everyone else none. rent = calc(sim, "benunit_rent") head = roles == "head" assert np.all(rent[~head] == 0) - rent_total = sim.map_result(rent, "benunit", "household") - assert np.allclose(rent_total, calc(sim, "rent"), atol=0.01) - # The previous formula: the head's family was charged for everyone - # outside it (and every other family likewise). - individual = calc(sim, "uc_individual_non_dep_deduction") + assert np.allclose(per_household(sim, rent), calc(sim, "rent"), atol=0.01) + # The previous formulas charged a family for everyone outside it, and + # gave it a bedroom for everyone aged 16 or over outside it. + claims_uc = calc(sim, "uc_non_dependants_counted") + assert np.array_equal( + claims_uc, + head & calc(sim, "is_uc_eligible") & calc(sim, "would_claim_uc"), + ) person_head_family = sim.map_result(head, "benunit", "person") > 0 - everyone = sim.map_result(individual, "person", "household") - head_family_own = sim.map_result( - individual * person_head_family, "person", "household" + individual = calc(sim, "uc_individual_non_dep_deduction") + outside = sim.map_result(individual * ~person_head_family, "person", "household") + head_deductions = per_household(sim, calc(sim, "uc_non_dep_deductions") * head) + head_claims = per_household(sim, claims_uc) > 0 + assert np.allclose(head_deductions[head_claims], outside[head_claims], atol=0.01) + # Bedrooms: what the head's family would have alone, plus one for each + # person aged 16 or over outside it. + alone = [dict(house, others=[]) for house in population] + alone_situation, _ = build(alone) + alone_sim = Simulation(situation=alone_situation) + bedrooms_alone = calc(alone_sim, "LHA_allowed_bedrooms") + aged_16_or_over = calc(sim, "age") >= 16 + adults_outside = sim.map_result( + aged_16_or_over & ~person_head_family, "person", "household" ) - head_deductions = sim.map_result( - calc(sim, "uc_non_dep_deductions") * head, "benunit", "household" + head_bedrooms = per_household(sim, calc(sim, "LHA_allowed_bedrooms") * head) + assert np.allclose( + head_bedrooms[head_claims], + (bedrooms_alone + adults_outside)[head_claims], ) - assert np.allclose(head_deductions, everyone - head_family_own, atol=0.01) diff --git a/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py b/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py index 14d4d0b3e..ce8e5543a 100644 --- a/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py +++ b/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py @@ -6,28 +6,18 @@ warnings.filterwarnings("ignore") -def bedrooms_for_children(benunit, period): - """Bedrooms for the family's children under 16. +def child_bedrooms(boys_under_10, boys_10_to_15, girls_under_10, girls_10_to_15): + """Bedrooms for children under 16, given counts by sex and age band. Children must share rooms in pairs unless they are opposite-sex and one is 10 or over. This is the minimum number of bedrooms that allocates them under those rules. """ - person = benunit.members - age = person("age", period) - male = person("is_male", period) - under_16 = age < 16 - under_10 = age < 10 - child_over_10 = ~under_10 & under_16 - boys_under_10 = benunit.sum(under_10 & male) - boys_over_10 = benunit.sum(child_over_10 & male) - girls_under_10 = benunit.sum(under_10 & ~male) - girls_over_10 = benunit.sum(child_over_10 & ~male) # First, have over-10s share where possible - over_10_rooms = (boys_over_10 + 1) // 2 + (girls_over_10 + 1) // 2 + over_10_rooms = (boys_10_to_15 + 1) // 2 + (girls_10_to_15 + 1) // 2 # There may children over 10 still not sharing - space_for_boy_under_10 = boys_over_10 % 2 - space_for_girl_under_10 = girls_over_10 % 2 + space_for_boy_under_10 = boys_10_to_15 % 2 + space_for_girl_under_10 = girls_10_to_15 % 2 # Have those spaces filled where possible by children under 10 left_over_boys_under_10 = max_(boys_under_10 - space_for_boy_under_10, 0) left_over_girls_under_10 = max_(girls_under_10 - space_for_girl_under_10, 0) @@ -36,6 +26,22 @@ def bedrooms_for_children(benunit, period): return over_10_rooms + under_10_rooms +def bedrooms_for_children(benunit, period): + """Bedrooms for the family's own children under 16.""" + person = benunit.members + age = person("age", period) + male = person("is_male", period) + under_16 = age < 16 + under_10 = age < 10 + child_over_10 = ~under_10 & under_16 + return child_bedrooms( + benunit.sum(under_10 & male), + benunit.sum(child_over_10 & male), + benunit.sum(under_10 & ~male), + benunit.sum(child_over_10 & ~male), + ) + + class LHA_allowed_bedrooms(Variable): value_type = float entity = BenUnit @@ -45,8 +51,8 @@ class LHA_allowed_bedrooms(Variable): "couple, one for each non-dependant aged 16 or over, and the " "children's bedrooms. Joint tenants and other sharers of the rent, " "boarders and lodgers are not non-dependants, so they add no bedroom " - "to anyone's entitlement; only the household head's family has " - "non-dependants (see is_non_dependant_of_household_head)." + "to anyone's entitlement; the household's non-dependants count in one " + "Universal Credit claim only (see uc_non_dependants_counted)." ) definition_period = YEAR reference = ( @@ -61,6 +67,6 @@ def formula(benunit, period, parameters): non_dependant = aged_16_or_over & person( "is_non_dependant_of_household_head", period ) - head_family = benunit.any(person("is_household_head", period)) - non_dependants = head_family * benunit.max(person.household.sum(non_dependant)) + counted = benunit("uc_non_dependants_counted", period) + non_dependants = counted * benunit.max(person.household.sum(non_dependant)) return 1 + non_dependants + bedrooms_for_children(benunit, period) diff --git a/policyengine_uk/variables/gov/dwp/LHA_cap.py b/policyengine_uk/variables/gov/dwp/LHA_cap.py index 0d26ce517..33e9436c1 100644 --- a/policyengine_uk/variables/gov/dwp/LHA_cap.py +++ b/policyengine_uk/variables/gov/dwp/LHA_cap.py @@ -11,12 +11,14 @@ class LHA_cap(Variable): entity = BenUnit label = "Applicable amount for LHA" documentation = ( - "Rent eligible for Housing Benefit where the Local Housing Allowance " - "applies: the lower of the rent and the Housing Benefit LHA rate. " - "Where the rent pays for meals, the rent officer route applies " - "instead and the fixed meals amount is deducted from the maximum " - "rent; the model takes the rent less that amount, capped at the LHA " - "rate, which stands in for the rent officer's determination." + "Rent eligible for Housing Benefit for a private renter. On the Local " + "Housing Allowance route it is the lower of the rent (gross, meals " + "included) and the Housing Benefit LHA rate. Where a rent officer has " + "found that a substantial part of the rent is for board and " + "attendance (housing_benefit_board_and_attendance_determination), the " + "LHA does not apply and the maximum rent is the rent officer's " + "figure less the fixed amount for meals; the model takes the rent " + "less that amount, as it has no rent officer determinations." ) definition_period = YEAR unit = GBP @@ -28,15 +30,20 @@ class LHA_cap(Variable): ) def formula(benunit, period, parameters): - # HB Regs 2006 regs 12D(2)(a) and 13D(5): the maximum rent (LHA) is - # the LHA rate or, if lower, the cap rent. A rent officer finding - # that a substantial part of the rent is for board and attendance - # takes the case off the LHA (reg 13C(5)(e)); the maximum rent then - # has the Sch 1 para 2 amount for meals deducted (reg 13(7)). - rent = max_( - 0, - benunit("benunit_rent", period) - - benunit("housing_benefit_meals_deduction", period), + rent = benunit("benunit_rent", period) + # HB Regs 2006 regs 12D(2)(a) and 13D(5), (12): the maximum rent + # (LHA) is the LHA rate or, if lower, the cap rent, which is the + # gross rent liability. + lha_route = min_(rent, benunit("housing_benefit_LHA_rate", period)) + # Reg 13C(5)(e): a rent officer finding that a substantial part of + # the rent is board and attendance takes the case off the LHA; the + # maximum rent then has the Sch 1 para 2 amount for meals deducted + # (reg 13(7)). + rent_officer_route = max_( + 0, rent - benunit("housing_benefit_meals_deduction", period) + ) + return where( + benunit("housing_benefit_board_and_attendance_determination", period), + rent_officer_route, + lha_route, ) - cap = benunit("housing_benefit_LHA_rate", period) - return min_(rent, cap) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/entitlement/housing_benefit_entitlement.py b/policyengine_uk/variables/gov/dwp/housing_benefit/entitlement/housing_benefit_entitlement.py index 84e19c27c..475ad97a2 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/entitlement/housing_benefit_entitlement.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/entitlement/housing_benefit_entitlement.py @@ -10,12 +10,20 @@ class housing_benefit_entitlement(Variable): def formula(benunit, period, parameters): # Charges for meals are not eligible to be met by Housing Benefit - # (HB Regs 2006 reg 12B(2)(b) and Sch 1 paras 1(a)(i) and 2). - rent = max_( - 0, - benunit("benunit_rent", period) - - benunit("housing_benefit_meals_deduction", period), + # (HB Regs 2006 reg 12B(2)(b) and Sch 1 paras 1(a)(i) and 2), except + # on the Local Housing Allowance route, where the cap rent is the + # gross liability (regs 12D, 13D(12)); a rent officer's board and + # attendance finding takes a private renter off that route (reg + # 13C(5)(e)), as LHA_cap applies. + lha_eligible = benunit("LHA_eligible", period.this_year) + gross_rent = benunit("benunit_rent", period) + rent_less_meals = max_( + 0, gross_rent - benunit("housing_benefit_meals_deduction", period) + ) + lha_route = lha_eligible & ~benunit( + "housing_benefit_board_and_attendance_determination", period ) + rent = where(lha_route, gross_rent, rent_less_meals) applicable_amount = benunit("housing_benefit_applicable_amount", period) income = benunit("housing_benefit_applicable_income", period) withdrawal_rate = parameters( @@ -24,6 +32,5 @@ def formula(benunit, period, parameters): reduced_income = max_(0, income - applicable_amount) final_amount = max_(0, rent - reduced_income * withdrawal_rate) capped_final_amount = min_(final_amount, benunit("LHA_cap", period)) - lha_eligible = benunit("LHA_eligible", period.this_year) amount = where(lha_eligible, capped_final_amount, final_amount) return max_(0, amount - benunit("housing_benefit_non_dep_deductions", period)) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_has_non_dependant.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_has_non_dependant.py new file mode 100644 index 000000000..a5904137b --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_has_non_dependant.py @@ -0,0 +1,37 @@ +from policyengine_uk.model_api import * + + +class housing_benefit_has_non_dependant(Variable): + value_type = bool + entity = BenUnit + label = "Has a non-dependant residing with them (Housing Benefit)" + documentation = ( + "Someone in this benefit unit who is neither a claimant or partner " + "nor a child or young person, or, for a family liable for the " + "household's rent (the household head's or a sharer's), anyone in " + "the household who is a non-dependant (see " + "is_non_dependant_of_household_head). A non-dependant normally " + "residing in the dwelling resides with each joint occupier. A " + "boarder's or lodger's family has none from the householder's " + "household." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/3", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/74", + ) + + def formula(benunit, period, parameters): + # HB Regs 2006 reg 3(1)-(2), (4) and reg 74(5). + person = benunit.members + claimant_or_partner = person("is_claimant_or_partner", period) + age = person("age", period) + child_or_young_person = person( + "is_child_or_young_person_for_legacy_benefits", period + ) | ((age >= 16) & (age < 17)) + within_benefit_unit = benunit.any(~claimant_or_partner & ~child_or_young_person) + liable_family = benunit.any(person("is_liable_for_household_rent", period)) + non_dependants = benunit.max( + person.household.sum(person("is_non_dependant_of_household_head", period)) + ) + return within_benefit_unit | (liable_family & (non_dependants > 0)) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_severe_disability_premium_applies.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_severe_disability_premium_applies.py new file mode 100644 index 000000000..c8fff7fa8 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_severe_disability_premium_applies.py @@ -0,0 +1,50 @@ +from policyengine_uk.model_api import * + + +class housing_benefit_severe_disability_premium_applies(Variable): + value_type = bool + entity = BenUnit + label = "Severe disability premium conditions met (Housing Benefit)" + documentation = ( + "Whether the claimant is a severely disabled person for the Housing " + "Benefit severe disability premium: the claimant (and any partner) " + "receives a qualifying disability benefit, and no non-dependant aged " + "18 or over normally resides with them, other than one who receives " + "a qualifying benefit. Not modelled: who receives Carer's Allowance, " + "Carer Support Payment or the UC carer element for caring for the " + "claimant, and the treatment of a blind partner. The legacy " + "severe_disability_premium variable uses the tax credit definition of " + "severe disability, which is narrower." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/schedule/3/paragraph/14", + "https://www.legislation.gov.uk/uksi/2006/214/schedule/3/paragraph/6", + ) + + def formula(benunit, period, parameters): + p = parameters(period).gov.dwp.housing_benefit.severe_disability_premium + person = benunit.members + qualifying = add(person, period, p.qualifying_benefits) > 0 + claimant_or_partner = person("is_claimant_or_partner", period) + # Sch 3 para 14(2)(a)(i), (b)(i)-(ii): every claimant or partner + # receives a qualifying benefit. + all_qualify = benunit.all(qualifying | ~claimant_or_partner) & benunit.any( + claimant_or_partner + ) + # Para 14(2)(a)(ii), (b)(iii) with (4): no non-dependant aged 18 or + # over, disregarding one who receives a qualifying benefit. Within the + # family, an adult who is neither claimant nor partner nor a child or + # young person; outside it, as for housing_benefit_has_non_dependant. + age = person("age", period) + adult_non_dependant = (age >= 18) & ~qualifying + young_person = person("is_child_or_young_person_for_legacy_benefits", period) + within = benunit.any(adult_non_dependant & ~claimant_or_partner & ~young_person) + liable_family = benunit.any(person("is_liable_for_household_rent", period)) + outside = benunit.max( + person.household.sum( + adult_non_dependant + & person("is_non_dependant_of_household_head", period) + ) + ) + return all_qualify & ~within & ~(liable_family & (outside > 0)) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_allowed_bedrooms.py b/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_allowed_bedrooms.py index 005d00c65..a5b667f8d 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_allowed_bedrooms.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_allowed_bedrooms.py @@ -1,6 +1,6 @@ from policyengine_uk.model_api import * from policyengine_uk.variables.gov.dwp.LHA_allowed_bedrooms import ( - bedrooms_for_children, + child_bedrooms, ) @@ -9,13 +9,17 @@ class housing_benefit_LHA_allowed_bedrooms(Variable): entity = BenUnit label = "Bedrooms in the Housing Benefit size criteria" documentation = ( - "Housing Benefit size criteria: one bedroom for the claimant or " - "couple, one for each other occupier aged 16 or over, and the " - "children's bedrooms. Occupiers are everyone who lives in the " - "dwelling as their home except a joint tenant outside the claimant's " - "household, so a householder's boarder or lodger adds a bedroom, as " - "does a non-dependant, but a sharer of the rent does not. A sharer's, " - "boarder's or lodger's own claim counts only their own family." + "Housing Benefit size criteria over the claim's occupiers: one " + "bedroom for each couple, one for each other occupier aged 16 or " + "over, and the children's bedrooms, pairing children across all the " + "occupiers. Occupiers are everyone who lives in the dwelling as their " + "home except a joint tenant outside the claimant's household. For " + "the household head's family or a sharer's, that is its own family, " + "the household's non-dependants and anyone paying the householder " + "rent (but not the other families liable for the rent). A boarder's " + "or lodger's own claim counts only their own family. Couples who " + "cannot share a bedroom and the additional bedrooms for overnight " + "care or foster parents are not modelled." ) definition_period = YEAR reference = ( @@ -24,14 +28,42 @@ class housing_benefit_LHA_allowed_bedrooms(Variable): ) def formula(benunit, period, parameters): + # HB Regs 2006 reg 13D(3) with "occupiers" as defined in 13D(12). person = benunit.members - aged_16_or_over = person("age", period) >= 16 - head_family = person.benunit.any(person("is_household_head", period)) - sharer = person.benunit("liable_for_share_of_household_rent", period) - # HB Regs 2006 reg 13D(3), with "occupiers" as defined in 13D(12). - other_occupier = aged_16_or_over & ~head_family & ~sharer - is_head_family = benunit.any(person("is_household_head", period)) - other_occupiers = is_head_family * benunit.max( - person.household.sum(other_occupier) + age = person("age", period) + claimant_or_partner = person("is_claimant_or_partner", period) + liable_family = person.benunit.any( + person("is_liable_for_household_rent", period) ) - return 1 + other_occupiers + bedrooms_for_children(benunit, period) + # Members of families not liable for the household's rent: + # non-dependants and people paying the householder. + other = ~liable_family + is_liable_claim = benunit.any(person("is_liable_for_household_rent", period)) + + def occupiers_total(values): + own = benunit.sum(values) + others = benunit.max(person.household.sum(values * other)) + return own + where(is_liable_claim, others, 0) + + # 13D(3)(zb), (a): one bedroom for each couple or single claimant. + adult_units = ( + occupiers_total( + claimant_or_partner & (person.benunit.sum(claimant_or_partner) > 0) + ) + - occupiers_total( + claimant_or_partner & (person.benunit.sum(claimant_or_partner) == 2) + ) + / 2 + ) + # 13D(3)(b): any other person who is not a child. + other_adults = occupiers_total((age >= 16) & ~claimant_or_partner) + under_16 = age < 16 + male = person("is_male", period) + under_10 = age < 10 + rooms_for_children = child_bedrooms( + occupiers_total(under_10 & male), + occupiers_total(~under_10 & under_16 & male), + occupiers_total(under_10 & ~male), + occupiers_total(~under_10 & under_16 & ~male), + ) + return max_(adult_units, 1) + other_adults + rooms_for_children diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_category.py b/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_category.py index 305cf304b..4711d7db9 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_category.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_category.py @@ -26,11 +26,13 @@ class housing_benefit_LHA_category(Variable): def formula(benunit, period, parameters): rooms = benunit("housing_benefit_LHA_allowed_bedrooms", period.this_year) # Schedule 3 paragraph 14 (severe disability premium) applies. - severe_disability = benunit("severe_disability_premium", period) > 0 + severe_disability = benunit( + "housing_benefit_severe_disability_premium_applies", period + ) # HB Regs 2006 reg 13D(2)(a)(i). young_individual = ( benunit("is_housing_benefit_young_individual", period) - & ~benunit("lha_renter_has_non_dependant", period) + & ~benunit("housing_benefit_has_non_dependant", period) & ~severe_disability ) # Reg 13D(2)(a)(ii): entitled to one bedroom but neither condition in diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit_shares_accommodation.py b/policyengine_uk/variables/gov/dwp/housing_benefit_shares_accommodation.py index fd4031f38..07cc454d3 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit_shares_accommodation.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit_shares_accommodation.py @@ -9,10 +9,12 @@ class housing_benefit_shares_accommodation(Variable): "Whether the family lacks exclusive use of two or more rooms, or of " "one room with a bathroom, toilet and kitchen. Rooms shared only with " "the family's own household, its non-dependants or people who pay it " - "rent still count as exclusive. A boarder or lodger shares the " - "householder's rooms, and in a household whose rent is shared every " - "liable family shares the others' rooms. The household input " - "is_shared_accommodation marks any other case." + "rent still count as exclusive. The data record no rooms, so the " + "model presumes that a boarder or lodger, and every family liable for " + "a household's shared rent, lacks exclusive use, and the household " + "input is_shared_accommodation marks any other case. The presumption " + "is rebuttable: set this variable directly for a family with, for " + "example, an exclusive bedroom and sitting room." ) definition_period = YEAR reference = "https://www.legislation.gov.uk/uksi/2006/213/regulation/13D" diff --git a/policyengine_uk/variables/gov/dwp/is_non_dependant_of_household_head.py b/policyengine_uk/variables/gov/dwp/is_non_dependant_of_household_head.py index 2c80db627..a2e8556fe 100644 --- a/policyengine_uk/variables/gov/dwp/is_non_dependant_of_household_head.py +++ b/policyengine_uk/variables/gov/dwp/is_non_dependant_of_household_head.py @@ -4,17 +4,17 @@ class is_non_dependant_of_household_head(Variable): value_type = bool entity = Person - label = "Non-dependant of the household head's family" + label = "Non-dependant of the families liable for the household's rent" documentation = ( - "Whether this person lives in the household outside the household " - "head's family and is not liable for rent. A joint tenant or other " + "Whether this person lives in the household outside every family " + "liable for its rent (the household head's family and any sharers) " + "and is not liable for rent themselves. A joint tenant or other " "sharer of the rent, a boarder and a lodger are liable on a " "commercial basis for their occupation, so none is a non-dependant " - "of anyone. The household head and their household are not " - "non-dependants of a boarder or lodger who pays them. Universal " - "Credit counts a non-dependant in one claim only, which the model " - "gives to the household head's family; Housing Benefit and Council " - "Tax Reduction apportion a non-dependant of several joint occupiers " + "of anyone, and the household head's household is not a boarder's or " + "lodger's. Universal Credit counts a non-dependant in one claim only " + "(uc_non_dependants_counted); Housing Benefit and Council Tax " + "Reduction apportion a non-dependant of several joint occupiers " "between them. Foster children and carers engaged through a charity " "are not identified." ) diff --git a/policyengine_uk/variables/gov/dwp/lha_renter_has_non_dependant.py b/policyengine_uk/variables/gov/dwp/lha_renter_has_non_dependant.py index 31f652b56..b832db4ac 100644 --- a/policyengine_uk/variables/gov/dwp/lha_renter_has_non_dependant.py +++ b/policyengine_uk/variables/gov/dwp/lha_renter_has_non_dependant.py @@ -4,15 +4,15 @@ class lha_renter_has_non_dependant(Variable): value_type = bool entity = BenUnit - label = "LHA renter has a non-dependant" + label = "LHA renter has a non-dependant (Universal Credit)" documentation = ( - "Someone in this benefit unit who is neither a claimant or partner " - "nor a child or young person, or, for the household head's family, a " - "claimant or partner of another family who is a non-dependant of the " - "household head (see is_non_dependant_of_household_head): joint " - "tenants and other sharers of the rent, boarders and lodgers are not " - "non-dependants, and a sharer, boarder or lodger has none from the " - "household head's family. Foster children are not identified." + "Universal Credit: someone in this benefit unit who is neither a " + "claimant or partner nor a child or young person, or, for the claim " + "that counts the household's non-dependants (see " + "uc_non_dependants_counted), a claimant or partner of a family not " + "liable for rent. Joint tenants and other sharers of the rent, " + "boarders and lodgers are not non-dependants. Foster children are not " + "identified. Housing Benefit uses housing_benefit_has_non_dependant." ) definition_period = YEAR reference = ( @@ -36,8 +36,8 @@ def formula(benunit, period, parameters): non_dependant_claimants = claimant_or_partner & person( "is_non_dependant_of_household_head", period ) - head_family = benunit.any(person("is_household_head", period)) - other_family_non_dependants = head_family * benunit.max( + counted = benunit("uc_non_dependants_counted", period) + other_family_non_dependants = counted * benunit.max( person.household.sum(non_dependant_claimants) ) return within_benefit_unit | (other_family_non_dependants > 0) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deductions.py b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deductions.py index eca4abcd2..c09e43cc4 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deductions.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deductions.py @@ -15,13 +15,12 @@ class uc_non_dep_deductions(Variable): def formula(benunit, period, parameters): # A housing cost contribution is deducted for each non-dependant in # the renter's extended benefit unit (UC Regs 2013 Sch 4 para 13). - # Non-dependants live outside the household head's family and are - # not liable for rent, and only the household head's family has them - # (para 9(2)(d)-(f)): a sharer, boarder or lodger has none from the - # household head's family. + # Non-dependants are not liable for rent (para 9(2)(d)), and each + # counts in one Universal Credit claim only (para 9(2)(f)): see + # uc_non_dependants_counted. person = benunit.members deductions = person("uc_individual_non_dep_deduction", period) * person( "is_non_dependant_of_household_head", period ) - head_family = benunit.any(person("is_household_head", period)) - return head_family * benunit.max(person.household.sum(deductions)) + counted = benunit("uc_non_dependants_counted", period) + return counted * benunit.max(person.household.sum(deductions)) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dependants_counted.py b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dependants_counted.py new file mode 100644 index 000000000..af1c22f5b --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dependants_counted.py @@ -0,0 +1,40 @@ +from policyengine_uk.model_api import * + + +class uc_non_dependants_counted(Variable): + value_type = bool + entity = BenUnit + label = "Universal Credit claim that counts the household's non-dependants" + documentation = ( + "Whether this family's Universal Credit claim counts the household's " + "non-dependants. A non-dependant normally lives with every renter " + "liable for the household's rent, but is not one for a renter if " + "already treated as a non-dependant in another Universal Credit claim " + "by someone liable for the same accommodation. The model gives them " + "to the first such claim: the household head's family if it is " + "eligible for and claims Universal Credit, otherwise the first family " + "liable for a share of the rent that does (in the order the families " + "are given). A boarder or lodger has " + "none: the householder's household is excluded from their extended " + "benefit unit." + ) + definition_period = YEAR + reference = "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/9" + + def formula(benunit, period, parameters): + # UC Regs 2013 Sch 4 para 9(2)(e)-(f). + person = benunit.members + liable_family = benunit.any(person("is_liable_for_household_rent", period)) + claims = ( + liable_family + & benunit("is_uc_eligible", period) + & benunit("would_claim_uc", period) + ) + head_family = benunit.any(person("is_household_head", period)) + # Order the claims: the household head's family first, then the + # others in the order the families were given. + position = np.arange(len(head_family)) + order = where(head_family, -1, position) + key = where(claims, order, np.inf) + first = benunit.max(person.household.min(benunit.project(key))) + return claims & (key == first) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_claimant_benunit.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_claimant_benunit.py index 449b9d080..8e5f3b9a4 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_claimant_benunit.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_claimant_benunit.py @@ -9,13 +9,15 @@ class council_tax_reduction_claimant_benunit(Variable): "Whether this family can claim Council Tax Reduction on the " "household's council tax. Where the household's rent is shared, every " "family liable for it is jointly and severally liable for the council " - "tax and claims on its part. Otherwise the family of the household's " + "tax and claims on its part, through a liable member who is not an " + "excluded full-time student. Otherwise the family of the household's " "oldest adult claims, as before." ) definition_period = YEAR reference = ( "https://www.legislation.gov.uk/ukpga/1992/14/section/6", "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/7", + "https://www.legislation.gov.uk/uksi/2012/2886/schedule/paragraph/75", ) def formula(benunit, period, parameters): @@ -25,7 +27,12 @@ def formula(benunit, period, parameters): person.benunit("liable_for_share_of_household_rent", period) ) ) - liable_family = benunit.any(person("is_liable_for_household_rent", period)) + # A full-time student is excluded from entitlement (Default Scheme + # Sch para 75(1)); the model takes a person in higher education as + # one, so a sharer family claims through a liable non-student. + liable_family = benunit.any( + person("is_liable_for_household_rent", period) & ~person("in_HE", period) + ) return where( rent_is_shared, liable_family, diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py index 459598f8d..861bd2dc5 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py @@ -6,11 +6,11 @@ class council_tax_reduction_individual_non_dep_deduction_eligible(Variable): entity = Person label = "eligible person for CTR non-dependent deduction" documentation = ( - "An adult outside the claimant's family who is not liable for rent. " - "Someone jointly and severally liable for the council tax with the " - "claimant (a joint tenant or other sharer of the rent) and someone " - "liable to the claimant on a commercial basis (a boarder or lodger) " - "is not a non-dependant." + "An adult outside every family claiming Council Tax Reduction for the " + "household. Someone jointly and severally liable for the council tax " + "with the claimant (a joint tenant or other sharer of the rent) and " + "someone liable to the claimant on a commercial basis (a boarder or " + "lodger) is not a non-dependant." ) definition_period = YEAR reference = ( @@ -23,6 +23,7 @@ def formula(person, period, parameters): # SI 2012/2885 reg 9(2)(d)-(e) and the Welsh and Scottish equivalents. return ( (person("age", period) >= 18) - & ~person.benunit("benunit_contains_household_head", period) - & ~person.benunit("benunit_is_rent_liable", period) + & ~person.benunit("council_tax_reduction_claimant_benunit", period) + & ~person.benunit("liable_for_share_of_household_rent", period) + & ~person("pays_rent_to_householder", period) ) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_joint_liability_share.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_joint_liability_share.py index 215992982..f5baa192f 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_joint_liability_share.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_joint_liability_share.py @@ -9,13 +9,14 @@ class council_tax_reduction_joint_liability_share(Variable): "Where the claimant is jointly and severally liable for the council " "tax with people other than their partner, the council tax used for " "the maximum reduction is divided by the number of people jointly and " - "severally liable, and a deduction for a non-dependant of two or more " - "of them is apportioned equally between them. The model takes the " - "people liable for the household's rent as those jointly liable for " - "its council tax (residents with the same interest, Local Government " - "Finance Act 1992 s.6) and follows the regulations' wording, dividing " - "by every person liable, the claimant's partner included. Students " - "are not excluded from the count. Otherwise the share is one." + "severally liable, leaving out students excluded from the scheme, and " + "a deduction for a non-dependant of two or more of them is apportioned " + "equally between them. The model takes the people liable for the " + "household's rent as those jointly liable for its council tax " + "(residents with the same interest, Local Government Finance Act 1992 " + "s.6), takes a person in higher education as an excluded full-time " + "student, and follows the regulations' wording, counting the " + "claimant's partner. Otherwise the share is one." ) definition_period = YEAR unit = "/1" @@ -23,6 +24,7 @@ class council_tax_reduction_joint_liability_share(Variable): "https://www.legislation.gov.uk/ukpga/1992/14/section/6", "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/7", "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/8", + "https://www.legislation.gov.uk/uksi/2012/2886/schedule/paragraph/75", "https://www.legislation.gov.uk/wsi/2013/3029", "https://www.legislation.gov.uk/ssi/2021/249", ) @@ -35,4 +37,11 @@ def formula(benunit, period, parameters): liable_people = benunit.max(person.household.sum(liable)) in_family = benunit.sum(liable) jointly_with_others = (in_family > 0) & (liable_people > in_family) - return where(jointly_with_others, 1 / max_(liable_people, 1), 1) + # Para 7(5): a student excluded from the scheme (Default Scheme Sch + # para 75(1)) is not counted among those jointly liable with the + # applicant. The applicant always counts. + counted = liable & ~person("in_HE", period) + counted_people = benunit.max(person.household.sum(counted)) + applicant_is_student = benunit.sum(counted) == 0 + divisor = counted_people + applicant_is_student + return where(jointly_with_others, 1 / max_(divisor, 1), 1) diff --git a/policyengine_uk/variables/household/consumption/rent/housing_benefit_board_and_attendance_determination.py b/policyengine_uk/variables/household/consumption/rent/housing_benefit_board_and_attendance_determination.py new file mode 100644 index 000000000..1562096b8 --- /dev/null +++ b/policyengine_uk/variables/household/consumption/rent/housing_benefit_board_and_attendance_determination.py @@ -0,0 +1,20 @@ +from policyengine_uk.model_api import * + + +class housing_benefit_board_and_attendance_determination(Variable): + value_type = bool + entity = BenUnit + label = "Rent officer found the rent substantially for board and attendance" + documentation = ( + "Whether a rent officer has determined that a substantial part of the " + "family's rent is fairly attributable to board and attendance. Housing " + "Benefit then leaves the Local Housing Allowance for a rent officer " + "determination, from which the fixed amount for meals is deducted. " + "Knowing that the rent includes meals does not by itself establish " + "this." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/13C", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/13D", + ) From 7769fa3f38ee8bc8d8b7545ade3fbf2de4fdd70d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 15:24:47 -0400 Subject: [PATCH 9/9] Pin the Housing Benefit LHA rate in the #1926 LHA-order tests Housing Benefit reads housing_benefit_LHA_rate in this branch, so the tests that pin the rate as an input set that variable rather than BRMA_LHA_rate. Co-Authored-By: Claude Opus 5.5 --- .../entitlement/housing_benefit_lha_order.yaml | 2 +- .../test_housing_benefit_lha_order_properties.py | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/entitlement/housing_benefit_lha_order.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/entitlement/housing_benefit_lha_order.yaml index 4714f2259..bf5af7f3f 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/entitlement/housing_benefit_lha_order.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/entitlement/housing_benefit_lha_order.yaml @@ -43,7 +43,7 @@ # The model's 2026 rate for this household (default BRMA, Maidstone, # one-bedroom category) is also 9,467.64; it is an input here so the # test pins the order of the cap and the taper, not the LHA table. - BRMA_LHA_rate: 9_467.64 + housing_benefit_LHA_rate: 9_467.64 # Inputs, so the earnings disregard and the pension-age allowance # cannot move the taper. The applicable income input overrides the # income computed from state_pension_reported, which only keeps diff --git a/policyengine_uk/tests/test_housing_benefit_lha_order_properties.py b/policyengine_uk/tests/test_housing_benefit_lha_order_properties.py index a61eda1df..ba3818a4f 100644 --- a/policyengine_uk/tests/test_housing_benefit_lha_order_properties.py +++ b/policyengine_uk/tests/test_housing_benefit_lha_order_properties.py @@ -170,7 +170,7 @@ def calculate(population): people[name] = {"age": {YEAR: 70}} benunits[f"b{i}"] = { "members": [name], - "BRMA_LHA_rate": {YEAR: unit["lha_rate"]}, + "housing_benefit_LHA_rate": {YEAR: unit["lha_rate"]}, "housing_benefit_applicable_amount": {YEAR: unit["applicable_amount"]}, "housing_benefit_applicable_income": {YEAR: unit["income"]}, "housing_benefit_non_dep_deductions": {YEAR: unit["non_dep_deductions"]}, @@ -188,7 +188,7 @@ def calculate(population): for variable in [ "housing_benefit_entitlement", "benunit_rent", - "BRMA_LHA_rate", + "housing_benefit_LHA_rate", "LHA_cap", "LHA_eligible", "housing_benefit_applicable_amount", @@ -231,7 +231,9 @@ def test_bounds_closed_form_and_differential(population): # LHA tenants are exactly the private renters here. assert np.array_equal(lha, tenures == "RENT_PRIVATELY") assert np.allclose( - values["LHA_cap"], np.minimum(rent, values["BRMA_LHA_rate"]), atol=TOLERANCE + values["LHA_cap"], + np.minimum(rent, values["housing_benefit_LHA_rate"]), + atol=TOLERANCE, ) # 1. Bounds. @@ -357,7 +359,7 @@ def end_to_end(population): names.append(name) benunits[f"b{i}"] = { "members": names, - "BRMA_LHA_rate": {YEAR: unit["lha_rate"]}, + "housing_benefit_LHA_rate": {YEAR: unit["lha_rate"]}, } household_members = list(names) if unit["non_dependant_earnings"] is not None: @@ -384,7 +386,7 @@ def end_to_end(population): "housing_benefit", "housing_benefit_eligible", "benunit_rent", - "BRMA_LHA_rate", + "housing_benefit_LHA_rate", "LHA_cap", "LHA_eligible", "housing_benefit_applicable_amount", @@ -424,5 +426,5 @@ def test_end_to_end_pension_age_housing_benefit(population): eligible = values["housing_benefit_eligible"].astype(bool) assert np.allclose(hb[eligible], expected[eligible], atol=TOLERANCE), population assert np.all(hb[~eligible] == 0) - lha_limit = np.minimum(rent, values["BRMA_LHA_rate"]) + lha_limit = np.minimum(rent, values["housing_benefit_LHA_rate"]) assert np.all(hb[lha] <= lha_limit[lha] + TOLERANCE), population