diff --git a/changelog.d/2039-unflagged-gap.changed.md b/changelog.d/2039-unflagged-gap.changed.md new file mode 100644 index 0000000000..95473ba611 --- /dev/null +++ b/changelog.d/2039-unflagged-gap.changed.md @@ -0,0 +1 @@ +- Without any parent flag, a member at least 20 years younger than the claimant is now presumed to be their child at any age (`presumed_child.minimum_age_gap_at_any_age`), so ages 50 and 25 entered without relationships are a single claimant and an adult child, not a couple. Members 16 to 19 years younger stay partners from age 20. The presumption only applies where `is_claimant_or_partner` is not supplied: a dataset that carries it (such as policyengine-uk-data's enhanced FRS, which has supplied the FRS roles since 1.58.0) is unaffected, while datasets without it are not. diff --git a/policyengine_uk/parameters/household/demographic/benefit_unit/presumed_child/age_limit.yaml b/policyengine_uk/parameters/household/demographic/benefit_unit/presumed_child/age_limit.yaml index 79f19d9244..4551e3332c 100644 --- a/policyengine_uk/parameters/household/demographic/benefit_unit/presumed_child/age_limit.yaml +++ b/policyengine_uk/parameters/household/demographic/benefit_unit/presumed_child/age_limit.yaml @@ -1,4 +1,4 @@ -description: Age below which a benefit-unit member much younger than the claimant is presumed to be the claimant's child rather than their partner, when relationships are not supplied. The limit does not apply when the claimant is flagged as a parent and no member under it, at least the minimum age gap younger, explains the flag. This is a PolicyEngine modelling presumption, not a statutory rule. +description: Age below which a benefit-unit member much younger than the claimant is presumed to be the claimant's child rather than their partner, when relationships are not supplied. The limit does not apply when the claimant is flagged as a parent and no member under it, at least the minimum age gap younger, explains the flag, nor to a member younger by at least the minimum age gap at any age. This is a PolicyEngine modelling presumption, not a statutory rule. values: 0001-01-01: 20 metadata: diff --git a/policyengine_uk/parameters/household/demographic/benefit_unit/presumed_child/minimum_age_gap.yaml b/policyengine_uk/parameters/household/demographic/benefit_unit/presumed_child/minimum_age_gap.yaml index ec32f6adc1..4ca6e3de3d 100644 --- a/policyengine_uk/parameters/household/demographic/benefit_unit/presumed_child/minimum_age_gap.yaml +++ b/policyengine_uk/parameters/household/demographic/benefit_unit/presumed_child/minimum_age_gap.yaml @@ -1,4 +1,4 @@ -description: Minimum age gap below the claimant at which a younger member is presumed to be the claimant's child rather than their partner, when relationships are not supplied. It applies at any age when the claimant is flagged as a parent and no younger member under the presumed child age limit explains the flag, and otherwise only under that limit. This is a PolicyEngine modelling presumption, not a statutory rule. +description: Minimum age gap below the claimant at which a younger member is presumed to be the claimant's child rather than their partner, when relationships are not supplied. It applies at any age when the claimant is flagged as a parent and no younger member under the presumed child age limit explains the flag, and otherwise only under that limit; a larger gap (the minimum age gap at any age) applies at any age. This is a PolicyEngine modelling presumption, not a statutory rule. values: 0001-01-01: 16 metadata: diff --git a/policyengine_uk/parameters/household/demographic/benefit_unit/presumed_child/minimum_age_gap_at_any_age.yaml b/policyengine_uk/parameters/household/demographic/benefit_unit/presumed_child/minimum_age_gap_at_any_age.yaml new file mode 100644 index 0000000000..c8fea14d8c --- /dev/null +++ b/policyengine_uk/parameters/household/demographic/benefit_unit/presumed_child/minimum_age_gap_at_any_age.yaml @@ -0,0 +1,6 @@ +description: Minimum age gap below the claimant at which a younger member is presumed to be the claimant's child rather than their partner at any age, even when the claimant is not flagged as a parent, when relationships are not supplied. A member flagged as a parent can still be the partner. This is a PolicyEngine modelling presumption, not a statutory rule. +values: + 0001-01-01: 20 +metadata: + unit: year + label: presumed child minimum age gap at any age diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.yaml index 7cf5e90192..6febf03035 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.yaml @@ -481,8 +481,10 @@ people: claimant: age: 70 + is_claimant_or_partner: true partner: age: 50 + is_claimant_or_partner: true employment_income: 3_000 benunits: benunit: diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_pension_age_personal_allowance.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_pension_age_personal_allowance.yaml index 213e6a0c50..1d72d96bda 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_pension_age_personal_allowance.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_pension_age_personal_allowance.yaml @@ -214,10 +214,12 @@ age: 80 months_since_last_birthday: 4.1613 is_male: true + is_claimant_or_partner: true younger: age: 60 months_since_last_birthday: 9.1613 is_male: false + is_claimant_or_partner: true benunits: benunit: members: [older, younger] @@ -227,6 +229,7 @@ housing_benefit_pension_age_regulations_apply: true benefits_premiums: 0 output: + relation_type: COUPLE housing_benefit_attained_pension_age_before_cutoff: true housing_benefit_applicable_amount: 383.35 * 52 # para 1(2)(b), older attained 1 June 2011, younger attained 1 January 2033 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_universal_credit_passport.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_universal_credit_passport.yaml index 4c7eabd6eb..7218062a7b 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_universal_credit_passport.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_universal_credit_passport.yaml @@ -249,9 +249,11 @@ age: 70 private_pension_income: 5_000 housing_benefit_reported: 1 + is_claimant_or_partner: true partner: age: 40 employment_income: 2_000 + is_claimant_or_partner: true benunits: benunit: members: [claimant, partner] @@ -266,6 +268,7 @@ rent: 5_720 savings: 9_000 output: + relation_type: COUPLE housing_benefit_pension_age_regulations_apply: false housing_benefit_on_passporting_benefit: true housing_benefit_applicable_income_disregard: 2_000 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/pension_age_premiums.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/pension_age_premiums.yaml index f48a31b1f6..63158af70c 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/pension_age_premiums.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/pension_age_premiums.yaml @@ -289,9 +289,11 @@ claimant: age: {2026: 50} is_disabled_for_benefits: {2026: true} + is_claimant_or_partner: true partner: age: {2026: 70} state_pension: {2026: 3_000} + is_claimant_or_partner: true benunits: benunit: members: [claimant, partner] @@ -303,6 +305,7 @@ rent: {2026: 6_240} council_tax: {2026: 2_000} output: + relation_type: COUPLE is_uc_entitled: true housing_benefit_pension_age_regulations_apply: false council_tax_reduction_pensioner: false diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/tax_credits_working_hours.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/tax_credits_working_hours.yaml index b4c1d9e90e..ebdf03cf6a 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/tax_credits_working_hours.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/tax_credits_working_hours.yaml @@ -13,18 +13,21 @@ is_parent: false is_disabled_for_benefits: false working_tax_credit_reported: 1 + is_claimant_or_partner: true adult_1: age: 40 hours_worked: 1040 employment_income: 5000 is_parent: false is_disabled_for_benefits: false + is_claimant_or_partner: true benunits: family: members: - adult_0 - adult_1 output: + relation_type: COUPLE working_tax_credit: 0 is_WTC_eligible: false - name: A worker aged sixty qualifies on their own sixteen hours @@ -39,18 +42,21 @@ is_parent: false is_disabled_for_benefits: false working_tax_credit_reported: 1 + is_claimant_or_partner: true adult_1: age: 40 hours_worked: 0 employment_income: 0 is_parent: false is_disabled_for_benefits: false + is_claimant_or_partner: true benunits: family: members: - adult_0 - adult_1 output: + relation_type: COUPLE is_WTC_eligible: true working_tax_credit: 4935 - name: A thirty hour worker cannot borrow a partners age twenty five diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_MIF_applies.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_MIF_applies.yaml index 8bd4119b6c..9120dc1082 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_MIF_applies.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_MIF_applies.yaml @@ -240,10 +240,13 @@ older: age: 70 self_employment_income: 3_000 + is_claimant_or_partner: true younger: age: 40 self_employment_income: 3_000 + is_claimant_or_partner: true output: + relation_type: COUPLE uc_mif_applies: [false, true] - name: A partner who cannot be a joint claimant is not a claimant diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_mif_earned_income.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_mif_earned_income.yaml index a7c6abd945..38778687b2 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_mif_earned_income.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_mif_earned_income.yaml @@ -531,15 +531,18 @@ age: 70 employment_income: 6_000 would_claim_marriage_allowance: false + is_claimant_or_partner: true self_employed: age: 40 self_employment_income: 3_600 would_claim_marriage_allowance: false + is_claimant_or_partner: true households: household: members: [older, self_employed] region: NORTH_EAST output: + relation_type: COUPLE uc_mif_applies: [false, true] uc_minimum_income_floor: [0, 20_386.03] # 20,386.03 - 6,000. diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_earnings_deductions.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_earnings_deductions.yaml index 309195440b..dd01419059 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_earnings_deductions.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_earnings_deductions.yaml @@ -105,9 +105,11 @@ pensioner: age: 70 state_pension: 16_000 + is_claimant_or_partner: true partner: age: 45 employment_income: 13_000 + is_claimant_or_partner: true benunits: benunit: members: [pensioner, partner] @@ -117,6 +119,7 @@ tenure_type: RENT_FROM_COUNCIL rent: 20_000 output: + relation_type: COUPLE # Pensioner: (16,000 - 12,570) x 20% = 686, on State Pension. # Partner: (13,000 - 12,570) x 20% = 86 and (13,000 - 12,569.96) x 8% # = 34.40. Both are above the personal allowance, so there is no @@ -304,9 +307,11 @@ employment_income: 20_000 married_couples_allowance: 11_000 would_claim_marriage_allowance: false + is_claimant_or_partner: true partner: age: 45 would_claim_marriage_allowance: false + is_claimant_or_partner: true benunits: benunit: members: [older, partner] @@ -314,6 +319,7 @@ household: members: [older, partner] output: + relation_type: COUPLE # Married couple's allowance reduction: 10% x 11,000 = 1,100. capped_mcad: [1_100, 0] # (20,000 - 12,570) x 20% = 1,486, less 1,100. @@ -331,9 +337,11 @@ private_pension_income: 5_000 married_couples_allowance: 11_000 would_claim_marriage_allowance: false + is_claimant_or_partner: true partner: age: 45 would_claim_marriage_allowance: false + is_claimant_or_partner: true benunits: benunit: members: [older, partner] @@ -341,6 +349,7 @@ household: members: [older, partner] output: + relation_type: COUPLE # 1,486 on earnings + 5,000 x 20% on the pension - 1,100. income_tax: [1_386, 0] # The reduction still comes off the earnings slice: 1,486 - 1,100. @@ -359,9 +368,11 @@ unused_pension_annual_allowance: 0 married_couples_allowance: 11_000 would_claim_marriage_allowance: false + is_claimant_or_partner: true partner: age: 45 would_claim_marriage_allowance: false + is_claimant_or_partner: true benunits: benunit: members: [older, partner] @@ -369,6 +380,7 @@ household: members: [older, partner] output: + relation_type: COUPLE # Contributions of 65,000 exceed the 60,000 annual allowance by 5,000, # charged at the basic rate: 1,000. personal_pension_contributions_tax: [1_000, 0] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_related_requirements/uc_work_related_group_apart_from_earnings.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_related_requirements/uc_work_related_group_apart_from_earnings.yaml index 536f66f1de..24a1192e83 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_related_requirements/uc_work_related_group_apart_from_earnings.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_related_requirements/uc_work_related_group_apart_from_earnings.yaml @@ -133,9 +133,12 @@ people: older: age: 70 + is_claimant_or_partner: true younger: age: 40 + is_claimant_or_partner: true output: + relation_type: COUPLE uc_work_related_group_apart_from_earnings: [NO_REQUIREMENTS, ALL_REQUIREMENTS] # Reg. 89(1)(a) reads "the qualifying age for state pension credit": State @@ -163,10 +166,13 @@ age: 64 months_since_last_birthday: 6 gender: MALE + is_claimant_or_partner: true younger: age: 40 gender: FEMALE + is_claimant_or_partner: true output: + relation_type: COUPLE has_attained_state_pension_credit_qualifying_age: [true, false] is_SP_age: [false, false] uc_work_related_group_apart_from_earnings: [NO_REQUIREMENTS, ALL_REQUIREMENTS] @@ -181,10 +187,13 @@ age: 63 months_since_last_birthday: 4 gender: MALE + is_claimant_or_partner: true younger: age: 40 gender: FEMALE + is_claimant_or_partner: true output: + relation_type: COUPLE has_attained_state_pension_credit_qualifying_age: [false, false] is_SP_age: [false, false] uc_work_related_group_apart_from_earnings: [ALL_REQUIREMENTS, ALL_REQUIREMENTS] @@ -197,10 +206,13 @@ age: 64 months_since_last_birthday: 4 gender: MALE + is_claimant_or_partner: true younger: age: 41 gender: FEMALE + is_claimant_or_partner: true output: + relation_type: COUPLE has_attained_state_pension_credit_qualifying_age: [true, false] is_SP_age: [false, false] uc_work_related_group_apart_from_earnings: [NO_REQUIREMENTS, ALL_REQUIREMENTS] @@ -215,10 +227,13 @@ age: 64 months_since_last_birthday: 6 gender: FEMALE + is_claimant_or_partner: true younger: age: 40 gender: MALE + is_claimant_or_partner: true output: + relation_type: COUPLE has_attained_state_pension_credit_qualifying_age: [true, false] is_SP_age: [true, false] uc_work_related_group_apart_from_earnings: [NO_REQUIREMENTS, ALL_REQUIREMENTS] @@ -234,10 +249,13 @@ age: 64 months_since_last_birthday: 10 gender: MALE + is_claimant_or_partner: true younger: age: 40 gender: FEMALE + is_claimant_or_partner: true output: + relation_type: COUPLE has_attained_state_pension_credit_qualifying_age: [false, false] is_SP_age: [false, false] uc_work_related_group_apart_from_earnings: [ALL_REQUIREMENTS, ALL_REQUIREMENTS] @@ -253,10 +271,13 @@ months_since_last_birthday: 6 gender: MALE self_employment_income: 5_000 + is_claimant_or_partner: true younger: age: 40 gender: FEMALE + is_claimant_or_partner: true output: + relation_type: COUPLE uc_is_in_gainful_self_employment: [true, false] uc_work_related_group_apart_from_earnings: [NO_REQUIREMENTS, ALL_REQUIREMENTS] uc_mif_applies: [false, false] diff --git a/policyengine_uk/tests/policy/baseline/gov/dfe/targeted_childcare_entitlement/targeted_childcare_entitlement_guarantee_credit.yaml b/policyengine_uk/tests/policy/baseline/gov/dfe/targeted_childcare_entitlement/targeted_childcare_entitlement_guarantee_credit.yaml index 01ca6e6035..78fa16e414 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dfe/targeted_childcare_entitlement/targeted_childcare_entitlement_guarantee_credit.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dfe/targeted_childcare_entitlement/targeted_childcare_entitlement_guarantee_credit.yaml @@ -245,9 +245,11 @@ people: person1: age: 70 + is_claimant_or_partner: true state_pension_reported: 10_000 person2: age: 40 + is_claimant_or_partner: true person3: age: 2 benunits: @@ -401,9 +403,11 @@ people: person1: age: 80 + is_claimant_or_partner: true state_pension_reported: 10_000 person2: age: 40 + is_claimant_or_partner: true person3: age: 2 benunits: diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/benefit_cap_pension_age.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/benefit_cap_pension_age.yaml index 0471cc7488..e4754a2f4c 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/benefit_cap_pension_age.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/benefit_cap_pension_age.yaml @@ -21,8 +21,10 @@ people: older: age: 70 + is_claimant_or_partner: true younger: age: 50 + is_claimant_or_partner: true child_1: age: 4 child_2: @@ -54,8 +56,10 @@ people: older: age: 70 + is_claimant_or_partner: true younger: age: 50 + is_claimant_or_partner: true employment_income: 20_000 child_1: age: 4 @@ -88,8 +92,10 @@ people: older: age: 70 + is_claimant_or_partner: true younger: age: 50 + is_claimant_or_partner: true benunits: benunit: members: [older, younger] @@ -101,6 +107,7 @@ members: [older, younger] region: LONDON output: + relation_type: COUPLE is_uc_entitled: false housing_benefit_pension_age_regulations_apply: true is_housing_benefit_benefit_cap_exempt: true @@ -118,8 +125,10 @@ people: older: age: 70 + is_claimant_or_partner: true younger: age: 50 + is_claimant_or_partner: true jsa_income_reported: 4_000 benunits: benunit: diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/is_uc_benefit_cap_exempt_earnings.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/is_uc_benefit_cap_exempt_earnings.yaml index dacaa41c45..07ad9c5aab 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/is_uc_benefit_cap_exempt_earnings.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/is_uc_benefit_cap_exempt_earnings.yaml @@ -161,8 +161,10 @@ age: 76 employment_income: 11_000 employee_pension_contributions: 1_000 + is_claimant_or_partner: true younger_partner: age: 40 + is_claimant_or_partner: true child1: age: 5 child2: @@ -177,6 +179,7 @@ tenure_type: RENT_FROM_COUNCIL rent: 30_000 output: + relation_type: COUPLE is_uc_assessed_claimant: [true, true, false, false] housing_benefit_pension_age_regulations_apply: false uc_individual_earned_income_before_mif: [11_000, 0, 0, 0] diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/lifetime_isa_capital.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/lifetime_isa_capital.yaml index 5f5f0b671d..844274edce 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/lifetime_isa_capital.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/lifetime_isa_capital.yaml @@ -343,15 +343,19 @@ pension_credit_assessable_capital: [4_000, 0] - name: Pension Credit counts a younger partner's Lifetime ISA after the charge + # The couple is 25 years apart, so the roles are supplied: without them the + # 45-year-old would be presumed the claimant's child. period: 2025 absolute_error_margin: 0 input: people: claimant: age: 70 + is_claimant_or_partner: true partner: age: 45 lifetime_isa_balance: 10_000 + is_claimant_or_partner: true benunits: couple: members: [claimant, partner] @@ -359,6 +363,7 @@ household: members: [claimant, partner] output: + is_claimant_or_partner: [true, true] pension_credit_assessable_capital: 7_500 - name: A dependent young person's Lifetime ISA does not count against the parent's claim diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/is_pension_credit_eligible.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/is_pension_credit_eligible.yaml index 00a7b4b387..25527b2b17 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/is_pension_credit_eligible.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/is_pension_credit_eligible.yaml @@ -160,17 +160,22 @@ is_pension_credit_eligible: true - name: Case 4, mixed-age couple without protected exception is ineligible. + # The couple is 25 years apart, so the roles are supplied: without them the + # 54-year-old would be presumed the claimant's child. period: 2026 input: people: person1: age: 79 + is_claimant_or_partner: true person2: age: 54 + is_claimant_or_partner: true benunits: benunit: members: [person1, person2] output: + is_claimant_or_partner: [true, true] is_pension_credit_eligible: false - name: Case 5, child in benefit unit does not block pension-age adult eligibility. diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_statutory_pay_earnings.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_statutory_pay_earnings.yaml index 18bf3646d7..2d01fe4069 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_statutory_pay_earnings.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_statutory_pay_earnings.yaml @@ -214,9 +214,11 @@ people: person1: age: 70 + is_claimant_or_partner: true state_pension: 0 person2: age: 35 + is_claimant_or_partner: true statutory_maternity_pay: 194.32 * 52 benunits: benunit: @@ -326,9 +328,11 @@ people: person1: age: 70 + is_claimant_or_partner: true state_pension: 20_000 person2: age: 40 + is_claimant_or_partner: true employment_income: 5_000 statutory_maternity_pay: 194.32 * 52 benunits: diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/state_pension_credit_qualifying_age.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/state_pension_credit_qualifying_age.yaml index 6dd710343d..0031940459 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/state_pension_credit_qualifying_age.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/state_pension_credit_qualifying_age.yaml @@ -557,8 +557,10 @@ age: 50 is_carer_for_benefits: true income_support_reported: 1_000 + is_claimant_or_partner: true partner: age: 70 + is_claimant_or_partner: true benunits: benunit: members: [carer, partner] @@ -566,6 +568,7 @@ household: members: [carer, partner] output: + relation_type: COUPLE pension_credit: 0 income_support_eligible: true @@ -579,9 +582,11 @@ younger: age: 50 income_support_reported: 1_000 + is_claimant_or_partner: true older: age: 70 is_carer_for_benefits: true + is_claimant_or_partner: true benunits: benunit: members: [younger, older] @@ -589,6 +594,7 @@ household: members: [younger, older] output: + relation_type: COUPLE income_support_eligible: false - name: Case 25, a couple that would claim Pension Credit is treated as on it even when its income is too high. diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner.yaml index f19bc2277c..e1dbac5588 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner.yaml @@ -19,9 +19,11 @@ people: older: age: 70 + is_claimant_or_partner: true council_tax_benefit_reported: 700 younger: age: 50 + is_claimant_or_partner: true benunits: benunit: members: [older, younger] @@ -47,8 +49,10 @@ older: age: 70 council_tax_benefit_reported: 700 + is_claimant_or_partner: true younger: age: 50 + is_claimant_or_partner: true benunits: benunit: members: [older, younger] @@ -61,6 +65,7 @@ council_tax: 2_000 savings: 0 output: + relation_type: COUPLE is_uc_entitled: false council_tax_reduction_pensioner: true council_tax_reduction_scheme_supported: true @@ -77,9 +82,11 @@ older: age: 70 council_tax_benefit_reported: 700 + is_claimant_or_partner: true younger: age: 50 jsa_income_reported: 4_000 + is_claimant_or_partner: true benunits: benunit: members: [older, younger] @@ -92,6 +99,7 @@ council_tax: 2_000 savings: 0 output: + relation_type: COUPLE jsa_income: 4_000 council_tax_reduction_pensioner: false council_tax_benefit: 700 @@ -126,8 +134,10 @@ people: older: age: 70 + is_claimant_or_partner: true younger: age: 50 + is_claimant_or_partner: true benunits: benunit: members: [older, younger] @@ -157,8 +167,10 @@ people: older: age: 70 + is_claimant_or_partner: true younger: age: 50 + is_claimant_or_partner: true son: age: 30 employment_income: 30_000 @@ -256,8 +268,10 @@ people: older: age: 70 + is_claimant_or_partner: true younger: age: 50 + is_claimant_or_partner: true benunits: benunit: members: [older, younger] @@ -279,8 +293,10 @@ people: older: age: 70 + is_claimant_or_partner: true younger: age: 50 + is_claimant_or_partner: true benunits: benunit: members: [older, younger] @@ -311,8 +327,10 @@ people: older: age: 70 + is_claimant_or_partner: true younger: age: 50 + is_claimant_or_partner: true child: age: 9 benunits: diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner_earnings_disregard.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner_earnings_disregard.yaml index 1f42bf1d23..71ed83563c 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner_earnings_disregard.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner_earnings_disregard.yaml @@ -384,8 +384,10 @@ claimant: age: 70 employment_income: 1_000 + is_claimant_or_partner: true partner: age: 50 + is_claimant_or_partner: true benunits: benunit: members: [claimant, partner] @@ -395,6 +397,7 @@ members: [claimant, partner] country: ENGLAND output: + relation_type: COUPLE # SI 2012/2885 reg 3(2): not a pensioner where the applicant or partner # is on income-related ESA, so the pensioner schedule does not apply. council_tax_reduction_pensioner: false diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/legacy_closure_migrant_nations.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/legacy_closure_migrant_nations.yaml index d2d2842137..1286eb7752 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/legacy_closure_migrant_nations.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/legacy_closure_migrant_nations.yaml @@ -156,8 +156,10 @@ employment_income: 8_000 weekly_hours: 30 working_tax_credit_reported: 2_000 + is_claimant_or_partner: true younger: age: 40 + is_claimant_or_partner: true benunits: family: members: [older, younger] @@ -172,6 +174,7 @@ council_tax: 2_000 savings: 0 output: + relation_type: COUPLE legacy_benefits_closed: true claims_uc_at_legacy_closure: true is_uc_eligible: true @@ -187,8 +190,10 @@ employment_income: 8_000 weekly_hours: 30 working_tax_credit_reported: 2_000 + is_claimant_or_partner: true younger: age: 40 + is_claimant_or_partner: true benunits: family: members: [older, younger] @@ -203,6 +208,7 @@ council_tax: 2_000 savings: 0 output: + relation_type: COUPLE legacy_benefits_closed: true claims_uc_at_legacy_closure: true is_uc_eligible: true diff --git a/policyengine_uk/tests/policy/baseline/household/demographic/benefit_unit_structure.yaml b/policyengine_uk/tests/policy/baseline/household/demographic/benefit_unit_structure.yaml index e2cdef6982..2d5ebe0204 100644 --- a/policyengine_uk/tests/policy/baseline/household/demographic/benefit_unit_structure.yaml +++ b/policyengine_uk/tests/policy/baseline/household/demographic/benefit_unit_structure.yaml @@ -374,8 +374,10 @@ # Below a claimant flagged as a parent, a member at least 16 years younger is # presumed their child at any age, unless a member under 20 and at least 16 -# years younger already explains the flag. Without flags, the under-20 limit -# stays: survey childless couples carry no flags and include large age gaps. +# years younger already explains the flag. Without flags, a member is presumed +# the claimant's child under 20 at a 16-year gap, or at any age at a 20-year +# gap: co-resident FRS adults 16-19 years apart are mostly couples, those 20 or +# more apart mostly not. Datasets and users can supply is_claimant_or_partner instead. - name: A flagged lone parent with an adult son is single period: 2025 @@ -398,8 +400,7 @@ relation_type: SINGLE is_couple: false -- name: Without a parent flag, a 25-year gap is still read as a couple - # Documented limitation: indistinguishable from a survey childless couple. +- name: Without flags, a member 25 years younger is presumed the claimant's child period: 2025 absolute_error_margin: 0 input: @@ -414,10 +415,138 @@ households: household: members: [older, younger] + output: + is_claimant_or_partner: [true, false] + relation_type: SINGLE + is_couple: false + +- name: Without flags, the 20-year gap at any age is a boundary + period: 2025 + absolute_error_margin: 0 + input: + people: + head_a: + age: 50 + gap_19: + age: 31 + head_b: + age: 50 + gap_20: + age: 30 + head_c: + age: 90 + gap_20_at_70: + age: 70 + benunits: + unit_a: + members: [head_a, gap_19] + unit_b: + members: [head_b, gap_20] + unit_c: + members: [head_c, gap_20_at_70] + households: + household_a: + members: [head_a, gap_19] + household_b: + members: [head_b, gap_20] + household_c: + members: [head_c, gap_20_at_70] + output: + is_claimant_or_partner: [true, true, true, false, true, false] + +- name: A younger head 20 years below the other adult keeps the couple + # The gap is measured below the claimant (the head), so an elder partner is + # never presumed the head's child. + period: 2025 + absolute_error_margin: 0 + input: + people: + head: + age: 50 + is_benunit_head: true + elder: + age: 70 + benunits: + benunit: + members: [head, elder] + households: + household: + members: [head, elder] output: is_claimant_or_partner: [true, true] is_couple: true +- name: A flagged partner 25 years younger stays the partner + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 60 + is_parent: true + partner: + age: 35 + is_parent: true + child: + age: 5 + benunits: + benunit: + members: [claimant, partner, child] + households: + household: + members: [claimant, partner, child] + output: + is_claimant_or_partner: [true, true, false] + is_couple: true + +- name: An unflagged couple 22 years apart with a child is assessed as a lone parent + # Known trade-off of the unflagged presumption: calculators that send ages + # only cannot tell this couple from a parent living with an adult child. + # Supplying is_claimant_or_partner keeps the couple (next case). + period: 2025 + absolute_error_margin: 0 + input: + people: + older: + age: 60 + younger: + age: 38 + child: + age: 10 + benunits: + benunit: + members: [older, younger, child] + households: + household: + members: [older, younger, child] + output: + is_claimant_or_partner: [true, false, false] + is_lone_parent: true + +- name: Supplied roles keep an unflagged couple 22 years apart + period: 2025 + absolute_error_margin: 0 + input: + people: + older: + age: 60 + is_claimant_or_partner: true + younger: + age: 38 + is_claimant_or_partner: true + child: + age: 10 + is_claimant_or_partner: false + benunits: + benunit: + members: [older, younger, child] + households: + household: + members: [older, younger, child] + output: + is_couple: true + is_lone_parent: false + - name: A flagged parent keeps an unflagged partner close in age, not the adult son period: 2025 absolute_error_margin: 0 @@ -467,10 +596,10 @@ output: is_claimant_or_partner: [true, false, true, true] -- name: A flagged grandparent's daughter is read as a partner when a grandchild explains the flag - # Documented limitation, as before this rule: the grandchild under 20 - # explains the grandparent's parent flag, so the under-20 limit stays and the - # 45-year-old is the partner. Supply is_claimant_or_partner for such homes. +- name: A flagged grandparent's daughter 25 years younger is not their partner + # The grandchild under 20 explains the grandparent's parent flag, so the + # under-20 limit stays, but the daughter is 20 or more years younger and so + # presumed the grandparent's child at any age. period: 2025 absolute_error_margin: 0 input: @@ -489,7 +618,7 @@ household: members: [grandparent, daughter, grandchild] output: - is_claimant_or_partner: [true, true, false] + is_claimant_or_partner: [true, false, false] - name: Without flags the under-20 limit still applies at the boundary period: 2025 @@ -497,11 +626,11 @@ input: people: head_a: - age: 40 + age: 37 aged_19: age: 19 head_b: - age: 40 + age: 38 aged_20: age: 20 benunits: @@ -599,10 +728,10 @@ uc_earned_income: 31_563.60 universal_credit: 0 -- name: A flagged lone parent with an adult child and a younger child is read as a couple - # Documented limitation, as before this rule: the 12-year-old explains the - # parent flag, so the 25-year-old (not under 20) is read as the partner. - # Supply is_claimant_or_partner to enter this household. +- name: A flagged lone parent with an adult child 25 years younger and a younger child is single + # The 12-year-old explains the parent flag, so the under-20 limit stays, but + # the 25-year-old is 20 or more years younger and so presumed the parent's + # child at any age. period: 2025 absolute_error_margin: 0 input: @@ -620,6 +749,31 @@ households: household: members: [parent, adult_child, child] + output: + is_claimant_or_partner: [true, false, false] + +- name: A flagged lone parent with an adult child 18 years younger and a younger child is read as a couple + # Documented limitation: the 12-year-old explains the parent flag, so the + # under-20 limit stays, and an unflagged 20-year-old 18 years younger is + # within the gap at which co-resident adults are mostly couples. Supply + # is_claimant_or_partner to enter this household. + period: 2025 + absolute_error_margin: 0 + input: + people: + parent: + age: 38 + is_parent: true + adult_child: + age: 20 + child: + age: 12 + benunits: + benunit: + members: [parent, adult_child, child] + households: + household: + members: [parent, adult_child, child] output: is_claimant_or_partner: [true, true, false] diff --git a/policyengine_uk/tests/test_benefit_cap_and_ctr_pension_age_properties.py b/policyengine_uk/tests/test_benefit_cap_and_ctr_pension_age_properties.py index 298b11d1ee..65e45a436e 100644 --- a/policyengine_uk/tests/test_benefit_cap_and_ctr_pension_age_properties.py +++ b/policyengine_uk/tests/test_benefit_cap_and_ctr_pension_age_properties.py @@ -20,9 +20,13 @@ Universal Credit needs a claimant or partner under the qualifying age (UC Regs 2013 reg 3(2)(a)). Claimant and partner are is_claimant_or_partner, which -presumes a member under 20 and at least 16 years younger than the claimant to -be their child, so a pensioner's qualifying young person is a dependant -whether or not is_parent is set; the draws cover both. "On Universal Credit" +the families supply: the drawn adults are the claimant and partner, and the +qualifying young person and children are not. Left to the age presumption, a +partner 20 or more years younger would be read as the claimant's child, and +a mixed-age couple as a single pensioner. (So these draws no longer exercise +the presumption that reads a pensioner's 18-year-old as a dependant, with or +without is_parent; this change leaves that rule as it was, and +benefit_unit_structure.yaml covers it.) "On Universal Credit" below means an award before the cap with a claimant or partner under State Pension age. Legacy income-related benefit is set as an input. @@ -199,7 +203,10 @@ def situation(units): has_young_person = any(role == "young" for role, _ in unit["members"]) for j, (role, age) in enumerate(unit["members"]): name = f"p{i}_{j}" - person = {"age": {PERIOD: age}} + person = { + "age": {PERIOD: age}, + "is_claimant_or_partner": {PERIOD: role == "adult"}, + } if role == "young": person["current_education"] = {PERIOD: "UPPER_SECONDARY"} elif age >= 67: @@ -215,7 +222,10 @@ def situation(units): names.append(name) for k in range(unit["children"]): name = f"p{i}_c{k}" - people[name] = {"age": {PERIOD: 2 + 3 * k}} + people[name] = { + "age": {PERIOD: 2 + 3 * k}, + "is_claimant_or_partner": {PERIOD: False}, + } names.append(name) benunit = { "members": names, @@ -241,6 +251,7 @@ def situation(units): BENUNIT = [ + "relation_type", "is_uc_entitled", "housing_benefit_pension_age_regulations_apply", "council_tax_reduction_pensioner", @@ -279,7 +290,13 @@ def calculate(units, before=False): names = BEFORE else: names = BENUNIT + HOUSEHOLD - return {name: np.asarray(simulation.calculate(name, PERIOD)) for name in names} + values = {name: simulation.calculate(name, PERIOD) for name in names} + return { + name: np.asarray( + value.decode_to_str() if hasattr(value, "decode_to_str") else value + ) + for name, value in values.items() + } def has_pension_age_claimant(unit): @@ -301,6 +318,9 @@ def test_benefit_cap_and_ctr_pension_age_invariants(units): before = calculate(units, before=True) cap_only = calculate(units, before="cap") for i, unit in enumerate(units): + # The drawn couples stay couples, however far apart their ages. + adults = sum(role == "adult" for role, _ in unit["members"]) + assert now["relation_type"][i] == ("COUPLE" if adults == 2 else "SINGLE") on_uc = bool(now["is_uc_entitled"][i]) and has_working_age_claimant(unit) legacy = unit["legacy"] > 0 # P1 @@ -379,8 +399,11 @@ def run(older_is_pension_age): names = [f"p{i}_a", f"p{i}_b"] + [ f"p{i}_c{k}" for k in range(c["children"]) ] - people[names[0]] = {"age": {PERIOD: older}} - people[names[1]] = {"age": {PERIOD: c["working_age"]}} + # The two adults are the couple whatever their age gap, so the + # roles are supplied rather than left to the age presumption. + couple = {"is_claimant_or_partner": {PERIOD: True}} + people[names[0]] = {"age": {PERIOD: older}, **couple} + people[names[1]] = {"age": {PERIOD: c["working_age"]}, **couple} for k, name in enumerate(names[2:]): people[name] = {"age": {PERIOD: 2 + 3 * k}} benunits[f"b{i}"] = { diff --git a/policyengine_uk/tests/test_child_and_adult_definitions_properties.py b/policyengine_uk/tests/test_child_and_adult_definitions_properties.py index e67b1f7183..24140408c9 100644 --- a/policyengine_uk/tests/test_child_and_adult_definitions_properties.py +++ b/policyengine_uk/tests/test_child_and_adult_definitions_properties.py @@ -14,13 +14,17 @@ An unflagged member at least 16 years younger than a claimant flagged as a parent is never their partner, at any age, unless a member under 20 and at least 16 years younger explains the flag; flagging the claimant as a - parent never adds a partner; and benefit units shaped like the FRS's - (one or two adults, all flagged as parents when there are dependants) - keep the earlier under-20 presumption exactly. + parent never adds a partner. On benefit units shaped like the FRS's (one + or two adults, all flagged as parents when there are dependants) the + any-age 20-year gap changes only childless couples whose claimant is 20 or + more years older than the other adult (intended), and supplied + claimant/partner roles are always kept. 2. HBAI types partition everyone. Any benefit unit with one head has at most two claimants/partners, all of them HBAI adults, and at least one if it has an HBAI adult; valid families (one or two adults aged 20+ and - dependants) have exactly their adults. Couple/single and + dependants) have exactly their adults, except an unflagged couple whose + head is 20+ years older than the other adult (the any-age presumption, + intended), and always when the roles are supplied. Couple/single and couple/lone-parent/single-person partition benefit units. Heads aged 16+ are claimants or partners, including when explicitly younger than others, unless two other members are flagged parents and the head is not. @@ -34,8 +38,10 @@ the added child carries a parent marker (an identified parent must be aged 16 or over; the minimised counterexample that found this is kept as a regression test). It does not hold below a flagged claimant whose flag - nothing explains: a flagged 50 with a 25-year-old is single, and adding a - 12-year-old explains the flag and makes them a couple. + nothing explains, for a member 16 to 19 years younger: a flagged 38 with a + 20-year-old is single, and adding a 12-year-old explains the flag and + makes them a couple. A member 20 or more years younger stays the + claimant's child either way (the any-age gap). Before/after families share one Simulation to avoid repeated model construction, but occupy distinct households and benefit units. @@ -227,21 +233,21 @@ def hbai_dependent_child(p, family): return True return ( p["age"] < 20 - and any(member["is_parent"] for member in family) + and any(member["is_parent"] and member["age"] >= 16 for member in family) and (non_advanced_education(p) or p["is_in_approved_training"]) ) -def claimants_or_partners(family, flagged_parent_lifts_age_limit=True): +def claimants_or_partners(family, any_age_gap=20): # is_claimant_or_partner's documented rule. The claimant is the head if an # HBAI adult, else the eldest HBAI adult. The partner is the eldest other # flagged parent, else the eldest other adult not presumed the claimant's # child: at least 16 years younger and either under 20 or below a claimant - # flagged as a parent whose flag no such under-20 member explains. If the - # claimant is not a flagged parent but two or - # more others are, the two eldest of those are the couple instead. Age ties - # go to the earlier member. flagged_parent_lifts_age_limit=False is the - # earlier rule (under 20 only), kept for the FRS-shape differential test. + # flagged as a parent whose flag no such under-20 member explains, or at + # least 20 years younger at any age. If the claimant is not a flagged + # parent but two or more others are, the two eldest of those are the + # couple instead. Age ties go to the earlier member. any_age_gap=None is + # the rule without the any-age gap, kept for the differential test. n = len(family) adult = [not hbai_dependent_child(p, family) for p in family] ages = [p["age"] for p in family] @@ -257,14 +263,16 @@ def claimants_or_partners(family, flagged_parent_lifts_age_limit=True): couple = sorted(other_parents, key=lambda i: (-ages[i], i))[:2] return [i in couple for i in range(n)] young_child = any(ages[j] < 20 and ages[claimant] - ages[j] >= 16 for j in range(n)) - no_age_limit = ( - flagged_parent_lifts_age_limit and parent[claimant] and not young_child - ) + flag_unexplained = parent[claimant] and not young_child + + def presumed_child(i): + gap = ages[claimant] - ages[i] + if any_age_gap is not None and gap >= any_age_gap: + return True + return (ages[i] < 20 or flag_unexplained) and gap >= 16 + pool = other_parents or [ - i - for i in adults - if i != claimant - and not ((ages[i] < 20 or no_age_limit) and ages[claimant] - ages[i] >= 16) + i for i in adults if i != claimant and not presumed_child(i) ] partner = max(pool, key=lambda i: (ages[i], -i)) if pool else None return [i == claimant or i == partner for i in range(n)] @@ -564,7 +572,16 @@ def test_valid_family_partition_and_adding_a_child_preserves_claimants( for family in families: old = claimants[old_offset : old_offset + len(family)] new = claimants[new_offset : new_offset + len(family)] - assert sum(old) == sum(p["age"] >= 20 for p in family) + np.testing.assert_array_equal(old, claimants_or_partners(family)) + # Every adult is selected, except in an unflagged couple whose head + # (the claimant) is 20+ years older: the any-age presumption (intended). + adults = [p for p in family if p["age"] >= 20] + wide_gap = ( + len(adults) == 2 + and not adults[0]["is_parent"] + and adults[0]["age"] - adults[1]["age"] >= 20 + ) + assert sum(old) == len(adults) - wide_gap assert sum(old) in (1, 2) np.testing.assert_array_equal(new, old) assert not claimants[new_offset + len(family)] @@ -768,9 +785,13 @@ def test_parent_marked_under_16_entrant_preserves_existing_claimants(): person(20, is_benunit_head=True), person(18, current_education="POST_SECONDARY"), ] - sim = simulate([family, family + [person(0, is_parent=True)]]) + entrant = family + [person(0, is_parent=True)] + sim = simulate([family, entrant]) actual = sim.calculate("is_claimant_or_partner", YEAR) np.testing.assert_array_equal(actual[:2], actual[2:4]) + # The reference ignores a parent flag under 16, as the model does. + expected = claimants_or_partners(family) + claimants_or_partners(entrant) + np.testing.assert_array_equal(actual, expected) @st.composite @@ -890,16 +911,84 @@ def frs_shaped_units(draw): return families +def survey_roles(family): + # The FRS adult table: everyone in a unit without dependants (nobody is + # flagged), otherwise the adults flagged as parents. + flagged = any(p["is_parent"] for p in family) + return [p["is_parent"] or not flagged for p in family] + + +def childless_couple_split_by_any_age_gap(family): + # A childless two-adult unit whose head (the claimant) is 20+ years older. + if len(family) != 2 or not all(survey_roles(family)): + return False + head, other = sorted(family, key=lambda p: not p["is_benunit_head"]) + return head["age"] - other["age"] >= 20 + + @PROPERTY_SETTINGS @given(frs_shaped_units()) -def test_frs_shaped_units_keep_the_under_20_presumption(families): - # Differential: on FRS-shaped units the rule equals the earlier, age-limited - # one, so the change cannot move the enhanced FRS microsimulation. +def test_frs_shaped_units_change_only_for_childless_couples_20_years_apart(families): + # Differential against the rule without the any-age gap. On FRS-shaped + # units the two can differ only in childless two-adult units 20 or more + # years apart, which carry no flags: there, if the claimant (the head) is + # the elder, the younger adult is presumed their child (intended); a + # younger head keeps the couple. A dataset that carries + # is_claimant_or_partner (policyengine-uk-data#524's enhanced FRS) does not + # depend on this; datasets without the role do. sim = simulate(families) - expected = [ - c - for family in families - for c in claimants_or_partners(family, flagged_parent_lifts_age_limit=False) - ] + expected = [c for family in families for c in claimants_or_partners(family)] assert_values(sim, "is_claimant_or_partner", expected) + for family in families: + if not childless_couple_split_by_any_age_gap(family): + assert claimants_or_partners(family) == claimants_or_partners( + family, any_age_gap=None + ) + + +@PROPERTY_SETTINGS +@given(frs_shaped_units()) +def test_supplied_claimant_and_partner_roles_are_kept(families): + # policyengine-uk-data#524 supplies is_claimant_or_partner from the FRS + # adult table: the head and any partner, whatever their ages and gap. Supplied + # roles override the presumption, so the FRS couples it would split stay + # couples, and every unit keeps one claimant and at most one partner. + roles = [role for family in families for role in survey_roles(family)] + sim = simulate(families) + sim.set_input("is_claimant_or_partner", YEAR, roles) + assert_values(sim, "is_claimant_or_partner", roles) + is_couple = sim.calculate("is_couple", YEAR) + offset = 0 + for i, family in enumerate(families): + n_roles = sum(roles[offset : offset + len(family)]) + assert is_couple[i] == (n_roles == 2) + offset += len(family) + + +def test_unflagged_adult_pairs_follow_the_20_year_gap(): + # Exhaustive over unflagged two-adult units, both 20 to 90, head the + # elder: the younger is the partner exactly when under 20 years younger. + pairs = [(a, b) for a in range(20, 91) for b in range(20, a + 1)] + families = [[person(a, is_benunit_head=True), person(b)] for a, b in pairs] + sim = simulate(families) + claimants = sim.calculate("is_claimant_or_partner", YEAR) + expected = [c for a, b in pairs for c in (True, a - b < 20)] + np.testing.assert_array_equal(claimants, expected) assert expected == [c for f in families for c in claimants_or_partners(f)] + + +def test_wide_gap_couples_by_head_order_and_with_supplied_roles(): + # An unflagged couple 25 years apart: with the elder as head (the claimant) + # the younger is presumed their child; with the younger as head the elder + # is the partner. Supplied roles keep the couple either way. + elder_head = [person(70, is_benunit_head=True), person(45)] + younger_head = [person(45, is_benunit_head=True), person(70)] + families = [elder_head, younger_head] + sim = simulate(families) + inferred = sim.calculate("is_claimant_or_partner", YEAR) + np.testing.assert_array_equal(inferred, [True, False, True, True]) + assert claimants_or_partners(elder_head) == [True, False] + assert claimants_or_partners(younger_head) == [True, True] + supplied = simulate(families) + supplied.set_input("is_claimant_or_partner", YEAR, [True] * 4) + np.testing.assert_array_equal(supplied.calculate("is_couple", YEAR), [True] * 2) diff --git a/policyengine_uk/tests/test_council_tax_reduction_applicant_properties.py b/policyengine_uk/tests/test_council_tax_reduction_applicant_properties.py index 82450619df..e690a00ecf 100644 --- a/policyengine_uk/tests/test_council_tax_reduction_applicant_properties.py +++ b/policyengine_uk/tests/test_council_tax_reduction_applicant_properties.py @@ -11,7 +11,10 @@ partner (#1896's parent-couple rule). She then applies alone. Invariants, for any generated population of households: heads alone, in a -couple (with partners aged 16 and over), or sharing a benefit unit with two +couple (with partners aged 16 and over; where the two are 20 or more years +apart, or 16 or more with the younger under 20, the model presumes the +younger the elder's child and reads no couple, and the invariants follow the +model's reading), or sharing a benefit unit with two parents aged 16 to 30 and their baby; another family, sometimes a sharer; income-based Jobseeker's Allowance and Universal Credit supplied for the head's benefit unit; the head's disability, Attendance Allowance, Carer's diff --git a/policyengine_uk/tests/test_council_tax_reduction_carer_income_properties.py b/policyengine_uk/tests/test_council_tax_reduction_carer_income_properties.py index e317bf68e5..f68de90e56 100644 --- a/policyengine_uk/tests/test_council_tax_reduction_carer_income_properties.py +++ b/policyengine_uk/tests/test_council_tax_reduction_carer_income_properties.py @@ -186,6 +186,11 @@ def situation(families): name = f"p{i}_{k}_{c}_{j}" person = { "age": {YEAR: a["age"]}, + # The drawn adults are the claimant and partner, + # however far apart their ages; the age presumption + # alone would read a partner 20 or more years younger + # as the claimant's child. + "is_claimant_or_partner": {YEAR: True}, "state_pension": {YEAR: a.get("state_pension", 0.0)}, "employment_income": {YEAR: a.get("employment_income", 0.0)}, # All private pension goes to the first adult. diff --git a/policyengine_uk/tests/test_council_tax_reduction_claimant_family_scheme_properties.py b/policyengine_uk/tests/test_council_tax_reduction_claimant_family_scheme_properties.py index 9849322beb..5a4c72c805 100644 --- a/policyengine_uk/tests/test_council_tax_reduction_claimant_family_scheme_properties.py +++ b/policyengine_uk/tests/test_council_tax_reduction_claimant_family_scheme_properties.py @@ -123,9 +123,11 @@ "armed_forces_independence_payment": 9_000, "is_blind": True, } -# Ages of 20 or more are never presumed to be a child of the family, so -# changing them never changes who is a claimant or partner. The sampled values -# sit on both sides of State Pension age. +# Adults are 20 or more. The situation supplies their claimant-or-partner +# role, so changing their ages never changes who is a claimant or partner +# (without the input, a member 20 or more years younger than the claimant +# would be presumed their child). The sampled values sit on both sides of +# State Pension age. adult_age = st.one_of( st.sampled_from([20, 30, 45, 64, 65, 66, 67, 70, 85]), st.integers(20, 100) ) diff --git a/policyengine_uk/tests/test_council_tax_reduction_claimant_properties.py b/policyengine_uk/tests/test_council_tax_reduction_claimant_properties.py index 6424555e09..37e6168269 100644 --- a/policyengine_uk/tests/test_council_tax_reduction_claimant_properties.py +++ b/policyengine_uk/tests/test_council_tax_reduction_claimant_properties.py @@ -43,8 +43,12 @@ person-level is_household_head flag. (Every programme reads the same head; see test_household_head_agreement_properties.py.) -6. Not age: with one head flagged, raising every other member's age above - the head's does not change who claims. +6. Not age: with one head flagged, raising every other adult's age above + the head's does not change who claims. Each person's claimant-or-partner + role is held at the model's reading of the drawn ages: the age + presumption (a member 20 or more years younger than the claimant is their + child) would otherwise read the head, or an adult of another family, as + a raised member's child. 7. No-op: where the flagged head is strictly the eldest member and the rent is not shared, the claimant family is the eldest adult's family, the previous rule. @@ -352,9 +356,11 @@ def older_than_head(house, adult_index, age, flagged): raised, _ = build(population, age_override=older_than_head) # 6. Not age. - before = calc( - Simulation(situation=situation), "council_tax_reduction_claimant_benunit" - ) + drawn = Simulation(situation=situation) + before = calc(drawn, "council_tax_reduction_claimant_benunit") + roles = calc(drawn, "is_claimant_or_partner") + for person, role in zip(raised["people"].values(), roles): + person["is_claimant_or_partner"] = {YEAR: bool(role)} after = calc(Simulation(situation=raised), "council_tax_reduction_claimant_benunit") assert np.array_equal(before, after) diff --git a/policyengine_uk/tests/test_council_tax_reduction_pensioner_earnings_disregard_properties.py b/policyengine_uk/tests/test_council_tax_reduction_pensioner_earnings_disregard_properties.py index 0db11ccbd2..922a844d15 100644 --- a/policyengine_uk/tests/test_council_tax_reduction_pensioner_earnings_disregard_properties.py +++ b/policyengine_uk/tests/test_council_tax_reduction_pensioner_earnings_disregard_properties.py @@ -117,11 +117,17 @@ def situation(units, year, earnings_bump=0.0, no_income_related_benefits=False): "weekly_hours": {year: adult["weekly_hours"]}, "is_disabled_for_benefits": {year: adult["disabled"]}, "state_pension": {year: adult["state_pension"]}, + # The adults are the claimant and partner whatever their age + # gap, so the roles are supplied rather than presumed. + "is_claimant_or_partner": {year: True}, } names.append(name) for k, child_age in enumerate(unit["children"]): name = f"c{i}_{k}" - people[name] = {"age": {year: child_age}} + people[name] = { + "age": {year: child_age}, + "is_claimant_or_partner": {year: False}, + } names.append(name) benunits[f"b{i}"] = { "members": names, diff --git a/policyengine_uk/tests/test_dfe_childcare_student_support_properties.py b/policyengine_uk/tests/test_dfe_childcare_student_support_properties.py index 4202d88b7e..35f5cee286 100644 --- a/policyengine_uk/tests/test_dfe_childcare_student_support_properties.py +++ b/policyengine_uk/tests/test_dfe_childcare_student_support_properties.py @@ -249,6 +249,9 @@ def test_extended_childcare_eligibility_matches_regs_13_to_15(year, families): for adult in adults: inputs = { "age": adult["age"], + # The drawn adults are the claimant and partner, as the + # oracle below reads them, however far apart their ages. + "is_claimant_or_partner": True, "is_parent": bool(children), "is_carer_for_benefits": adult["status"] == "carer", "employment_income": adult["earnings"], @@ -261,7 +264,11 @@ def test_extended_childcare_eligibility_matches_regs_13_to_15(year, families): people.append(inputs) for age, looked_after in children: people.append( - {"age": age, "is_looked_after_by_local_authority": looked_after} + { + "age": age, + "is_claimant_or_partner": False, + "is_looked_after_by_local_authority": looked_after, + } ) benunit = {"universal_credit": uc, "uc_carer_element": carer_element} built.append((people, benunit, {"country": "ENGLAND"})) diff --git a/policyengine_uk/tests/test_housing_benefit_earnings_disregard_properties.py b/policyengine_uk/tests/test_housing_benefit_earnings_disregard_properties.py index 06c0663331..412178af99 100644 --- a/policyengine_uk/tests/test_housing_benefit_earnings_disregard_properties.py +++ b/policyengine_uk/tests/test_housing_benefit_earnings_disregard_properties.py @@ -136,11 +136,18 @@ def situation(units, earnings_bump=0.0, income_support=0.0, age_map=None): YEAR: adult["pension_contributions"] }, "childcare_expenses": {YEAR: unit["childcare"] if j == 0 else 0.0}, + # The generated adults are the claimant and partner whatever + # their age gap (or mapped ages), so the roles are supplied + # rather than left to the age presumption. + "is_claimant_or_partner": {YEAR: True}, } names.append(name) for k, child_age in enumerate(unit["children"]): name = f"c{i}_{k}" - people[name] = {"age": {YEAR: child_age}} + people[name] = { + "age": {YEAR: child_age}, + "is_claimant_or_partner": {YEAR: False}, + } names.append(name) benunits[f"b{i}"] = { "members": names, @@ -300,3 +307,27 @@ def test_additional_disregard_history(): for instant, amount in expected.items(): p = system.parameters(instant).gov.dwp.housing_benefit.means_test assert p.income_disregard.worker == amount, instant + + +def test_a_couple_twenty_years_apart_keeps_the_couple_disregard(): + # Supplied as a couple, ages 60 and 40 keep the couple amount; without + # roles the age presumption would read the 40-year-old as the claimant's + # child and apply the single amount instead. + unit = dict( + shape="couple", + adults=[ + dict( + age=age, + employment_income=income, + weekly_hours=0, + disabled=False, + pension_contributions=0.0, + ) + for age, income in ((60, 1_000.0), (40, 0.0)) + ], + children=[], + childcare=0.0, + ) + values = calculate([unit]) + disregard = values["housing_benefit_applicable_income_disregard"][0] + assert abs(disregard - DISREGARD.couple * WEEKS) < 0.01 diff --git a/policyengine_uk/tests/test_housing_benefit_income_related_benefit_passport_properties.py b/policyengine_uk/tests/test_housing_benefit_income_related_benefit_passport_properties.py index 9a9fccd960..ef5625528f 100644 --- a/policyengine_uk/tests/test_housing_benefit_income_related_benefit_passport_properties.py +++ b/policyengine_uk/tests/test_housing_benefit_income_related_benefit_passport_properties.py @@ -108,6 +108,10 @@ def situation(units, bump=0.0, awards=None, ages=None): age = adult["age"] if ages is None else ages[i][j] person = { "age": {YEAR: age}, + # The drawn adults are the claimant and partner, however far + # apart their ages; the age presumption alone would read a + # partner 20 or more years younger as the claimant's child. + "is_claimant_or_partner": {YEAR: True}, "employment_income": {YEAR: adult["employment_income"] + bump}, "private_pension_income": { YEAR: adult["private_pension_income"] + bump @@ -121,7 +125,10 @@ def situation(units, bump=0.0, awards=None, ages=None): members.append(name) for k, child_age in enumerate(unit["children"]): name = f"c{i}_{k}" - people[name] = {"age": {YEAR: child_age}} + people[name] = { + "age": {YEAR: child_age}, + "is_claimant_or_partner": {YEAR: False}, + } members.append(name) family_awards = unit["awards"] if awards is None else awards[i] benunits[f"b{i}"] = { @@ -134,6 +141,8 @@ def situation(units, bump=0.0, awards=None, ages=None): name = f"n{i}" people[name] = { "age": {YEAR: 30}, + # The claimant of their own benefit unit. + "is_claimant_or_partner": {YEAR: True}, "employment_income": {YEAR: unit["non_dependant_earnings"]}, } benunits[f"nb{i}"] = {"members": [name], "would_claim_uc": {YEAR: False}} 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 ba3818a4f9..13400ccaa5 100644 --- a/policyengine_uk/tests/test_housing_benefit_lha_order_properties.py +++ b/policyengine_uk/tests/test_housing_benefit_lha_order_properties.py @@ -317,9 +317,9 @@ def pension_age_households(draw): # The single pensioner from the issue (State Pension 16,000, above the # minimum guarantee; rent 30,000 against an LHA of 9,467.64), alone and with -# a working-age non-dependant, and a pension-age couple in the same flat. -# Each has rent above the LHA and income above the applicable amount, where -# the two orders differ. +# a working-age non-dependant, and a pension-age couple in the same flat, +# also 22 years apart. Each has rent above the LHA and income above the +# applicable amount, where the two orders differ. END_TO_END_EXAMPLE = [ dict( ages=[70], @@ -333,7 +333,7 @@ def pension_age_households(draw): for non_dependant_earnings in [None, 20_000.0] ] + [ dict( - ages=[70, 68], + ages=ages, state_pension=12_000.0, private_pension=0.0, tenure="RENT_PRIVATELY", @@ -341,6 +341,7 @@ def pension_age_households(draw): lha_rate=9_467.64, non_dependant_earnings=None, ) + for ages in [[70, 68], [90, 68]] ] @@ -353,6 +354,9 @@ def end_to_end(population): name = f"p{i}_{j}" people[name] = { "age": {YEAR: age}, + # The drawn pensioners are the claimant and partner, however + # far apart their ages. + "is_claimant_or_partner": {YEAR: True}, "state_pension_reported": {YEAR: unit["state_pension"]}, "private_pension_income": {YEAR: unit["private_pension"]}, } @@ -368,6 +372,8 @@ def end_to_end(population): name = f"n{i}" people[name] = { "age": {YEAR: 30}, + # The claimant of their own benefit unit. + "is_claimant_or_partner": {YEAR: True}, "employment_income": {YEAR: unit["non_dependant_earnings"]}, } benunits[f"n{i}"] = {"members": [name]} diff --git a/policyengine_uk/tests/test_housing_benefit_pension_age_cohorts.py b/policyengine_uk/tests/test_housing_benefit_pension_age_cohorts.py index 3334321751..f252da765d 100644 --- a/policyengine_uk/tests/test_housing_benefit_pension_age_cohorts.py +++ b/policyengine_uk/tests/test_housing_benefit_pension_age_cohorts.py @@ -65,7 +65,9 @@ def build(units: list, year: int, region: str = "LONDON") -> Simulation: member over State Pension age is under the pension-age Housing Benefit regulations and is a Council Tax Reduction pensioner. That leaves the cohort rule as the only thing under test (a mixed-age couple on Universal - Credit would otherwise be in the working-age tables).""" + Credit would otherwise be in the working-age tables). Every member is the + claimant or partner, and says so: the age presumption would read a + partner 20 or more years younger as the claimant's child.""" people, benunits = {}, {} mid_year = grid_months(date(year, 10, 6)) for u, members in enumerate(units): @@ -78,6 +80,7 @@ def build(units: list, year: int, region: str = "LONDON") -> Simulation: "age": {year: age}, "months_since_last_birthday": {year: age_in_months - 12 * age}, "is_male": {year: male}, + "is_claimant_or_partner": {year: True}, } names.append(name) pension_age = at_pension_age(members, year) diff --git a/policyengine_uk/tests/test_housing_benefit_savings_credit_only_properties.py b/policyengine_uk/tests/test_housing_benefit_savings_credit_only_properties.py index 6d19738615..10a39e6a78 100644 --- a/policyengine_uk/tests/test_housing_benefit_savings_credit_only_properties.py +++ b/policyengine_uk/tests/test_housing_benefit_savings_credit_only_properties.py @@ -19,7 +19,9 @@ the savings credit withdrawn, or 39%, and the combined withdrawal is 91%. Invariants, for single people and couples aged 80 or over (so State Pension -age was reached before April 2016 and savings credit is possible), who rent +age was reached before April 2016 and savings credit is possible; a couple's +members supply is_claimant_or_partner, since the age presumption would read a +partner 20 or more years younger as the claimant's child), who rent from the council in England, Wales or Scotland and have no children, carers or earnings, with private pension on a £250 grid: @@ -45,7 +47,7 @@ """ import numpy as np -from hypothesis import HealthCheck, given, settings +from hypothesis import HealthCheck, example, given, settings from hypothesis import strategies as st from policyengine_uk import Simulation @@ -127,6 +129,7 @@ def situation(families): name = f"p{i}_{k}_{j}" person = { "age": {YEAR: a["age"]}, + "is_claimant_or_partner": {YEAR: True}, "state_pension": {YEAR: a["state_pension"]}, # All private pension goes to the first adult. "private_pension_income": {YEAR: float(pension) * (j == 0)}, @@ -203,8 +206,25 @@ def describe(fam, g, i, k): ) +# Couples 20 years apart, the pension on the younger and then the elder. +WIDE_GAP_COUPLES = [ + dict( + adults=[ + dict(age=first, disability={}, blind=False, state_pension=6_000.0), + dict(age=second, disability={}, blind=False, state_pension=6_000.0), + ], + country="ENGLAND", + rent=6_000.0, + council_tax=1_500.0, + savings=0.0, + ) + for first, second in [(80, 100), (100, 80)] +] + + @PROPERTY_SETTINGS @given(st.lists(family(), min_size=1, max_size=4)) +@example(WIDE_GAP_COUPLES) def test_net_income_rises_with_private_pension_on_savings_credit_only(families): g = grid(families) net = g["household_net_income"] + g["tv_licence"] @@ -223,6 +243,7 @@ def test_net_income_rises_with_private_pension_on_savings_credit_only(families): @PROPERTY_SETTINGS @given(st.lists(family(), min_size=1, max_size=4)) +@example(WIDE_GAP_COUPLES) def test_net_income_does_not_fall_where_savings_credit_ends(families): g = grid(families) net = ( @@ -249,6 +270,7 @@ def deemed_income(capital): @PROPERTY_SETTINGS @given(st.lists(family(), min_size=1, max_size=4)) +@example(WIDE_GAP_COUPLES) def test_income_follows_the_route_for_the_award(families): g = grid(families) savings = np.array([fam["savings"] for fam in families])[:, None] diff --git a/policyengine_uk/tests/test_housing_benefit_universal_credit_passport_properties.py b/policyengine_uk/tests/test_housing_benefit_universal_credit_passport_properties.py index e7d2d7143a..caab1a85b0 100644 --- a/policyengine_uk/tests/test_housing_benefit_universal_credit_passport_properties.py +++ b/policyengine_uk/tests/test_housing_benefit_universal_credit_passport_properties.py @@ -168,6 +168,10 @@ def situation(units, bump=0.0, overrides=None): name = f"a{i}_{j}" person = { "age": {YEAR: adult["age"]}, + # The drawn adults are the claimant and partner, however far + # apart their ages; the age presumption alone would read a + # partner 20 or more years younger as the claimant's child. + "is_claimant_or_partner": {YEAR: True}, "employment_income": {YEAR: adult["employment_income"] + bump}, "private_pension_income": { YEAR: adult["private_pension_income"] + bump @@ -179,7 +183,10 @@ def situation(units, bump=0.0, overrides=None): members.append(name) for k, child_age in enumerate(unit["children"]): name = f"c{i}_{k}" - people[name] = {"age": {YEAR: child_age}} + people[name] = { + "age": {YEAR: child_age}, + "is_claimant_or_partner": {YEAR: False}, + } members.append(name) benunit = { "members": list(members), @@ -202,6 +209,8 @@ def situation(units, bump=0.0, overrides=None): name = f"n{i}" people[name] = { "age": {YEAR: 30}, + # The claimant of their own benefit unit. + "is_claimant_or_partner": {YEAR: True}, "employment_income": {YEAR: unit["non_dependant_earnings"]}, } benunits[f"nb{i}"] = {"members": [name], "would_claim_uc": {YEAR: False}} diff --git a/policyengine_uk/tests/test_legacy_carer_premium_properties.py b/policyengine_uk/tests/test_legacy_carer_premium_properties.py index 79c0c51fbf..420a91ceb0 100644 --- a/policyengine_uk/tests/test_legacy_carer_premium_properties.py +++ b/policyengine_uk/tests/test_legacy_carer_premium_properties.py @@ -33,8 +33,9 @@ when the one who cares holds the Income Support award. Adults are 18 to 85, so pension-age families are covered. No adult under 20 -is 16 or more years younger than the other adult, whom the model would then -presume to be their parent. +is 16 or more years younger than the other adult, and no adult is 20 or more +years younger, either of which the model would read as the other adult's +child. Each example builds every family twice in one simulation, in separate households and benefit units: once as drawn and once with a changed caring @@ -98,8 +99,9 @@ def families(draw): family = [head] if draw(st.booleans()): # Within 15 years of the head in both directions when either could be - # under 20, so neither is presumed the other's child. - oldest_partner = 85 if head_age >= 20 else head_age + 15 + # under 20, and under 20 years older otherwise, so neither is presumed + # the other's child. + oldest_partner = min(85, head_age + 19) if head_age >= 20 else head_age + 15 family.append( { "age": draw(st.integers(max(18, head_age - 15), oldest_partner)), @@ -160,11 +162,23 @@ def situation(units, same_person=None): }, {"age": 33, "is_parent": False, **CARING}, ] +# And above 20: a partner 19 years older than a 40-year-old head is drawn; 20 +# years older is not (the model would presume the head to be their child). +OLDER_PARTNER_BOUNDARY = [ + { + "age": 40, + "is_parent": False, + "income_support_reported": 1_000, + **CARING, + }, + {"age": 59, "is_parent": False, **CARING}, +] @SETTINGS @given(st.lists(families(), min_size=1, max_size=6)) @example(drawn=[(BOUNDARY, 2)]) +@example(drawn=[(OLDER_PARTNER_BOUNDARY, 2)]) def test_dependants_caring_never_changes_premium_or_is_eligibility(drawn): units = [family for family, _ in drawn] adults = [n for _, n in drawn] diff --git a/policyengine_uk/tests/test_means_test_income_properties.py b/policyengine_uk/tests/test_means_test_income_properties.py index 9f1a3a7e4a..c03ab5f698 100644 --- a/policyengine_uk/tests/test_means_test_income_properties.py +++ b/policyengine_uk/tests/test_means_test_income_properties.py @@ -49,7 +49,9 @@ @st.composite def families(draw): - # A head, an optional partner, and one member who by construction is + # A head, an optional partner under 20 years younger (so the age + # presumption reads them as the partner, not the head's child), and one + # member who by construction is # outside every programme's family: a 16-19-year-old at least 16 years # younger than the head (presumed the head's child) who is not in # non-advanced education, so no programme's young person; or a third adult @@ -71,7 +73,7 @@ def families(draw): } ] if kind == "third_adult" or draw(st.booleans()): - partner_age = draw(st.integers(20, head_age)) + partner_age = draw(st.integers(max(20, head_age - 19), head_age)) family.append({"age": partner_age, "employment_income": draw(money)}) if kind == "third_adult": other_age = draw(st.integers(20, partner_age)) diff --git a/policyengine_uk/tests/test_mixed_age_couple_properties.py b/policyengine_uk/tests/test_mixed_age_couple_properties.py index 598bc8b093..812fad9ef6 100644 --- a/policyengine_uk/tests/test_mixed_age_couple_properties.py +++ b/policyengine_uk/tests/test_mixed_age_couple_properties.py @@ -164,6 +164,9 @@ def cases(draw): people["dependant"] = {"age": 18, "current_education": "UPPER_SECONDARY"} people["older"]["is_parent"] = True for name, person in people.items(): + # The older and younger members are the couple whatever their age gap, + # so the roles are supplied rather than left to the age presumption. + person["is_claimant_or_partner"] = name != "dependant" if name == "dependant": continue if person["age"] >= 67: diff --git a/policyengine_uk/tests/test_pawhp_household_income_properties.py b/policyengine_uk/tests/test_pawhp_household_income_properties.py index 4ff4255422..05d244a83f 100644 --- a/policyengine_uk/tests/test_pawhp_household_income_properties.py +++ b/policyengine_uk/tests/test_pawhp_household_income_properties.py @@ -142,6 +142,9 @@ def situation(population): for a, adult in enumerate(unit["adults"]): name = f"h{h}_u{u}_a{a}" people[name] = {key: {year: value} for key, value in adult.items()} + # Each unit's adults are its claimant and partner, however far + # apart their ages (40 and 85 are drawn). + people[name]["is_claimant_or_partner"] = {year: True} unit_members.append(name) benunits[f"h{h}_u{u}"] = {"members": unit_members} if unit["pension_credit"] is not None: diff --git a/policyengine_uk/tests/test_pension_age_severe_disability_premium_properties.py b/policyengine_uk/tests/test_pension_age_severe_disability_premium_properties.py index a0deb726ef..ccf296eb39 100644 --- a/policyengine_uk/tests/test_pension_age_severe_disability_premium_properties.py +++ b/policyengine_uk/tests/test_pension_age_severe_disability_premium_properties.py @@ -6,8 +6,11 @@ The structural and boundary properties draw single people and couples aged at least 72 without children, carers or other household residents, who rent -from the council in England in 2026. The schedule property also draws -younger claimants: +from the council in England in 2026. A couple's members supply +is_claimant_or_partner, since the age presumption would read a partner 20 or +more years younger (72 and 95 are drawn) as the claimant's child. The +schedule property also draws younger claimants, and reads the model's own +claimant and partner: 1. Structural: where Housing Benefit is available, its applicable amount is at least the Pension Credit minimum guarantee, and so is the Council Tax @@ -107,6 +110,7 @@ def situation(families): name = f"p{i}_{k}_{j}" person = { "age": {YEAR: a["age"]}, + "is_claimant_or_partner": {YEAR: True}, "state_pension": {YEAR: a["state_pension"]}, # All private pension goes to the first adult. "private_pension_income": {YEAR: float(pension) * (j == 0)}, diff --git a/policyengine_uk/tests/test_pension_credit_carer_income_properties.py b/policyengine_uk/tests/test_pension_credit_carer_income_properties.py index b0d4fcf75c..5035c68372 100644 --- a/policyengine_uk/tests/test_pension_credit_carer_income_properties.py +++ b/policyengine_uk/tests/test_pension_credit_carer_income_properties.py @@ -148,6 +148,9 @@ def situation(families): "state_pension": {YEAR: state_pension}, # All private pension goes to the first adult. "private_pension_income": {YEAR: float(pension) * (j == 0)}, + # The adults are the claimant and partner whatever their + # age gap, so the roles are supplied rather than presumed. + "is_claimant_or_partner": {YEAR: True}, } if is_carer: if fam["by_hours"]: diff --git a/policyengine_uk/tests/test_pension_credit_earnings_disregard_properties.py b/policyengine_uk/tests/test_pension_credit_earnings_disregard_properties.py index 8b8696e236..bfb6ea78c3 100644 --- a/policyengine_uk/tests/test_pension_credit_earnings_disregard_properties.py +++ b/policyengine_uk/tests/test_pension_credit_earnings_disregard_properties.py @@ -110,6 +110,10 @@ def simulate(case): name: {k: {str(YEAR): v} for k, v in person.items()} for name, person in people.items() } + # The claimant and partner say so: the age presumption would read a + # partner 20 or more years younger (85 and 64) as the claimant's child. + for name, person in dated.items(): + person["is_claimant_or_partner"] = {str(YEAR): name != "child"} return Simulation( situation={ "people": dated, diff --git a/policyengine_uk/tests/test_pension_credit_prescribed_benefits_properties.py b/policyengine_uk/tests/test_pension_credit_prescribed_benefits_properties.py index cbdfaa5d61..1a7a52a654 100644 --- a/policyengine_uk/tests/test_pension_credit_prescribed_benefits_properties.py +++ b/policyengine_uk/tests/test_pension_credit_prescribed_benefits_properties.py @@ -14,7 +14,9 @@ ESA non-qualifying income for Savings Credit; IIDB stays qualifying income. Draws: single people and couples over State Pension age in England, Wales or -Scotland, renting from the council in 2026, with State Pension, a private +Scotland (the drawn adults supply is_claimant_or_partner, since the age +presumption would read a partner 20 or more years younger as the claimant's +child), renting from the council in 2026, with State Pension, a private pension and any of the four benefits (each on a drawn member, at a drawn amount). Some families also have a carer receiving Carer's Allowance or, in Scotland, Carer Support Payment, so the sources #1952 added are covered @@ -65,7 +67,7 @@ """ import numpy as np -from hypothesis import HealthCheck, given, settings +from hypothesis import HealthCheck, example, given, settings from hypothesis import strategies as st from policyengine_uk import Simulation @@ -141,6 +143,7 @@ def situation(rows): name = f"p{r}_{j}" person = { "age": {YEAR: a["age"]}, + "is_claimant_or_partner": {YEAR: True}, "state_pension": {YEAR: a["state_pension"]}, # All private pension goes to the first adult. "private_pension_income": {YEAR: float(pension) * (j == 0)}, @@ -229,8 +232,24 @@ def test_pension_credit_and_housing_benefit_assess_the_same_prescribed_benefits( ) +# A couple 23 years apart, the IIDB on the younger partner: their benefit is +# the couple's income. +WIDE_GAP_COUPLE = dict( + adults=[dict(age=90, state_pension=4_000.0), dict(age=67, state_pension=4_000.0)], + benefits={benefit: (1, 0.0) for benefit in PRESCRIBED_BENEFITS} + | {"iidb": (1, 2_000.0)}, + carer=None, + by_hours=False, + country="ENGLAND", + rent=6_000.0, + council_tax=1_500.0, + savings=0.0, +) + + @PROPERTY_SETTINGS @given(st.lists(family(state_pension=8_000), min_size=1, max_size=4)) +@example([WIDE_GAP_COUPLE]) def test_guarantee_credit_falls_pound_for_pound_with_each_prescribed_benefit( families, ): diff --git a/policyengine_uk/tests/test_pension_credit_reported_capital_properties.py b/policyengine_uk/tests/test_pension_credit_reported_capital_properties.py index 0c60e1dcaa..61c0fe3d0a 100644 --- a/policyengine_uk/tests/test_pension_credit_reported_capital_properties.py +++ b/policyengine_uk/tests/test_pension_credit_reported_capital_properties.py @@ -85,9 +85,15 @@ def simulate(people, units, household, overrides=None): "members": unit["members"], "pension_credit_reported_capital": {year: value}, } + # Every unit is a single adult or a couple whatever the age gap, so the + # roles are supplied rather than left to the age presumption (which reads + # a member 20+ years younger as the claimant's child). situation = { "people": { - name: {k: {year: v} for k, v in person.items()} + name: { + **{k: {year: v} for k, v in person.items()}, + "is_claimant_or_partner": {year: True}, + } for name, person in people.items() }, "benunits": benunits, diff --git a/policyengine_uk/tests/test_pension_credit_statutory_pay_properties.py b/policyengine_uk/tests/test_pension_credit_statutory_pay_properties.py index dcee71bf6d..3ec1cfeb27 100644 --- a/policyengine_uk/tests/test_pension_credit_statutory_pay_properties.py +++ b/policyengine_uk/tests/test_pension_credit_statutory_pay_properties.py @@ -139,6 +139,12 @@ def situation(families, steps, vary): for j, inputs in enumerate(vary(i, step)): name = f"p{i}_{k}_{j}" person = {key: {YEAR: value} for key, value in inputs.items()} + # Adults are drawn independently (from 67 to 95, and from 40 + # for invariant 2's partners), so a couple can be 20 or more + # years apart, where the age presumption reads the younger as + # the elder's child. Every person here is the claimant or the + # partner. + person["is_claimant_or_partner"] = {YEAR: True} if j == fam["carer"]: if fam["by_hours"]: person["care_hours"] = {YEAR: 35} diff --git a/policyengine_uk/tests/test_pension_credit_war_pension_properties.py b/policyengine_uk/tests/test_pension_credit_war_pension_properties.py index 122cfcb68e..ef90b4e19d 100644 --- a/policyengine_uk/tests/test_pension_credit_war_pension_properties.py +++ b/policyengine_uk/tests/test_pension_credit_war_pension_properties.py @@ -122,6 +122,10 @@ def family(draw, min_age=67): def person_inputs(fam, j, a, private_pension, afcs, war_widows_pension): person = { "age": {YEAR: a["age"]}, + # The drawn adults are the claimant and partner, however far apart + # their ages; the age presumption alone would read a partner 20 or + # more years younger as the claimant's child. + "is_claimant_or_partner": {YEAR: True}, "state_pension": {YEAR: a["state_pension"]}, # All private pension goes to the first adult. "private_pension_income": {YEAR: float(private_pension) * (j == 0)}, diff --git a/policyengine_uk/tests/test_severe_disability_afcs_properties.py b/policyengine_uk/tests/test_severe_disability_afcs_properties.py index 0e6b2b32ae..c2ed896051 100644 --- a/policyengine_uk/tests/test_severe_disability_afcs_properties.py +++ b/policyengine_uk/tests/test_severe_disability_afcs_properties.py @@ -96,6 +96,9 @@ def situation(units, afcs=None, afip=None): values["armed_forces_independence_payment"] = afip( name, values["armed_forces_independence_payment"] ) + # The adults are the claimant and partner, however far apart + # their ages (25 and 60 are drawn); dependants are neither. + values["is_claimant_or_partner"] = role == "a" if role == "a": values["is_parent"] = bool(unit["dependants"]) elif values["age"] >= 16: diff --git a/policyengine_uk/tests/test_severe_disability_premium_properties.py b/policyengine_uk/tests/test_severe_disability_premium_properties.py index 6e8764ddb8..071df1b402 100644 --- a/policyengine_uk/tests/test_severe_disability_premium_properties.py +++ b/policyengine_uk/tests/test_severe_disability_premium_properties.py @@ -98,9 +98,10 @@ def test_northern_ireland_commencements_by_model_year(): "afcs_other": ({"afcs_reported": 1_000.0}, False), } benefit = st.sampled_from(sorted(BENEFITS)) -# Adults aged 20 to 60 and children under 16 are the claimant and partner, and -# their children, under any reading of the benefit unit (18 and 19 year olds -# can be qualifying young persons). +# Adults aged 20 to 60 are the claimant and partner, and children under 16 +# their children; simulate() supplies those roles, since the age presumption +# would read a partner 20 or more years younger as the claimant's child (18 +# and 19 year olds can be qualifying young persons). ADULT_AGE = st.integers(20, 60) @@ -183,11 +184,19 @@ def simulate(units, variables=("severe_disability_premium",)): family = [] for j, p in enumerate(family_members(unit)): people[f"h{i}_f{j}"] = person_inputs(p) + # The generated adults are the claimant and partner; children and + # 16-19 year olds are not. + people[f"h{i}_f{j}"]["is_claimant_or_partner"] = { + YEAR: j < len(unit["adults"]) + } family.append(f"h{i}_f{j}") benunits[f"h{i}_family"] = {"members": family} members = list(family) for j, p in enumerate(unit["others"]): people[f"h{i}_o{j}"] = person_inputs(p) + # Each is the claimant of their own benefit unit from age 16, as + # the carer reference reads them. + people[f"h{i}_o{j}"]["is_claimant_or_partner"] = {YEAR: p["age"] >= 16} benunits[f"h{i}_o{j}"] = {"members": [f"h{i}_o{j}"]} members.append(f"h{i}_o{j}") hh[f"h{i}"] = {"members": members} @@ -292,6 +301,12 @@ def _adult(benefit, blind=False, carer=False): children=[], others=[], ), + # The same couple 20 years apart (60 and 40) keeps the double rate. + dict( + adults=[dict(_adult("pip_standard"), age=60), _adult("aa_lower")], + children=[], + others=[], + ), # One qualifies, the other is blind: the single rate. dict( adults=[_adult("dla_middle"), _adult("none", blind=True)], diff --git a/policyengine_uk/tests/test_trading_loss_properties.py b/policyengine_uk/tests/test_trading_loss_properties.py index 530980c84a..0d50303a2a 100644 --- a/policyengine_uk/tests/test_trading_loss_properties.py +++ b/policyengine_uk/tests/test_trading_loss_properties.py @@ -404,9 +404,13 @@ def families(draw): adults = [head_age] if draw(st.booleans()): adults.append(draw(st.integers(18, 80))) + # The adults are the claimant and partner, however far apart their ages; + # the age presumption alone would read a partner 20 or more years younger + # as the head's child. Children, without the input, are neither. people = [ { "age": age, + "is_claimant_or_partner": True, "employment_income": draw(money(60_000)), "self_employment_income": draw(money(30_000)), "private_pension_income": draw(money(20_000)), @@ -458,6 +462,7 @@ def family_situation(units, overrides=None, benunit_overrides=None, year=YEAR): def adult(**inputs): base = { "age": 40, + "is_claimant_or_partner": True, "employment_income": 0.0, "self_employment_income": 0.0, "private_pension_income": 0.0, diff --git a/policyengine_uk/tests/test_uc_claimant_elements_properties.py b/policyengine_uk/tests/test_uc_claimant_elements_properties.py index d695d46772..5f170ac18e 100644 --- a/policyengine_uk/tests/test_uc_claimant_elements_properties.py +++ b/policyengine_uk/tests/test_uc_claimant_elements_properties.py @@ -192,6 +192,10 @@ def situation( values.update(first_claimant_overrides or {}) person = {k: {year: v} for k, v in values.items()} person["is_parent"] = {year: bool(unit["dependants"])} + # The claimants' ages are drawn independently, so they can be 20 + # or more years apart, where the age presumption reads the younger + # as the elder's child. They are the claimants by construction. + person["is_claimant_or_partner"] = {year: True} members.append((f"p{i}_{j}", person)) for k, dependant in enumerate(unit["dependants"]): person = { diff --git a/policyengine_uk/tests/test_uc_claimant_income_properties.py b/policyengine_uk/tests/test_uc_claimant_income_properties.py index 19ac9c1b77..858430fc5e 100644 --- a/policyengine_uk/tests/test_uc_claimant_income_properties.py +++ b/policyengine_uk/tests/test_uc_claimant_income_properties.py @@ -73,6 +73,19 @@ "mixed_age": [PENSION_AGE, WORKING_AGE], } amount = st.one_of(st.just(0.0), st.floats(0, 30_000)) + + +def claimant_ages(draw, strategies): + """The claimants' ages, each within 19 years of the first. The age + presumption reads a member 20 or more years younger than the claimant as + their child, and invariants 2 and 5 check the model's own reading of who + the claimants are, so the draws keep to couples it reads as couples.""" + ages = [draw(strategies[0])] + for strategy in strategies[1:]: + ages.append(draw(strategy.filter(lambda a, first=ages[0]: abs(a - first) < 20))) + return ages + + # What a dependant may receive. The last two are contributory benefits, which # count towards the benefit cap total when they are the claimant's. # Contributory ESA is left out: any member's ESA exempts the benefit unit from @@ -143,7 +156,7 @@ def families(draw): if kind == "capped": # Claimants without earnings, several children and a high council # rent: over the cap unless something exempts them. - claimants = [dict(age=draw(age)) for age in SHAPES[shape]] + claimants = [dict(age=age) for age in claimant_ages(draw, SHAPES[shape])] return dict( claimants=claimants, dependants=draw(dependants(minimum=3)), @@ -156,8 +169,8 @@ def families(draw): # Every claimant works, so the work condition is the claimants' to # meet. claimants = [ - dict(age=draw(age), employment_income=draw(st.floats(3_000, 30_000))) - for age in SHAPES[shape] + dict(age=age, employment_income=draw(st.floats(3_000, 30_000))) + for age in claimant_ages(draw, SHAPES[shape]) ] return dict( claimants=claimants, @@ -169,12 +182,12 @@ def families(draw): ) claimants = [ dict( - age=draw(age), + age=age, employment_income=draw(amount), savings_interest_income=draw(amount), private_pension_income=draw(amount), ) - for age in SHAPES[shape] + for age in claimant_ages(draw, SHAPES[shape]) ] return dict( claimants=claimants, @@ -258,8 +271,9 @@ def capped_families(draw): """Working-age claimants with no income, four or five children and a high council rent, so the benefit cap always bites; at least one child has earnings well over the earnings exception's threshold.""" + n = draw(st.integers(1, 2)) claimants = [ - dict(age=draw(st.integers(25, 60))) for _ in range(draw(st.integers(1, 2))) + dict(age=age) for age in claimant_ages(draw, [st.integers(25, 60)] * n) ] children = [ dict(age=draw(st.integers(0, 15)), employment_income=draw(amount)) @@ -330,14 +344,15 @@ def test_assessed_claimants_are_the_claimant_and_partner(units, year): def untaxed_families(draw): """Working-age claimants whose earnings carry no tax or NI, with capital under the £16,000 limit (reg. 18).""" + n = draw(st.integers(1, 2)) claimants = [ dict( - age=draw(st.integers(25, 60)), + age=age, employment_income=draw(untaxed_earnings), savings_interest_income=draw(st.one_of(st.just(0.0), st.floats(0, 500))), private_pension_income=draw(st.one_of(st.just(0.0), st.floats(0, 500))), ) - for _ in range(draw(st.integers(1, 2))) + for age in claimant_ages(draw, [st.integers(25, 60)] * n) ] return dict( claimants=claimants, diff --git a/policyengine_uk/tests/test_uc_company_owner_properties.py b/policyengine_uk/tests/test_uc_company_owner_properties.py index 84301ea83c..beafcec75b 100644 --- a/policyengine_uk/tests/test_uc_company_owner_properties.py +++ b/policyengine_uk/tests/test_uc_company_owner_properties.py @@ -178,6 +178,10 @@ def situation(variants): facts = {**adult["company"], **overrides.get(j, {})} people[name] = { "age": {YEAR: adult["age"]}, + # The drawn adults are the claimant and partner, however far + # apart their ages; the age presumption alone would read a + # partner 20 or more years younger as the claimant's child. + "is_claimant_or_partner": {YEAR: True}, "employment_income": {YEAR: adult["employment_income"]}, "self_employment_income": {YEAR: adult["self_employment_income"]}, "uc_is_in_startup_period": {YEAR: adult["uc_is_in_startup_period"]}, @@ -189,7 +193,10 @@ def situation(variants): names.append(name) for k in range(unit["children"]): name = f"c{i}_{k}" - people[name] = {"age": {YEAR: 5 + k}} + people[name] = { + "age": {YEAR: 5 + k}, + "is_claimant_or_partner": {YEAR: False}, + } names.append(name) benunits[f"b{i}"] = {"members": names, "would_claim_uc": {YEAR: True}} households[f"h{i}"] = { @@ -622,6 +629,8 @@ def test_household_capital_is_allocated_and_conserved_across_benefit_units(pairs c = adult["company"] people[name] = { "age": {YEAR: adult["age"]}, + # Each unit's adults are its claimant and partner. + "is_claimant_or_partner": {YEAR: True}, **{k: {YEAR: v} for k, v in c.items()}, } if applies(c): @@ -690,12 +699,14 @@ def build(facts): name = f"p{i}_{j}" people[name] = { "age": {YEAR: adult["age"]}, + "is_claimant_or_partner": {YEAR: True}, "employment_income": {YEAR: adult["employment_income"]}, "self_employment_income": {YEAR: adult["self_employment_income"]}, } names.append(name) people[f"d{i}"] = { "age": {YEAR: 17}, + "is_claimant_or_partner": {YEAR: False}, "is_in_non_advanced_education": {YEAR: True}, **{k: {YEAR: v} for k, v in facts.items()}, } @@ -748,15 +759,22 @@ def build(with_holdings): for h, (family, other) in enumerate(pairs): family_names = [] for j, adult in enumerate(family["adults"]): - people[f"h{h}_f{j}"] = {"age": {YEAR: adult["age"]}} + people[f"h{h}_f{j}"] = { + "age": {YEAR: adult["age"]}, + "is_claimant_or_partner": {YEAR: True}, + } family_names.append(f"h{h}_f{j}") dependant = { "age": {YEAR: 17}, + "is_claimant_or_partner": {YEAR: False}, "is_in_non_advanced_education": {YEAR: True}, } other_names = [] for j, adult in enumerate(other["adults"]): - people[f"h{h}_o{j}"] = {"age": {YEAR: adult["age"]}} + people[f"h{h}_o{j}"] = { + "age": {YEAR: adult["age"]}, + "is_claimant_or_partner": {YEAR: True}, + } other_names.append(f"h{h}_o{j}") shares = 0.0 if with_holdings: diff --git a/policyengine_uk/tests/test_uc_minimum_income_floor_properties.py b/policyengine_uk/tests/test_uc_minimum_income_floor_properties.py index 0f7b5eeb53..178697640a 100644 --- a/policyengine_uk/tests/test_uc_minimum_income_floor_properties.py +++ b/policyengine_uk/tests/test_uc_minimum_income_floor_properties.py @@ -12,7 +12,9 @@ Invariants, for any generated population of single people, couples, couples with an older partner aged 65 to 80 (mixed-age once past State -Pension age) and couples with an adult child in their benefit unit, with +Pension age) and couples with an adult child in their benefit unit (the +drawn adults supply is_claimant_or_partner: true for the couple, false for +the adult child and children), with employment, self-employment profits and losses, pension contributions and start-up periods, in England, Wales and Scotland: @@ -72,9 +74,11 @@ # the tax year: Class 4 stays due for that year (SI 2001/1004 reg. 91(a)), # but primary Class 1 and Class 2 stop. PENSION_AGE = st.integers(65, 80) -# The last shape has an adult child in the parents' benefit unit. The model -# flags at most two claimants (is_claimant_or_partner), here the parents; the -# couple is the two eldest claimants. The YAML tests flag a third by input. +# The last shape has an adult child in the parents' benefit unit. The +# situation supplies is_claimant_or_partner for the parents and not the adult +# child, since the age presumption would read a parent 20 or more years +# younger than the other as their child; the couple is the two eldest +# claimants. The YAML tests flag a third by input. SHAPES = { "single": [WORKING_AGE], "couple": [WORKING_AGE, WORKING_AGE], @@ -145,12 +149,14 @@ def situation(units, year, bump=None): if bump is not None and bump[:2] == (i, j): variable, amount = bump[2], bump[3] person[variable] = {year: adult[variable] + amount} + # The couple are the first two adults; an adult child is neither. + person["is_claimant_or_partner"] = {year: j < 2} person["would_claim_marriage_allowance"] = {year: False} people[name] = person names.append(name) for k, age in enumerate(unit["children"]): name = f"c{i}_{k}" - people[name] = {"age": {year: age}} + people[name] = {"age": {year: age}, "is_claimant_or_partner": {year: False}} names.append(name) benunits[f"b{i}"] = {"members": names} households[f"h{i}"] = { diff --git a/policyengine_uk/tests/test_uc_state_pension_properties.py b/policyengine_uk/tests/test_uc_state_pension_properties.py index 0afad1e6ff..c8915c65dc 100644 --- a/policyengine_uk/tests/test_uc_state_pension_properties.py +++ b/policyengine_uk/tests/test_uc_state_pension_properties.py @@ -115,9 +115,10 @@ def situation( for j, age in enumerate(unit["ages"]): name = f"p{i}_{j}" # The generated adults are the claimant and partner; say so, so the - # claimant-or-partner presumption (a member under 20 and much - # younger is the head's child) does not turn a 67-and-18 couple - # into a parent and child. Children get False below. + # claimant-or-partner presumption (a much younger member is the + # head's child: under 20 and 16+ years younger, or 20+ years + # younger at any age) does not turn a 67-and-18 or 90-and-60 + # couple into a parent and child. Children get False below. person = { "age": {year: age}, "state_pension": {year: 0.0}, diff --git a/policyengine_uk/tests/test_uc_work_related_groups_properties.py b/policyengine_uk/tests/test_uc_work_related_groups_properties.py index b9c7105e3e..a969396ad2 100644 --- a/policyengine_uk/tests/test_uc_work_related_groups_properties.py +++ b/policyengine_uk/tests/test_uc_work_related_groups_properties.py @@ -472,7 +472,9 @@ def test_partner_who_cannot_be_a_joint_claimant(units, year): def qualifying_age_couples(): """Each birth date, for each sex, as the elder member of a couple whose partner is 40 and has no section 19 to 21 circumstance. The elder is - listed first in each benefit unit.""" + listed first in each benefit unit. Both are given the claimant-or-partner + role, since the age presumption would read a partner 20 or more years + younger as the elder's child.""" people, benunits, households, keys = {}, {}, {}, [] for male in (True, False): for birth in QUALIFYING_AGE_BIRTHS: @@ -487,10 +489,12 @@ def qualifying_age_couples(): "age": ages, "months_since_last_birthday": months, "is_male": {year: male for year in QUALIFYING_AGE_YEARS}, + "is_claimant_or_partner": {year: True for year in QUALIFYING_AGE_YEARS}, } people[partner] = { "age": {year: 40 for year in QUALIFYING_AGE_YEARS}, "is_male": {year: not male for year in QUALIFYING_AGE_YEARS}, + "is_claimant_or_partner": {year: True for year in QUALIFYING_AGE_YEARS}, } benunits[f"b_{elder}"] = {"members": [elder, partner]} households[f"h_{elder}"] = {"members": [elder, partner]} diff --git a/policyengine_uk/variables/household/demographic/is_claimant_or_partner.py b/policyengine_uk/variables/household/demographic/is_claimant_or_partner.py index bc37290872..8163855ec5 100644 --- a/policyengine_uk/variables/household/demographic/is_claimant_or_partner.py +++ b/policyengine_uk/variables/household/demographic/is_claimant_or_partner.py @@ -22,21 +22,28 @@ class is_claimant_or_partner(Variable): parent (`is_parent`) if there is one, otherwise the eldest other adult who is not presumed to be the claimant's child. A member at least 16 years younger than the claimant is presumed to be their child if they - are under 20 (a PolicyEngine presumption for households entered without - relationships). + are under 20, and a member at least 20 years younger is presumed their + child at any age (PolicyEngine presumptions for households entered + without relationships). - The under-20 limit is lifted when the claimant is flagged as a parent and no member under 20 and at least 16 years younger is there to explain the flag. So a lone parent flagged `is_parent` who is the claimant and lives only with an unflagged son or daughter at least 16 years younger is single, and the son or daughter is neither claimant nor partner. - When a child under 20 does explain the flag, the limit stays. A flagged - claimant with an unflagged adult 16 or more years younger and a younger + claimant with an unflagged adult 16 to 19 years younger and a younger child is read as a couple with that child, and a lone parent with both - an adult child and a younger child is read the same way. - - Without any parent flag, a member aged 20 or over is never presumed a - child. Survey benefit units carry no flags for childless couples, and - those include large age gaps, so the model cannot tell such a couple - from a parent and adult child. + an adult child 16 to 19 years younger and a younger child is read the + same way. + - Without any parent flag, a member aged 20 or over is presumed a child + only when at least 20 years younger. In the Family Resources Survey, + co-resident adults 20 or more years apart are mostly in separate benefit + units, while those 16 to 19 years apart are mostly couples. A couple 20 + or more years apart entered without roles, whose elder is the claimant + (the head, or the eldest adult when no adult head is given), is + therefore assessed as a single claimant unless the younger is flagged as + a parent; supply `is_claimant_or_partner` for such a couple. Survey + datasets can supply it from the survey's own benefit units. - Flags follow the Family Resources Survey convention: in a benefit unit with children, both members of the couple are flagged as parents. When relationships are known, supply this variable for everyone (true for @@ -89,12 +96,16 @@ def formula(person, period, parameters): parent_couple = other_parent & ( person.get_rank(person.benunit, -age, condition=other_parent) < 2 ) - large_gap = claimant_age - age >= p.minimum_age_gap + gap = claimant_age - age + large_gap = gap >= p.minimum_age_gap young_child = (age < p.age_limit) & large_gap # A flagged claimant with no young child to explain the flag is the - # parent of a much younger member at any age. + # parent of a much younger member at any age. Without that, only the + # larger any-age gap overrides the age limit. flag_unexplained = claimant_is_parent & ~person.benunit.any(young_child) - presumed_child = ((age < p.age_limit) | flag_unexplained) & large_gap + presumed_child = (((age < p.age_limit) | flag_unexplained) & large_gap) | ( + gap >= p.minimum_age_gap_at_any_age + ) other_adult = adult & ~claimant & ~presumed_child pool = where(person.benunit.any(other_parent), other_parent, other_adult) partner = pool & (person.get_rank(person.benunit, -age, condition=pool) == 0)