Skip to content
Open
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
3 changes: 3 additions & 0 deletions changelog.d/pawhp-household-income.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- Count the Pension Age Winter Heating Payment (`pawhp`) in `household_benefits`, `hbai_household_net_income` and `hbai_benefits`, as the Winter Fuel Payment is. It was in `gov_spending` only, so Scottish pensioners' winter heating payment was missing from household net income and HBAI income from the 2024 qualifying week.
- Make `gov.contrib.policyengine.disable_simulated_benefits` run. `Simulation` now sets `tax_benefit_system.simulation`, through which structural reforms reach the simulation, and the reform no longer names reported amounts that do not exist (Attendance Allowance, DLA and PIP follow the reported award through their categories).
- Under `disable_simulated_benefits`, count the reported winter heating payment once: as `pawhp` for a household in Scotland in the years PAWHP is paid, and as `winter_fuel_allowance` otherwise.
103 changes: 71 additions & 32 deletions policyengine_uk/reforms/policyengine/disable_simulated_benefits.py
Original file line number Diff line number Diff line change
@@ -1,58 +1,97 @@
from policyengine_core.model_api import *

# Benefits set to the amount the survey reports, <benefit>_reported, in place
# of the simulated amount. Attendance Allowance, DLA and PIP are not listed:
# they are paid at the rate of the award category the survey reports
# (aa_category, dla_sc_category, dla_m_category, pip_dl_category and
# pip_m_category), so they already follow the reported award. The winter
# heating payments are set separately (see below).
BENEFITS = [
"afcs",
"bsp",
"carers_allowance",
"child_benefit",
"child_tax_credit",
"council_tax_benefit",
"esa_contrib",
"esa_income",
"housing_benefit",
"iidb",
"incapacity_benefit",
"income_support",
"jsa_contrib",
"jsa_income",
"pension_credit",
"sda",
"ssmg",
"state_pension",
"universal_credit",
"working_tax_credit",
]

YEARS_IN_FUTURE = 10


def disable_simulated_benefits(parameters, period):
if parameters(period).gov.contrib.policyengine.disable_simulated_benefits:

class DisableSimulatedBenefits(Reform):
def apply(self):
simulation = self.simulation
time_period = int(simulation.dataset.time_period)
years = range(time_period, time_period + YEARS_IN_FUTURE)

BENEFITS = [
"afcs",
"attendance_allowance",
"bsp",
"carers_allowance",
"child_benefit",
"child_tax_credit",
"council_tax_benefit",
"dla_m",
"dla_sc",
"esa_contrib",
"esa_income",
"housing_benefit",
"iidb",
"incapacity_benefit",
"income_support",
"jsa_contrib",
"jsa_income",
"pension_credit",
"pip_dl",
"pip_m",
"sda",
"ssmg",
"state_pension",
"universal_credit",
"winter_fuel_allowance",
"working_tax_credit",
]
time_period = simulation.dataset.time_period
YEARS_IN_FUTURE = 10
for variable in BENEFITS:
entity = simulation.tax_benefit_system.variables[
variable
].entity.key
reported_value = simulation.calculate(
variable + "_reported", time_period, map_to=entity
)
for year in range(time_period, time_period + YEARS_IN_FUTURE):
for year in years:
simulation.set_input(variable, year, reported_value)

if variable in ["child_tax_credit", "working_tax_credit"]:
# CTC and WTC have their own pre_minimum variables because tax credits aren't paid if
# below a threshold.
variable = variable + "_pre_minimum"
for year in range(time_period, time_period + YEARS_IN_FUTURE):
for year in years:
simulation.set_input(variable, year, reported_value)

# The survey reports one winter heating payment: FRS benefit
# code 62, which policyengine-uk-data loads into
# winter_fuel_allowance_reported for every respondent,
# Scotland included. Scotland's payment has been the Pension
# Age Winter Heating Payment since the 2024 qualifying week,
# so in each year a Scottish household's report becomes its
# pawhp while PAWHP is paid, and its winter_fuel_allowance
# before then. Both variables are household income, so each
# report is counted once, under the scheme that pays it.
reported_winter_heating = np.asarray(
simulation.calculate(
"winter_fuel_allowance_reported",
time_period,
map_to="household",
)
)
in_scotland = (
np.asarray(simulation.calculate("country", time_period))
== "SCOTLAND"
)
for year in years:
paid_as_pawhp = (
in_scotland
& parameters(year).gov.social_security_scotland.pawhp.active
)
simulation.set_input(
"winter_fuel_allowance",
year,
np.where(paid_as_pawhp, 0, reported_winter_heating),
)
simulation.set_input(
"pawhp",
year,
np.where(paid_as_pawhp, reported_winter_heating, 0),
)

return DisableSimulatedBenefits
7 changes: 7 additions & 0 deletions policyengine_uk/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,13 @@ def __init__(

self.tax_benefit_system.reset_parameter_caches()

# Core applies a reform by calling its apply() with the tax-benefit
# system as self, so reforms that change the data (such as
# disable_simulated_benefits) reach the simulation through
# tax_benefit_system.simulation. Core's Simulation.__init__ sets it;
# this __init__ replaces core's, so it sets it too.
self.tax_benefit_system.simulation = self

# Apply structural reforms based on parameters
structural_reform = create_structural_reforms_from_parameters(
self.tax_benefit_system.parameters,
Expand Down
63 changes: 63 additions & 0 deletions policyengine_uk/tests/code_health/test_household_income_lists.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Government spending on households is household income.

gov_spending counted the Pension Age Winter Heating Payment (pawhp), but
household_benefits and the HBAI income lists did not: when the Winter Fuel
Payment stopped covering Scotland, Scottish pensioners' winter heating
payment fell out of household net income and HBAI income. These tests tie
the lists together so a payment added to one is not left out of the others.
"""

from policyengine_uk import CountryTaxBenefitSystem
from policyengine_uk.variables.gov.gov_spending import GOV_SPENDING_VARIABLES
from policyengine_uk.variables.household.income.hbai_household_net_income import (
HBAI_HOUSEHOLD_NET_INCOME_ADDS,
)
from policyengine_uk.variables.household.income.household_benefits import (
HOUSEHOLD_BENEFIT_VARIABLES,
)

SYSTEM = CountryTaxBenefitSystem()

# Spending in gov_spending that is not a benefit received by the household.
NOT_HOUSEHOLD_BENEFITS = {
"other_public_spending_budget_change": (
"the contrib lever gov.contrib.policyengine.budget.other_public_spending, "
"spread across households by income decile; zero unless set"
),
}

# The winter heating payment a pensioner household receives: the Winter Fuel
# Payment, or in Scotland from the 2024 qualifying week the Pension Age
# Winter Heating Payment.
WINTER_HEATING_PAYMENTS = ["winter_fuel_allowance", "pawhp"]


def test_government_spending_on_households_is_household_income():
outside = set(GOV_SPENDING_VARIABLES) - set(HOUSEHOLD_BENEFIT_VARIABLES)
assert outside == set(NOT_HOUSEHOLD_BENEFITS), (
"gov_spending counts these but household_benefits does not: "
f"{sorted(outside - set(NOT_HOUSEHOLD_BENEFITS))}; listed as not "
"household benefits but now in household_benefits: "
f"{sorted(set(NOT_HOUSEHOLD_BENEFITS) - outside)}"
)


def test_winter_heating_payments_are_counted_in_every_income_list():
lists = {
"HOUSEHOLD_BENEFIT_VARIABLES": HOUSEHOLD_BENEFIT_VARIABLES,
"HBAI_HOUSEHOLD_NET_INCOME_ADDS": HBAI_HOUSEHOLD_NET_INCOME_ADDS,
"hbai_benefits.adds": SYSTEM.variables["hbai_benefits"].adds,
"GOV_SPENDING_VARIABLES": GOV_SPENDING_VARIABLES,
}
missing = {
name: [payment for payment in WINTER_HEATING_PAYMENTS if payment not in names]
for name, names in lists.items()
}
assert not any(missing.values()), missing
repeated = {
name: [
payment for payment in WINTER_HEATING_PAYMENTS if names.count(payment) > 1
]
for name, names in lists.items()
}
assert not any(repeated.values()), repeated
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# Winter heating payments in household income. Scotland's Pension Age Winter
# Heating Payment (pawhp) replaced the Winter Fuel Payment
# (winter_fuel_allowance) from the 2024 qualifying week, so it counts in
# household_benefits, household_net_income, hbai_household_net_income and
# hbai_benefits exactly as the Winter Fuel Payment does.
#
# Each household's other income is pinned: a State Pension entered directly,
# Pension Credit entered on the benefit unit, and no council tax or TV
# licence. The margin of 10p covers the expected Stamp Duty Land Tax that
# household_net_income subtracts (a few pence for these households).
# Payment amounts: SI 2025/969 reg. 3 (Winter Fuel Payment, 2025); SSI
# 2024/351 reg. 10 as made (PAWHP, 2024) and as substituted by SSI 2025/282
# (2025), with the 2026 amounts from SSI 2026/170.

- name: 2025 a Scottish pensioner's PAWHP counts in household and HBAI income
# Reg. 10(5)(a): under 80, living alone, £203.40.
period: 2025
absolute_error_margin: 0.1
input:
people:
pensioner:
age: 70
state_pension_reported: 12_000
benunits:
unit:
members: [pensioner]
pension_credit: 0
households:
household:
members: [pensioner]
country: SCOTLAND
council_tax: 0
tv_licence: 0
output:
winter_fuel_allowance: 0
pawhp: 203.40
household_benefits: 12_203.40
household_net_income: 12_203.40
hbai_benefits: 12_203.40
hbai_household_net_income: 12_203.40

- name: 2025 the same pensioner in England has the Winter Fuel Payment counted the same way
# SI 2025/969 reg. 3(1): under 80, living alone, £200.
period: 2025
absolute_error_margin: 0.1
input:
people:
pensioner:
age: 70
state_pension_reported: 12_000
benunits:
unit:
members: [pensioner]
pension_credit: 0
households:
household:
members: [pensioner]
country: ENGLAND
council_tax: 0
tv_licence: 0
output:
winter_fuel_allowance: 200
pawhp: 0
household_benefits: 12_200
household_net_income: 12_200
hbai_benefits: 12_200
hbai_household_net_income: 12_200

- name: 2026 a Scottish couple under 80 not on a relevant benefit has both shared payments counted
# Reg. 10(5)(b)(i) at the SSI 2026/170 amount: £105.55 each. Neither
# State Pension reaches the personal allowance, so no income tax.
period: 2026
absolute_error_margin: 0.1
input:
people:
partner_1:
age: 70
state_pension_reported: 12_000
partner_2:
age: 72
state_pension_reported: 12_000
benunits:
couple:
members: [partner_1, partner_2]
pension_credit: 0
households:
household:
members: [partner_1, partner_2]
country: SCOTLAND
council_tax: 0
tv_licence: 0
output:
pension_age_winter_heating_payment: [105.55, 105.55]
pawhp: 211.10
household_benefits: 24_211.10
household_net_income: 24_211.10
hbai_benefits: 24_211.10
hbai_household_net_income: 24_211.10

- name: 2024 a Scottish pensioner on Pension Credit has PAWHP counted with the Pension Credit
# Reg. 10(c) as made: under 80 and entitled to a relevant benefit, £200.
period: 2024
absolute_error_margin: 0.1
input:
people:
pensioner:
age: 70
state_pension_reported: 10_000
benunits:
unit:
members: [pensioner]
pension_credit: 1_000
households:
household:
members: [pensioner]
country: SCOTLAND
council_tax: 0
tv_licence: 0
output:
winter_fuel_allowance: 0
pawhp: 200
cost_of_living_support_payment: 0
household_benefits: 11_200
household_net_income: 11_200
hbai_benefits: 11_200
hbai_household_net_income: 11_200

- name: 2023 a Scottish pensioner keeps the Winter Fuel Payment and the pensioner Cost-of-Living Payment
# SI 2000/729 covered Great Britain up to the 2023 qualifying week, so the
# payment is the Winter Fuel Payment (£200 under 80) and there is no PAWHP.
# The £300 Pensioner Cost-of-Living Payment is keyed on the Winter Fuel
# Payment, so counting PAWHP as income leaves it unchanged.
period: 2023
absolute_error_margin: 0.1
input:
people:
pensioner:
age: 70
state_pension_reported: 12_000
benunits:
unit:
members: [pensioner]
pension_credit: 0
households:
household:
members: [pensioner]
country: SCOTLAND
council_tax: 0
tv_licence: 0
output:
winter_fuel_allowance: 200
pawhp: 0
cost_of_living_support_payment: 300
Loading
Loading