Skip to content
Merged
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/2390.fixed.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 5 additions & 1 deletion docs/book/validation/hbai.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`|
Expand All @@ -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)).
Expand Down
Original file line number Diff line number Diff line change
@@ -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
158 changes: 158 additions & 0 deletions policyengine_uk/tests/test_hbai_property_income_properties.py
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion policyengine_uk/variables/household/income/market_income.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading