diff --git a/changelog.d/2029.fixed.md b/changelog.d/2029.fixed.md new file mode 100644 index 0000000000..cd33b9672f --- /dev/null +++ b/changelog.d/2029.fixed.md @@ -0,0 +1 @@ +- Count a non-dependant who normally resides with several joint tenants in each one's Housing Benefit size criteria and shared accommodation test, apportioning the HB and Council Tax Reduction deductions only between the joint tenants they reside with (new input `non_dependant_normally_resides_with`). diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_joint_tenants_shared_non_dependant.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_joint_tenants_shared_non_dependant.yaml new file mode 100644 index 0000000000..d7bfd00078 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_joint_tenants_shared_non_dependant.yaml @@ -0,0 +1,697 @@ +# Joint tenants and a non-dependant who lives with them. +# +# Housing Benefit (HB Regs 2006 and HB (SPC) Regs 2006): a non-dependant is +# anyone who "normally resides with a claimant" outside the claimant's family +# and the joint occupiers (reg 3(1), (2)(d)), and "resides with" requires +# sharing accommodation other than a bathroom, lavatory or communal area +# (reg 3(4)). Whose non-dependant someone is in a joint tenancy is a question +# of fact (JP v Bournemouth BC [2018] UKUT 75 (AAC), para 34). DWP: +# - LHA Guidance Manual (April 2014) para 2.110: "Joint tenants who have a +# shared non-dependant have the person counted for the size criteria, but +# only have an apportioned non-dependant deduction" (John, Alex and Peter: +# each tenant gets the two-bedroom rate; also para 2.123 example 4). +# - Para 2.093 example 2: Sarah's sister Susan lives with joint tenants Sarah +# and Rachel and "is treated as Sarah's non-dependant so Sarah is allowed the +# two bedroom LHA rate. Rachel is allowed the shared room rate." +# - HB Guidance Manual A5 para 5.622: "If a person is a non-dependant of only +# one of the joint occupiers, take the whole of the deduction from that +# joint occupier's entitlement." +# Reg 74(5) (SPC reg 55(5)) apportions the deduction for "a non-dependant in +# respect of more than one joint occupier" by the number of joint occupiers +# and their shares of the payments. Council Tax Reduction apportions it +# equally between the liable persons it is a non-dependant of (SI 2012/2885 +# Sch 1 para 8(5) and the Welsh and Scottish equivalents). +# +# The input non_dependant_normally_resides_with, on the non-dependant's +# family, says which joint occupiers they reside with; by default all of them. +# +# Universal Credit is different: a non-dependant already treated as one in +# another UC claim for the same accommodation is not one again (UC Sch 4 para +# 9(2)(f)), so they count in one claim only, which the model gives to the +# household head's family. +# +# Rent: £20,800 a year (£400 a week), split by the people liable (HB reg +# 12B(4)). Maidstone rates (VOA, April 2024, held for 2025-26): B £172.60, C +# £208.27 a week. The individual deductions are inputs, so the tests do not +# depend on the deduction rates. + +- name: Joint tenants with a shared non-dependant each count them (LHA Guidance Manual 2.110) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + john: + age: 30 + is_household_head: true + alex: + age: 30 + peter: + age: 30 + household_benefits_individual_non_dep_deduction: 1_000 + council_tax_reduction_individual_non_dep_deduction: 300 + benunits: + john_family: + members: [john] + alex_family: + members: [alex] + liable_for_share_of_household_rent: true + peter_family: + members: [peter] + households: + home: + members: [john, alex, peter] + rent: 20_800 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + non_dependant_normally_resides_with: + [EVERY_JOINT_OCCUPIER, EVERY_JOINT_OCCUPIER, EVERY_JOINT_OCCUPIER] + # HB: Peter is an occupier (reg 13D(12)) and a non-dependant (reg 3) of + # each joint tenant: John and Alex are each entitled to 2 bedrooms, are + # not young individuals without a non-dependant (13D(2)(a)(i)), and get + # the two-bedroom rate (C). + housing_benefit_LHA_allowed_bedrooms: [2, 2, 1] + housing_benefit_claimant_has_non_dependant: [true, true, false] + housing_benefit_LHA_category: [C, C, A] + # Reg 74(5): equal shares of the rent, so half of the deduction each. + housing_benefit_non_dep_deductions: [500, 500, 0] + # CTR Sch 1 para 8(5): equally between the two liable persons. + council_tax_reduction_non_dep_deductions: [150, 150, 0] + # UC: Peter counts in John's claim only (Sch 4 para 9(2)(f)). + LHA_allowed_bedrooms: [2, 1, 1] + lha_renter_has_non_dependant: [true, false, false] + +- name: A joint tenant's sister counts only for that tenant (LHA Guidance Manual 2.093 example 2) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + sarah: + age: 30 + is_household_head: true + is_male: false + rachel: + age: 30 + is_male: false + susan: + age: 28 + is_male: false + household_benefits_individual_non_dep_deduction: 1_000 + council_tax_reduction_individual_non_dep_deduction: 300 + benunits: + sarah_family: + members: [sarah] + rachel_family: + members: [rachel] + liable_for_share_of_household_rent: true + susan_family: + members: [susan] + non_dependant_normally_resides_with: HOUSEHOLD_HEAD_FAMILY + households: + home: + members: [sarah, rachel, susan] + rent: 20_800 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + # Sarah: herself and Susan, 2 bedrooms, the two-bedroom rate. Rachel: + # 1 bedroom, a young individual with no non-dependant (13D(2)(a)(i)), and + # she shares the dwelling's rooms in any case (13D(2)(a)(ii)): the shared + # accommodation rate. + housing_benefit_LHA_allowed_bedrooms: [2, 1, 1] + housing_benefit_claimant_has_non_dependant: [true, false, false] + housing_benefit_LHA_category: [C, A, A] + # Susan is a non-dependant of one joint occupier only, so reg 74(5) does + # not apply and Sarah bears the whole deduction (HBGM A5 para 5.622); so + # too for CTR, which apportions only a non-dependant of two or more + # liable persons. + housing_benefit_non_dep_deductions: [1_000, 0, 0] + council_tax_reduction_non_dep_deductions: [300, 0, 0] + +- name: The sister of a joint tenant who is not the household head counts only for her + period: 2026 + absolute_error_margin: 0.01 + input: + people: + rachel: + age: 31 + is_household_head: true + is_male: false + sarah: + age: 30 + is_male: false + susan: + age: 28 + is_male: false + household_benefits_individual_non_dep_deduction: 1_000 + council_tax_reduction_individual_non_dep_deduction: 300 + benunits: + rachel_family: + members: [rachel] + sarah_family: + members: [sarah] + liable_for_share_of_household_rent: true + susan_family: + members: [susan] + non_dependant_normally_resides_with: OTHER_JOINT_OCCUPIERS + households: + home: + members: [rachel, sarah, susan] + rent: 20_800 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + housing_benefit_LHA_allowed_bedrooms: [1, 2, 1] + housing_benefit_claimant_has_non_dependant: [false, true, false] + housing_benefit_LHA_category: [A, C, A] + housing_benefit_non_dep_deductions: [0, 1_000, 0] + council_tax_reduction_non_dep_deductions: [0, 300, 0] + # UC is unchanged: one claim only, the household head's. + LHA_allowed_bedrooms: [2, 1, 1] + +- name: Joint tenants sharing a kitchen and bathroom each get the shared room rate (LHA Guidance Manual 2.093 example 1) + period: 2026 + input: + people: + ali: + age: 40 + is_household_head: true + mustafa: + age: 40 + benunits: + ali_family: + members: [ali] + mustafa_family: + members: [mustafa] + liable_for_share_of_household_rent: true + households: + home: + members: [ali, mustafa] + rent: 20_800 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + housing_benefit_LHA_allowed_bedrooms: [1, 1] + housing_benefit_claimant_has_non_dependant: [false, false] + housing_benefit_LHA_category: [A, A] + +- name: A shared non-dependant is apportioned by the joint occupiers' shares of the rent + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 45 + is_household_head: true + head_partner: + age: 44 + sharer_1: + age: 40 + sharer_2: + age: 41 + friend: + age: 38 + household_benefits_individual_non_dep_deduction: 1_000 + council_tax_reduction_individual_non_dep_deduction: 300 + benunits: + head_family: + members: [head, head_partner] + sharer_1_family: + members: [sharer_1] + liable_for_share_of_household_rent: true + sharer_2_family: + members: [sharer_2] + liable_for_share_of_household_rent: true + friend_family: + members: [friend] + households: + home: + members: [head, head_partner, sharer_1, sharer_2, friend] + rent: 20_800 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + # Four people liable: the couple's share is 2/4, each single sharer's + # 1/4 (reg 12B(4)), and the deduction follows (reg 74(5)). + share_of_household_rent: [0.5, 0.25, 0.25, 0] + housing_benefit_non_dep_deductions: [500, 250, 250, 0] + # CTR: equally between the four liable persons; the applicant couple's + # family bears one person's part, as for the council tax itself (see + # council_tax_reduction_joint_liability_share). + council_tax_reduction_non_dep_deductions: [75, 75, 75, 0] + # The friend counts in each joint occupier's size criteria. + housing_benefit_LHA_allowed_bedrooms: [2, 2, 2, 1] + +- name: A non-dependant of the joint occupiers other than the household head's family + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 45 + is_household_head: true + head_partner: + age: 44 + sharer_1: + age: 40 + sharer_2: + age: 41 + friend: + age: 38 + household_benefits_individual_non_dep_deduction: 1_000 + council_tax_reduction_individual_non_dep_deduction: 300 + benunits: + head_family: + members: [head, head_partner] + sharer_1_family: + members: [sharer_1] + liable_for_share_of_household_rent: true + sharer_2_family: + members: [sharer_2] + liable_for_share_of_household_rent: true + friend_family: + members: [friend] + non_dependant_normally_resides_with: OTHER_JOINT_OCCUPIERS + households: + home: + members: [head, head_partner, sharer_1, sharer_2, friend] + rent: 20_800 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + # The friend lives with the two sharers only: each bears half the HB + # deduction (equal shares of the rent between them) and half the CTR + # deduction; the household head's couple bears none and does not count + # the friend. + housing_benefit_non_dep_deductions: [0, 500, 500, 0] + council_tax_reduction_non_dep_deductions: [0, 150, 150, 0] + housing_benefit_LHA_allowed_bedrooms: [1, 2, 2, 1] + +- name: Without joint occupiers every value means the household head's family + period: 2026 + absolute_error_margin: 0.01 + input: + people: + renter: + age: 55 + is_household_head: true + daughter: + age: 27 + household_benefits_individual_non_dep_deduction: 1_000 + council_tax_reduction_individual_non_dep_deduction: 300 + benunits: + renter_family: + members: [renter] + daughter_family: + members: [daughter] + non_dependant_normally_resides_with: OTHER_JOINT_OCCUPIERS + households: + home: + members: [renter, daughter] + rent: 20_800 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + housing_benefit_LHA_allowed_bedrooms: [2, 1] + housing_benefit_claimant_has_non_dependant: [true, false] + housing_benefit_non_dep_deductions: [1_000, 0] + council_tax_reduction_non_dep_deductions: [300, 0] + +- name: A shared non-dependant couple is one bedroom for each joint tenant + period: 2026 + input: + people: + john: + age: 30 + is_household_head: true + alex: + age: 30 + peter: + age: 30 + paula: + age: 29 + benunits: + john_family: + members: [john] + alex_family: + members: [alex] + liable_for_share_of_household_rent: true + peter_family: + members: [peter, paula] + households: + home: + members: [john, alex, peter, paula] + rent: 20_800 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + # HB reg 13D(3)(a): an occupier couple is one category, so one bedroom, + # in each joint tenant's size criteria. + housing_benefit_LHA_allowed_bedrooms: [2, 2, 1] + # UC: each non-dependant not a child has a bedroom (Sch 4 para + # 10(1)(c)), in the household head's claim only. + LHA_allowed_bedrooms: [3, 1, 1] + +- name: A shared non-dependant's child counts for each joint tenant + period: 2026 + input: + people: + john: + age: 30 + is_household_head: true + alex: + age: 30 + peter: + age: 30 + peter_child: + age: 5 + benunits: + john_family: + members: [john] + alex_family: + members: [alex] + liable_for_share_of_household_rent: true + peter_family: + members: [peter, peter_child] + households: + home: + members: [john, alex, peter, peter_child] + rent: 20_800 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + # Peter and his child occupy each joint tenant's dwelling (reg 13D(12)): + # 1 + Peter + the child (13D(3)(e)) = 3 bedrooms each. + housing_benefit_LHA_allowed_bedrooms: [3, 3, 2] + +- name: A lodger of the household head counts only for the household head + period: 2026 + input: + people: + john: + age: 40 + is_household_head: true + alex: + age: 40 + lodger: + age: 40 + rent_paid_as_lodger: 5_200 + benunits: + john_family: + members: [john] + alex_family: + members: [alex] + liable_for_share_of_household_rent: true + lodger_family: + members: [lodger] + households: + home: + members: [john, alex, lodger] + rent: 20_800 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + # A lodger, who pays the household head, is not a non-dependant (reg + # 3(2)(e)) and occupies the head's dwelling only. + housing_benefit_LHA_allowed_bedrooms: [2, 1, 1] + housing_benefit_claimant_has_non_dependant: [false, false, false] + +- name: A shared non-dependant who needs overnight care gives each joint tenant an additional bedroom + period: 2026 + input: + people: + john: + age: 40 + is_household_head: true + alex: + age: 40 + peter: + age: 40 + attendance_allowance: 5_000 + has_non_resident_overnight_carer: true + benunits: + john_family: + members: [john] + alex_family: + members: [alex] + liable_for_share_of_household_rent: true + peter_family: + members: [peter] + households: + home: + members: [john, alex, peter] + rent: 20_800 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + # HB reg 13D(3A)(a)(iii): a person who occupies the claimant's dwelling + # and requires overnight care. + housing_benefit_LHA_additional_bedrooms: [1, 1, 1] + housing_benefit_LHA_allowed_bedrooms: [3, 3, 2] + +- name: Supplied rent shares set each joint tenant's part of a shared non-dependant's deduction + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + is_household_head: true + sharer: + age: 40 + friend: + age: 40 + household_benefits_individual_non_dep_deduction: 1_000 + benunits: + head_family: + members: [head] + share_of_household_rent: 0.75 + sharer_family: + members: [sharer] + liable_for_share_of_household_rent: true + share_of_household_rent: 0.25 + friend_family: + members: [friend] + share_of_household_rent: 0 + households: + home: + members: [head, sharer, friend] + rent: 20_800 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + # Reg 74(5): "having regard to ... the proportion of the payments in + # respect of the dwelling payable by each of them". + housing_benefit_non_dep_deductions: [750, 250, 0] + +- name: A non-dependant of some joint tenants is split by their supplied rent shares + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + is_household_head: true + sharer_1: + age: 40 + sharer_2: + age: 41 + friend: + age: 38 + household_benefits_individual_non_dep_deduction: 1_000 + benunits: + head_family: + members: [head] + share_of_household_rent: 0.5 + sharer_1_family: + members: [sharer_1] + liable_for_share_of_household_rent: true + share_of_household_rent: 0.3 + sharer_2_family: + members: [sharer_2] + liable_for_share_of_household_rent: true + share_of_household_rent: 0.2 + friend_family: + members: [friend] + share_of_household_rent: 0 + non_dependant_normally_resides_with: OTHER_JOINT_OCCUPIERS + households: + home: + members: [head, sharer_1, sharer_2, friend] + rent: 20_800 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + # 0.3 / (0.3 + 0.2) and 0.2 / (0.3 + 0.2) of the deduction. + housing_benefit_non_dep_deductions: [0, 600, 400, 0] + +- name: A sharer with no rent is never its own or the head's non-dependant + period: 2026 + input: + people: + head: + age: 30 + is_household_head: true + sharer: + age: 30 + benunits: + head_family: + members: [head] + housing_benefit_shares_accommodation: false + sharer_family: + members: [sharer] + liable_for_share_of_household_rent: true + housing_benefit_shares_accommodation: false + households: + home: + members: [head, sharer] + rent: 0 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + # HB reg 3(2)(a), (d): neither joint occupier is a non-dependant of + # either, whatever the rent; so each 30-year-old is a young individual + # with no non-dependant (13D(2)(a)(i)). + housing_benefit_claimant_has_non_dependant: [false, false] + housing_benefit_LHA_allowed_bedrooms: [1, 1] + housing_benefit_LHA_category: [A, A] + +- name: Without sharers the CTR deductions keep the previous formula, national and local + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + is_household_head: true + council_tax_reduction_individual_non_dep_deduction: 0 + merton_council_tax_reduction_individual_non_dep_deduction: 0 + adult_1: + age: 40 + council_tax_reduction_individual_non_dep_deduction: 300 + merton_council_tax_reduction_individual_non_dep_deduction: 300 + adult_2: + age: 40 + council_tax_reduction_individual_non_dep_deduction: 200 + merton_council_tax_reduction_individual_non_dep_deduction: 200 + benunits: + head_family: + members: [head] + family_1: + members: [adult_1] + family_2: + members: [adult_2] + households: + home: + members: [head, adult_1, adult_2] + rent: 20_800 + tenure_type: RENT_PRIVATELY + output: + # Every family has every other family's deductions (only the claimant's + # value is used): 300 + 200, 200 and 300. + council_tax_reduction_non_dep_deductions: [500, 200, 300] + merton_council_tax_reduction_non_dep_deductions: [500, 200, 300] + +- name: A non-dependant of only a couple who share the rent with a sharer falls wholly on the couple's CTR claim + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 45 + is_household_head: true + head_partner: + age: 44 + sharer: + age: 40 + relative: + age: 30 + council_tax_reduction_individual_non_dep_deduction: 300 + benunits: + head_family: + members: [head, head_partner] + sharer_family: + members: [sharer] + liable_for_share_of_household_rent: true + relative_family: + members: [relative] + non_dependant_normally_resides_with: HOUSEHOLD_HEAD_FAMILY + households: + home: + members: [head, head_partner, sharer, relative] + rent: 20_800 + tenure_type: RENT_PRIVATELY + output: + # The model treats a couple as one claim here, as it does when no one + # else is liable: the relative resides with that family alone, so its + # claim bears the whole deduction. Where both partners are named joint + # tenants (liable under LGFA 1992 s.6, not only s.9), the literal Sch 1 + # para 8(5) would split it between the two of them; the model cannot + # tell how a couple is liable. + council_tax_reduction_non_dep_deductions: [300, 0, 0] + +- name: A sharer flag on the household head's own family shares the rent with no one + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + is_household_head: true + council_tax_reduction_individual_non_dep_deduction: 0 + merton_council_tax_reduction_individual_non_dep_deduction: 0 + adult_1: + age: 40 + council_tax_reduction_individual_non_dep_deduction: 300 + merton_council_tax_reduction_individual_non_dep_deduction: 300 + adult_2: + age: 40 + council_tax_reduction_individual_non_dep_deduction: 200 + merton_council_tax_reduction_individual_non_dep_deduction: 200 + benunits: + head_family: + members: [head] + liable_for_share_of_household_rent: true + family_1: + members: [adult_1] + family_2: + members: [adult_2] + households: + home: + members: [head, adult_1, adult_2] + rent: 20_800 + tenure_type: RENT_PRIVATELY + output: + # The head's family is always liable; the flag matters only for other + # families, so this is the no-sharer case and the previous formula holds. + council_tax_reduction_non_dep_deductions: [500, 200, 300] + merton_council_tax_reduction_non_dep_deductions: [500, 200, 300] + +- name: Without sharers every residence value takes the supplied rent share + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + is_household_head: true + daughter: + age: 22 + household_benefits_individual_non_dep_deduction: 1_000 + benunits: + head_family: + members: [head] + share_of_household_rent: 0.6 + daughter_family: + members: [daughter] + share_of_household_rent: 0 + non_dependant_normally_resides_with: HOUSEHOLD_HEAD_FAMILY + households: + home: + members: [head, daughter] + rent: 20_800 + tenure_type: RENT_PRIVATELY + output: + # As for the default value: the household head's family's supplied + # share of the rent times the deduction. + housing_benefit_non_dep_deductions: [600, 0] diff --git a/policyengine_uk/tests/test_joint_tenant_shared_non_dependant_properties.py b/policyengine_uk/tests/test_joint_tenant_shared_non_dependant_properties.py new file mode 100644 index 0000000000..e87bf9c295 --- /dev/null +++ b/policyengine_uk/tests/test_joint_tenant_shared_non_dependant_properties.py @@ -0,0 +1,403 @@ +"""Property-based tests for non-dependants of joint occupiers in Housing +Benefit and Council Tax Reduction (non_dependant_normally_resides_with). + +Invariants, for any generated population of households: + +1. Conservation (HB reg 74(5)): in every household, the families' Housing + Benefit non-dependant deductions sum to the household's individual + deductions for its non-dependants, whichever joint occupiers each one + resides with. +2. Bounds (CTR Sch 1 para 8(5)): each family's Council Tax Reduction part is + between zero and the household's deductions; where another family shares + the rent, the joint occupiers' parts sum to at most the household's + deductions (a non-dependant of several jointly liable families is split + equally per liable person, each family bearing one person's part; one of + a single family, a couple included, is that family's whole deduction) + and no other family bears any. Where no other family shares the rent, + every family's deductions equal the previous formula (differential). +3. Differential: with every non-dependant shared (the default), the Housing + Benefit deductions equal the previous formula (each family's share of the + rent, computed or supplied, times the household's deductions) and the + Council Tax Reduction deductions equal the previous formula (the + deductions outside the family times + council_tax_reduction_joint_liability_share) in every household where the + previous formulas classified the joint occupiers correctly: where every + joint-occupier family has rent of its own. The previous rent-based flags + counted a joint occupier with no rent of its own (no household rent, or a + supplied zero share) as a non-dependant; that is the intended correction. +4. Attribution: a shared non-dependant counts in the household head's + family's size criteria exactly as when they are the head family's only, + and in a sharer's exactly as when they are the other joint occupiers' + only; a family that is not a joint occupier bears no deduction. +5. No-op: in a household with no sharers, the residence input changes + nothing. +6. Universal Credit does not depend on the residence input. +7. Monotonicity: marking a non-dependant as one joint occupier's only never + raises another joint occupier's bedrooms or deductions. +""" + +import numpy as np +from hypothesis import HealthCheck, assume, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +YEAR = 2026 +PROPERTY_SETTINGS = settings( + max_examples=8, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +RESIDENCE = [ + "EVERY_JOINT_OCCUPIER", + "HOUSEHOLD_HEAD_FAMILY", + "OTHER_JOINT_OCCUPIERS", +] +income = st.floats(0, 60_000, allow_nan=False, allow_infinity=False) +# Amounts are float32 in the model, so sums taken in different orders differ +# by up to about 1e-3 at these magnitudes: compare money to the penny. +MONEY_TOLERANCE = 0.01 + + +@st.composite +def family(draw, max_adults=2): + return dict( + ages=draw(st.lists(st.integers(18, 90), min_size=1, max_size=max_adults)), + child_age=draw(st.one_of(st.none(), st.integers(0, 15))), + income=draw(income), + ) + + +@st.composite +def households(draw): + return dict( + head=draw(family()), + sharers=draw(st.lists(family(), min_size=0, max_size=2)), + non_dependants=draw( + st.lists( + st.tuples(family(), st.sampled_from(RESIDENCE)), + min_size=0, + max_size=2, + ) + ), + lodger=draw(st.booleans()), + rent=draw(st.one_of(st.just(0.0), st.floats(2_000, 30_000, allow_nan=False))), + # Supplied rent-share weights for the head and up to two sharers + # (normalised over the families liable for the rent), or None. + share_weights=draw( + st.one_of( + st.none(), + st.lists( + st.one_of(st.just(0.0), st.floats(0.05, 1)), + min_size=3, + max_size=3, + ), + ) + ), + ) + + +population = st.lists(households(), min_size=1, max_size=5) + + +def build(population, residence_override=None): + """One situation for the whole population, with each family's role.""" + people, benunits, homes, roles = {}, {}, {}, [] + supplied = any(house["share_weights"] is not None for house in population) + for h, house in enumerate(population): + members = [] + + def add_family(name, fam, role, extra): + ids = [] + for i, age in enumerate(fam["ages"]): + pid = f"{name}_adult_{i}" + people[pid] = { + "age": {YEAR: age}, + "is_claimant_or_partner": {YEAR: True}, + "is_household_head": {YEAR: False}, + "employment_income": {YEAR: fam["income"] if i == 0 else 0}, + } + ids.append(pid) + if fam["child_age"] is not None: + pid = f"{name}_child" + people[pid] = { + "age": {YEAR: fam["child_age"]}, + "is_claimant_or_partner": {YEAR: False}, + "is_household_head": {YEAR: False}, + } + ids.append(pid) + benunits[name] = {"members": ids, **extra} + members.extend(ids) + roles.append(role) + return ids + + weights = house["share_weights"] + shares = [None] * (1 + len(house["sharers"])) + if weights is not None and sum(weights[: len(shares)]) > 0: + used = weights[: len(shares)] + shares = [w / sum(used) for w in used] + elif supplied: + # policyengine-core treats a variable given for some families as + # an input for all, defaulting the rest to zero, so supply the + # formula's shares (people liable over people liable in the + # household; every adult here is a claimant or partner). + adults = [len(house["head"]["ages"])] + [ + len(sharer["ages"]) for sharer in house["sharers"] + ] + shares = [a / sum(adults) for a in adults] + + def share_input(i): + if shares[i] is None: + return {} + return {"share_of_household_rent": {YEAR: shares[i]}} + + head_ids = add_family(f"h{h}_head", house["head"], "head", share_input(0)) + people[head_ids[0]]["is_household_head"] = {YEAR: True} + for f, sharer in enumerate(house["sharers"]): + add_family( + f"h{h}_s{f}", + sharer, + "sharer", + { + "liable_for_share_of_household_rent": {YEAR: True}, + **share_input(f + 1), + }, + ) + for f, (non_dep, residence) in enumerate(house["non_dependants"]): + add_family( + f"h{h}_n{f}", + non_dep, + "non_dependant", + { + "non_dependant_normally_resides_with": { + YEAR: residence_override or residence + }, + **({"share_of_household_rent": {YEAR: 0.0}} if supplied else {}), + }, + ) + if house["lodger"]: + ids = add_family( + f"h{h}_l", + dict(ages=[40], child_age=None, income=10_000), + "lodger", + {"share_of_household_rent": {YEAR: 0.0}} if supplied else {}, + ) + people[ids[0]]["rent_paid_as_lodger"] = {YEAR: 4_000} + homes[f"h{h}"] = { + "members": members, + "rent": {YEAR: house["rent"]}, + "tenure_type": {YEAR: "RENT_PRIVATELY"}, + "brma": {YEAR: "MAIDSTONE"}, + } + situation = {"people": people, "benunits": benunits, "households": homes} + return Simulation(situation=situation), np.array(roles) + + +def calc(sim, variable): + return np.asarray(sim.calculate(variable, YEAR), dtype=float) + + +def benunit_household(sim): + """Household index of each benefit unit.""" + person_benunit = sim.populations["benunit"].members_entity_id + person_household = sim.populations["household"].members_entity_id + out = np.zeros(sim.populations["benunit"].count, dtype=int) + out[person_benunit] = person_household + return out + + +def per_household(sim, benunit_values): + return np.bincount( + benunit_household(sim), + weights=benunit_values, + minlength=sim.populations["household"].count, + ) + + +def person_per_household(sim, person_values): + return np.bincount( + sim.populations["household"].members_entity_id, + weights=person_values, + minlength=sim.populations["household"].count, + ) + + +def in_joint_occupier_family(sim, roles): + """Whether each person belongs to the household head's or a sharer's + family: such people are never anyone's non-dependant.""" + person_role = roles[sim.populations["benunit"].members_entity_id] + return (person_role == "head") | (person_role == "sharer") + + +def has_sharer(sim, roles): + return ( + per_household(sim, (roles == "sharer").astype(float))[benunit_household(sim)] + > 0 + ) + + +@PROPERTY_SETTINGS +@given(population) +def test_housing_benefit_deductions_are_conserved(population): + sim, roles = build(population) + # A sharer with no rent is not rent-liable, but is still a joint + # occupier and so no one's non-dependant (HB reg 3(2)(d)). + deductions = ( + calc(sim, "household_benefits_individual_non_dep_deduction") + * calc(sim, "is_non_dependant_of_household_head") + * ~in_joint_occupier_family(sim, roles) + ) + families = calc(sim, "housing_benefit_non_dep_deductions") + np.testing.assert_allclose( + per_household(sim, families), + person_per_household(sim, deductions), + atol=MONEY_TOLERANCE, + ) + # A family that is not a joint occupier bears none. + assert np.all(families[(roles == "non_dependant") | (roles == "lodger")] == 0) + + +@PROPERTY_SETTINGS +@given(population) +def test_council_tax_reduction_deductions_are_bounded(population): + sim, roles = build(population) + deductions = calc(sim, "council_tax_reduction_individual_non_dep_deduction") + in_household = person_per_household(sim, deductions) + families = calc(sim, "council_tax_reduction_non_dep_deductions") + assert np.all(families >= -MONEY_TOLERANCE) + assert np.all(families <= in_household[benunit_household(sim)] + MONEY_TOLERANCE) + # Where the rent is shared, the joint occupiers' parts never exceed the + # household's deductions, and no one else bears any. + shared = has_sharer(sim, roles) + joint_occupier = (roles == "head") | (roles == "sharer") + joint_parts = per_household(sim, families * (shared & joint_occupier)) + assert np.all(joint_parts <= in_household + MONEY_TOLERANCE) + assert np.all(families[shared & ~joint_occupier] == 0) + + +@PROPERTY_SETTINGS +@given(population) +def test_council_tax_reduction_is_unchanged_without_sharers(population): + """Differential: where no family shares the rent, every family's CTR + non-dependant deductions equal the previous formula (the deductions of + every other family), whoever the oldest adult's claiming family is.""" + sim, roles = build(population) + deductions = calc(sim, "council_tax_reduction_individual_non_dep_deduction") + own = np.bincount( + sim.populations["benunit"].members_entity_id, + weights=deductions, + minlength=sim.populations["benunit"].count, + ) + previous = person_per_household(sim, deductions)[benunit_household(sim)] - own + without_sharer = ~has_sharer(sim, roles) + np.testing.assert_allclose( + calc(sim, "council_tax_reduction_non_dep_deductions")[without_sharer], + previous[without_sharer], + atol=MONEY_TOLERANCE, + ) + + +@PROPERTY_SETTINGS +@given(population) +def test_shared_default_matches_the_previous_formulas(population): + sim, roles = build(population, residence_override="EVERY_JOINT_OCCUPIER") + household = benunit_household(sim) + hb_individual = calc(sim, "household_benefits_individual_non_dep_deduction") * calc( + sim, "is_non_dependant_of_household_head" + ) + share = calc(sim, "share_of_household_rent") + previous_hb = share * person_per_household(sim, hb_individual)[household] + ctr_individual = calc(sim, "council_tax_reduction_individual_non_dep_deduction") + own = np.bincount( + sim.populations["benunit"].members_entity_id, + weights=ctr_individual, + minlength=sim.populations["benunit"].count, + ) + previous = (person_per_household(sim, ctr_individual)[household] - own) * calc( + sim, "council_tax_reduction_joint_liability_share" + ) + joint_occupier = (roles == "head") | (roles == "sharer") + # The previous formulas classified a joint occupier with no rent of its + # own (no household rent, or a supplied zero share) as a non-dependant; + # compare only households where every joint occupier has rent. + rentless = joint_occupier & (calc(sim, "benunit_rent") <= 0) + positive_rent = per_household(sim, rentless.astype(float))[household] == 0 + hb = calc(sim, "housing_benefit_non_dep_deductions") + np.testing.assert_allclose( + hb[positive_rent], previous_hb[positive_rent], atol=MONEY_TOLERANCE + ) + compared = joint_occupier & positive_rent + np.testing.assert_allclose( + calc(sim, "council_tax_reduction_non_dep_deductions")[compared], + previous[compared], + atol=MONEY_TOLERANCE, + ) + + +@PROPERTY_SETTINGS +@given(population) +def test_shared_counts_as_head_only_for_the_head_and_as_others_only_for_sharers( + population, +): + # Only households with both a sharer and a non-dependant test anything. + assume(any(h["sharers"] and h["non_dependants"] for h in population)) + shared, roles = build(population, residence_override="EVERY_JOINT_OCCUPIER") + head_only, _ = build(population, residence_override="HOUSEHOLD_HEAD_FAMILY") + others_only, _ = build(population, residence_override="OTHER_JOINT_OCCUPIERS") + rooms = "housing_benefit_LHA_allowed_bedrooms" + head = roles == "head" + sharer = roles == "sharer" + np.testing.assert_array_equal( + calc(shared, rooms)[head], calc(head_only, rooms)[head] + ) + np.testing.assert_array_equal( + calc(shared, rooms)[sharer], calc(others_only, rooms)[sharer] + ) + # Monotonicity: one joint occupier's non-dependant never raises another's + # bedrooms or deductions. + deductions = "housing_benefit_non_dep_deductions" + assert np.all(calc(head_only, rooms)[sharer] <= calc(shared, rooms)[sharer]) + assert np.all( + calc(head_only, deductions)[sharer] + <= calc(shared, deductions)[sharer] + MONEY_TOLERANCE + ) + with_sharer = has_sharer(shared, roles) + assert np.all( + calc(others_only, rooms)[head & with_sharer] + <= calc(shared, rooms)[head & with_sharer] + ) + assert np.all( + calc(others_only, deductions)[head & with_sharer] + <= calc(shared, deductions)[head & with_sharer] + MONEY_TOLERANCE + ) + + +@PROPERTY_SETTINGS +@given(population) +def test_residence_is_a_no_op_without_sharers_and_for_universal_credit(population): + sims = [build(population, residence_override=r) for r in RESIDENCE] + roles = sims[0][1] + without_sharer = ~has_sharer(sims[0][0], roles) + for variable in [ + "housing_benefit_LHA_allowed_bedrooms", + "housing_benefit_LHA_additional_bedrooms", + "housing_benefit_non_dep_deductions", + "council_tax_reduction_non_dep_deductions", + "housing_benefit_claimant_has_non_dependant", + ]: + reference = calc(sims[0][0], variable)[without_sharer] + for sim, _ in sims[1:]: + np.testing.assert_allclose( + calc(sim, variable)[without_sharer], reference, atol=MONEY_TOLERANCE + ) + for variable in [ + "LHA_allowed_bedrooms", + "uc_non_dep_deductions", + "lha_renter_has_non_dependant", + ]: + reference = calc(sims[0][0], variable) + for sim, _ in sims[1:]: + np.testing.assert_allclose( + calc(sim, variable), reference, atol=MONEY_TOLERANCE + ) diff --git a/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py b/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py index e80ae2d80e..0eb4a0f384 100644 --- a/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py +++ b/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py @@ -353,8 +353,14 @@ def test_exact_effect_of_adding_a_person_aged_16_to_19(case): head = keys.index((target[0], 0)) if fam["role"] == "non_dependant": expected_uc[head] += 0 if added["unclaimed"] else 1 - if fam["role"] in ("non_dependant", "boarder", "lodger"): + if fam["role"] in ("boarder", "lodger"): expected_hb[head] += 0 if added["placed"] else 1 + if fam["role"] == "non_dependant": + # HB: a non-dependant resides with every joint occupier by default + # (non_dependant_normally_resides_with; LHA Guidance Manual 2.110). + for g, other in enumerate(population[target[0]]): + if other["role"] in ("head", "sharer"): + expected_hb[keys.index((target[0], g))] += 0 if added["placed"] else 1 assert np.array_equal(uc_after - uc_before, expected_uc) assert np.array_equal(hb_after - hb_before, expected_hb) @@ -424,10 +430,21 @@ def test_bedrooms_match_a_reference_count_of_the_size_criteria(case): hb_overnight = True uc_foster = fosters(fam) or (is_target and added["fostered"]) hb_carer = fosters(fam) or (is_target and added["placed"]) - if fam["role"] == "head": - for g, other in enumerate(families[1:], start=1): + if fam["role"] in ("head", "sharer"): + # HB: boarders and lodgers occupy the household head's dwelling; + # a non-dependant resides with every joint occupier by default + # (non_dependant_normally_resides_with; LHA Guidance Manual + # 2.110). UC counts non-dependants for the head's family only. + hb_roles = ( + ("non_dependant", "boarder", "lodger") + if fam["role"] == "head" + else ("non_dependant",) + ) + for g, other in enumerate(families): + if g == f: + continue joins = target == (h, g) - if other["role"] == "non_dependant": + if fam["role"] == "head" and other["role"] == "non_dependant": uc_rooms += len(other["adults"]) + int( joins and not added["unclaimed"] ) @@ -438,7 +455,7 @@ def test_bedrooms_match_a_reference_count_of_the_size_criteria(case): uc_overnight |= ( joins and added["overnight"] and not added["unclaimed"] ) - if other["role"] in ("non_dependant", "boarder", "lodger"): + if other["role"] in hb_roles: # HB reg 13D(3)(a): the family's claimant or couple # has one bedroom. hb_rooms += 1 + int(joins and not added["placed"]) diff --git a/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py b/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py index c210ae2f12..f676631a6d 100644 --- a/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py +++ b/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py @@ -26,7 +26,9 @@ def rooms_for_children(boys_under_10, older_boys, girls_under_10, older_girls): return over_10_rooms + under_10_rooms -def bedrooms_for_children(benunit, period, own_child=None, other_child=None): +def bedrooms_for_children( + benunit, period, own_child=None, other_child=None, count_other=None +): """Bedrooms for the children under 16 in the family's size criteria. Children must share rooms in pairs unless they are opposite-sex and one @@ -37,7 +39,9 @@ def bedrooms_for_children(benunit, period, own_child=None, other_child=None): default, every member under 16). ``other_child`` marks people under 16 in other families of the household who count in the household head's family's size criteria, such as a non-dependant's child; they share rooms - with the head family's own children. + with the head family's own children. ``count_other``, if given, maps a + person mask to the number of those people counted for each family, in + place of counting them all for the household head's family. """ person = benunit.members age = person("age", period) @@ -53,8 +57,10 @@ def count(group): own = benunit.sum(own_child & group) if other_child is None: return own - others = person.household.sum(other_child & under_16 & group) - return own + head_family * benunit.max(others) + others = other_child & under_16 & group + if count_other is not None: + return own + count_other(others) + return own + head_family * benunit.max(person.household.sum(others)) return rooms_for_children( count(under_10 & male), 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 e5e9e3a933..58f47e83c9 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 @@ -1,4 +1,7 @@ from policyengine_uk.model_api import * +from policyengine_uk.variables.household.consumption.rent.non_dependant_normally_resides_with import ( + apportioned_non_dependant_deductions, +) class housing_benefit_non_dep_deductions(Variable): @@ -11,6 +14,7 @@ class housing_benefit_non_dep_deductions(Variable): "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", + "https://assets.publishing.service.gov.uk/media/5a7ce2b840f0b6629523c64c/hbgm-a5-calculating-benefit.pdf", ) def formula(benunit, period, parameters): @@ -19,12 +23,19 @@ def formula(benunit, period, parameters): # boarders, lodgers and the landlord's household are not # 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. + # of the payments (reg 74(5); SPC reg 55(5)); one who resides with + # only one joint occupier is deducted in full from that occupier's + # award (HB Guidance Manual A5 para 5.622). Which joint occupiers a + # non-dependant resides with is non_dependant_normally_resides_with; + # 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) - share = benunit("share_of_household_rent", period) - return share * benunit.max(person.household.sum(deductions)) + return apportioned_non_dependant_deductions( + benunit, + period, + deductions, + equally=False, + every_joint_occupier_part=benunit("share_of_household_rent", period), + ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_additional_bedrooms.py b/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_additional_bedrooms.py index d43048fd17..fdca7dc704 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_additional_bedrooms.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_additional_bedrooms.py @@ -1,4 +1,7 @@ from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp.housing_benefit_LHA_allowed_bedrooms import ( + housing_benefit_other_occupiers, +) class housing_benefit_LHA_additional_bedrooms(Variable): @@ -11,12 +14,14 @@ class housing_benefit_LHA_additional_bedrooms(Variable): "meets_lha_overnight_care_condition), and one if the claimant or " "partner is a qualifying parent or carer (see " "is_housing_benefit_qualifying_parent_or_carer): two if both. " - "Occupiers outside the family count for the household head's family: " - "non-dependants, boarders and lodgers and their children, but not a " - "sharer of the rent or a child placed with them as a foster child or " - "for adoption. Before 1 April 2017 only the claimant and partner " - "counted for overnight care. The definitions require a bedroom in " - "the dwelling for " + "Occupiers outside the family count as in " + "housing_benefit_LHA_allowed_bedrooms: boarders and lodgers and their " + "children for the household head's family, and non-dependants and " + "their children for each joint occupier they normally reside with, " + "but not a sharer of the rent or a child placed with them as a foster " + "child or for adoption. Before 1 April 2017 only the claimant and " + "partner counted for overnight care. The definitions require a " + "bedroom in the dwelling for " "the carer or the foster child; the model does not observe the " "dwelling's bedrooms and assumes there is one. Children and couples " "who cannot share a bedroom because of disability are not modelled." @@ -44,8 +49,7 @@ def formula(benunit, period, parameters): sharer = person.benunit("liable_for_share_of_household_rent", period) placed = person("is_child_or_young_person_placed_with_family", period) other = overnight_care & ~head_family & ~sharer & ~placed - is_head_family = benunit.any(person("is_household_head", period)) - others = is_head_family * benunit.max(person.household.sum(other)) + others = housing_benefit_other_occupiers(benunit, period, other) # Reg 13D(3A)(a)(i)-(iv): the claimant, partner, other members of the # family, and a child or young person for whom the claimant or # partner is a qualifying parent or carer. Before 1 April 2017 (SI 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 bbf65f77cb..3fbdba58d0 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 @@ -2,6 +2,30 @@ from policyengine_uk.variables.gov.dwp.LHA_allowed_bedrooms import ( bedrooms_for_children, ) +from policyengine_uk.variables.household.consumption.rent.non_dependant_normally_resides_with import ( + non_dependants_residing_with, +) + + +def housing_benefit_other_occupiers(benunit, period, other_occupier): + """For each family, the sum of ``other_occupier`` (a count or weight for + each occupier outside every family liable for the household's rent, zero + for anyone else) over the people who occupy its dwelling (HB Regs 2006 + reg 13D(12)). A boarder or lodger, who + pays the householder, counts for the household head's family. A + non-dependant counts for each joint occupier they normally reside with + (LHA Guidance Manual paras 2.093 and 2.110; see + non_dependant_normally_resides_with).""" + person = benunit.members + boarder_or_lodger = person("pays_rent_to_householder", period) + head_family = benunit.any(person("is_household_head", period)) + of_householder = head_family * benunit.max( + person.household.sum(other_occupier * boarder_or_lodger) + ) + non_dependants = non_dependants_residing_with( + benunit, period, other_occupier * ~boarder_or_lodger + ) + return of_householder + non_dependants class housing_benefit_LHA_allowed_bedrooms(Variable): @@ -24,8 +48,12 @@ class housing_benefit_LHA_allowed_bedrooms(Variable): "non-dependant, boarder or " "lodger are occupiers too, and share rooms with the claimant's " "children. A child or young person placed with a family in the " - "household as a foster child or for adoption is not an occupier. A " - "sharer's, boarder's or lodger's own claim counts only their own " + "household as a foster child or for adoption is not an occupier. " + "Where the rent is shared, a non-dependant and their children count " + "for each joint occupier they normally reside with, by default every " + "one (see non_dependant_normally_resides_with), while a boarder or " + "lodger, who pays the household head, counts for the head's family " + "only. A boarder's or lodger's own claim counts only their own " "family." ) definition_period = YEAR @@ -71,9 +99,8 @@ def formula(benunit, period, parameters): "is_couple", period ) occupier_rooms = counted * where(claimant_or_partner & couple, 0.5, 1) - is_head_family = benunit.any(person("is_household_head", period)) - other_occupiers = is_head_family * benunit.max( - person.household.sum(occupier_rooms) + other_occupiers = housing_benefit_other_occupiers( + benunit, period, occupier_rooms ) # Reg 13D(3)(c)-(e): the occupiers' children, the claimant's own and # other families', share rooms with each other. @@ -82,6 +109,9 @@ def formula(benunit, period, parameters): period, own_child=occupier, other_child=other_occupier, + count_other=lambda mask: housing_benefit_other_occupiers( + benunit, period, mask + ), ) # Reg 13D(3A) and (3B): additional bedrooms. additional = benunit("housing_benefit_LHA_additional_bedrooms", 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 index 305cf304bf..16d12d7032 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_category.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_category.py @@ -8,7 +8,8 @@ class housing_benefit_LHA_category(Variable): 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 " + "rate applies to a young individual with no non-dependant residing " + "with them (see housing_benefit_claimant_has_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 " @@ -30,7 +31,7 @@ def formula(benunit, period, parameters): # 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_claimant_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_claimant_has_non_dependant.py b/policyengine_uk/variables/gov/dwp/housing_benefit_claimant_has_non_dependant.py new file mode 100644 index 0000000000..b5ab95cddf --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit_claimant_has_non_dependant.py @@ -0,0 +1,47 @@ +from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp.lha_renter_has_non_dependant import ( + has_non_dependant_in_benefit_unit, +) +from policyengine_uk.variables.household.consumption.rent.non_dependant_normally_resides_with import ( + non_dependants_residing_with, +) + + +class housing_benefit_claimant_has_non_dependant(Variable): + value_type = bool + entity = BenUnit + label = "Housing Benefit claimant has a non-dependant residing with them" + documentation = ( + "Whether a non-dependant resides with the claimant, for the young " + "individual's shared accommodation rate: someone in the benefit unit " + "who is neither a claimant or partner nor a child or young person, or " + "a non-dependant of another family (see " + "is_non_dependant_of_household_head) who normally resides with this " + "family. In a household whose rent is shared, a non-dependant resides " + "with each joint occupier given by non_dependant_normally_resides_with " + "(by default all of them); otherwise with the household head's " + "family. Joint tenants and other sharers of the rent, boarders and " + "lodgers are not non-dependants." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/3", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/13D", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/21", + "https://assets.publishing.service.gov.uk/media/5a758238ed915d6faf2b38a2/lha-guidance-manual.pdf#page=33", + ) + + def formula(benunit, period, parameters): + person = benunit.members + within_benefit_unit = has_non_dependant_in_benefit_unit(benunit, period) + # HB Regs 2006 reg 3(1), (2)(d)-(e) and (4): a non-dependant of + # several joint occupiers resides with each of them (LHA Guidance + # Manual paras 2.050 and 2.110); one who lives with one joint tenant + # only resides with that tenant alone (para 2.093, example 2). + non_dependant_claimants = person("is_claimant_or_partner", period) & person( + "is_non_dependant_of_household_head", period + ) + other_families = non_dependants_residing_with( + benunit, period, non_dependant_claimants + ) + return within_benefit_unit | (other_families > 0) 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 c5228ab41c..1ca4343d59 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 @@ -13,14 +13,17 @@ class is_non_dependant_of_household_head(Variable): "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 " - "between them. The flag covers every member of such a family, " - "including children. A non-dependant's own children are " - "non-dependants too, but a child or qualifying young person looked " - "after by a local authority is not (UC Sch 4 para 9(2)(g)); the " - "size criteria apply that exclusion (see LHA_allowed_bedrooms). " - "Carers engaged through a charity are not identified." + "gives to the household head's family. For Housing Benefit and " + "Council Tax Reduction a non-dependant is the non-dependant of each " + "joint occupier they normally reside with (see " + "non_dependant_normally_resides_with): they count in each one's size " + "criteria and their deduction is apportioned between them. The flag " + "covers every member of such a family, including children. A " + "non-dependant's own children are non-dependants too, but a child or " + "qualifying young person looked after by a local authority is not " + "(UC Sch 4 para 9(2)(g)); the size criteria apply that exclusion (see " + "LHA_allowed_bedrooms). Carers engaged through a charity are not " + "identified." ) definition_period = YEAR reference = ( 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 5d0f6f3ce0..091e20739d 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 @@ -1,45 +1,54 @@ from policyengine_uk.model_api import * +def has_non_dependant_in_benefit_unit(benunit, period): + """Whether someone in the benefit unit is neither a claimant or partner + nor a child or young person, nor a child or young person placed with the + family as a foster child or for adoption.""" + person = benunit.members + claimant_or_partner = person("is_claimant_or_partner", period) + # A child or young person under either scheme (UC reg 5; HB reg 19) + # is a dependant, not a non-dependant. + age = person("age", period) + # A foster child or a child placed for adoption is not a non-dependant + # either (UC Sch 4 para 9(2)(c) and (g); HB reg 3(2)(c) and reg 21(3)). + child_or_qyp = ( + person("is_child_or_qualifying_young_person_for_universal_credit", period) + | person("is_child_or_young_person_for_legacy_benefits", period) + | person("is_child_or_young_person_placed_with_family", period) + | ((age >= 16) & (age < 17)) + ) + return benunit.any(~claimant_or_partner & ~child_or_qyp) + + 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. A child or young person placed with the " - "family as a foster child or for adoption is not a non-dependant." + "Universal Credit: 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 non-dependant counts in one Universal Credit claim only, which " + "the model gives to the household head's family. A child or young " + "person placed with the family as a foster child or for adoption is " + "not a non-dependant. Housing Benefit has its own test: see " + "housing_benefit_claimant_has_non_dependant." ) 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/213/regulation/21", + "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/28", ) def formula(benunit, period, parameters): person = benunit.members - claimant_or_partner = person("is_claimant_or_partner", period) - # A child or young person under either scheme (UC reg 5; HB reg 19) - # is a dependant, not a non-dependant. - age = person("age", period) - # A foster child or a child placed for adoption is not a - # non-dependant either (UC Sch 4 para 9(2)(c) and (g); HB reg 3(2)(c) - # and reg 21(3)). - child_or_qyp = ( - person("is_child_or_qualifying_young_person_for_universal_credit", period) - | person("is_child_or_young_person_for_legacy_benefits", period) - | person("is_child_or_young_person_placed_with_family", period) - | ((age >= 16) & (age < 17)) - ) - within_benefit_unit = benunit.any(~claimant_or_partner & ~child_or_qyp) - # 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( + within_benefit_unit = has_non_dependant_in_benefit_unit(benunit, period) + # UC Regs 2013 Sch 4 para 9(2)(d)-(f). + non_dependant_claimants = person("is_claimant_or_partner", period) & person( "is_non_dependant_of_household_head", period ) head_family = benunit.any(person("is_household_head", period)) 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 966d230ff7..745c4f3975 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 @@ -1,4 +1,8 @@ from policyengine_uk.model_api import * +from policyengine_uk.variables.household.consumption.rent.non_dependant_normally_resides_with import ( + apportioned_non_dependant_deductions, + rent_shared_with_another_family, +) from policyengine_uk.variables.household.demographic.highest_education import ( EducationType, ) @@ -10,6 +14,38 @@ def is_full_time_student_non_dep(person, period): ) +def council_tax_reduction_joint_liability_non_dep_deductions( + benunit, period, deductions, own +): + """Each family's deductions for the household's non-dependants + (``deductions``, one amount per person; ``own``, the family's own amount). + + Where a family other than the household head's shares the rent, a + non-dependant of two or more jointly liable families is apportioned + equally between their liable people (SI 2012/2885 Sch 1 para 8(5); WSI + 2013/3029 Sch 1 para 3(5) and Sch 6 para 5(5); SSI 2021/249 reg 90(5); + SSI 2012/319 reg 48(5)), each family bearing one person's part as for + the council tax itself (council_tax_reduction_joint_liability_share). + One who resides with only one of them is deducted in full from that + family, a couple included: the model cannot tell whether both partners + are named tenants liable under LGFA 1992 s.6 (when the literal para 8(5) + would split it between them) or liable only as spouses (s.9). See + non_dependant_normally_resides_with. Otherwise the claimant, the family + of the household's oldest adult (see + council_tax_reduction_claimant_benunit), has every other family's + deductions, as before. + """ + person = benunit.members + rent_is_shared = rent_shared_with_another_family(benunit, period) + in_household = benunit.max(person.household.sum(deductions)) + share = benunit("council_tax_reduction_joint_liability_share", period) + sole = (in_household - own) * share + joint = apportioned_non_dependant_deductions( + benunit, period, deductions, equally=True, every_joint_occupier_part=share + ) + return where(rent_is_shared, joint, sole) + + def legacy_council_tax_reduction( benunit, period, @@ -85,13 +121,9 @@ def local_non_dep_deductions( ) is_benunit_head = benunit.members("is_benunit_head", period) deductions_to_count = is_benunit_head * benunit.project(deduction_for_benunit) - deductions_in_household = benunit.max( - benunit.members.household.sum(deductions_to_count) + return council_tax_reduction_joint_liability_non_dep_deductions( + benunit, period, deductions_to_count, 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_non_dep_deductions.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py index 6241abf4c7..4ea0d2b3e8 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 @@ -1,4 +1,7 @@ from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.local_authorities.council_tax_reduction._legacy import ( + council_tax_reduction_joint_liability_non_dep_deductions, +) class council_tax_reduction_non_dep_deductions(Variable): @@ -7,14 +10,19 @@ class council_tax_reduction_non_dep_deductions(Variable): label = "CTR non-dependent deductions" definition_period = YEAR unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/2012/2885/regulation/9", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/8", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/3", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/6/paragraph/5", + "https://www.legislation.gov.uk/ssi/2021/249/regulation/90", + "https://www.legislation.gov.uk/ssi/2012/319/regulation/48", + ) def formula(benunit, period, parameters): deductions = benunit.members( "council_tax_reduction_individual_non_dep_deduction", period ) - deductions_in_household = benunit.max(benunit.members.household.sum(deductions)) - deductions_in_benunit = benunit.sum(deductions) - # 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 + return council_tax_reduction_joint_liability_non_dep_deductions( + benunit, period, deductions, benunit.sum(deductions) + ) diff --git a/policyengine_uk/variables/household/consumption/rent/non_dependant_normally_resides_with.py b/policyengine_uk/variables/household/consumption/rent/non_dependant_normally_resides_with.py new file mode 100644 index 0000000000..fadc43cb8c --- /dev/null +++ b/policyengine_uk/variables/household/consumption/rent/non_dependant_normally_resides_with.py @@ -0,0 +1,179 @@ +from policyengine_uk.model_api import * + + +class NonDependantResidence(Enum): + EVERY_JOINT_OCCUPIER = "Every family liable for the household's rent" + HOUSEHOLD_HEAD_FAMILY = "The household head's family only" + OTHER_JOINT_OCCUPIERS = ( + "The families sharing the household's rent with the household head's " + "family only" + ) + + +class non_dependant_normally_resides_with(Variable): + value_type = Enum + possible_values = NonDependantResidence + default_value = NonDependantResidence.EVERY_JOINT_OCCUPIER + entity = BenUnit + label = "Joint occupiers this family's non-dependants normally reside with" + documentation = ( + "For a family of non-dependants in a household whose rent is shared " + "by joint occupiers (joint tenants and other sharers of the rent), " + "which of those joint occupiers the family normally resides with for " + "Housing Benefit and Council Tax Reduction. This is a question of " + "fact. A non-dependant who lives with all of them, such as a friend " + "of two joint tenants, is each one's non-dependant: they count in " + "each joint tenant's size criteria and their deduction is apportioned " + "between them. One who lives with only one joint tenant, such as that " + "tenant's sister, counts only for that tenant, who bears the whole " + "deduction. The default is every joint occupier. With more than one " + "family sharing the rent with the household head's family, the third " + "value covers all of those families together. Where no family shares " + "the rent, every value means the household head's family. Universal " + "Credit does not use this input: a non-dependant counts in one Universal " + "Credit claim only, which the model gives to the household head's " + "family (see is_non_dependant_of_household_head)." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/3", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/13D", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/74", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/3", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/55", + "https://www.legislation.gov.uk/uksi/2012/2885/regulation/9", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/8", + "https://assets.publishing.service.gov.uk/media/5a758238ed915d6faf2b38a2/lha-guidance-manual.pdf#page=33", + ) + + +def rent_shared_with_another_family(benunit, period): + """Whether a family other than the household head's shares the + household's rent (liable_for_share_of_household_rent). The head's family + is always liable, so the flag on the head's own family means nothing.""" + person = benunit.members + in_sharer_family = person.benunit( + "liable_for_share_of_household_rent", period + ) & ~person.benunit.any(person("is_household_head", period)) + return benunit.any(person.household.any(in_sharer_family)) + + +def _joint_occupiers_by_residence(benunit, period): + """Pairs of (residence value, whether each family is one of the joint + occupiers a non-dependant with that value normally resides with).""" + person = benunit.members + values = NonDependantResidence + head_family = benunit.any(person("is_household_head", period)) + sharer = benunit("liable_for_share_of_household_rent", period) & ~head_family + rent_is_shared = rent_shared_with_another_family(benunit, period) + return ( + (values.EVERY_JOINT_OCCUPIER, head_family | sharer), + (values.HOUSEHOLD_HEAD_FAMILY, head_family), + (values.OTHER_JOINT_OCCUPIERS, where(rent_is_shared, sharer, head_family)), + ) + + +def _outside_joint_occupiers(benunit, period): + """Whether each person is outside every family liable for the household's + rent (the household head's family and the sharers). A joint occupier is + never a non-dependant of itself or of another joint occupier (HB Regs + 2006 reg 3(2)(a), (d); SI 2012/2885 reg 9(2)(a), (d)), whatever its own + rent.""" + person = benunit.members + head_family = person.benunit.any(person("is_household_head", period)) + sharer = person.benunit("liable_for_share_of_household_rent", period) + return ~head_family & ~sharer + + +def _household_total_over_families(benunit, period, value, families): + """For each family, the household total of a family-level ``value`` over + the families marked by ``families``.""" + person = benunit.members + on_head = person("is_benunit_head", period) + return benunit.max( + person.household.sum(on_head * benunit.project(value * families)) + ) + + +def non_dependants_residing_with(benunit, period, non_dependant): + """For each family, the sum of ``non_dependant`` (a count or weight for + each member of a non-dependant family, zero for anyone else) over the + household's people who normally reside with it under + non_dependant_normally_resides_with. Members of the joint occupiers' + families are never counted.""" + person = benunit.members + residence = person.benunit("non_dependant_normally_resides_with", period) + source = non_dependant * _outside_joint_occupiers(benunit, period) + count = 0 + for value, joint_occupier in _joint_occupiers_by_residence(benunit, period): + in_household = benunit.max(person.household.sum(source * (residence == value))) + count = count + joint_occupier * in_household + return count + + +def apportioned_non_dependant_deductions( + benunit, period, deductions, equally, every_joint_occupier_part +): + """For each family, its part of the deductions for the household's + non-dependants (``deductions``, one amount per person and zero for anyone + who is not a non-dependant; members of the joint occupiers' families are + never counted), each split between the joint occupiers the non-dependant + normally resides with. + + For a non-dependant of every joint occupier, each family's part is + ``every_joint_occupier_part``: its share of the rent for Housing Benefit + (share_of_household_rent) and its joint liability share for Council Tax + Reduction (council_tax_reduction_joint_liability_share), as before. + + For a non-dependant of some of them, with ``equally`` false (Housing + Benefit, reg 74(5)), each joint occupier's part is its share of the rent + over the shares of all of them, or its people liable for the rent over + theirs where those shares sum to zero; with ``equally`` true (Council Tax + Reduction, SI 2012/2885 Sch 1 para 8(5)), each part is one over the + number of people liable among them, and the whole where only the family + is liable. A family that is a non-dependant's only host, a couple + included, bears the whole deduction: the model cannot tell whether both + partners are named tenants liable under LGFA 1992 s.6 (when the literal + para 8(5) would split it between them) or liable only as spouses (s.9). + + Where no family other than the household head's shares the rent, every + residence value means the head's family alone, which takes + ``every_joint_occupier_part``. + """ + person = benunit.members + residence = person.benunit("non_dependant_normally_resides_with", period) + source = deductions * _outside_joint_occupiers(benunit, period) + # Without another family sharing the rent, every value means the + # household head's family alone, so each takes the caller's part. + rent_is_shared = rent_shared_with_another_family(benunit, period) + liable = person("is_liable_for_household_rent", period) + liable_in_family = benunit.sum(liable) + share = benunit("share_of_household_rent", period) + values = NonDependantResidence + total = 0 + for value, joint_occupier in _joint_occupiers_by_residence(benunit, period): + liable_in_joint_occupiers = benunit.max( + person.household.sum(liable & benunit.project(joint_occupier)) + ) + if value == values.EVERY_JOINT_OCCUPIER: + part = every_joint_occupier_part + elif equally: + part = where( + liable_in_joint_occupiers > liable_in_family, + 1 / max_(liable_in_joint_occupiers, 1), + 1, + ) + else: + share_of_joint_occupiers = _household_total_over_families( + benunit, period, share, joint_occupier + ) + part = where( + share_of_joint_occupiers > 0, + share + / where(share_of_joint_occupiers > 0, share_of_joint_occupiers, 1), + liable_in_family / max_(liable_in_joint_occupiers, 1), + ) + part = where(rent_is_shared, part, every_joint_occupier_part) + pool = benunit.max(person.household.sum(source * (residence == value))) + total = total + joint_occupier * part * pool + return total