Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/frs-shared-household-rent.added.md
Original file line number Diff line number Diff line change
@@ -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.
34 changes: 34 additions & 0 deletions policyengine_uk_data/datasets/frs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
)
Expand Down
76 changes: 76 additions & 0 deletions policyengine_uk_data/tests/test_frs_shared_household_rent.py
Original file line number Diff line number Diff line change
@@ -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]