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/2179.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- From 2031-32, index the personal allowance and basic rate limit by the previous September's CPI with the statutory round-ups (Income Tax Act 2007 ss57 and 21), keep the primary threshold, upper earnings limit and Class 4 profits limits aligned with them, and raise the secondary threshold by September CPI. Before, they followed calendar-year CPI series without rounding.
4 changes: 3 additions & 1 deletion docs/book/policy/model-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ PolicyEngine UK models the UK tax and benefit system as of 2025, incorporating m

### Budget 2025 threshold freezes

Budget 2025 keeps the income tax personal allowance and basic rate limit, and the equivalent National Insurance thresholds, at their current levels until April 2031. The model holds the [primary threshold](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/primary_threshold.yaml) and [lower profits limit](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/thresholds/lower_profits_limit.yaml) at £12,570, the [upper earnings limit](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/upper_earnings_limit.yaml) and [upper profits limit](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/thresholds/upper_profits_limit.yaml) at £50,270, and the [secondary threshold](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/secondary_threshold.yaml) at £96 a week through 2030-31, then uprates them with CPI from 2031-32. For Class 1, regulations set only the 2026-27 amounts; the later years are announced policy. The Class 4 limits are standing figures in SSCBA 1992 s.15(3). The model annualises the weekly secondary threshold as 52 × £96 = £4,992; the annual figure in law is £5,000 ([#1967](https://github.com/PolicyEngine/policyengine-uk/issues/1967)). The [lower earnings limit](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/lower_earnings_limit.yaml) is not frozen: it rose with CPI to £129 a week in 2026-27.
Budget 2025 keeps the income tax personal allowance and basic rate limit, and the equivalent National Insurance thresholds, at their current levels until April 2031. Finance Act 2021 s.5, as amended by Finance Act 2023 s.5 and Finance Act 2026 s.10, fixes the [personal allowance](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/parameters/gov/hmrc/income_tax/allowances/personal_allowance/amount.yaml) at £12,570 and the [basic rate limit](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/parameters/gov/hmrc/income_tax/rates/uk.yaml) at £37,700 for 2022-23 to 2030-31. The model holds the [primary threshold](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/primary_threshold.yaml) and [lower profits limit](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/thresholds/lower_profits_limit.yaml) at £12,570, the [upper earnings limit](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/upper_earnings_limit.yaml) and [upper profits limit](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/thresholds/upper_profits_limit.yaml) at £50,270, and the [secondary threshold](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/secondary_threshold.yaml) at £5,000 a year through 2030-31. For Class 1, regulations set only the 2026-27 amounts; the later years are announced policy. The Class 4 limits are standing figures in SSCBA 1992 s.15(3). The [lower earnings limit](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/lower_earnings_limit.yaml) is not frozen: it rose with CPI to £129 a week in 2026-27.

From 2031-32 the model indexes these thresholds by September CPI, in [`create_threshold_indexation.py`](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/parameters/gov/hmrc/create_threshold_indexation.py). The personal allowance and basic rate limit follow Income Tax Act 2007 ss.57 and 21 year by year: each rises by the CPI increase to the previous September, with the allowance's increase rounded up to a multiple of £10 and the limit rounded up to a multiple of £100. The government says the National Insurance thresholds will also be uprated by September CPI after 2030-31 (Explanatory Memorandum to SI 2026/231, para 5.1), and it has kept them aligned with income tax through the freeze (HMRC's Budget 2025 policy paper on the threshold freeze; Explanatory Memorandum to SI 2026/231, paras 5.14 and 5.15). The model assumes the alignment continues, round-ups included: the primary threshold and lower profits limit equal the personal allowance, and the upper earnings and profits limits equal the higher rate threshold (the personal allowance plus the basic rate limit). It also assumes the secondary threshold rises by September CPI, unrounded; the Autumn Budget 2024 policy paper says only that it will be increased in line with CPI. None of these National Insurance rules after 2030-31 is law.

## 2025 reforms

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@
from policyengine_uk.parameters.gov.economic_assumptions.create_statutory_uprating_inputs import (
AWE_OBSERVATION_MONTH_DAY,
CPI_OBSERVATION_MONTH_DAY,
PUBLISHED_PRECISION,
last_input_year,
round_to_published_precision,
)

FIRST_UPRATING_YEAR = 2011
# Published statistics are quoted to one decimal place of a percentage.
PUBLISHED_PRECISION = Decimal("0.001")
# Guards the round-up of a guaranteed minimum against floating-point noise,
# so a ratio of 1.025000000000001 is not taken up to 2.6%.
RATIO_PRECISION = Decimal("0.000000001")
Expand All @@ -66,13 +66,6 @@ def uprating_instant(year: int) -> str:
return f"{year}-04-30"


def round_to_published_precision(rate: float) -> float:
"""Round a growth rate to 0.1 percentage points, halves away from zero."""
return float(
Decimal(repr(float(rate))).quantize(PUBLISHED_PRECISION, ROUND_HALF_UP)
)


def round_up_to_published_precision(rate: float) -> float:
"""Round a guaranteed minimum rate up to the next 0.1 percentage points."""
cleaned = Decimal(repr(float(rate))).quantize(RATIO_PRECISION, ROUND_HALF_UP)
Expand Down
20 changes: 20 additions & 0 deletions policyengine_uk/parameters/gov/economic_assumptions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,26 @@ September. Then replace May-July earnings with the October release's figure,
which the review uses: the April 2025 and April 2026 rises used October's
4.1% and 4.8%, where September's first estimates were 4.0% and 4.7%.

## September CPI uprating

[`create_september_cpi_uprating.py`](./create_september_cpi_uprating.py)
builds `yoy_growth.september_cpi_uprating`, the rise taking effect in April
of each year (keyed to 1 January, like the other series), from
`statutory_uprating_inputs.cpi_september`: September CPI of the year before,
rounded to the published 0.1 percentage points, and zero when prices did not
rise. `create_economic_assumption_indices` compounds it into
`indices.september_cpi_uprating`. The triple lock reads the same rounded
September figure, so a scenario that edits calendar-year CPI or sets
September CPI directly moves both.

Income Tax Act 2007 ss21 and 57 index the basic rate limit and the personal
allowance by this measure once no freeze applies.
[`../hmrc/create_threshold_indexation.py`](../hmrc/create_threshold_indexation.py)
applies them from 2031-32, keeps the primary threshold, upper earnings limit
and Class 4 profits limits aligned with them, and raises the secondary
threshold by the same rises, up to the last year of
`indices.september_cpi_uprating`.

## Refreshing after a new EFO

1. Replace the 2025-2030 block in each series with values from the new EFO
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""Build the April uprating by the previous September's CPI.

Income Tax Act 2007 ss21 and 57 index the basic rate limit and the personal
allowance for a tax year by the percentage increase in the consumer prices
index for the September before it, and only when prices rose (ss21(1) and
57(2)). The Explanatory Memorandum to SI 2026/231 (paras 5.1 and 5.2) says the
equivalent NICs thresholds will be uprated by September CPI once their freeze
ends, and that the Class 2 and Class 3 rates, the lower earnings limit and the
small profits threshold were raised for 2026-27 by September 2025's 3.8%.

``gov.economic_assumptions.yoy_growth.september_cpi_uprating`` holds the rise
taking effect in April of each year, keyed to 1 January as the other growth
series are. It is September CPI of the previous year from
``statutory_uprating_inputs.cpi_september``, rounded to the 0.1 percentage
points the ONS publishes, as the State Pension triple lock reads it. When
prices fall the rise is zero, as ss21 and 57 only raise amounts.

``create_economic_assumption_indices`` compounds the rises into
``gov.economic_assumptions.indices.september_cpi_uprating``.
``policyengine_uk.parameters.gov.hmrc.create_threshold_indexation`` uses the
rises, up to the index's last year, to index the income tax and NICs
thresholds after their freeze. The series starts from a 2010 base, the year
of the first September CPI input, and runs to one year past the statutory
inputs, as the triple lock does.
"""

from typing import Dict, Iterable

from policyengine_core.parameters import Parameter, ParameterNode

from policyengine_uk.parameters.gov.economic_assumptions.create_statutory_uprating_inputs import (
CPI_OBSERVATION_MONTH_DAY,
last_input_year,
round_to_published_precision,
)

# The index starts at 1 in this year; its value holds no rise.
BASE_YEAR = 2010
FIRST_UPRATING_YEAR = BASE_YEAR + 1


def september_cpi_uprating_rate(september_cpi: float) -> float:
"""April rise for a September CPI rate: the published rate, never a cut."""
rate = round_to_published_precision(september_cpi)
# Not max(rate, 0.0), which keeps a rounded -0.0.
return rate if rate > 0 else 0.0


def uprating_years(parameters: ParameterNode) -> Iterable[int]:
"""Aprils covered: from April 2011 to one year past the statutory inputs."""
return range(FIRST_UPRATING_YEAR, last_input_year(parameters) + 2)


def september_cpi_uprating_rates(parameters: ParameterNode) -> Dict[int, float]:
"""The rise taking effect in April of each year, in year order."""
cpi_september = (
parameters.gov.economic_assumptions.statutory_uprating_inputs.cpi_september
)
rates = {}
for year in uprating_years(parameters):
september = cpi_september(f"{year - 1}-{CPI_OBSERVATION_MONTH_DAY}")
if september is None:
raise ValueError(
f"Missing September {year - 1} CPI for the April {year} uprating."
)
rates[year] = september_cpi_uprating_rate(september)
return rates


def add_september_cpi_uprating(parameters: ParameterNode) -> ParameterNode:
"""Add ``gov.economic_assumptions.yoy_growth.september_cpi_uprating``."""
values = {f"{BASE_YEAR}-01-01": None}
values.update(
{
f"{year}-01-01": rate
for year, rate in september_cpi_uprating_rates(parameters).items()
}
)
new_parameter = Parameter(
"gov.economic_assumptions.yoy_growth.september_cpi_uprating",
data={
"description": (
"Uprating taking effect in April of each year: September "
"CPI inflation of the previous year, and no rise when prices "
"fall."
),
"values": values,
"metadata": {
"unit": "/1",
"label": "September CPI uprating rate",
},
},
)
parameters.gov.economic_assumptions.yoy_growth.add_child(
"september_cpi_uprating", new_parameter
)
return parameters
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,34 @@
covers.
"""

from decimal import ROUND_HALF_UP, Decimal

from policyengine_core.parameters import Parameter, ParameterNode

CPI_OBSERVATION_MONTH_DAY = "09-01"
AWE_OBSERVATION_MONTH_DAY = "07-01"

# Published statistics are quoted to one decimal place of a percentage.
PUBLISHED_PRECISION = Decimal("0.001")

# Statutory input: (calendar-year series in yoy_growth.obr, observation date).
STATUTORY_INPUTS = {
"cpi_september": ("consumer_price_index", CPI_OBSERVATION_MONTH_DAY),
"awe_total_pay_may_july": ("average_earnings", AWE_OBSERVATION_MONTH_DAY),
}


def round_to_published_precision(rate: float) -> float:
"""Round a growth rate to 0.1 percentage points, halves away from zero.

Uprating reviews use the figure as published, so forecasts of an input
are rounded the same way before they set an uprating.
"""
return float(
Decimal(repr(float(rate))).quantize(PUBLISHED_PRECISION, ROUND_HALF_UP)
)


def _last_year(parameter: Parameter) -> int:
return max(
int(value.instant_str[:4])
Expand Down
Loading
Loading