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
1 change: 1 addition & 0 deletions changelog.d/1862-savings-interest-per-person.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Project `savings_interest_income` and `non_cash_isa_cash_interest` (and carried-forward ISA interest totals) by households' interest received per person: ONS HAXV outturn to 2025 (-2.76% in 2025, not the +3.49% non-labour income proxy), then OBR household financial assets growth times the Table 1.9 deposit rate ratio, net of `ons.population` growth so that growing household weights do not count population twice. The index twin is anchored at 2020, so direct index uprating from 2020 to 2021 includes the 2021 growth, as dataset projection does.
8 changes: 7 additions & 1 deletion docs/book/assumptions/growthfactors.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,13 @@ We derive these rates as calendar-year nominal GDP growth from the OBR's October
|-------------|------|------|------|------|------|------|------|------|------|------|
| Growth rate | 10.0% | 5.2% | 4.0% | 4.6% | 4.0% | 3.1% | 3.4% | 3.6% | 3.4% | 3.3% |

We use per capita GDP for these variables: `benunit_reported_capital`, `capital_gains`, `capital_gains_badr`, `capital_gains_before_response`, `capital_gains_carried_interest`, `capital_gains_residential_property`, `cash_isa`, `cash_isa_balance`, `corporate_wealth`, `directly_held_shares`, `dividend_income`, `gross_financial_wealth`, `household_lifetime_isa_balance`, `lifetime_isa_balance`, `lump_sum_income`, `main_residence_value`, `maintenance_income`, `miscellaneous_income`, `mortgage_capital_repayment`, `net_financial_wealth`, `non_residential_property_value`, `other_investment_income`, `other_residential_property_value`, `owned_land`, `pension_credit_reported_capital`, `pension_income`, `private_pension_wealth`, `private_transfer_income`, `property_income`, `property_rental_income`, `savings`, `savings_interest_income`, `spousal_maintenance_income`, `stocks_and_shares_isa`, `stocks_and_shares_isa_balance`, `sublet_income`, `uc_reported_capital`, `unit_and_investment_trusts`
We use per capita GDP for these variables: `benunit_reported_capital`, `capital_gains`, `capital_gains_badr`, `capital_gains_before_response`, `capital_gains_carried_interest`, `capital_gains_residential_property`, `cash_isa`, `cash_isa_balance`, `corporate_wealth`, `directly_held_shares`, `dividend_income`, `gross_financial_wealth`, `household_lifetime_isa_balance`, `lifetime_isa_balance`, `lump_sum_income`, `main_residence_value`, `maintenance_income`, `miscellaneous_income`, `mortgage_capital_repayment`, `net_financial_wealth`, `non_residential_property_value`, `other_investment_income`, `other_residential_property_value`, `owned_land`, `pension_credit_reported_capital`, `pension_income`, `private_pension_wealth`, `private_transfer_income`, `property_income`, `property_rental_income`, `savings`, `spousal_maintenance_income`, `stocks_and_shares_isa`, `stocks_and_shares_isa_balance`, `sublet_income`, `uc_reported_capital`, `unit_and_investment_trusts`

## Interest received per person

Households' interest received is ONS series HAXV (D.41) to 2025. From 2026 the OBR's March 2026 EFO gives household financial assets (Table 1.11) and the deposit rate (Table 1.9), and interest grows by (1 + asset growth) × (this year's rate / last year's) − 1. Household weights grow with population, so each person's interest grows by (1 + interest growth) / (1 + population growth) − 1.

We apply this to: `non_cash_isa_cash_interest`, `savings_interest_income`

## Council tax

Expand Down
18 changes: 9 additions & 9 deletions docs/book/assumptions/isa-inputs-and-capital-flows.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,15 @@ income a second time.

For household inputs carried into later years, ISA dividends use the same
nominal GDP-per-capita index as ordinary `dividend_income`.
`non_cash_isa_cash_interest` uses the same ONS household-interest-income index
as ordinary `savings_interest_income`. An explicit observation for a later
year takes priority and provides the starting amount for subsequent years.
Dataset projections grow stored ISA dividends and non-cash ISA cash interest
by nominal GDP per capita, matching the dataset rules for ordinary dividends
and savings interest. This also applies before 2031. The non-cash interest
detail therefore follows the existing dataset interest rule, while direct
household inputs use the ONS index above. Alignment of the interest indices
is tracked separately in [#2638](https://github.com/PolicyEngine/policyengine-uk/pull/2638).
`non_cash_isa_cash_interest` and an ISA-interest total carried forward under
the additive contract use the same index as ordinary `savings_interest_income`:
households' interest received per person
(`ons.per_capita.household_interest_income`, see
[#2638](https://github.com/PolicyEngine/policyengine-uk/pull/2638)). An
explicit observation for a later year takes priority and provides the starting
amount for subsequent years. Dataset projections grow stored ISA dividends by
nominal GDP per capita, as for ordinary dividends, and stored non-cash ISA cash
interest by the per-person interest index, as for ordinary savings interest.
The legacy and additive interest contracts retain their recorded conventions.

From 6 April 2027, `isa_cash_interest_flat_rate_charge` applies the savings
Expand Down
4 changes: 2 additions & 2 deletions docs/engineering/skills/dataset-sources.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ and `additive`/`legacy_gross` for normalized legacy data. Representation and
projection origin are separate: legacy projection grows the original gross
amount before subtracting the effective exemption. It must not grow the
already-normalized ordinary amount instead. New additive dataset projections
grow ISA interest alongside ordinary interest by the dataset's GDP-per-capita
rule; outside the stored horizon new inputs use their variable's own index.
grow ISA interest alongside ordinary interest by the per-person interest
series (`yoy_growth.ons.per_capita.household_interest_income`); outside the stored horizon new inputs use their variable's own index.
Legacy-origin fallback retains the old gross-index calculation and unindexed
ISA carry-over, while explicit inputs and applicable ISA detail formulas take
priority. Legacy files containing only an aggregate exemption still have that
Expand Down
19 changes: 11 additions & 8 deletions policyengine_uk/data/economic_assumptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@
# below threshold.
_GRADUATE_LOAN_TAKE_UP = 0.4

# Households' interest received per person (#1862). Gross legacy interest and
# additive ISA interest totals grow by it, as savings_interest_income does.
INTEREST_GROWTH = (
"gov.economic_assumptions.yoy_growth.ons.per_capita.household_interest_income"
)

_ENGLAND_REGIONS = {
"NORTH_EAST",
"NORTH_WEST",
Expand Down Expand Up @@ -374,23 +380,20 @@ def apply_single_year_uprating(
current_year.isa_income_contract["projection"] == "legacy_gross"
and SAVINGS_INTEREST in current_year.person
):
growth = parameters.get_child(
"gov.economic_assumptions.yoy_growth.obr.per_capita.gdp"
)(current_year.time_period)
growth = parameters.get_child(INTEREST_GROWTH)(current_year.time_period)
values = gross_interest(previous_year) * (1 + growth)
if current_year.isa_income_contract["representation"] == "additive":
values = values - effective_tax_free(current_year.person)
current_year.person[SAVINGS_INTEREST] = values

# New additive ISA receipts use the same dataset GDP rule as ordinary
# interest. Legacy receipts retain their former unindexed treatment.
# New additive ISA receipts use the same per-person interest index as
# ordinary interest. Legacy receipts retain their former unindexed
# treatment.
if (
current_year.isa_income_contract["projection"] == "additive"
and ISA_INTEREST in current_year.person
):
growth = parameters.get_child(
"gov.economic_assumptions.yoy_growth.obr.per_capita.gdp"
)(current_year.time_period)
growth = parameters.get_child(INTEREST_GROWTH)(current_year.time_period)
current_year.person[ISA_INTEREST] = previous_year.person[ISA_INTEREST] * (
1 + growth
)
Expand Down
2 changes: 1 addition & 1 deletion policyengine_uk/data/isa_income.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def isa_interest_income_default(simulation, period):
):
return values
index = simulation.tax_benefit_system.parameters.get_child(
"gov.economic_assumptions.indices.ons.household_interest_income"
"gov.economic_assumptions.indices.ons.per_capita.household_interest_income"
)
source_index = _uprating_index_value(index, source.start)
target_index = _uprating_index_value(index, period.start)
Expand Down
7 changes: 5 additions & 2 deletions policyengine_uk/data/uprating_indices.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,6 @@ gov.economic_assumptions.yoy_growth.obr.per_capita.gdp:
- property_income
- property_rental_income
- savings
- savings_interest_income
- non_cash_isa_cash_interest
- spousal_maintenance_income
- stocks_and_shares_isa
- stocks_and_shares_isa_balance
Expand All @@ -164,6 +162,11 @@ gov.economic_assumptions.yoy_growth.obr.private_pension_index:
- private_pension_income
gov.economic_assumptions.yoy_growth.obr.lagged_average_earnings:
- housing_service_charges
# Interest received per person, as household weights grow by population
# (#1862). ISA cash interest in non-cash ISAs follows the same index.
gov.economic_assumptions.yoy_growth.ons.per_capita.household_interest_income:
- non_cash_isa_cash_interest
- savings_interest_income
# ORR passenger journeys per person, as household weights grow by population
# (#1862).
gov.economic_assumptions.yoy_growth.orr.per_capita.rail_passenger_journeys:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,20 @@ def create_economic_assumption_indices(
)
else:
start_year = int(descendant.values_list[-1].instant_str[:4])
# By default the first growth year is the index base, so its
# growth is not in the index. A series can set
# `index_base_year` to an earlier year to anchor the index there
# instead, so the first growth step is included. The anchor is
# an index level of 1, not a growth observation.
base_year = (descendant.metadata or {}).get("index_base_year")
if base_year is None:
base_year = start_year
elif base_year >= start_year:
raise ValueError(
f"{full_name}: index_base_year {base_year} must be before "
f"the first growth year {start_year}."
)
start_year = base_year
values = {start_year: 1.0}

for year in range(start_year + 1, 2040):
Expand Down
67 changes: 54 additions & 13 deletions policyengine_uk/parameters/gov/economic_assumptions/yoy_growth.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1154,33 +1154,74 @@ ons:
href: https://obr.uk/supplementary-forecast-information-release-long-term-economic-determinants-march-2026/
- title: ONS, "National population projections - 2024-based" (28 April 2026; principal projection, used by the OBR from 2031-32)
href: https://www.ons.gov.uk/peoplepopulationandcommunity/populationandmigration/populationprojections/bulletins/nationalpopulationprojections/2024based
# Households' (S.14) interest received, D.41 resources, ONS series HAXV
# (UK Economic Accounts, release of 30 September 2026), calendar years in
# £m. D.41 is interest after the FISIM adjustment, not the unadjusted
# D.41g. Each value is growth over the year before, rounded to 4 decimals.
#
# The OBR does not forecast household interest received. From 2026 the
# series is interest = stock x rate: (1 + growth in household financial
# assets) x (this year's deposit rate / last year's) - 1, both from the OBR
# March 2026 EFO economy tables, calendar-year rows. Financial assets
# (Table 1.11, ONS NNML) are the only household asset stock the OBR
# forecasts; they include pensions and equities, so they stand in for
# deposits. The deposit rate is Table 1.9's, in per cent.
#
# This is an aggregate. Dataset projection grows household weights by
# ons.population, so each person's interest grows by
# per_capita.household_interest_income instead (#1862).
household_interest_income:
description: Household interest income year-on-year growth from ONS National Accounts D.41g.
description: Growth in households' total interest received (ONS HAXV, D.41).
values:
# Outturn (2020-2025), ONS HAXV, total household interest received
# (D.41), £m, 30 September 2026 release
# Outturn (2020-2025), HAXV £m
2020-01-01: -0.3025 # 15,988 / 22,923 - 1
2021-01-01: 0.2238 # 19,566 / 15,988 - 1
2022-01-01: 1.2121 # 43,282 / 19,566 - 1
2023-01-01: 0.9891 # 86,092 / 43,282 - 1
2024-01-01: 0.1405 # 98,189 / 86,092 - 1
2025-01-01: -0.0276 # 95,477 / 98,189 - 1
# Forecast (2026-2031): OBR non-labour income growth as a proxy
# (October 2026 EFO, Table 1.12)
2026-01-01: 0.0302
2027-01-01: 0.0468
2028-01-01: 0.0419
2029-01-01: 0.0409
2030-01-01: 0.0382
2031-01-01: 0.0332
# Forecast (2026-2030): (assets_t / assets_t-1) x (rate_t / rate_t-1) - 1
2026-01-01: -0.0937 # (7,109.48 / 6,858.72) x (1.9997 / 2.2870) - 1
2027-01-01: 0.0653 # (7,287.74 / 7,109.48) x (2.0782 / 1.9997) - 1
2028-01-01: 0.1123 # (7,460.84 / 7,287.74) x (2.2579 / 2.0782) - 1
2029-01-01: 0.0933 # (7,619.20 / 7,460.84) x (2.4172 / 2.2579) - 1
2030-01-01: 0.0843 # (7,768.51 / 7,619.20) x (2.5706 / 2.4172) - 1
metadata:
unit: /1
label: Household interest income growth
reference:
- title: ONS, Households (S.14) interest (D.41) resources, current prices, £m, NSA (HAXV), UK Economic Accounts, 30 September 2026
href: https://www.ons.gov.uk/economy/grossdomesticproductgdp/timeseries/haxv/ukea
- title: OBR EFO October 2026 - Non-labour income growth used as proxy for 2026+ (Table 1.12)
href: https://obr.uk/efo/economic-and-fiscal-outlook-october-2026/
- title: OBR EFO March 2026, detailed forecast tables, economy, Table 1.9 (deposit rate) and Table 1.11 (household financial assets)
href: https://obr.uk/efo/economic-and-fiscal-outlook-march-2026/
per_capita:
household_interest_income:
description: Growth in households' interest received per person.
values:
# (1 + household_interest_income) / (1 + population) - 1, both above
2021-01-01: 0.2194 # 1.2238 / 1.0036 - 1
2022-01-01: 1.1891 # 2.2121 / 1.0105 - 1
2023-01-01: 0.9628 # 1.9891 / 1.0134 - 1
2024-01-01: 0.1295 # 1.1405 / 1.0097 - 1
2025-01-01: -0.0308 # 0.9724 / 1.0033 - 1
2026-01-01: -0.0955 # 0.9063 / 1.0020 - 1
2027-01-01: 0.0639 # 1.0653 / 1.0013 - 1
2028-01-01: 0.1104 # 1.1123 / 1.0017 - 1
2029-01-01: 0.0910 # 1.0933 / 1.0021 - 1
2030-01-01: 0.0816 # 1.0843 / 1.0025 - 1
metadata:
unit: /1
label: Household interest income growth per person
# The series starts at 2021 because ons.population does, so 2020
# growth per person is not defined. Anchor the index at 1 on
# 1 January 2020 so its first step is the 2021 growth, matching
# dataset projection from 2020 to 2021.
index_base_year: 2020
reference:
- title: ONS, Households (S.14) interest (D.41) resources, current prices, £m, NSA (HAXV), UK Economic Accounts, 30 September 2026
href: https://www.ons.gov.uk/economy/grossdomesticproductgdp/timeseries/haxv/ukea
- title: OBR EFO March 2026, detailed forecast tables, economy, Table 1.9 (deposit rate) and Table 1.11 (household financial assets)
href: https://obr.uk/efo/economic-and-fiscal-outlook-march-2026/
private_rental_prices:
# Historical values are calendar-year averages from ONS PIPR. Forecast
# values (2026-2031) are the OBR's UK private rents forecast, "Private
Expand Down
10 changes: 1 addition & 9 deletions policyengine_uk/tests/code_health/test_uprating_declarations.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,7 @@

# Variables the YAML deliberately uprates by an index other than the one they
# declare, with the reason.
KNOWN_INDEX_OVERRIDES = {
# Declares ons.household_interest_income; the YAML applies per_capita.gdp.
# Flagged in #1862 for someone with the context to say which is intended.
"savings_interest_income",
# Like ordinary savings interest, noncash ISA interest declares the ONS
# household index but follows the existing GDP dataset rule. Broader
# alignment of those two surfaces remains separate work in #2638.
"non_cash_isa_cash_interest",
}
KNOWN_INDEX_OVERRIDES: set[str] = set()


def _listed_indices():
Expand Down
4 changes: 3 additions & 1 deletion policyengine_uk/tests/test_isa_accounting.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,9 @@ def test_noncash_isa_interest_charge_reaches_tax_and_final_current_income_once()


GDP_INDEX = "gov.economic_assumptions.indices.obr.per_capita.gdp"
INTEREST_INDEX = "gov.economic_assumptions.indices.ons.household_interest_income"
INTEREST_INDEX = (
"gov.economic_assumptions.indices.ons.per_capita.household_interest_income"
)
ISA_INCOME_PAIRS = [
("individual_savings_account_dividend_income", "dividend_income", [100, 125, 150]),
("non_cash_isa_cash_interest", "savings_interest_income", [100, 150, 200]),
Expand Down
Loading
Loading