From a9e3371bcb3f3eef76b6ad6aca4a76b10025780f Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 1 Oct 2026 11:58:51 -0400 Subject: [PATCH 1/2] Mark the benefit units that share their household's rent In FRS shared households (HHSTAT 2), later benefit units report the rent they pay (SRENTAMT) and their housing benefit (HBOTHAMT); HHRENT is the rent of the whole dwelling. Store liable_for_share_of_household_rent on those units so policyengine-uk can split the household's rent among the people liable for it. The FRS's own BURENT split is not used: in shared households it sums to about two and a half times HHRENT. Co-Authored-By: Claude Opus 5.5 --- policyengine_uk_data/datasets/frs.py | 34 +++++++++ .../tests/test_frs_shared_household_rent.py | 76 +++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 policyengine_uk_data/tests/test_frs_shared_household_rent.py diff --git a/policyengine_uk_data/datasets/frs.py b/policyengine_uk_data/datasets/frs.py index 7e33f440..3309d807 100644 --- a/policyengine_uk_data/datasets/frs.py +++ b/policyengine_uk_data/datasets/frs.py @@ -275,6 +275,37 @@ def frs_boarder_and_lodger_rent(person: pd.DataFrame) -> tuple[np.ndarray, np.nd return rent_paid * is_boarder, rent_paid * ~is_boarder +def frs_liable_for_share_of_household_rent( + benunit: pd.DataFrame, person: pd.DataFrame, household: pd.DataFrame +) -> np.ndarray: + """Whether each benefit unit shares liability for its household's rent. + + In a shared household (HHSTAT 2: "shared on an equal basis", the head of + household unclear or arbitrary), each benefit unit after the first is + asked how much rent it pays (SRENTAMT, on its adults' records) and how + much housing benefit it gets (HBOTHAMT). HHRENT is the rent of the whole + dwelling, built from benefit unit 1's rent plus the others' SRENTAMT and + HBOTHAMT, so a later unit with either amount positive is one of the + people liable for HHRENT. policyengine-uk splits ``rent`` among them. + + The FRS's own split (BURENT) is not used: in shared households of the + 2023-24 and 2024-25 releases it gives the later units all of HHRENT + between them and benefit unit 1 a further amount, so the units' BURENT + sum to about two and a half times HHRENT. + """ + unit_number = benunit.benunit_id.values % 100 + shared = household.hhstat.reindex(benunit.household_id.values).values == 2 + srent = ( + person.srentamt.where(person.srentamt > 0, 0) + .groupby(person.benunit_id.values) + .sum() + .reindex(benunit.benunit_id.values, fill_value=0) + .values + ) + hb = np.maximum(0, benunit.hbothamt.fillna(0).values) + return (unit_number > 1) & shared & ((srent > 0) | (hb > 0)) + + def derive_is_in_non_advanced_education( current_education, is_apprentice=None, @@ -1320,6 +1351,9 @@ def determine_education_level(fted_val, typeed2_val, age_val): * WEEKS_IN_YEAR ) pe_household["rent"] = household.hhrent.fillna(0).values * WEEKS_IN_YEAR + pe_benunit["liable_for_share_of_household_rent"] = ( + frs_liable_for_share_of_household_rent(benunit, person, household) + ) pe_household["mortgage_interest_repayment"] = ( household.mortint.fillna(0).values * WEEKS_IN_YEAR ) diff --git a/policyengine_uk_data/tests/test_frs_shared_household_rent.py b/policyengine_uk_data/tests/test_frs_shared_household_rent.py new file mode 100644 index 00000000..6d557d72 --- /dev/null +++ b/policyengine_uk_data/tests/test_frs_shared_household_rent.py @@ -0,0 +1,76 @@ +import numpy as np +import pandas as pd +import pytest + +from policyengine_uk_data.datasets.frs import frs_liable_for_share_of_household_rent + +CONVENTIONAL, SHARED = 1, 2 + + +def frames(units, adults, hhstat): + benunit = pd.DataFrame(units, columns=["benunit_id", "household_id", "hbothamt"]) + person = pd.DataFrame(adults, columns=["person_id", "benunit_id", "srentamt"]) + household = pd.DataFrame( + {"hhstat": list(hhstat.values())}, index=list(hhstat.keys()) + ) + return benunit, person, household + + +def test_later_units_of_a_shared_household_that_pay_rent_are_liable(): + benunit, person, household = frames( + [(1_01, 1, 0), (1_02, 1, 0), (1_03, 1, 0)], + [(1_001, 1_01, 0), (1_002, 1_02, 120), (1_003, 1_03, 110)], + {1: SHARED}, + ) + liable = frs_liable_for_share_of_household_rent(benunit, person, household) + # Benefit unit 1 is the household head's, liable for the rent anyway. + assert liable.tolist() == [False, True, True] + + +def test_housing_benefit_alone_marks_a_unit_liable(): + benunit, person, household = frames( + [(1_01, 1, 0), (1_02, 1, 85)], + [(1_001, 1_01, 0), (1_002, 1_02, 0)], + {1: SHARED}, + ) + liable = frs_liable_for_share_of_household_rent(benunit, person, household) + assert liable.tolist() == [False, True] + + +@pytest.mark.parametrize("srentamt", [0, -1, np.nan]) +def test_a_later_unit_paying_nothing_is_not_liable(srentamt): + benunit, person, household = frames( + [(1_01, 1, 0), (1_02, 1, np.nan)], + [(1_001, 1_01, 0), (1_002, 1_02, srentamt)], + {1: SHARED}, + ) + liable = frs_liable_for_share_of_household_rent(benunit, person, household) + assert liable.tolist() == [False, False] + + +def test_units_of_conventional_households_are_never_sharers(): + benunit, person, household = frames( + [(1_01, 1, 0), (1_02, 1, 50), (2_01, 2, 0), (2_02, 2, 0)], + [(1_001, 1_01, 0), (1_002, 1_02, 0), (2_001, 2_01, 0), (2_002, 2_02, 90)], + {1: CONVENTIONAL, 2: CONVENTIONAL}, + ) + liable = frs_liable_for_share_of_household_rent(benunit, person, household) + assert not liable.any() + + +def test_a_couple_counts_once_and_households_do_not_leak(): + # A couple in unit 2 of a shared household reports SRENTAMT on one + # partner's record; the next household is conventional. + benunit, person, household = frames( + [(1_01, 1, 0), (1_02, 1, 0), (2_01, 2, 0), (2_02, 2, 0)], + [ + (1_001, 1_01, 0), + (1_002, 1_02, 200), + (1_003, 1_02, 0), + (2_001, 2_01, 0), + (2_002, 2_02, 0), + ], + {1: SHARED, 2: CONVENTIONAL}, + ) + liable = frs_liable_for_share_of_household_rent(benunit, person, household) + assert liable.tolist() == [False, True, False, False] From 479dd3eaaeb22aeee897891a2953f000ee0e219f Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 1 Oct 2026 12:02:44 -0400 Subject: [PATCH 2/2] Changelog for the shared-household rent flag Co-Authored-By: Claude Opus 5.5 --- changelog.d/frs-shared-household-rent.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/frs-shared-household-rent.added.md diff --git a/changelog.d/frs-shared-household-rent.added.md b/changelog.d/frs-shared-household-rent.added.md new file mode 100644 index 00000000..998a5ef7 --- /dev/null +++ b/changelog.d/frs-shared-household-rent.added.md @@ -0,0 +1 @@ +Mark the benefit units of FRS shared households that pay part of the rent (liable_for_share_of_household_rent), so policyengine-uk can split the household's rent among them.