diff --git a/changelog.d/2390.fixed.md b/changelog.d/2390.fixed.md new file mode 100644 index 0000000000..b47d3647e8 --- /dev/null +++ b/changelog.d/2390.fixed.md @@ -0,0 +1 @@ +- Count landlords' property income after mortgage interest (`property_income_after_finance_costs`) in `hbai_household_net_income`, `market_income` and `household_market_income`, as the Family Resources Survey records it (rent from property after the mortgage payments and loan interest on show card K6). They added `property_income`, the profit before finance costs, so finance costs raised HBAI income by the tax reduction they earn. Income tax and the means tests are unchanged. diff --git a/docs/book/validation/hbai.md b/docs/book/validation/hbai.md index 557ea35f81..1de2bbf680 100644 --- a/docs/book/validation/hbai.md +++ b/docs/book/validation/hbai.md @@ -26,7 +26,7 @@ The table below shows how the HBAI income components map to PolicyEngine variabl | Income received from dividends (from FYE 2022) |`dividend_income`| | State support - all benefits and tax credits |`hbai_benefits`| | Income from occupational and private pensions |`private_pension_income` (includes occupational pension income)| -| Investment income |`dividend_income`, `savings_interest_income`, `property_income`| +| Investment income |`dividend_income`, `savings_interest_income`, `property_income_after_finance_costs`| | Maintenance payments |`maintenance_income`| | Income from educational grants and scholarships (including, for students, student loans and parental contributions) |`student_payments`| | The cash value of certain forms of income in kind (free school meals, free school breakfast, free school milk, free school fruit and vegetables, Healthy Start vouchers and free TV licences for those aged 75 and over who receive Pension Credit) |`free_school_meals`, `free_school_fruit_veg`, `free_school_milk`, `healthy_start_vouchers`, `free_tv_licence_value`| @@ -45,6 +45,10 @@ The table below shows how the HBAI income components map to PolicyEngine variabl | Structural insurance premiums (for owner occupiers) | `structural_insurance_payments` | | Ground rent and service charges |`housing_service_charges`| +## Property income + +HBAI income counts a landlord's profit after mortgage interest. The Family Resources Survey asks for rent from property "AFTER paying for any of the things on this card", and show card K6 lists mortgage payments and interest on a loan to buy the property ([FRS 2023–24 question instructions](https://doc.ukdataservice.ac.uk/doc/9367/mrdoc/pdf/9367_frs_2023-24_question_instructions_final.pdf), question PropRent, page 302). PolicyEngine's `property_income` is the profit before those finance costs, because income tax no longer deducts them (ITTOIA 2005 s. 272A(4)) and relieves them with a tax reduction instead (s. 274A). So `hbai_household_net_income`, `market_income` and `household_market_income` count `property_income_after_finance_costs`: `property_income` less `property_finance_costs`, and nil where the costs exceed a positive profit. The tax reduction itself is in `income_tax`. Interest on a let property is not a housing cost; only the household's own `mortgage_interest_repayment` is deducted after housing costs. + ## Negative incomes HBAI resets a negative income before housing costs to zero: "Negative incomes BHC are reset to zero, but negative AHC incomes calculated from the adjusted BHC incomes are possible. Where incomes have been adjusted to zero BHC, income AHC is derived from the adjusted BHC income." ([HBAI Background Information and Methodology report FYE 2025](https://www.gov.uk/government/statistics/households-below-average-income-for-financial-years-ending-1995-to-2025/households-below-average-income-background-information-and-methodology-report-fye-2025#negative-incomes)). diff --git a/policyengine_uk/tests/policy/baseline/household/income/hbai_property_income.yaml b/policyengine_uk/tests/policy/baseline/household/income/hbai_property_income.yaml new file mode 100644 index 0000000000..d097928079 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/household/income/hbai_property_income.yaml @@ -0,0 +1,115 @@ +# HBAI income and market income count a landlord's profit after mortgage +# interest. The Family Resources Survey, which HBAI is built from, asks for +# rent from property "AFTER paying for any of the things on this card"; show +# card K6 lists mortgage payments and interest on a loan to buy the property +# (FRS 2023-24 question instructions, PropRent, p. 302). property_income is +# the profit before those costs: since 2020-21 they are not deducted for +# income tax (ITTOIA 2005 s. 272A(4)) and are relieved by a tax reduction at +# the basic rate instead (s. 274A). 2026-27: personal allowance £12,570, +# basic rate 20% on the first £37,700 of taxable income, employee NI 8% +# from £12,570 to £50,270. + +- name: A landlord's mortgage interest is deducted from market and HBAI income + period: 2026 + absolute_error_margin: 0.01 + input: + age: 40 + employment_income: 40_000 + property_income: 10_000 + property_finance_costs: 4_000 + output: + property_income_after_finance_costs: 6_000 + # 40,000 + 10,000 - 4,000. + market_income: 46_000 + household_market_income: 46_000 + # (50,000 - 12,570) x 20% = 7,486, less 20% x 4,000 = 800 (s. 274A). + income_tax: 6_686 + # (40,000 - 12,570) x 8%. + national_insurance: 2_194.40 + # 46,000 - 6,686 - 2,194.40. + hbai_household_net_income: 37_119.60 + +- name: Finance costs above the profit leave the landlord with the income of a non-landlord + period: 2026 + absolute_error_margin: 0.01 + input: + people: + landlord: + age: 40 + employment_income: 40_000 + property_income: 3_000 + property_finance_costs: 8_000 + employee: + age: 40 + employment_income: 40_000 + benunits: + benunit_1: + members: [landlord] + benunit_2: + members: [employee] + households: + household_1: + members: [landlord] + household_2: + members: [employee] + output: + # max(3,000 - 8,000, 0). + property_income_after_finance_costs: [0, 0] + market_income: [40_000, 40_000] + # Landlord: (43,000 - 12,570) x 20% = 6,086, less 20% x min(8,000, + # 3,000) = 600. Employee: (40,000 - 12,570) x 20%. + income_tax: [5_486, 5_486] + # 40,000 - 5,486 - 2,194.40 for both. + hbai_household_net_income: [32_319.60, 32_319.60] + +- name: Interest on the home is a housing cost and interest on a let property is not + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income: 40_000 + property_income: 10_000 + property_finance_costs: 4_000 + households: + household: + members: [person1] + mortgage_interest_repayment: 3_000 + output: + # As the first case: 46,000 - 6,686 - 2,194.40. + hbai_household_net_income: 37_119.60 + # Less the 3,000 of interest on the household's own mortgage only. + hbai_household_net_income_ahc: 34_119.60 + +- name: Joint landlords each count their share of the profit after interest + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income: 30_000 + property_income: 5_000 + property_finance_costs: 2_000 + person2: + age: 40 + employment_income: 30_000 + property_income: 5_000 + property_finance_costs: 2_000 + benunits: + benunit: + members: [person1, person2] + households: + household: + members: [person1, person2] + output: + # 30,000 + 5,000 - 2,000 each. + market_income: [33_000, 33_000] + household_market_income: 66_000 + # Each: (35,000 - 12,570) x 20% = 4,486, less 20% x 2,000 = 400. + income_tax: [4_086, 4_086] + # Each: (30,000 - 12,570) x 8%. + national_insurance: [1_394.40, 1_394.40] + # 66,000 - 2 x 4,086 - 2 x 1,394.40. + hbai_household_net_income: 55_039.20 diff --git a/policyengine_uk/tests/test_hbai_property_income_properties.py b/policyengine_uk/tests/test_hbai_property_income_properties.py new file mode 100644 index 0000000000..8c6f6c7020 --- /dev/null +++ b/policyengine_uk/tests/test_hbai_property_income_properties.py @@ -0,0 +1,158 @@ +"""Property tests for landlords' finance costs in household income (#2390). + +HBAI income and market income count a landlord's profit after the costs of +dwelling-related loans, as the Family Resources Survey records it: rent from +property after the mortgage payments and loan interest on show card K6. +Income tax does not deduct those costs (ITTOIA 2005 s. 272A(4)) and relieves +them by a reduction at the property basic rate instead (s. 274A). So finance +costs lower market income by the costs taken off the profit, and lower HBAI +income by those costs less the tax they save: they never raise it. + +Each generated landlord is simulated twice, without and with finance costs, +and the two are compared. +""" + +import numpy as np +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +pytestmark = pytest.mark.usefixtures("cloned_uk_tax_benefit_system") + +YEAR = 2027 +OTHER_EMPLOYMENT_INCOME = [0, 8_000, 13_000, 30_000, 60_000, 110_000, 200_000] +# Every third person is a Scottish taxpayer. +SCOTTISH = [i % 3 == 2 for i in range(7)] +CASES = 21 # Each case is simulated without and with its finance costs. +PEOPLE = 2 * CASES +# Benefits that can count property income after finance costs, and so can +# rise with them; HBAI counts each of them. +MEANS_TESTED_BENEFITS = [ + "universal_credit", + "housing_benefit", + "council_tax_benefit", + "income_support", + "pension_credit", + "working_tax_credit", + "child_tax_credit", + "esa_income", + "jsa_income", +] + +amount = st.one_of( + st.floats(min_value=0, max_value=2_500), + st.floats(min_value=0, max_value=60_000), + st.sampled_from([0.0, 1_000.0, 12_570.0]), +) +case = st.tuples( + st.one_of(amount, st.floats(min_value=-3_000, max_value=0)), # profit + amount, # finance costs + amount, # expenses + st.booleans(), # receipts known (profit + expenses) or unknown +) +cases = st.lists(case, min_size=CASES, max_size=CASES) + + +def person_index(i): + return i % CASES % len(OTHER_EMPLOYMENT_INCOME) + + +@pytest.fixture(scope="module") +def sim(): + people = { + f"person_{i}": { + "age": {YEAR: 40}, + "employment_income": {YEAR: OTHER_EMPLOYMENT_INCOME[person_index(i)]}, + "property_income": {YEAR: 0}, + "property_rental_income": {YEAR: 0}, + "property_finance_costs": {YEAR: 0}, + } + for i in range(PEOPLE) + } + households = { + f"household_{i}": { + "members": [f"person_{i}"], + "region": {YEAR: "SCOTLAND" if SCOTTISH[person_index(i)] else "LONDON"}, + } + for i in range(PEOPLE) + } + return Simulation( + situation={ + "people": people, + "benunits": { + f"benunit_{i}": {"members": [f"person_{i}"]} for i in range(PEOPLE) + }, + "households": households, + } + ) + + +def run(sim, drawn): + profit, costs, expenses, known = map(np.array, zip(*drawn)) + receipts = np.where(known, np.maximum(profit, 0) + expenses, 0) + sim.set_input("property_income", YEAR, np.concatenate([profit, profit])) + sim.set_input("property_rental_income", YEAR, np.concatenate([receipts] * 2)) + sim.set_input( + "property_finance_costs", YEAR, np.concatenate([np.zeros(CASES), costs]) + ) + sim.reset_calculations() + + def change(variable): + values = sim.calculate(variable, YEAR).astype(float) + return values[:CASES], values[CASES:] - values[:CASES] + + stored_profit, _ = change("property_income") + _, stored_costs = change("property_finance_costs") + income_tax, income_tax_change = change("income_tax") + benefits_change = sum(change(variable)[1] for variable in MEANS_TESTED_BENEFITS) + return dict( + # The costs taken off the profit: all of them up to a positive profit, + # and none from a loss. + deducted=np.minimum(stored_costs, np.maximum(stored_profit, 0)), + market_income=change("market_income")[1], + household_market_income=change("household_market_income")[1], + hbai=change("hbai_household_net_income_before_reset_to_zero")[1], + national_insurance=change("national_insurance")[1], + income_tax=income_tax, + income_tax_change=income_tax_change, + benefits=benefits_change, + rate=sim.tax_benefit_system.parameters( + f"{YEAR}-01-01" + ).gov.hmrc.income_tax.rates.property.basic, + ) + + +def tolerance(*values): + """A penny plus float32 rounding on the amounts involved.""" + return 0.01 + 1e-6 * sum(np.abs(v) for v in values) + + +SETTINGS = settings( + max_examples=30, + deadline=None, + suppress_health_check=[HealthCheck.too_slow], +) + + +@SETTINGS +@given(cases) +def test_finance_costs_lower_market_and_hbai_income(sim, drawn): + r = run(sim, drawn) + tax_saved = -r["income_tax_change"] + margin = tolerance(r["deducted"], r["income_tax"], r["hbai"]) + # Market income falls by the costs taken off the profit. + assert (np.abs(r["market_income"] + r["deducted"]) <= margin).all() + assert (np.abs(r["household_market_income"] + r["deducted"]) <= margin).all() + # The reduction saves at most the property basic rate on those costs, and + # finance costs never raise tax or change NI. + assert (tax_saved >= -margin).all() + assert (tax_saved <= r["rate"] * r["deducted"] + margin).all() + assert (np.abs(r["national_insurance"]) <= margin).all() + # HBAI income moves by the costs, the tax saved and any means-tested + # benefit that counts property income after the costs, and nothing else. + expected = -r["deducted"] + tax_saved + r["benefits"] + assert (np.abs(r["hbai"] - expected) <= margin).all() + # So, before any benefit response, finance costs never raise HBAI income. + assert (r["hbai"] - r["benefits"] <= margin).all() diff --git a/policyengine_uk/variables/household/income/hbai_household_net_income.py b/policyengine_uk/variables/household/income/hbai_household_net_income.py index 08c2d514af..ff1a8913f3 100644 --- a/policyengine_uk/variables/household/income/hbai_household_net_income.py +++ b/policyengine_uk/variables/household/income/hbai_household_net_income.py @@ -9,7 +9,10 @@ "savings_interest_income", "dividend_income", "miscellaneous_income", - "property_income", + # Landlords' profit after mortgage interest: HBAI uses FRS rent from + # property, reported after the mortgage payments and loan interest on + # show card K6. property_income is before those finance costs. + "property_income_after_finance_costs", "private_pension_income", "private_transfer_income", "maintenance_income", diff --git a/policyengine_uk/variables/household/income/household_market_income.py b/policyengine_uk/variables/household/income/household_market_income.py index ec622f987f..156abbf36b 100644 --- a/policyengine_uk/variables/household/income/household_market_income.py +++ b/policyengine_uk/variables/household/income/household_market_income.py @@ -9,7 +9,8 @@ "savings_interest_income", "dividend_income", "miscellaneous_income", - "property_income", + # Landlords' profit after mortgage interest, as in HBAI income. + "property_income_after_finance_costs", "private_pension_income", "private_transfer_income", "maintenance_income", diff --git a/policyengine_uk/variables/household/income/market_income.py b/policyengine_uk/variables/household/income/market_income.py index 6ba591808e..7fa92da969 100644 --- a/policyengine_uk/variables/household/income/market_income.py +++ b/policyengine_uk/variables/household/income/market_income.py @@ -18,7 +18,8 @@ def formula(person, period, parameters): "savings_interest_income", "dividend_income", "miscellaneous_income", - "property_income", + # Landlords' profit after mortgage interest, as in HBAI income. + "property_income_after_finance_costs", "private_pension_income", "private_transfer_income", "maintenance_income", diff --git a/policyengine_uk/variables/household/income/property_income_after_finance_costs.py b/policyengine_uk/variables/household/income/property_income_after_finance_costs.py index 1fbfc37134..820ba453bc 100644 --- a/policyengine_uk/variables/household/income/property_income_after_finance_costs.py +++ b/policyengine_uk/variables/household/income/property_income_after_finance_costs.py @@ -14,14 +14,23 @@ class property_income_after_finance_costs(Variable): "The legacy means tests also deduct the person's income tax, which " "the reduction lowers, so counting the profit before the costs would " "make finance costs raise the income they count. Costs above the " - "profit leave nil, not a loss set against other income." + "profit leave nil, not a loss set against other income. HBAI " + "household net income and market income count it too: the Family " + "Resources Survey records rent from property after mortgage payments " + "and interest on loans to buy the property (show card K6)." ) definition_period = YEAR unit = GBP - reference = dict( - title="The Tax Credits (Definition and Calculation of Income) Regulations 2002 reg. 11(1) and (2A)", - href="https://www.legislation.gov.uk/uksi/2002/2006/regulation/11", - ) + reference = [ + dict( + title="The Tax Credits (Definition and Calculation of Income) Regulations 2002 reg. 11(1) and (2A)", + href="https://www.legislation.gov.uk/uksi/2002/2006/regulation/11", + ), + dict( + title="Family Resources Survey 2023-24 question instructions, PropRent and show card K6 (p. 302)", + href="https://doc.ukdataservice.ac.uk/doc/9367/mrdoc/pdf/9367_frs_2023-24_question_instructions_final.pdf", + ), + ] def formula(person, period, parameters): profit = person("property_income", period)