From d50ee85a9664791d6c617135fed999418762f6a9 Mon Sep 17 00:00:00 2001 From: vahid-ahmadi Date: Mon, 12 Oct 2026 15:09:55 +0200 Subject: [PATCH 1/7] Add twenty planning and validation docs pages in one batch Combines the docs-only PRs #1712, #1714, #1718-#1732 and #1735-#1737, each re-checked against current main today. One release instead of twenty. Co-Authored-By: Claude Opus 5.5 --- .../1008-regional-childcare-plan.added.md | 1 + changelog.d/1069-pip-points-plan.added.md | 1 + changelog.d/1114.added.md | 1 + changelog.d/1209.added.md | 1 + changelog.d/1320.added.md | 1 + changelog.d/1354.added.md | 1 + changelog.d/1398.added.md | 1 + changelog.d/1621.added.md | 1 + changelog.d/1628.added.md | 1 + changelog.d/1692.added.md | 1 + changelog.d/435.added.md | 1 + changelog.d/466.added.md | 1 + changelog.d/502.added.md | 1 + changelog.d/636.added.md | 1 + changelog.d/669.added.md | 1 + changelog.d/672.added.md | 1 + changelog.d/704.added.md | 1 + changelog.d/806.added.md | 1 + changelog.d/818.added.md | 1 + changelog.d/842.added.md | 1 + .../capital-gains-distribution-plan.md | 133 ++++++++++++ docs/book/assumptions/mp30k-uk-switch-plan.md | 104 +++++++++ .../non-uk-benefit-receipt-plan.md | 130 +++++++++++ docs/book/assumptions/takeup-analysis-plan.md | 146 +++++++++++++ .../assumptions/uk-pipeline-alignment-plan.md | 173 +++++++++++++++ docs/book/policy/future-date-announcements.md | 141 ++++++++++++ docs/book/policy/indirect-tax-effects-plan.md | 136 ++++++++++++ .../programs/gov/cms/child-maintenance.md | 135 ++++++++++++ .../gov/dfe/regional-childcare-plan.md | 170 +++++++++++++++ .../gov/dwp/bereavement-support-plan.md | 162 ++++++++++++++ .../gov/dwp/cold-weather-payment-plan.md | 152 +++++++++++++ docs/book/programs/gov/dwp/pip-points-plan.md | 171 +++++++++++++++ docs/book/programs/gov/hmrc/firm-vat.md | 203 ++++++++++++++++++ .../programs/gov/hmrc/pension-tax-relief.md | 124 +++++++++++ .../gov/hmrc/pensions-comparison-stbm.md | 96 +++++++++ .../gov/ofgem/warm-home-discount-plan.md | 136 ++++++++++++ docs/book/validation/child-poverty-tcl.md | 120 +++++++++++ .../policyengine-py-household-impact.md | 119 ++++++++++ .../reform-impacts-dashboard-plan.md | 174 +++++++++++++++ docs/book/validation/ubuntu-vs-macos.md | 98 +++++++++ 40 files changed, 2843 insertions(+) create mode 100644 changelog.d/1008-regional-childcare-plan.added.md create mode 100644 changelog.d/1069-pip-points-plan.added.md create mode 100644 changelog.d/1114.added.md create mode 100644 changelog.d/1209.added.md create mode 100644 changelog.d/1320.added.md create mode 100644 changelog.d/1354.added.md create mode 100644 changelog.d/1398.added.md create mode 100644 changelog.d/1621.added.md create mode 100644 changelog.d/1628.added.md create mode 100644 changelog.d/1692.added.md create mode 100644 changelog.d/435.added.md create mode 100644 changelog.d/466.added.md create mode 100644 changelog.d/502.added.md create mode 100644 changelog.d/636.added.md create mode 100644 changelog.d/669.added.md create mode 100644 changelog.d/672.added.md create mode 100644 changelog.d/704.added.md create mode 100644 changelog.d/806.added.md create mode 100644 changelog.d/818.added.md create mode 100644 changelog.d/842.added.md create mode 100644 docs/book/assumptions/capital-gains-distribution-plan.md create mode 100644 docs/book/assumptions/mp30k-uk-switch-plan.md create mode 100644 docs/book/assumptions/non-uk-benefit-receipt-plan.md create mode 100644 docs/book/assumptions/takeup-analysis-plan.md create mode 100644 docs/book/assumptions/uk-pipeline-alignment-plan.md create mode 100644 docs/book/policy/future-date-announcements.md create mode 100644 docs/book/policy/indirect-tax-effects-plan.md create mode 100644 docs/book/programs/gov/cms/child-maintenance.md create mode 100644 docs/book/programs/gov/dfe/regional-childcare-plan.md create mode 100644 docs/book/programs/gov/dwp/bereavement-support-plan.md create mode 100644 docs/book/programs/gov/dwp/cold-weather-payment-plan.md create mode 100644 docs/book/programs/gov/dwp/pip-points-plan.md create mode 100644 docs/book/programs/gov/hmrc/firm-vat.md create mode 100644 docs/book/programs/gov/hmrc/pension-tax-relief.md create mode 100644 docs/book/programs/gov/hmrc/pensions-comparison-stbm.md create mode 100644 docs/book/programs/gov/ofgem/warm-home-discount-plan.md create mode 100644 docs/book/validation/child-poverty-tcl.md create mode 100644 docs/book/validation/policyengine-py-household-impact.md create mode 100644 docs/book/validation/reform-impacts-dashboard-plan.md create mode 100644 docs/book/validation/ubuntu-vs-macos.md diff --git a/changelog.d/1008-regional-childcare-plan.added.md b/changelog.d/1008-regional-childcare-plan.added.md new file mode 100644 index 0000000000..339f8f401f --- /dev/null +++ b/changelog.d/1008-regional-childcare-plan.added.md @@ -0,0 +1 @@ +Added a regional childcare planning page (docs/book/programs/gov/dfe/regional-childcare-plan.md) listing the Scottish, Welsh and Northern Ireland childcare schemes not yet modelled and how to add them. diff --git a/changelog.d/1069-pip-points-plan.added.md b/changelog.d/1069-pip-points-plan.added.md new file mode 100644 index 0000000000..759d59fe3d --- /dev/null +++ b/changelog.d/1069-pip-points-plan.added.md @@ -0,0 +1 @@ +Added a PIP point system planning page (docs/book/programs/gov/dwp/pip-points-plan.md) setting out the SI 2013/377 scoring rules and a phased route to modelling them, starting from #1661. diff --git a/changelog.d/1114.added.md b/changelog.d/1114.added.md new file mode 100644 index 0000000000..1d2a96c92b --- /dev/null +++ b/changelog.d/1114.added.md @@ -0,0 +1 @@ +Add a planning docs page (`docs/book/policy/indirect-tax-effects-plan.md`) on letting indirect tax revenue respond to direct tax reforms through a consumption elasticity, from a single household-level factor to per-category elasticities and a link to `consumer_incident_tax_revenue_change`. diff --git a/changelog.d/1209.added.md b/changelog.d/1209.added.md new file mode 100644 index 0000000000..cf2da497f8 --- /dev/null +++ b/changelog.d/1209.added.md @@ -0,0 +1 @@ +Documented the Ubuntu vs macOS reproducibility issue (#1209) at `docs/book/validation/ubuntu-vs-macos.md`: the import-time uprating cause, how it was resolved, and the October 2026 check that the parameter tree is byte-identical across macOS and Linux. diff --git a/changelog.d/1320.added.md b/changelog.d/1320.added.md new file mode 100644 index 0000000000..0c7cedbd66 --- /dev/null +++ b/changelog.d/1320.added.md @@ -0,0 +1 @@ +Add a planning docs page for firm-level VAT (`docs/book/programs/gov/hmrc/firm-vat.md`): a proposed `Firm` entity, the registration threshold and supply split, output, input and net VAT, the data needed for a synthetic firm panel, and open questions. diff --git a/changelog.d/1354.added.md b/changelog.d/1354.added.md new file mode 100644 index 0000000000..e21dfc8a1e --- /dev/null +++ b/changelog.d/1354.added.md @@ -0,0 +1 @@ +Added an effective take-up analysis plan (docs/book/assumptions/takeup-analysis-plan.md) listing each programme's take-up flag, reference parameter and current handling, where the data builds' seed rates live, and how to measure drift from them. diff --git a/changelog.d/1398.added.md b/changelog.d/1398.added.md new file mode 100644 index 0000000000..3119715c17 --- /dev/null +++ b/changelog.d/1398.added.md @@ -0,0 +1 @@ +Added a child poverty validation page (docs/book/validation/child-poverty-tcl.md) comparing the two-child limit removal on Microcosm UK 2024-25 (about 463,000 children lifted after housing costs, £3.26bn in 2029-30) with the DWP, Budget 2025 (OBR-certified) and Resolution Foundation estimates. diff --git a/changelog.d/1621.added.md b/changelog.d/1621.added.md new file mode 100644 index 0000000000..6c13eaceb8 --- /dev/null +++ b/changelog.d/1621.added.md @@ -0,0 +1 @@ +Added a UK pipeline alignment planning page (docs/book/assumptions/uk-pipeline-alignment-plan.md): the model-side take-up gap still open, and what Microcosm UK now covers (reported-anchored take-up, the SPI stage-2 fill). diff --git a/changelog.d/1628.added.md b/changelog.d/1628.added.md new file mode 100644 index 0000000000..9bf39b9e40 --- /dev/null +++ b/changelog.d/1628.added.md @@ -0,0 +1 @@ +Documented the policyengine.py household-impact zero-return diagnosis (docs/book/validation/policyengine-py-household-impact.md); the UK model returns the expected values, re-checked on 2.123.7. diff --git a/changelog.d/1692.added.md b/changelog.d/1692.added.md new file mode 100644 index 0000000000..1816c55e99 --- /dev/null +++ b/changelog.d/1692.added.md @@ -0,0 +1 @@ +Add a planning docs page (`docs/book/assumptions/mp30k-uk-switch-plan.md`) setting out the gates and rollback path for changing the default dataset. It was written for Microplex UK and now applies to its successor, Microcosm UK. diff --git a/changelog.d/435.added.md b/changelog.d/435.added.md new file mode 100644 index 0000000000..4ac12a8377 --- /dev/null +++ b/changelog.d/435.added.md @@ -0,0 +1 @@ +Added a plan for modelling Cold Weather Payment and the Scottish Winter Heating Payment (#435) at `docs/book/programs/gov/dwp/cold-weather-payment-plan.md`. diff --git a/changelog.d/466.added.md b/changelog.d/466.added.md new file mode 100644 index 0000000000..5bf393a6b5 --- /dev/null +++ b/changelog.d/466.added.md @@ -0,0 +1 @@ +Added a plan for a rule-based Bereavement Support Payment and historical Widowed Parent's Allowance (#466, #465) at `docs/book/programs/gov/dwp/bereavement-support-plan.md`. diff --git a/changelog.d/502.added.md b/changelog.d/502.added.md new file mode 100644 index 0000000000..6f0751e936 --- /dev/null +++ b/changelog.d/502.added.md @@ -0,0 +1 @@ +Added a plan for modelling the Warm Home Discount (#502) at `docs/book/programs/gov/ofgem/warm-home-discount-plan.md`, reflecting the benefit-based eligibility in England and Wales from winter 2025-26 and the separate Scottish scheme. diff --git a/changelog.d/636.added.md b/changelog.d/636.added.md new file mode 100644 index 0000000000..aefcb63f8e --- /dev/null +++ b/changelog.d/636.added.md @@ -0,0 +1 @@ +Added a page (docs/book/policy/future-date-announcements.md) on encoding announced policy changes as date-keyed parameter values, and how those dates become model-year values. diff --git a/changelog.d/669.added.md b/changelog.d/669.added.md new file mode 100644 index 0000000000..be24e5dbce --- /dev/null +++ b/changelog.d/669.added.md @@ -0,0 +1 @@ +Add a planning docs page for child maintenance (`docs/book/programs/gov/cms/child-maintenance.md`): how `maintenance_income` and `maintenance_expenses` are treated today, the gap for spousal maintenance in Universal Credit, and a route to modelling the Child Maintenance Service calculation. diff --git a/changelog.d/672.added.md b/changelog.d/672.added.md new file mode 100644 index 0000000000..2e0a22b781 --- /dev/null +++ b/changelog.d/672.added.md @@ -0,0 +1 @@ +Add a documentation page (`docs/book/programs/gov/hmrc/pensions-comparison-stbm.md`) comparing PolicyEngine UK's treatment of pension contributions in income tax with the Scottish Tax-Benefit Model's source code, including a gap both share: contributions do not reduce adjusted net income for the personal allowance taper. diff --git a/changelog.d/704.added.md b/changelog.d/704.added.md new file mode 100644 index 0000000000..6cd2db94ea --- /dev/null +++ b/changelog.d/704.added.md @@ -0,0 +1 @@ +Add a documentation page at `docs/book/programs/gov/hmrc/pension-tax-relief.md` explaining how PolicyEngine UK models personal pension tax relief (full marginal-rate reduction in taxable income via `pension_contributions_relief`, with separate annual-allowance excess charge via `personal_pension_contributions_tax`), and clarifying that the model correctly reports the *total* relief (both relief-at-source + higher-rate top-up combined) for the scenarios reported in #704. diff --git a/changelog.d/806.added.md b/changelog.d/806.added.md new file mode 100644 index 0000000000..3a5e0d597d --- /dev/null +++ b/changelog.d/806.added.md @@ -0,0 +1 @@ +Added a plan for a reform impacts dashboard (#806) at `docs/book/validation/reform-impacts-dashboard-plan.md`, tracking the regression fixtures in `reforms_config.yaml` over model and dataset versions. diff --git a/changelog.d/818.added.md b/changelog.d/818.added.md new file mode 100644 index 0000000000..ee8c0776c2 --- /dev/null +++ b/changelog.d/818.added.md @@ -0,0 +1 @@ +Documented how capital gains are modelled and imputed (#818, #817) at `docs/book/assumptions/capital-gains-distribution-plan.md`, including the Microcosm UK stages that now fit gains jointly to HMRC Table 3 and what is left to do. diff --git a/changelog.d/842.added.md b/changelog.d/842.added.md new file mode 100644 index 0000000000..a6930079a4 --- /dev/null +++ b/changelog.d/842.added.md @@ -0,0 +1 @@ +Add a planning docs page (`docs/book/assumptions/non-uk-benefit-receipt-plan.md`) on benefit spending that does not reach UK private households (payments abroad, institutions, fraud and error) and how calibration targets should exclude it. diff --git a/docs/book/assumptions/capital-gains-distribution-plan.md b/docs/book/assumptions/capital-gains-distribution-plan.md new file mode 100644 index 0000000000..5e47427100 --- /dev/null +++ b/docs/book/assumptions/capital-gains-distribution-plan.md @@ -0,0 +1,133 @@ +# Capital gains distribution + +```{note} +**Planning page, updated October 2026.** Tracks +[#818](https://github.com/PolicyEngine/policyengine-uk/issues/818) +(model gains jointly across income groups) and +[#817](https://github.com/PolicyEngine/policyengine-uk/issues/817) +(don't impute gains to households with no wealth). The imputation lives +in the data build, not in this repo. Most of what this page proposed +in May 2026 has since been built in +[Microcosm UK](https://github.com/PolicyEngine/microcosm), which builds +the replacement for the Enhanced FRS. The certified UK default dataset is +still the Enhanced FRS from +[policyengine-uk-data](https://github.com/PolicyEngine/policyengine-uk-data) +(release 1.58.0). What remains is listed at the end of this page. +``` + +## How the model uses capital gains + +`capital_gains_before_response` is a person-level input, uprated by OBR +per-capita GDP growth. `capital_gains_tax` charges it at the main CGT +rates, with three components charged at their own rates (all added by +[#1861](https://github.com/PolicyEngine/policyengine-uk/pull/1861)): + +- `capital_gains_badr`: gains qualifying for Business Asset Disposal + Relief or Investors' Relief, at the relief rate up to the lifetime + limit; +- `capital_gains_residential_property`: gains on UK residential + property, at the residential rates; +- `capital_gains_carried_interest`: carried interest, at the carried + interest rates. + +Each is a component of the total, not an addition to it. + +Behavioural responses: `capital_gains_behavioural_response` changes +realisations using an elasticity with respect to the retention rate +(`relative_capital_gains_retention_rate_change`) or the marginal tax +rate. Gains use `gov.simulation.capital_gains_responses.elasticity`, +which is 0 by default, so there is no response unless a user sets it. +When `gov.simulation.capital_gains_responses.separate_badr_elasticity` +is true (false by default), gains qualifying for BADR respond at +`badr_elasticity` instead (1.4, the value the OBR used for these gains +in its January 2025 costing), through `capital_gains_badr_elasticity`. + +## How the gains are imputed + +### Enhanced FRS: one distribution per income band + +The Enhanced FRS (policyengine-uk-data, the certified default) fits a +spline to the Advani and Summers gains percentiles within each income +band and draws each household's gains from its band. The spline alone +cannot produce gains above about £2m, so policyengine-uk-data main (not +yet in a release) also stacks 30 donor households per HMRC +size-of-gain band and calibrates to HMRC counts and amounts of gains by +band. #818 pointed out the problems with the +band-by-band fit: + +- each band's upper tail was fitted on few observations, so the tails + were noisy and the joint distribution of income and gains had breaks + at band boundaries; +- two people in the same band drew from the same distribution however + different their incomes; +- gains depended on income only, not on wealth, so they could be + imputed to households with no assets (#817). + +### Microcosm UK + +Microcosm builds gains in six stages (see the UK +[sources specification](https://github.com/PolicyEngine/microcosm/blob/main/packages/microcosm-build/src/microcosm/build/uk/spec/sources.yaml)): + +1. **Support split** (`cgt_support_split`). Within each income band of + HMRC Capital Gains Tax statistics Table 3, the households with the + most investable wealth (financial wealth, business wealth and + property other than the main home) are split into lighter copies, so + that there are enough rows to hold the largest gains. +2. **Who has gains** (`cgt_incidence_clone`). The set and order of + gainers come from the incidence distribution in Advani and Summers + (2020), *Capital Gains and UK Inequality* (CAGE Working Paper 465). +3. **How much** (`hmrc_cgt_gains_spine`). Amounts are redrawn so that + gains match HMRC Table 3 for 2024-25, which counts taxpayers and + gains **jointly by size of gain and taxable income**, conditioned on + Tables 1, 2.1a, 5 and 6. +4. **Weight anchor** (`cgt_incidence_anchor`). Clones below the annual + exempt amount carry more weighted people than HMRC publishes, so the + excess weight is moved back to each clone's paired original. +5. **Residential property** (`cgt_residential_split`). Gains on UK + residential property are fitted to Table 8 (2024-25 residential + property disposals) by splitting households into weighted copies, + which fills `capital_gains_residential_property`. +6. **What kind** (`hmrc_cgt_asset_type_spine`). Of the remaining gains, + those qualifying for BADR or Investors' Relief are fitted to Table + 4.1, which fills `capital_gains_badr`, and the rest are given asset + types from Table 7 (gains by asset type, 2023-24). + +All the HMRC tables come from the Capital Gains Tax statistics July +2026 release, through Microcosm's pinned Chronicle feed. + +Against #818: gains are now fitted to the published joint distribution +of gain size and taxable income, not band by band. Against #817: +the largest gains are placed on the wealthiest households in each +income band. Microcosm's release checks also include a projection check +on people just below the annual exempt amount +(`uk_cgt_projection_entrants` in `gates.json`), so that +uprating does not push implausible numbers of them into paying CGT in +later years. + +## What is left + +- **A zero-wealth test.** Add a test, here or in Microcosm, that no + positive gains sit on households with zero investable wealth. Neither + repo asserts this directly yet, so #817 should stay open until it + does. +- **Close #818** once a Microcosm UK dataset is the certified default + and its diagnostics confirm the Table 3 fit; link them from the issue. +- **Elasticities by group.** Responses use one main elasticity and one + BADR elasticity. With the joint distribution in place, an elasticity + that varies by income or size of gain becomes possible, at the cost + of more parameters to justify. +- **Wealth source.** Investable wealth comes from Microcosm's Wealth + and Assets Survey imputation. Its quality limits how well #817 can be + met. + +The quantile regression forest approach proposed here in May is no +longer needed for this. + +## References + +- Issues: [#818](https://github.com/PolicyEngine/policyengine-uk/issues/818), [#817](https://github.com/PolicyEngine/policyengine-uk/issues/817). +- CGT components: [#1861](https://github.com/PolicyEngine/policyengine-uk/pull/1861). +- HMRC, [Capital Gains Tax statistics](https://www.gov.uk/government/statistics/capital-gains-tax-statistics). +- Advani, A. and Summers, A. (2020), *Capital Gains and UK Inequality*, CAGE Working Paper 465. +- Microcosm UK [sources specification](https://github.com/PolicyEngine/microcosm/blob/main/packages/microcosm-build/src/microcosm/build/uk/spec/sources.yaml) (stages `cgt_support_split`, `cgt_incidence_clone`, `hmrc_cgt_gains_spine`, `cgt_incidence_anchor`, `cgt_residential_split`, `hmrc_cgt_asset_type_spine`). +- OBR (2025), [Costing of changes to the main, BADR and IR rates of capital gains tax](https://obr.uk/docs/dlm_uploads/CGT-supplementary-release-Jan-2025.pdf). diff --git a/docs/book/assumptions/mp30k-uk-switch-plan.md b/docs/book/assumptions/mp30k-uk-switch-plan.md new file mode 100644 index 0000000000..39f9d64841 --- /dev/null +++ b/docs/book/assumptions/mp30k-uk-switch-plan.md @@ -0,0 +1,104 @@ +# Switching the default dataset: gates and rollback + +```{note} +**Planning page.** [#1692](https://github.com/PolicyEngine/policyengine-uk/issues/1692) +asked for a switch and rollback path from the Enhanced FRS (eFRS) to Microplex UK +(`mp-30k-uk`). The `microplex-uk` repository has since been archived, and its +successor is [Microcosm UK](https://github.com/PolicyEngine/microcosm), whose +UK national line publishes `microcosm_uk_2024_25.h5`. This page applies the +same switch and rollback path to Microcosm UK. +``` + +## Current state + +PolicyEngine UK has no built-in default dataset. `Simulation()` uses, in order: + +1. an explicit `dataset=` argument; +2. the `POLICYENGINE_UK_DEFAULT_DATASET` environment variable + (`DEFAULT_DATASET_ENV_VAR` in `tax_benefit_system.py`); +3. otherwise, unless an inline `situation` is passed, it raises a `ValueError` + (`get_default_dataset_url` in `simulation.py`). + +The test suite sets the variable to the private +`hf://policyengine/policyengine-uk-data-private/enhanced_frs_2023_24.h5@1.40.3` +when a Hugging Face token is available (`policyengine_uk/tests/conftest.py`). +So the eFRS is still the dataset the model is tested against, and the +default in downstream tools such as policyengine.py. + +Direct microsimulation through `Microsimulation()` in this package is +deprecated in favour of [policyengine.py](https://github.com/PolicyEngine/policyengine.py), +whose managed bundle pins certified datasets (see the repository README). The +default most users meet is therefore the one that bundle pins, so a switch has +to be made there as well as in this repository's tests. + +## What the switch needs + +### Loader + +- `UKMultiYearDataset` and `UKSingleYearDataset` + (`policyengine_uk/data/dataset_schema.py`) are the containers both datasets + load into. A Microcosm file must use the same table and column conventions, + because `_pre_encode_enum_columns` (in `simulation.py`) walks each year's + tables. +- The dataset's `years` drive uprating. Microcosm's base year is 2024-25, + one year later than the eFRS file used in tests, so uprating paths from the + earlier base year need checking. +- Enum columns must use the labels in this repo's variable definitions. + Unknown labels do not encode correctly. + +### Variables + +- `policyengine_uk/variables/input/` is the data contract. A switch should + report the input columns present in one dataset but not the other, and + decide for each whether a missing column is acceptable. +- Take-up and other reported-versus-simulated inputs have to come from the + new data, not be carried over from the eFRS. + +### Calibration + +- Microcosm calibrates its own weights against its target register, and + certifies each release through its own checks. The model side should + compare results against those targets, not re-weight. + +## Switch criteria + +A documented gate must pass before any environment's default changes: + +1. **Aggregates:** headline totals (income tax, NICs, UC, Child Benefit, + State Pension) on both datasets for the same year, each set against OBR + outturn, with any difference of more than £1bn or 2% (whichever is wider) + explained. +2. **Distribution:** decile mean net income and HBAI poverty rates (before + and after housing costs) within 2 percentage points, or explained. +3. **Reforms:** at least three standard reforms (for example +1p on the basic + rate, +£5 a week on the UC standard allowance, abolishing the two-child + limit) give the same sign and are within 5% of each other. +4. **Tests:** the suite passes with the new default, including + `test_latest_data_smoke.py`, `test_deterministic_variables.py` and + `test_no_economic_assumptions.py`. +5. **Documentation:** the `assumptions/` section records the switch and any + remaining aggregate gaps. + +## Rollback path + +1. Dataset choice stays a runtime setting: no code in this repo depends on + which dataset is loaded. +2. While both are in use, CI runs the dataset-dependent tests on both. +3. The default is set per environment through + `POLICYENGINE_UK_DEFAULT_DATASET`, with the eFRS file kept available. +4. Rolling back is changing that variable and redeploying, with no code + change here. This assumes the eFRS file stays published while the switch + beds in. + +## Acceptance + +PolicyEngine UK switches its default only after the gates above pass, with a +rollback that needs no code change in this repo. Until then either dataset can +be used by passing `dataset=` or setting the environment variable. + +## References + +- Tracking issue: [#1692](https://github.com/PolicyEngine/policyengine-uk/issues/1692) (still open). Its umbrella discussion is in the archived [`microplex-uk`](https://github.com/PolicyEngine/microplex-uk/discussions/2) repository. +- Related: [#1621](https://github.com/PolicyEngine/policyengine-uk/issues/1621) (aligning the UK model and data pipeline). +- Microcosm UK: [repository](https://github.com/PolicyEngine/microcosm). +- Dataset loading: `policyengine_uk/simulation.py`, `policyengine_uk/data/dataset_schema.py`. diff --git a/docs/book/assumptions/non-uk-benefit-receipt-plan.md b/docs/book/assumptions/non-uk-benefit-receipt-plan.md new file mode 100644 index 0000000000..928177417f --- /dev/null +++ b/docs/book/assumptions/non-uk-benefit-receipt-plan.md @@ -0,0 +1,130 @@ +# Benefit expenditure paid outside UK private households (planned) + +```{note} +**Planning page.** The data behind PolicyEngine UK is calibrated so that +household benefit totals match published **DWP/HMRC/OBR totals**. +Some of that outturn doesn't go to **UK private households**. It +goes to people living abroad (exported pensions), people in +institutional accommodation outside the FRS sampling frame (care homes, +hostels), and a small administrative-leakage residual. This page +sets out the proposed treatment, tracked in +[#842](https://github.com/PolicyEngine/policyengine-uk/issues/842) (still +open, October 2026). Calibration targets are now set in +[Microcosm UK](https://github.com/PolicyEngine/microcosm), so most of the +work below belongs there. +``` + +## Why this matters + +Calibrating FRS household weights so that simulated `state_pension` +matches a national spending total assumes every pound of that total lands +on a household the FRS samples. It doesn't: + +- **State Pension paid abroad**: pensioners living overseas receive the + State Pension they built up. DWP's benefit expenditure tables report + the amount paid to recipients abroad, and Stat-Xplore gives the + caseload by country. None of these people are in the FRS. +- **Disability Living Allowance and PIP paid abroad**: limited cases + under reciprocal agreements (EU and EFTA countries and a handful of + others). +- **Institutional residents**: care-home residents receive most + benefits but the FRS doesn't sample care homes. Their entitlements + feed into the DWP outturn but not into PolicyEngine's `state_pension` + totals. +- **Administrative leakage**: fraud, error and advance payments that + appear in DWP cashflow but never reach an entitled person. + +Where a target includes these groups, calibration pushes their spending +onto FRS households and **overstates** what UK private households receive. + +**Already done for State Pension.** Microcosm no longer binds the OBR State +Pension line. State Pension is now bound to resident figures from DWP +Stat-Xplore (Great Britain) and the Northern Ireland Department for +Communities, and the OBR line and DWP's "paid outside UK" line are kept as +diagnostics. The plan is +[microcosm#1069](https://github.com/PolicyEngine/microcosm/issues/1069) and +the change landed in +[microcosm#1084](https://github.com/PolicyEngine/microcosm/pull/1084) +(merged 2 October 2026). The same question is open for other programmes. + +## Scope + +### Phase 1: quantify the gap by programme + +For each benefit programme the calibration targets, identify the share +of the published total that goes outside UK private households. DWP and HMRC +publish enough data to assemble a first-pass table: + +| Programme | UK private households | Overseas | Institutional | Admin / error | +|-----------|----------------------|----------|---------------|---------------| +| State Pension | published | DWP overseas tables | -- | -- | +| Pension Credit | published | small | minimal | minor | +| Universal Credit | published | minimal | small | F&E published | +| Housing Benefit | published | -- | care-home component | F&E published | +| Child Benefit | published | EU-treaty residue | -- | -- | +| Disability Living Allowance / PIP | published | reciprocal residue | -- | -- | +| Attendance Allowance | published | -- | care-home residue | -- | +| Winter Fuel Payment | published | overseas eligible cohort | -- | -- | + +Sources: DWP *Benefit expenditure and caseload tables* and Stat-Xplore +(payments abroad), the DWP *Fraud and Error in the Benefit System* annual +release, and programme-specific breakdowns. + +### Phase 2: subtract from calibration targets + +Once the gap is quantified per programme, each **calibration target** +should be the **UK-private-household part** of the total, not the full +total. Microcosm's State Pension change does this by binding a resident +series instead. Where no resident series exists, the target can carry an +explicit private-household share (0–1, default 1) with a cited source for +each override. + +This avoids the overweighting bias without trying to synthesise +out-of-scope households. + +### Phase 3: explicit out-of-scope correction (optional) + +For headline aggregates that need to match the **full** outturn (e.g. +fiscal cost of a reform), maintain an out-of-scope additive correction +per programme rather than synthesising fake households. This is +preferable because: + +- Distributional analyses already get the right answer at Phase 2 (UK + private households are correctly weighted to their own outturn). +- Synthesising overseas / institutional households would require strong + assumptions about their characteristics that aren't validated against + any micro-data source. + +The additive correction would live in the data pipeline and appear in +PolicyEngine UK as an explicit non-household spending line. + +## Implementation outline + +This is mainly a **data-side** change in Microcosm UK's target +register. (`policyengine_uk/programs.yaml` is metadata on model coverage, +not calibration targets, so it is not the place for these shares.) The +changes in this repo are small: + +- a non-household spending line for the Phase 3 correction, if adopted; +- documentation linking the affected benefit variables to this page. + +## Open questions + +- Should the `private_household_share` be year-varying (e.g. overseas + State Pension share rose post-EU exit) or held flat? +- For Universal Credit, where the FRS *does* sample some + institutionalised people (in supported accommodation), what's the + correct private-household-share value: strictly < 1, or 1 minus a + smaller residual? +- For the household-calculator (single-household) interface, the + private-household-share correction shouldn't apply, because the user + represents their own household. Confirm the correction is only on the + microsim weighted aggregates. + +## References + +- DWP, [Benefit expenditure and caseload tables](https://www.gov.uk/government/collections/benefit-expenditure-and-caseload-tables): spending by benefit, including payments abroad. +- DWP, [Fraud and Error in the Benefit System](https://www.gov.uk/government/collections/fraud-and-error-in-the-benefit-system): administrative leakage estimates. +- DWP, [Stat-Xplore](https://stat-xplore.dwp.gov.uk/): caseloads by benefit, including State Pension recipients by country of residence. +- HMRC, [Tax credits and Child Benefit statistics](https://www.gov.uk/government/collections/personal-tax-credits-statistics): EU-treaty Child Benefit residue. +- Issue: [#842](https://github.com/PolicyEngine/policyengine-uk/issues/842). Related: [#1621](https://github.com/PolicyEngine/policyengine-uk/issues/1621) (UK pipeline alignment) and [microcosm#1069](https://github.com/PolicyEngine/microcosm/issues/1069) (State Pension on resident figures). diff --git a/docs/book/assumptions/takeup-analysis-plan.md b/docs/book/assumptions/takeup-analysis-plan.md new file mode 100644 index 0000000000..d4836b0de7 --- /dev/null +++ b/docs/book/assumptions/takeup-analysis-plan.md @@ -0,0 +1,146 @@ +# Effective take-up rate analysis plan + +```{note} +**Planning page.** Tracks +[#1354](https://github.com/PolicyEngine/policyengine-uk/issues/1354), +which asks for a comprehensive analysis of how the data processing +pipeline (reweighting, SPI integration) changes effective take-up +rates away from the seed values. This page documents the methodology +and per-programme target list. +``` + +```{admonition} Status, October 2026 +The analysis below has not been run yet. Take-up flags are drawn in the +data build, not in this repository. The certified UK default dataset is +still the Enhanced FRS built by +[`policyengine-uk-data`](https://github.com/PolicyEngine/policyengine-uk-data) +(release 1.58.0). [Microcosm UK](https://github.com/PolicyEngine/microcosm) +builds the replacement national dataset with the same take-up design. +Both draw Universal Credit, Pension Credit and Child Benefit take-up +anchored on reported receipt, so the analysis should run on whichever +dataset is the certified default at the time. +``` + +## Why this matters + +PolicyEngine UK does not compute benefit take-up from scratch. The data +build **seeds** stochastic take-up flags (`would_claim_*`) per programme +from published target rates, then runs them through: + +1. **Reported anchoring.** Universal Credit, Pension Credit and Child + Benefit flags are inputs, drawn so that units reporting receipt + claim. Housing Benefit, Income Support, the tax credits and Council + Tax Reduction instead derive their flags at runtime from reported + receipt (or `claims_all_entitled_benefits`); see + [#1621](https://github.com/PolicyEngine/policyengine-uk/issues/1621). +2. **Reweighting** to match aggregate caseload and expenditure + targets. +3. **SPI integration** for income-tax-relevant variables (which can + shift the income distribution and therefore who falls into each + benefit's eligible population). + +By the end of the pipeline the *effective* take-up rate, the share of +the eligible population that ends up with `would_claim_X == True`, can +diverge from the seed rate by a few percentage points. The seeded rates +themselves were calibrated against pre-reweighted aggregates, so the +real interest is how the post-pipeline shares stack up against +published outturn share figures. + +## Where the seed rates live + +The take-up rate parameters in this repository (for example +`gov.dwp.universal_credit.takeup_rate` and `gov.dwp.housing_benefit.takeup`) +are reference values: no formula reads them. The rates the data build +draws from are in `policyengine_uk_data/parameters/take_up/` in +policyengine-uk-data and in `build/uk/take_up_contract.json` in +Microcosm. The analysis should read the seed rate from the data build +that produced the dataset, and check the matching parameter here is in +sync. + +## Programmes to cover + +Per #1354, with current PolicyEngine UK take-up handling: + +| Programme | Variable | Take-up flag | Reference parameter here | Handling | +|-----------|----------|--------------|--------------------------|----------| +| Universal Credit | `universal_credit` | `would_claim_uc` (input) | `gov.dwp.universal_credit.takeup_rate` | Drawn in the data build, anchored on reported receipt | +| UC childcare element | `uc_childcare_element_claimed` | `would_claim_uc_childcare` (input) | – | Drawn in Microcosm at single and couple rates; the Enhanced FRS does not set it, so it defaults to true | +| Pension Credit | `pension_credit` | `would_claim_pc` (input) | `gov.dwp.pension_credit.takeup` | Drawn in the data build, anchored on reported receipt | +| Child Benefit | `child_benefit` | `would_claim_child_benefit` (input) | `gov.hmrc.child_benefit.takeup.overall` and `.by_age` | Drawn in the data build, anchored on reported receipt | +| Housing Benefit | `housing_benefit` | `would_claim_housing_benefit` (formula) | `gov.dwp.housing_benefit.takeup` | Follows reported receipt (#1621) | +| Income Support | `income_support` | `would_claim_IS` (formula) | `gov.dwp.income_support.takeup` | Follows reported receipt (#1621) | +| Child Tax Credit | `child_tax_credit` | `would_claim_CTC` (formula) | `gov.dwp.tax_credits.child_tax_credit.takeup` | Follows reported receipt; scheme ended 2025-04-06 | +| Working Tax Credit | `working_tax_credit` | `would_claim_WTC` (formula) | `gov.dwp.tax_credits.working_tax_credit.takeup` | Follows reported receipt; scheme ended 2025-04-06 | +| Council Tax Reduction | `council_tax_reduction` | `would_claim_council_tax_reduction` (formula) | – | Local schemes simulated; take-up follows reported receipt (#1669 open) | +| Tax-Free Childcare | `tax_free_childcare` | `would_claim_tfc` (input) | – | Drawn in the data build at a flat rate | +| Universal childcare entitlement | `universal_childcare_entitlement` | `would_claim_universal_childcare` (input) | – | Drawn in the data build at a flat rate | +| Extended childcare entitlement | `extended_childcare_entitlement` | `would_claim_extended_childcare` (input) | – | Drawn in the data build at a flat rate | +| Targeted childcare entitlement | `targeted_childcare_entitlement` | `would_claim_targeted_childcare` (input) | – | Drawn in the data build at a flat rate | +| Marriage Allowance | `marriage_allowance` | `would_claim_marriage_allowance` (input) | `gov.hmrc.income_tax.allowances.marriage_allowance.takeup_rate` (0.5, the post-2019 steady state) | Drawn in the data build at a flat rate | +| Scottish Child Payment | `scottish_child_payment` | `would_claim_scp` (input) | `gov.social_security_scotland.scottish_child_payment.takeup_rate` (under 6, 6 and over) | Drawn in the data build by age | +| Carer's Allowance | `carers_allowance` | `would_claim_carers_allowance` (input) | – | Set from reported receipt in Microcosm; the Enhanced FRS does not set it, so it defaults to true | +| 16 to 19 Bursary Fund | `bursary_fund_16_to_19` | `would_claim_bursary_fund_16_to_19` (input) | – | Defaults to true | +| Student support grants | `adult_dependants_grant`, `childcare_grant`, `parents_learning_allowance`, `travel_grant`, `disabled_students_allowance` | `would_claim_adult_dependants_grant`, `would_claim_childcare_grant`, `would_claim_parents_learning_allowance`, `would_claim_travel_grant`, `would_claim_disabled_students_allowance` (inputs) | – | Default to true | +| Care to Learn | `care_to_learn` | `would_claim_care_to_learn` (input) | – | Defaults to true | +| State Pension | `state_pension` | n/a (effectively universal) | – | See [state-pension.md](../programs/gov/dwp/state-pension.md) | +| PIP / DLA / AA / SDA | various | none | – | Category inputs set from reported receipt; see [disability-legacy-benefits.md](../programs/gov/dwp/disability-legacy-benefits.md) | + +## Methodology + +For each programme above, the analysis should produce: + +1. **Seed take-up rate:** the rate the data build drew from for the + analysis year, plus the source (HMRC, DWP or an academic study) and + vintage. +2. **Effective take-up rate post-pipeline:** counted from the built + dataset as `sum(would_claim_X * weight) / sum(eligible_X * weight)`, + where `eligible_X` is computed from the runtime formulas. +3. **Published outturn share** for the same year: DWP and HMRC + statistical publications giving claimant counts and either eligible + population counts (where DWP estimates them) or a published take-up + rate. +4. **Pipeline drift:** `effective − seed` per programme, with a sign + convention so positive means more claimants than seeded. + +The published reference rates change roughly annually. The analysis +should be repeatable, ideally as a notebook under +[`docs/book/validation/`](../validation/) so future builds can re-run +it and update the gaps table. + +## Output + +A new validation notebook +`docs/book/validation/take-up-rates.ipynb` should: + +- pull each programme's seed take-up parameter, +- compute the effective rate against the built dataset, +- compare to the most recent published share (DWP take-up tables for + income-related benefits, HMRC Tax-Free Childcare and tax credits + statistics for the rest), +- flag drifts of more than ±2 percentage points for follow-up. + +The first run is the deliverable for #1354. After that the notebook +becomes a part of the validation page and gets re-run on each EFO / +data refresh. + +## Related work + +- **Reported-anchored take-up** is in both data builds for UC, Pension + Credit and Child Benefit. The programmes whose flags still follow + reported receipt at runtime are the most likely source of drift + between seed and effective rates. +- The **`would_claim_*` input-only conversion** (also in #1621) would + give the analysis a cleaner ladder: with all of `would_claim_*` as + inputs, the effective rate is exactly the share assigned by the + data build's take-up stage, and pipeline drift becomes a pure + reweighting question. + +## References + +- Issue: [#1354](https://github.com/PolicyEngine/policyengine-uk/issues/1354). +- Initial analysis (UC and CTC): +- DWP, [Income-related benefits: estimates of take-up](https://www.gov.uk/government/collections/income-related-benefits-estimates-of-take-up): the main reference rates for Housing Benefit, Income Support, Pension Credit and Universal Credit. +- HMRC, [Personal tax credits statistics](https://www.gov.uk/government/collections/personal-tax-credits-statistics). +- HMRC, [Child Benefit statistics](https://www.gov.uk/government/collections/child-benefit-statistics) and Marriage Allowance statistics. +- Pipeline alignment: [#1621](https://github.com/PolicyEngine/policyengine-uk/issues/1621). +- Cross-links: [`state-pension.md`](../programs/gov/dwp/state-pension.md), [`disability-legacy-benefits.md`](../programs/gov/dwp/disability-legacy-benefits.md), [`tax-credits.md`](../programs/gov/dwp/tax-credits.md). diff --git a/docs/book/assumptions/uk-pipeline-alignment-plan.md b/docs/book/assumptions/uk-pipeline-alignment-plan.md new file mode 100644 index 0000000000..34def30814 --- /dev/null +++ b/docs/book/assumptions/uk-pipeline-alignment-plan.md @@ -0,0 +1,173 @@ +# UK model/data pipeline alignment plan + +```{note} +**Planning page** setting out the gaps between the UK model and data +pipeline and the US `policyengine-us` / `policyengine-us-data` patterns, +plus a sequence of scoped PRs that would close them. Tracked in +[#1621](https://github.com/PolicyEngine/policyengine-uk/issues/1621). +Most items here are **cross-repo**: they touch the data pipeline rather +than this repo, but the model-side contract is documented here so the +receiving side has a stable target. +``` + +```{admonition} Status, October 2026 +The target data pipeline is now +[Microcosm UK](https://github.com/PolicyEngine/microcosm), which replaced +the Enhanced FRS build in `policyengine-uk-data`; its UK national line is +built on the Family Resources Survey 2024-25 (`microcosm_uk_2024_25`). +Gaps 2 and 3 below, and PR 4 of the sequence, are implemented there; +gap 1 is still open on the model side. The aggregate residuals are the +May 2026 Enhanced FRS measurements and have not been re-measured on the +Microcosm release. +``` + +## Architectural gaps vs US + +### 1. `_reported` consulted at runtime in some `would_claim_*` formulas + +**US pattern**: `_reported` columns are consulted **once, in the +data-build**, to anchor stochastic take-up flags (reported recipient → +takeup = True with certainty; rest filled probabilistically to a target +rate). Runtime formulas only read the pre-computed +`takes_up_X_if_eligible` flag, so reforms that change eligibility flow +through cleanly to take-up. + +**UK pattern (partial)**: some UK `would_claim_*` variables already +match: `would_claim_uc` and `would_claim_pc` are input-only with +`default_value = True`, populated by the data build (now Microcosm +UK's `frs_take_up` stage). But several still derive from +`_reported` at runtime: + +- `would_claim_housing_benefit` +- `would_claim_IS` +- `would_claim_WTC` +- `would_claim_CTC` + +The runtime pattern is `claims_all_entitled_benefits | (foo_reported > 0)`, +which means a reform expanding eligibility only reaches existing +`_reported > 0` claimants. Non-claimants who newly qualify are +invisible unless the user manually sets +`claims_all_entitled_benefits = True` everywhere. + +**Fix**: convert each to input-only `default_value = True` (matching +`would_claim_uc`), and add each to Microcosm UK's reported-anchored +take-up stages. Tax credits stopped paying ongoing awards on 5 April +2025, and `gov.dwp.tax_credits.active` is false from 2025-04-06 (see +[tax-credits.md](../programs/gov/dwp/tax-credits.md)), so the WTC and CTC +fix matters only for simulations of earlier years. + +### 2. Take-up assignment ignores reported data + +**Done in Microcosm UK.** Its `frs_take_up`, `pension_credit_take_up` +and `child_benefit_take_up` stages keep every benefit unit that reports +receipt as a claimant and draw the rest to the target rate +([`source_stages.json` at `75167a6`](https://github.com/PolicyEngine/microcosm/blob/75167a68/packages/microcosm-build/src/microcosm/build/uk/source_stages.json)). +The description below is the Enhanced FRS behaviour this replaced. + +The Enhanced FRS take-up assignment was a pure random draw: + +```python +pe_benunit["would_claim_uc"] = generator.random(len(pe_benunit)) < universal_credit_rate +``` + +This discards information: respondents who reported receiving UC +clearly took it up, and should have `would_claim_uc = True` with +certainty. + +**Fix**: port `policyengine_us_data/utils/takeup.py::assign_takeup_with_reported_anchors` +into `policyengine-uk-data` and apply it to each take-up variable where +a `_reported` column exists. This tightens per-variable calibration +without changing the target rates. + +### 3. Second-stage imputation gap on SPI donors + +**Done in Microcosm UK.** `hmrc_spi_income_spine` trains an FRS-only +second stage on the new SPI incomes (its outputs include `gift_aid` and +`charitable_investment_gifts`), `spi_housing_shell` redraws tenure, +dwelling and council tax band for the SPI households, and the wealth, +spending and VAT stages now run after the SPI block, so they are drawn +for the new incomes rather than inherited from the FRS parent. The +description below is the Enhanced FRS behaviour this replaced. + +`policyengine_uk_data/datasets/imputations/income.py::impute_income` +trains a QRF that replaces only six income variables on SPI donor +rows; everything else (rent, wealth, pension contributions, gift aid, +benefits-reported flags) stays as whatever middle-income FRS donor +happened to be sampled. So a synthetic SPI donor ending up with £2M +imputed self-employment income still carries an unchanged middle-FRS- +donor's savings balance and zero gift aid. + +The US equivalent ([`policyengine-us-data#589`](https://github.com/PolicyEngine/policyengine-us-data/pull/589)) +fixed this by adding a second-stage QRF: train on CPS with predictors = +demographics + newly-imputed income vars, outputs = the ~60 CPS-only +variables; for PUF-clone prediction, substitute imputed PUF incomes as +predictors so CPS-only variables come out consistent with imputed +income. + +**Fix**: add the analogous second-stage QRF in +`policyengine_uk_data/datasets/imputations/`, training on FRS with +predictors = demographics + 6 income vars, outputs = a wide set of +"FRS-only" consequential variables including rent, +`mortgage_interest_repayment`, `gift_aid`, `covenanted_payments`, +`charitable_investment_gifts`, `other_deductions`, +`employee_pension_contributions*`, `employer_pension_contributions`, +total wealth components, `capital_allowances`, `deficiency_relief`, +and the full list of `*_reported` benefits. + +## Specific aggregate residuals + +After the formula-side fixes already merged (BASIC/NEW classification +in [PR #1618](https://github.com/PolicyEngine/policyengine-uk/pull/1618), +new State Pension pro-rating + Protected Payment in +[PR #1634](https://github.com/PolicyEngine/policyengine-uk/pull/1634)), +the residual benefit-aggregate gaps against OBR on the Enhanced FRS +(measured May 2026, except the State Pension row, which is taken from +the State Pension page; none re-measured on Microcosm UK) were: + +| Variable | Model | Target | Gap | Likely cause | +|----------|------:|------:|----:|--------------| +| `income_support` | ~£1.07 bn | ~£0.2 bn | **+£0.9 bn** | Legacy benefit near-fully migrated; same `reported > 0` retention issue as the (pre-fix) tax credits. | +| `esa_contrib` | ~£5.56 bn | ~£2.5 bn | **+£3.1 bn** | Large. Likely reported-based take-up plus no phase-out for migrated cases. | +| `attendance_allowance` | ~£8.76 bn | ~£7.0 bn | +£1.8 bn | Cap-rate or take-up issue. | +| `tax_free_childcare` | ~£0.41 bn | ~£0.9 bn | **-£0.5 bn** | Understated by about half: under-imputed eligible families. | +| `state_pension` | £119.0 bn | £131.3 bn | -£12.2 bn | 2024-25, against DWP's Spring Forecast 2026 less payments abroad, on policyengine-uk-data-private 1.57.4, as documented in [state-pension.md](../programs/gov/dwp/state-pension.md). The shortfall arises in building the Enhanced FRS ([policyengine-uk-data#493](https://github.com/PolicyEngine/policyengine-uk-data/issues/493)). | + +Each warrants a scoped PR pair (model + data) once the architectural +fixes above land. + +## Items already fixed since #1621 was filed + +The issue body flagged three small bugs; all are now resolved: + +- `tax_credits/min_benefit.yaml` unit was `currency-USD`; it is now `currency-GBP`. +- `benunit_weekly_hours` label said "Average" but the formula sums; label now reads "Total weekly hours worked by adults in the benefit unit". +- `state_pension_type` BASIC/NEW classification (the `is_SP_age` heuristic in the issue) was rewritten in PR #1618. + +This page reflects the *current* gap rather than the issue body, so the +next reviewer working through the queue doesn't waste time on +already-fixed items. + +## Candidate PR sequence + +1. **Convert remaining `would_claim_*` formulas to input-only** (model + side) + reported-anchored assignment in Microcosm UK's `frs_take_up` + stage. Still open: the four formulas above read `_reported` at + runtime on `main`. +2. ~~Port `assign_takeup_with_reported_anchors`~~: done in Microcosm UK + (gap 2). +3. ~~Add second-stage QRF for FRS-only variables on SPI donors~~: done + in Microcosm UK (gap 3). +4. ~~Add `gift_aid` to `IMPUTATIONS`~~: done in Microcosm UK, where + `gift_aid` is a stage-2 output. +5. **Residual benefit-aggregate follow-ups**: re-measure on the + Microcosm release first, then separate small PRs for + IS phase-out (analogous to the WTC/CTC reactive scheme close-out), + ESA contrib investigation, AA calibration, TFC under-imputation, + the State Pension data-side fix (#1632, policyengine-uk-data#493). + +## References + +- Tracking issue: [#1621](https://github.com/PolicyEngine/policyengine-uk/issues/1621). +- US pattern reference: [`policyengine-us-data#589`](https://github.com/PolicyEngine/policyengine-us-data/pull/589). +- Related state pension docs: [state-pension.md](../programs/gov/dwp/state-pension.md), [#1632](https://github.com/PolicyEngine/policyengine-uk/issues/1632). +- Related calibration scope: [non-uk-benefit-receipt-plan.md](./non-uk-benefit-receipt-plan.md) (#842), separate from this pipeline alignment but interacting with it on the calibration target side. diff --git a/docs/book/policy/future-date-announcements.md b/docs/book/policy/future-date-announcements.md new file mode 100644 index 0000000000..b0649e838b --- /dev/null +++ b/docs/book/policy/future-date-announcements.md @@ -0,0 +1,141 @@ +# Future-dating policy announcements + +PolicyEngine UK supports modelling policy changes that have been +**announced but not yet implemented**, or that take effect at a known +future date. The mechanism is the same one that handles legislated rate +schedules: parameters in `policyengine_uk/parameters/` carry a list of +values keyed by effective date. + +This was originally requested in [#636](https://github.com/PolicyEngine/policyengine-uk/issues/636) +in the context of the Truss "growth plan" announcements in autumn 2022. +The feature has been live and well-exercised since the model moved to +date-keyed parameter values; this page documents the current usage +pattern so that future analyses don't need to reinvent it. + +## How it works + +Every parameter is a `values:` map from effective date to value. When +the tax-benefit system is built, `convert_to_fiscal_year_parameters` +(`policyengine_uk/utils/parameters.py`) turns each parameter into one +value per model year, where model year 2026 is the fiscal year 6 April +2026 to 5 April 2027: + +- by default, the value in force on 30 April of that year; +- for parameters with `fiscal_year_blend: true` in their metadata (fuel + duty, for example), the day-weighted average over the fiscal year, so a + change part-way through the year counts for the share of the year it + applies; +- parameters with `preserve_calendar_dates: true` keep their statutory + dates, and their formulas annualise explicitly. + +Example from +[`gov/hmrc/income_tax/allowances/personal_allowance/amount.yaml`](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/parameters/gov/hmrc/income_tax/allowances/personal_allowance/amount.yaml): + +```yaml +values: + ... + 2021-04-06: + value: 12_570 + ... + 2027-04-06: + value: 12_570 + ... + 2030-04-06: + value: 12_570 + metadata: + reference: + - title: Finance Act 2021 s. 5 (2), as amended by Finance Act 2026 s. 10 (3) + href: https://www.legislation.gov.uk/ukpga/2026/11/section/10 +``` + +The 2030-04-06 row records a future-dated decision: the freeze to +2030-31, announced at the 2025 Budget and since enacted by Finance Act +2026 s. 10. The file has no uprating. From the tax year after its last +row, `policyengine_uk/parameters/gov/hmrc/create_threshold_indexation.py` +applies the Income Tax Act 2007 s. 57 CPI indexation year by year. So +model years up to 2030 return £12,570, and indexation starts in 2031. + +## Patterns for common cases + +### Announced but reversed before taking effect + +Some announcements are walked back before the effective date. The +Truss "growth plan" basic-rate cut to 19% (announced September 2022, +reversed October 2022) is the canonical example. To represent this +honestly: + +1. Add the announced value at the announced effective date with a + reference to the announcement. +2. Add a *second* row at the reversal date with the original value (or + the actual landing value) and a reference to the reversal + announcement. + +The baseline then follows what was finally enacted. PolicyEngine UK has +no "policy as announced on date X" switch, so to score an announcement as +it stood before a reversal, apply it as a reform (see +the [excise duties page](../../engineering/excise-duties.md) for how +dated reforms are annualised). + +### Phased-in or staggered changes + +Phased rate changes are the most common future-dating pattern. Each +phase is a row at its effective date with a separate `reference`. The +`national_insurance/class_1/rates/employee/main.yaml` trajectory is a +good worked example: 13.25% from April 2022 under the Health and Social +Care Levy, back to 12% from November 2022 when the levy was repealed, +then the cuts to 10% in January 2024 and 8% in April 2024. + +### Announced abolitions + +For an abolition announced ahead of its effective date (for example the +2025 Budget removal of the two-child limit, in +[`gov/dwp/universal_credit/elements/child/limit/child_count.yaml`](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/parameters/gov/dwp/universal_credit/elements/child/limit/child_count.yaml)): + +- The relevant parameter (`gov.dwp.universal_credit.elements.child.limit.child_count`) + has its limiting value (2) up to the announced effective date + (2026-04-06) and `.inf` from that date onwards. +- Model year 2025 (fiscal year 2025-26) returns 2, so the limit applies. +- From model year 2026 the value is `.inf`, so the limit no longer + restricts the UC child element. + +The same pattern applies in reverse to one-off or time-limited +payments. The means-tested Cost-of-Living Payment in +[`gov/treasury/cost_of_living_support/means_tested_households/amount.yaml`](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/parameters/gov/treasury/cost_of_living_support/means_tested_households/amount.yaml) +is £650 from 2022, £900 from 2023 and goes back to zero from +2024-01-01. + +## Best practices + +1. **Cite the announcement in `metadata.reference`** at every + future-dated row. Where possible, cite both the announcement and + the legislation that enacts it once it is published. The 2022-04-06 + row of `national_insurance/class_1/rates/employee/main.yaml`, for + example, cites both the Health and Social Care Levy Act 2021 and the + Budget document. +2. **Don't extrapolate**. Each future-dated row should reflect a + specific announcement. If a parameter would otherwise move with + uprating, let the uprating handle it. Adding a "frozen forever" row is + a policy assertion that needs its own reference. +3. **Check how the parameter is annualised.** A change dated part-way + through a fiscal year only counts for part of the year if the + parameter has `fiscal_year_blend: true`; otherwise the model year takes + the value in force on 30 April. + +## What this isn't + +This is not the same as **scenario analysis** or **policy reform** +parameters under `gov.contrib.*`. Those are reform-side toggles for +hypothetical changes the user wants to evaluate; future-dated parameter +rows under `gov.hmrc.*`, `gov.dwp.*` etc. are **baseline** policy +representing what is actually announced. + +If a future change is uncertain or contested, model it under +`gov.contrib.*` until it's announced; once announced, move it into the +baseline tree with an effective-date row. + +## References + +- Issue [#636](https://github.com/PolicyEngine/policyengine-uk/issues/636): the original request, from the Truss growth plan period. +- PolicyEngine research: [Tax cuts in Prime Minister Truss's growth plan](https://www.policyengine.org/uk/research/tax-cuts-in-prime-minister-trusss-growth-plan), a worked example of modelling an announcement before its reversal. +- [PolicyEngine Core](https://github.com/PolicyEngine/policyengine-core): the date-keyed parameter mechanism underneath. +- `convert_to_fiscal_year_parameters` in `policyengine_uk/utils/parameters.py`: how dated values become model-year values. diff --git a/docs/book/policy/indirect-tax-effects-plan.md b/docs/book/policy/indirect-tax-effects-plan.md new file mode 100644 index 0000000000..7c1fb0b296 --- /dev/null +++ b/docs/book/policy/indirect-tax-effects-plan.md @@ -0,0 +1,136 @@ +# Indirect tax effects of reforms (planned) + +```{note} +**Planning page.** PolicyEngine UK already routes ad-hoc indirect-tax +revenue changes through `consumer_incident_tax_revenue_change` at the +household level, but this is a **manual** lever the user has to set via +the `gov.contrib.policyengine.budget.consumer_incident_tax_change` +parameter. There is no automatic pass-through from changes in direct +taxes (e.g. income tax, NI) to consumption-mediated indirect taxes. +This page sets out a proposed automatic route, tracked in +[#1114](https://github.com/PolicyEngine/policyengine-uk/issues/1114) (still +open, October 2026). +``` + +## What this is about + +When a direct tax reform changes a household's disposable income, real +households respond by adjusting consumption. That changes the indirect +tax revenue (VAT, excise duties, fuel duty) the household generates. +Right now PolicyEngine UK computes: + +1. **Direct effect**: the full structural change to income tax, NI, UC + and so on under the reform. +2. **Behavioural response on labour supply**: modelled with the + substitution and income elasticities under + `gov.simulation.labour_supply_responses` (`policyengine_uk/dynamics/labour_supply.py`, + `employment_income_behavioral_response`). +3. **Indirect-tax response**: *not modelled*. Households' consumption + bundles are read from the dataset and don't shift in response to + income changes. + +#1114 reports that UKMOD handles this in its indirect tax (TCO) module with +an aggregate **consumption elasticity of 0.8**: a 1% rise in net income gives a 0.8% rise in +consumption, which then flows through VAT and excise revenue. That figure +has not yet been checked against UKMOD's own documentation, so it needs a +primary source before it is used as a default. + +## Scope + +### Phase 1: household-level consumption response + +- New parameter `gov/simulation/indirect_tax_response/consumption_elasticity.yaml`, + defaulting to 0 (no response) until a sourced value such as UKMOD's is + confirmed and cited in its metadata. +- New variable `consumption_response_factor` (Household, YEAR) = + `1 + elasticity * (net_income_change / baseline_net_income)`. +- Apply the factor to the twelve COICOP categories that `consumption` + adds up (`food_and_non_alcoholic_beverages_consumption`, + `alcohol_and_tobacco_consumption`, `transport_consumption` and so on). + VAT reads `consumption` (through `full_rate_vat_consumption` and + `reduced_rate_vat_consumption`), so it picks up the change. +- The duties do not read those categories. Fuel duty reads + `petrol_litres` and `diesel_litres`, which divide the `petrol_spending` + and `diesel_spending` inputs by pump prices. Alcohol duty reads the + `*_litres` inputs under `variables/input/consumption/alcohol/`, and + tobacco duty reads `cigarettes_per_week` and the `*_grams_per_week` + inputs under `variables/input/consumption/tobacco/`. The same factor + needs to be applied to those spending and quantity inputs. + +### Phase 2: heterogeneity by category + +- A single economy-wide elasticity hides that some categories (food + staples, domestic energy) are nearly income-inelastic while others + (recreation, restaurants) are not. Replace the single + `consumption_elasticity.yaml` with a per-category set, calibrated to + ONS *Living Costs and Food Survey* income-elasticity estimates. + +### Phase 3: incidence link + +- Today's `consumer_incident_tax_revenue_change` distributes a + user-specified aggregate across households using consumption shares + (see + [`consumer_incident_tax_revenue_change.py`](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/variables/contrib/policyengine/consumer_incident_tax_revenue_change.py)). + In Phase 3 the variable derives its aggregate **from** the Phase-2 + variables so the user doesn't need to set a separate budget lever. + +## Implementation outline + +### Variables + +- `consumption_response_factor` (Household, YEAR), Phase 1. +- `disposable_income_change` (Household, YEAR): the difference between + reform and baseline `household_net_income`. The labour-supply module + already computes a clipped relative version of this per person + (`relative_income_change`, under + `variables/gov/simulation/labour_supply_response/`); Phase 1 needs a + household-level version in pounds. +- Optional `consumption_response_factor_by_category` (Household, + per category), Phase 2. + +### Hook points + +Each `*_consumption` input variable currently reads straight from the +dataset. Two options for the hook: + +1. **Decorate at the input layer**: add `defined_for` / + `default_formula` that multiplies by `consumption_response_factor`. + This is clean but ties the consumption inputs to indirect-tax + modelling. +2. **Insert a derived layer**: keep `*_consumption_baseline` (reads from + data) and introduce `*_consumption` formulas that apply the response + factor. This is the safer route: users who do not need the indirect + tax response keep the pre-reform consumption as + `*_consumption_baseline`. + +Recommendation: route (2). The dataset-side variable becomes +`*_consumption_baseline`; the response factor is applied at +`*_consumption`. + +### Tests + +- A reform that drops income tax for low-income households should + produce a *positive* `consumer_incident_tax_revenue_change` aggregate + (more take-home pay -> more consumption -> more VAT). +- A reform that raises NI should produce a *negative* aggregate. +- The aggregate should track elasticity × (sum of net income change × VAT-equivalent rate) + to within a small tolerance. + +## Open questions + +- Should the elasticity vary by household income decile? Empirical + evidence suggests low-income households have higher marginal + consumption propensity. +- Should saving be modelled explicitly (the residual after consumption)? + Phase 1 implicitly assumes any income change above the elasticity + applies to saving. +- How does this interact with the existing labour-supply response so + we don't double-count? + +## References + +- UKMOD documentation, to confirm the 0.8 consumption elasticity reported in #1114 (not yet checked). +- ONS [Living Costs and Food Survey](https://www.ons.gov.uk/peoplepopulationandcommunity/personalandhouseholdfinances/expenditure/bulletins/familyspendingintheuk/latest): income-elasticity calibration source for Phase 2. +- HMRC, [VAT annual statistics](https://www.gov.uk/government/statistics/value-added-tax-vat-annual-statistics): calibration target for the aggregate. +- Existing infrastructure: [`consumer_incident_tax_revenue_change`](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/variables/contrib/policyengine/consumer_incident_tax_revenue_change.py) and the `gov/simulation/labour_supply_responses/` parameter tree for the parallel labour-supply pattern. +- Related issue: [#1114](https://github.com/PolicyEngine/policyengine-uk/issues/1114). diff --git a/docs/book/programs/gov/cms/child-maintenance.md b/docs/book/programs/gov/cms/child-maintenance.md new file mode 100644 index 0000000000..7dc437cd20 --- /dev/null +++ b/docs/book/programs/gov/cms/child-maintenance.md @@ -0,0 +1,135 @@ +# Child maintenance (planned) + +```{warning} +**Not yet modelled in detail.** PolicyEngine UK has two generic person-level +inputs, `maintenance_income` and `maintenance_expenses`. They do not tell +child maintenance apart from maintenance between former partners, and the +model does not calculate a Child Maintenance Service (CMS) award. This page +sets out the scope for doing that, tracked in +[#669](https://github.com/PolicyEngine/policyengine-uk/issues/669) (still +open, October 2026). +``` + +## What the model does today + +- `maintenance_income` is added to `market_income`, `household_market_income` + and `hbai_household_net_income`. +- `maintenance_expenses` is subtracted in `market_income` and + `hbai_household_net_income`. +- Neither variable feeds any means test. Universal Credit unearned income is + the list in `gov.dwp.universal_credit.means_test.income_definitions.unearned`, + which contains no maintenance, and UC earned income does not net off + maintenance paid. +- `uc_deductions` does cover child maintenance collected from a paying + parent's UC award, but only in aggregate: it sits in the above-cap excess + of `uc_deduction_rate` with last resort deductions, estimated from DWP + deduction data rather than from `maintenance_expenses`. + +For **child** maintenance that is the right answer for UC: + +- **Receiving parent:** child maintenance is not unearned income under + [UC Regulations 2013 reg. 66(1)](https://www.legislation.gov.uk/uksi/2013/376/regulation/66), + so it is fully disregarded. +- **Paying parent:** maintenance paid is not deducted, so the paying parent is + means-tested on their full earnings. + +This asymmetry is the work-incentive problem described by the Centre for Social +Justice in [*The Hidden Parent Poverty Trap: Child Maintenance and Universal +Credit*][csj]. The model already reproduces it, because neither input reaches +UC. + +The gap is **maintenance between former spouses or civil partners**. +Reg. 66(1)(d) counts it as unearned income in UC, but the model cannot see it, +because it shares `maintenance_income` with child maintenance. Those +recipients' UC is overstated. + +## Scope + +### Phase 1: separate the inputs + +- New input `child_maintenance_received` (Person, GBP, annual). +- New input `child_maintenance_paid` (Person, GBP, annual). +- Keep `maintenance_income` and `maintenance_expenses` for maintenance + between former partners, and say so in their documentation. +- In the data pipeline + ([Microcosm UK](https://github.com/PolicyEngine/microcosm)), route the FRS + child-maintenance items to the new inputs and the spousal items to the + existing ones. +- HBAI income keeps both kinds, received and paid. + +### Phase 2: count spousal maintenance in UC + +- Add `maintenance_income` (now spousal only) to the UC unearned income list + from 2013-04-29, citing reg. 66(1)(d). +- Leave `child_maintenance_received` and `child_maintenance_paid` out of UC. +- Check the legacy benefits and Pension Credit the same way before adding + either input to their income definitions. + +### Phase 3: model the CMS calculation + +Derive the CMS weekly amount when the inputs are available, from the paying +parent's gross weekly income +([GOV.UK](https://www.gov.uk/how-child-maintenance-is-worked-out); +[Child Support Act 1991 Sch. 1](https://www.legislation.gov.uk/ukpga/1991/48/schedule/1)): + +| Paying parent's gross weekly income | Rate | Weekly amount | +|---|---|---| +| Below £7 | Nil | £0 | +| £7 to £100, or on benefits | Flat | £7 | +| £100.01 to £199.99 | Reduced | Formula | +| £200 to £3,000 | Basic | 12% / 16% / 19% of income up to £800 for 1 / 2 / 3+ children, then 9% / 12% / 15% of income from £800 to £3,000 | +| Above £3,000 | Basic, capped | Income above £3,000 is ignored; the receiving parent can apply to the courts | +| Not enough information | Default | £38 / £51 / £64 | + +Then: + +- **Shared care:** the amount falls by 1/7 for 52 to 103 nights a year, 2/7 + for 104 to 155, 3/7 for 156 to 174, and by half plus £7 a week for 175 or + more. +- **Other children:** an adjustment applies for other children living with + the paying parent. + +The income bands are fixed in legislation, not uprated each year. + +Phase 3 is optional for UC analysis, which needs only Phases 1 and 2. It +allows reforms to the CMS rates or bands to be scored. + +## Implementation outline + +### Parameters under `gov/cms/` + +- `income_bands/`: the £7, £100, £200, £800 and £3,000 limits. +- `rate/flat.yaml`, `rate/default/` and `rate/basic/` (12/16/19% and + 9/12/15%). +- `rate/reduced/`: the reduced-rate formula. +- `shared_care/`: the night bands and fractions. + +### Variables under `variables/gov/cms/` + +- `child_maintenance_gross_weekly_income` (Person) +- `child_maintenance_obligation_pre_shared_care` (Person) +- `child_maintenance_shared_care_reduction` (Person) +- `child_maintenance_paid_cms_basis` (Person, derived award) + +## Data needs + +- **FRS** records maintenance received and paid at person level. Phase 1 + needs a check of how far its source items separate child maintenance + from spousal maintenance. +- **DWP [Child Maintenance Service statistics][cms-stats]** (quarterly, latest + data to June 2026) give the CMS caseload and amounts. They support checking + the new inputs, bearing in mind that many families use private + ("family-based") arrangements outside the CMS. + +## References + +- Centre for Social Justice, [The Hidden Parent Poverty Trap: Child + Maintenance and Universal Credit][csj]. +- GOV.UK, [How child maintenance is worked out](https://www.gov.uk/how-child-maintenance-is-worked-out). +- [Child Support Act 1991](https://www.legislation.gov.uk/ukpga/1991/48/contents) and [Schedule 1](https://www.legislation.gov.uk/ukpga/1991/48/schedule/1). +- [The Child Support Maintenance Calculation Regulations 2012 (SI 2012/2677)](https://www.legislation.gov.uk/uksi/2012/2677/contents). +- [The Universal Credit Regulations 2013, reg. 66](https://www.legislation.gov.uk/uksi/2013/376/regulation/66). +- DWP, [Child Maintenance Service statistics][cms-stats]. + +[csj]: https://www.centreforsocialjustice.org.uk/library/the-hidden-parent-poverty-trap-child-maintenance-and-universal-credit +[cms-stats]: https://www.gov.uk/government/collections/statistics-on-the-2012-statutory-child-maintenance-scheme diff --git a/docs/book/programs/gov/dfe/regional-childcare-plan.md b/docs/book/programs/gov/dfe/regional-childcare-plan.md new file mode 100644 index 0000000000..225a5291e6 --- /dev/null +++ b/docs/book/programs/gov/dfe/regional-childcare-plan.md @@ -0,0 +1,170 @@ +# Regional childcare programmes (planned) + +```{warning} +**Partially modelled.** PolicyEngine UK models the **England** funded +childcare programmes (Universal, Extended and Targeted Childcare +Entitlements; Care to Learn; Student Finance England Childcare Grant) +and the **UK-wide** Tax-Free Childcare scheme. The devolved +administrations operate parallel schemes that are not yet modelled +beyond the Scottish Universal Funded ELC for 3- and 4-year-olds added +in [#1643][pr-1643]. This page captures the proposed scope, tracked +under [#1008](https://github.com/PolicyEngine/policyengine-uk/issues/1008) +and the broader [#1644 audit][issue-1644]. +``` + +## What is modelled today + +The current childcare coverage is captured in +[`policyengine_uk/programs.yaml`](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/programs.yaml) (see the +[#1644 split into per-programme rows][issue-1644]) and works as follows: + +- **UK-wide**: Tax-Free Childcare (HMRC); Universal Credit childcare + element (DWP); Working Tax Credit childcare element (legacy, HMRC + administered, modelled under `gov/dwp/`; see + [tax-credits.md](./../dwp/tax-credits.md)). +- **England-only** (gate on `country == ENGLAND`): + Universal Childcare Entitlement (15 hours for 3- and 4-year-olds), + Extended Childcare Entitlement (working parents, now reaching down to + children aged nine months), Targeted Childcare Entitlement (2-year-olds + in families on qualifying benefits or low incomes), Care to Learn, + Student Finance England Childcare Grant. +- **Scotland**: eligibility for the universal funded ELC for 3- and + 4-year-olds (`scottish_funded_early_learning_childcare_eligible`, + including the statutory start-date rule for 3-year-olds) and its 1,140 + hours a year, added in [#1643][pr-1643]. No funding value is attached + yet, so it does not reach household income. +- **Wales, Northern Ireland**: no programmes modelled. + +## Proposed scope by jurisdiction + +### Scotland: three remaining strands + +The [#1644 audit][issue-1644] flagged three Scottish gaps: + +1. **Eligible-two-year-old strand** of the funded ELC programme + (vulnerable or low-income 2-year-olds under the + [Provision of Early Learning and Childcare (Specified Children) + (Scotland) Order 2014][sssi-196-2014]). Modelled the same way as the + 3- and 4-year-old strand but gated on UC, tax credits or other + qualifying benefits. +2. **Student Awards Agency Scotland (SAAS) Lone Parents' Grant, + Dependants' Grant, and Childcare Fund** for full-time HE students: + the Scottish equivalent of Student Finance England's Childcare + Grant. Income-tested. +3. **Support for young parents in further education**, the role Care to + Learn plays in England. The [Education Maintenance Allowance][scot-ema] + still runs in Scotland for 16- to 19-year-olds, applied for through + local councils and colleges, but it is a maintenance payment rather + than childcare support, so the childcare side needs its own scoping. + +### Wales + +The audit identified four Welsh programmes: + +1. **Childcare Offer for Wales**: 30 hours a week for working parents + of 3- and 4-year-olds (term time plus some holiday weeks). + Modelled the same as the English Extended Childcare Entitlement + with Welsh-specific income tests and hour caps. Statutory basis: + [Childcare Funding (Wales) Act 2019][wales-act]. +2. **Flying Start**: a universal offer in disadvantaged areas for + 2- to 3-year-olds (12.5 hours a week). Eligibility is geographic, + based on lower layer super output areas. +3. **Student Finance Wales Childcare Grant**, parallel to the English + Childcare Grant. +4. **Welsh EMA**, parallel to the Scottish EMA. + +### Northern Ireland + +The audit identified four NI programmes: + +1. **Pre-school Education Programme (PSEP)**: 12.5 hours a week in the + year before school. Universal. +2. **Sure Start**: younger children in disadvantaged areas, including a + programme for 2-year-olds. +3. **Student Finance NI Childcare Grant**. +4. **NI EMA**. + +## Implementation outline + +For each new programme: + +- New parameter tree under `parameters/gov///` + capturing the income tests, hours offered, and weekly funding rates. +- New variable(s) under `variables/gov///` gated on + the appropriate `country == ...` check. +- A `_eligible` companion variable that the new programme + variable composes with the rate parameter, the same pattern as the + English entitlement trio. +- A `programs.yaml` row per programme, bound to the new variable and + parameter prefix. + +### Structural items deferred to this work + +The [#1644 audit][issue-1644] also flagged two structural items that +become more pressing once the regional programmes land: + +- `weeks_per_year` and `childcare_funding_rate` currently sit at + `gov.dfe.*` root but encode English term-week and funding-rate + assumptions. They should be regionalised (for example + `gov.scottish_government.funded_early_learning_childcare.weeks_per_year`) + once the Scotland, Wales and NI programmes are modelled. +- No aggregating variable across programmes exists (for example "total + subsidised childcare hours per child per week"). The API surfaces + the per-programme variables but can't answer "what's this family's + combined childcare support" without the caller summing. + +Both belong in a separate cleanup PR rather than mixed into the +per-programme work. + +## Data needs + +Most regional programmes are point-in-time per-hour funding rather +than per-household payments, so the data side is comparatively easy: + +- **FRS** provides `region` and `country` at the household level, + enough to gate eligibility. +- **Welsh Government Statistical Bulletins** publish Flying Start + and Childcare Offer caseloads by local authority. +- **Scottish Government Childcare and Early Years statistics** provide + Scottish funded ELC caseloads. +- **Department of Education NI** publishes PSEP caseloads. +- The SAAS, Student Finance Wales and NI childcare grants need the same + treatment as the English `childcare_grant`: calibration against + published award totals. + +The aggregating challenge is choosing whether to expose **subsidised +hours** or **subsidised cost** as the primary household-level +variable. Recommendation: subsidised cost, since that is how the English +entitlements already enter `household_benefits` and `gov_spending`. + +## Open questions + +- Should the Scottish ELC universal strand and the eligible-two-year-old + strand be modelled as two separate variables (matching how England + splits Universal / Extended / Targeted), or as one variable with the + age-and-eligibility logic inside? Recommendation: separate, mirroring + the English pattern for queryability. +- Welsh Flying Start is geographic. Microcosm UK, the dataset being + built to replace the Enhanced FRS, assigns each household in England + and Wales to a 2021 Output Area, which nests in LSOAs, so eligibility + by area becomes possible on that dataset once the Flying Start area + list is sourced. +- EMA ended in England but still runs in Scotland, Wales and Northern + Ireland. Today `child_ema` and `adult_ema` are reported amounts with + no formula. Should the devolved schemes be modelled under the existing + `gov.dfe.care_to_learn` umbrella or as separate regional schemes? + +## References + +- Audit: [#1644](https://github.com/PolicyEngine/policyengine-uk/issues/1644), the full childcare coverage audit. +- Tracking: [#1008](https://github.com/PolicyEngine/policyengine-uk/issues/1008), the original Scotland, Wales and NI gap report. +- Recent landed work: [#1643][pr-1643], Scottish universal funded ELC. +- Existing England docs (for the pattern to mirror): the entitlement variables under [`policyengine_uk/variables/gov/dfe/`](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/variables/gov/dfe). +- Welsh childcare statute: [Childcare Funding (Wales) Act 2019][wales-act]. +- Scottish eligible-2-year-old statute: [SSSI 2014/196][sssi-196-2014]. + +[issue-1644]: https://github.com/PolicyEngine/policyengine-uk/issues/1644 +[pr-1643]: https://github.com/PolicyEngine/policyengine-uk/pull/1643 +[wales-act]: https://www.legislation.gov.uk/anaw/2019/1/contents +[sssi-196-2014]: https://www.legislation.gov.uk/ssi/2014/196/contents/made +[scot-ema]: https://www.mygov.scot/ema diff --git a/docs/book/programs/gov/dwp/bereavement-support-plan.md b/docs/book/programs/gov/dwp/bereavement-support-plan.md new file mode 100644 index 0000000000..86da510bd1 --- /dev/null +++ b/docs/book/programs/gov/dwp/bereavement-support-plan.md @@ -0,0 +1,162 @@ +# Bereavement support: BSP formula and historical WPA (planned) + +```{warning} +**Currently reported-only.** PolicyEngine UK models the Bereavement +Support Payment (BSP) as `adds = ["bsp_reported"]`, so the value +flows through from the dataset, with no derived eligibility or +rule-based amount. This page captures the proposed formula-based +extension covering both BSP and the legacy Widowed Parent's Allowance +(WPA) for historical analysis. Tracks +[#466](https://github.com/PolicyEngine/policyengine-uk/issues/466) and +the related historical-WPA ask in +[#465](https://github.com/PolicyEngine/policyengine-uk/issues/465). +Updated October 2026: still reported-only; the facts below were checked +against GOV.UK and legislation.gov.uk. The data notes cover both the +certified Enhanced FRS (policyengine-uk-data) and Microcosm UK, the +dataset being built to replace it. +``` + +## What's currently modelled + +Just the FRS-reported amount: + +```python +class bsp(Variable): + value_type = float + entity = Person + label = "Bereavement Support Payment" + definition_period = YEAR + unit = GBP + + adds = ["bsp_reported"] +``` + +That means: + +- Reform analyses changing BSP rates have no household-level surface. +- Anyone bereaved after the FRS reference week never appears as a BSP + recipient even if eligible. +- The legacy WPA has no variable of its own. Both the Enhanced FRS + ([`frs.py`](https://github.com/PolicyEngine/policyengine-uk-data/blob/main/policyengine_uk_data/datasets/frs.py)) + and Microcosm UK fill `bsp_reported` from FRS benefit codes 6 and 9 + ([`frs_spine.py`](https://github.com/PolicyEngine/microcosm/blob/75167a68/packages/microcosm-build/src/microcosm/build/uk_runtime/frs_spine.py#L825)), + and Microcosm's own + [source notes](https://github.com/PolicyEngine/microcosm/blob/d1fda720/packages/microcosm-build/src/microcosm/build/uk/spec/sources.yaml#L1870) + say code 6 mixes benefits and code 9 is tax-free War Widow's Pension. So the reported "BSP" also carries legacy + widow's benefits and War Widow's Pension. + +## Proposed scope + +### Phase 1: rule-based BSP formula + +The BSP under the [Pensions Act 2014, Part 5][pa-2014-5] and the +[Bereavement Support Payment Regulations 2017][bsp-regs] (SI 2017/410) +pays: + +- A **lump sum** of either £2,500 (standard rate) or £3,500 (higher + rate, with a dependent child). +- **18 monthly payments** of £100 (standard) or £350 (higher), for a + death on or after 6 April 2017 + ([GOV.UK](https://www.gov.uk/bereavement-support-payment/what-youll-get)). + +The higher rate applies if, when the partner died, the claimant was +getting (or entitled to) Child Benefit for a child living with them, or +was pregnant. The +[Bereavement Benefits (Remedial) Order 2023](https://www.legislation.gov.uk/uksi/2023/134/contents) +(SI 2023/134) extended BSP and WPA to cohabiting partners with +children, with effect back to 30 August 2018, after the Supreme Court's +2018 McLaughlin judgment. It didn't change the number of payments. + +To model this we need: + +- `is_bereaved_recently` (Person, YEAR): a new input flagging + whether the person has been bereaved in the last 18 months. +- `bsp_higher_rate_eligible` (Person, YEAR): whether they have a + dependent child. +- `bsp_months_remaining` (Person, YEAR): how many of the 18 monthly + instalments fall within the simulation year. + +The BSP formula then becomes: + +``` +bsp = ( + lump_sum_amount * is_bereaved_recently + + monthly_amount * bsp_months_remaining +) +``` + +Both `lump_sum_amount` and `monthly_amount` parameter trees split on +`bsp_higher_rate_eligible` (£3,500 / £350 vs £2,500 / £100). + +### Phase 2: historical WPA under the same formula + +Widowed Parent's Allowance (under [Social Security Contributions and +Benefits Act 1992 s. 39A][sscba-39a]) was paid for deaths from April +2001 up to 5 April 2017. The benefit: + +- Continued for as long as the surviving spouse had a dependent child. +- Was uprated annually. +- Was replaced by BSP for deaths from 6 April 2017. + +For historical analyses the model should: + +- Add `wpa_eligible` (Person, YEAR): partner died between April 2001 + and 5 April 2017, the survivor is under State Pension age and is + entitled to Child Benefit. +- Add `widowed_parents_allowance` (Person, YEAR) as a derived + formula reading the WPA rate parameter (which would need + backfilling under `gov/dwp/wpa/rate.yaml`). + +WPA has no new awards, but existing awards continue while the +surviving parent has a dependent child, so some recipients remain. In +the current data they sit inside `bsp_reported` (see above), so they +are already counted in income; the gap is that they can't be told +apart from BSP or reformed separately. + +### Phase 3: data-side bereavement flag + +`is_bereaved_recently` doesn't exist in the FRS. The FRS is a +cross-section, not a panel, and doesn't record when a partner died, so +the flag would need imputing in the data (Microcosm UK, as the +replacement dataset): + +- start from FRS respondents who report BSP (once it is separated from + the other codes, as above) and from widowed respondents; +- calibrate to DWP's BSP and WPA caseloads ([Stat-Xplore](https://stat-xplore.dwp.gov.uk/)) by age, sex + and region. + +This is the heaviest of the three phases; without it the new +variables work for the household calculator (which can input the flag +directly) but not for microsim aggregates. + +## Open questions + +- BSP isn't uprated each year. The rates have been £2,500/£3,500 and + £100/£350 since 2017. The model's `bsp_reported` is nonetheless + uprated with CPI, which suits the War Widow's Pension and legacy + widow's benefits it also contains but not BSP itself. Should the new + parameter tree carry an explicit + `uprating: gov.economic_assumptions.indices.obr.consumer_price_index` + to mirror what other DWP rates do, or hold flat to match the + historical reality? +- WPA recipients who would have been on BSP under the new rules: do we + want a reform-side parameter "treat all WPA receipts as if they were + BSP" for counterfactual analysis? +- The 2023 Remedial Order extended BSP to cohabiting partners with + children, with effect from 30 August 2018. Eligibility should not + require marriage or civil partnership for deaths from that date where + there are children. + +## References + +- [Pensions Act 2014, Part 5](https://www.legislation.gov.uk/ukpga/2014/19/part/5): primary statute for BSP. +- [Bereavement Support Payment Regulations 2017 (SI 2017/410)][bsp-regs]. +- [Bereavement Benefits (Remedial) Order 2023 (SI 2023/134)](https://www.legislation.gov.uk/uksi/2023/134/contents): cohabiting-parent extension, effective from 30 August 2018. +- [SSCBA 1992 s. 39A][sscba-39a]: legacy WPA basis. +- gov.uk, [Bereavement Support Payment](https://www.gov.uk/bereavement-support-payment) and [Widowed Parent's Allowance](https://www.gov.uk/widowed-parents-allowance). +- DWP, [Stat-Xplore](https://stat-xplore.dwp.gov.uk/): Bereavement Support Payment and Widowed Parent's Allowance caseloads. +- Issues: [#466](https://github.com/PolicyEngine/policyengine-uk/issues/466) (BSP formula), [#465](https://github.com/PolicyEngine/policyengine-uk/issues/465) (historical WPA). + +[pa-2014-5]: https://www.legislation.gov.uk/ukpga/2014/19/part/5 +[bsp-regs]: https://www.legislation.gov.uk/uksi/2017/410/contents +[sscba-39a]: https://www.legislation.gov.uk/ukpga/1992/4/section/39A diff --git a/docs/book/programs/gov/dwp/cold-weather-payment-plan.md b/docs/book/programs/gov/dwp/cold-weather-payment-plan.md new file mode 100644 index 0000000000..b24fd6315b --- /dev/null +++ b/docs/book/programs/gov/dwp/cold-weather-payment-plan.md @@ -0,0 +1,152 @@ +# Cold Weather Payment (planned) + +```{warning} +**Not yet modelled.** The Cold Weather Payment (CWP) is a DWP payment +of **£25 per 7-day period of very cold weather** to qualifying +households in England and Wales. Northern Ireland runs its own scheme +on the same terms (1 November 2026 to 31 March 2027, +[nidirect](https://www.nidirect.gov.uk/articles/cold-weather-payment)). +PolicyEngine UK doesn't model it. This page sets out the proposed +scope, tracked in +[#435](https://github.com/PolicyEngine/policyengine-uk/issues/435). + +In Scotland the equivalent is the **Winter Heating Payment**, a flat +annual payment (£62.00 from April 2026, regulation 9 of +[SSI 2023/16](https://www.legislation.gov.uk/ssi/2023/16/contents) as +amended), which +replaced Cold Weather Payment there in 2023. It isn't modelled either. +The separate Pension Age Winter Heating Payment, which replaced Winter +Fuel Payment in Scotland, is modelled under +`gov.social_security_scotland.pawhp`. Updated October 2026: amounts, +legislation and the Scotland and Northern Ireland position checked. +``` + +## What CWP is + +Defined in [The Social Fund Cold Weather Payments (General) +Regulations 1988][cwp-1988] (SI 1988/1724). A household qualifies if +**all** of the following hold: + +- The address is in a postcode linked to a weather station where the + average temperature is recorded as, or forecast to be, **0°C or below + over 7 consecutive days** between 1 November and 31 March. +- A member of the benefit unit receives a **qualifying benefit**: + Pension Credit; income-related ESA in the support or work-related + activity group, or with a severe or enhanced disability premium, a + pensioner premium, a disabled child or a child under 5 (the + Regulations extend the same premium test to Income Support and + income-based JSA); Universal Credit when neither the claimant nor a + partner is employed or gainfully self-employed and someone has + limited capability for work or there is a child under 5, or, whether + working or not, when the award includes a disabled child amount; or + Support for Mortgage Interest with one of those premiums or children. + The [GOV.UK eligibility page](https://www.gov.uk/cold-weather-payment/eligibility) + has the exact conditions. + +The payment is £25 per 7-day cold period, and each further cold +period triggers another payment. Spending varies a lot with the winter: in +2024-25, for example, about 1.4 million payments worth around £35 +million were made in England and Wales +([DWP, April 2025](https://www.gov.uk/government/news/over-35-million-in-cold-weather-payments-support-paid-this-winter)). +DWP doesn't count actual payments through the winter; it publishes the +estimated number of eligible people at the start of each season and the +weekly triggers, from which payments can be estimated. + +## Why this is harder than other DWP benefits + +The eligibility test combines an administrative benefit check (clean +for PolicyEngine) with a **geographic weather event** (no FRS +analogue). Modelling the weather side has three plausible approaches: + +1. **Expected-value parameterisation**: a single national + `cold_weather_payments_expected_periods` parameter capturing the + long-run-average number of 7-day cold periods triggered per winter. + Multiply by £25 and by the qualifying household flag to produce the expected + annual payment. Easy to implement, accurate in aggregate, wrong in + any specific winter. +2. **Regional weather mixing**: a per-region weather-event + distribution (for example, the North East has more cold periods than London on + average), parameterised from Met Office historic data. Captures the + distributional skew without forecasting any particular winter. +3. **Year-specific lookups**: tabulate the DWP-published *Cold Weather + Payment statistics* by region by year and use those as historical + inputs. Most accurate for back-cast simulations; needs ongoing + updating. + +Recommendation: **option 2 (regional mixing)** for prospective analysis +and option 3 for back-casts where the data is available. + +## Proposed scope + +### Phase 1: qualifying-benefit gate and expected-value payment + +- New parameter `gov.dwp.cold_weather_payment.amount` = £25. The amount + was lower in earlier years, so take any backdated values from the + amendment history of SI 1988/1724. +- New parameter + `gov.dwp.cold_weather_payment.expected_periods_per_year` = + long-run national average from DWP CWP statistics. +- New variable `cold_weather_payment_eligible` (BenUnit, YEAR) that + fires if any benefit unit member receives one of the qualifying benefits + with the relevant addition / age trigger. +- New variable `cold_weather_payment` (BenUnit, YEAR) = `amount` × + `expected_periods_per_year` × `eligible`. + +This delivers a model that gets the policy-relevant slice right +(*who* qualifies for CWP and how it interacts with their other +benefits) at the cost of smoothing year-to-year weather variance. + +### Phase 2: regional weather mixing + +Add a `gov.dwp.cold_weather_payment.expected_periods_by_region` table +parameterised from Met Office observation data, and a household-side +lookup by `region`. + +### Phase 3: Scotland's Winter Heating Payment + +Paid by Social Security Scotland under +[The Winter Heating Assistance (Low Income) (Scotland) Regulations 2023](https://www.legislation.gov.uk/ssi/2023/16/contents) +(SSI 2023/16). The qualifying benefits are essentially the same as for +Cold Weather Payment (Pension Credit with no further condition; the +other benefits with the same disability, pensioner-premium or +child-under-5 conditions), assessed in a qualifying week (2 to 8 +November in 2026), but the payment is a flat annual amount with no +weather trigger. So: + +- `winter_heating_payment_eligible`: the Phase 1 eligibility rule plus + residence in Scotland. Reuse the Phase 1 variable rather than a second + copy of the benefit conditions. +- `winter_heating_payment` = `amount` × eligible, with `amount` + uprated each year (£62.00 from April 2026). + +## Data needs + +- DWP, [Cold Weather Payment statistics](https://www.gov.uk/government/collections/social-fund-cold-weather-payments): estimated eligible numbers at the start of each season and weekly triggers by weather station. The main calibration source. +- Social Security Scotland's Winter Heating Payment statistics: Phase 3 calibration. +- Met Office, [UK climate historic stations](https://www.metoffice.gov.uk/research/climate/maps-and-data/historic-station-data): Phase 2 regional expected-period parameterisation. + +The FRS gives `region` cleanly, so the geographic gate is no extra +data work for Phases 1 and 2. + +## Open questions + +- The qualifying-benefit list is narrower than for the Warm Home + Discount in England and Wales, which now covers everyone on + Universal Credit, Housing Benefit, income-related ESA or Pension + Credit. Model the eligibility as one composite variable, shared with + the Scottish Winter Heating Payment, or as separate flags by route + (Universal Credit, legacy benefits, Pension Credit)? +- Spending is small next to most modelled benefits, but the payment + matters to the low-income pensioners and disabled households it + targets. Phase 1 is worth doing even if Phase 2 doesn't follow. + +## References + +- [The Social Fund Cold Weather Payments (General) Regulations 1988 (SI 1988/1724)][cwp-1988]. +- gov.uk, [Cold Weather Payment](https://www.gov.uk/cold-weather-payment). +- DWP, [Cold Weather Payment statistics](https://www.gov.uk/government/collections/social-fund-cold-weather-payments). +- Scottish equivalent: [The Winter Heating Assistance (Low Income) (Scotland) Regulations 2023 (SSI 2023/16)](https://www.legislation.gov.uk/ssi/2023/16/contents); [mygov.scot](https://www.mygov.scot/winter-heating-payment). +- Northern Ireland: [nidirect, Cold Weather Payment](https://www.nidirect.gov.uk/articles/cold-weather-payment). +- Issue: [#435](https://github.com/PolicyEngine/policyengine-uk/issues/435). + +[cwp-1988]: https://www.legislation.gov.uk/uksi/1988/1724 diff --git a/docs/book/programs/gov/dwp/pip-points-plan.md b/docs/book/programs/gov/dwp/pip-points-plan.md new file mode 100644 index 0000000000..33afd2ef86 --- /dev/null +++ b/docs/book/programs/gov/dwp/pip-points-plan.md @@ -0,0 +1,171 @@ +# PIP point system (planned) + +```{warning} +**Not yet modelled.** PolicyEngine UK currently takes the PIP daily-living +and mobility **band** as a categorical input (`pip_dl_category` and +`pip_m_category`, each `STANDARD | ENHANCED | NONE`) and multiplies by +the published weekly rate. The **points-system** logic that determines +which band a claimant falls into (ten daily-living activities and two +mobility activities, with descriptors worth 0 to 12 points under the +legislation) is **not** modelled. +This page captures the proposed scope, tracked under +[#1069](https://github.com/PolicyEngine/policyengine-uk/issues/1069). +``` + +```{admonition} Status, October 2026 +A first step is open as +[#1661](https://github.com/PolicyEngine/policyengine-uk/pull/1661): total +points inputs (`pip_dl_points`, `pip_m_points`) and the 8- and 12-point +thresholds under `gov.dwp.pip.{daily_living,mobility}.points_thresholds`, +with the category derived from points only when it is not given as an +input. That is a simpler form of Phase 1 below: no per-activity scores +and no `use_point_system` switch, since a category input already takes +precedence over the formula. + +PR #1661 is waiting on a data-side path. Its companion, +[policyengine-uk-data #408](https://github.com/PolicyEngine/policyengine-uk-data/pull/408) +(points imputed from the observed categories), was closed in June 2026 +in the uk-data archive clean-up, with PIP points derivation to move into +Microcosm UK's source stages +([microcosm #145](https://github.com/PolicyEngine/microcosm/issues/145)) +once the input variables exist here. +``` + +## What PIP scoring actually is + +The Personal Independence Payment is awarded on a **points-based** +assessment defined in Schedule 1 to [SI 2013/377][si-377] (the Social +Security (Personal Independence Payment) Regulations 2013): + +- **Part 2: Daily living activities**: ten activities (preparing food, + taking nutrition, managing therapy, washing and bathing, managing + toilet needs, dressing and undressing, communicating verbally, + reading and understanding signs and symbols, engaging with other + people face-to-face, making budgeting decisions). Each activity has + a set of descriptors with point values from 0 up to 12; the top + value varies by activity. +- **Part 3: Mobility activities**: two activities (planning and + following a journey, moving around), each with descriptors worth up + to 12 points. + +The award rule: + +| Total points (per component) | Award | +|-----------------------------|-------| +| 0–7 | None | +| 8–11 | Standard rate | +| 12+ | Enhanced rate | + +Daily living and mobility are scored independently, so a claimant can be +on standard daily-living and enhanced mobility, or any other +combination. + +## Why the current categorical model is reasonable for microsim + +For population-aggregate simulation, the categorical bands are +sufficient because: + +- the FRS doesn't capture activity-level point scores at all, +- DWP's published caseload reports band counts (standard / enhanced) + rather than activity scores, +- the data (both the certified Enhanced FRS from policyengine-uk-data + and Microcosm UK, which is being built to replace it) sets + `pip_dl_category` and `pip_m_category` by comparing the weekly PIP daily-living and mobility + amounts respondents report in the FRS with the standard and enhanced + rates. + +So for headline distributional and fiscal analysis the categorical +inputs are correct. + +## Where the point system would matter + +Three reform scenarios that the categorical model can't answer: + +1. **Activity-level descriptor reform**: for example, the April 2024 + green paper consulted on changing the descriptors, and the March 2025 *Pathways to + Work* green paper proposed requiring at least four points in one + daily-living activity for the daily-living component. The government + withdrew the four-point change in July 2025, pending a review of the + PIP assessment. A categorical input can't capture + this; the categorical band wouldn't shift unless an analyst manually + re-bands a synthetic distribution. +2. **Threshold reform**: changing the band thresholds (for example raising + the standard-rate floor from 8 to 10 points). Same limitation. +3. **Targeted analysis by descriptor**: distributional questions like + "how does the enhanced cohort split between mobility activities" need + the underlying activity scores. + +## Proposed scope + +### Phase 1: points-to-category mapping + +- Add the threshold parameters under `gov/dwp/pip/`: + - `daily_living/standard_threshold.yaml` (8) + - `daily_living/enhanced_threshold.yaml` (12) + - `mobility/standard_threshold.yaml` (8) + - `mobility/enhanced_threshold.yaml` (12) +- Add per-activity score inputs as `pip_dl_activity_1_score` ... + `pip_dl_activity_10_score`, `pip_m_activity_1_score`, + `pip_m_activity_2_score`. Type: integer (Person, YEAR), default 0. +- Add derived variables `pip_dl_total_score` and `pip_m_total_score` + summing the activity scores. +- Add `pip_dl_category_from_points` and `pip_m_category_from_points` + that look up the band from the total score against the threshold + parameters. +- Keep `pip_dl_category` / `pip_m_category` as the **input** the rest of + the model reads, with a new switch parameter + `gov.dwp.pip.use_point_system` (default `false`) that, when `true`, + overrides the categorical input with the points-derived band. + +This is a strict superset of the current model: the categorical input +remains authoritative by default, and analysts opting into the points +machinery (for reform analysis) get the activity-level surface. + +### Phase 2: descriptor-level parameters + +- Per-activity descriptor tables under `gov/dwp/pip/descriptors/`, + encoding the point value of each descriptor in SI 2013/377. The + representation can be flat (one YAML per activity with a list of + `(descriptor_id, points)` entries). +- A `pip_dl_activity_1_descriptor` (etc.) enum input that selects which + descriptor applies; the activity score is then a parameter lookup. +- Reform-side: descriptor point values are then parameters that can be + perturbed for "what if descriptor X scored 4 instead of 6" analyses. + +### Phase 3: data-side imputation + +- The FRS provides PIP payment amounts but not descriptor selections. + The data (Microcosm UK, as the replacement dataset) would need to + impute descriptor selections per claimant that are consistent with the observed band. The plausible + approach is to draw uniformly across descriptor combinations that + sum to the observed band, weighted by DWP-published "activities most + commonly scoring above zero" tables. +- This phase is the heaviest and is only required to make Phase 2 + reforms work on the calibrated microsim. For the household + calculator, users can input activity scores directly. + +## Open questions + +- Should the score inputs default to `None` (sentinel) or 0? Sentinel + is cleaner when the points machinery is opt-in via the + `use_point_system` flag. +- DWP's PIP review process distinguishes "ongoing" vs "fixed-term" + awards. Should the model expose the award duration, or treat all + current awards as ongoing? (Current model assumes ongoing.) +- The outcome of the PIP assessment review may change descriptors or + the scoring rule; the descriptor-parameter design in Phase 2 should + make such changes a parameter edit. + +## References + +- [The Social Security (Personal Independence Payment) Regulations 2013 (SI 2013/377)][si-377]: Schedule 1, Parts 2 and 3 define the point system. +- DWP, [Personal Independence Payment: Assessment guide][assess-guide]. +- DWP, [PIP statistics](https://www.gov.uk/government/collections/personal-independence-payment-statistics: caseload by component and band. +- DWP, [Modernising support for independent living: the health and disability green paper (April 2024)][green-paper]: proposals on the descriptors. +- DWP, [Pathways to Work: Reforming Benefits and Support to Get Britain Working (March 2025)](https://www.gov.uk/government/consultations/pathways-to-work-reforming-benefits-and-support-to-get-britain-working-green-paper): the four-point proposal. +- Issue: [#1069](https://github.com/PolicyEngine/policyengine-uk/issues/1069). +- Existing model: `pip_dl_category`, `pip_m_category`, `pip_dl`, `pip_m`. + +[si-377]: https://www.legislation.gov.uk/uksi/2013/377 +[assess-guide]: https://www.gov.uk/government/publications/personal-independence-payment-assessment-guide-for-assessment-providers +[green-paper]: https://www.gov.uk/government/consultations/modernising-support-for-independent-living-the-health-and-disability-green-paper diff --git a/docs/book/programs/gov/hmrc/firm-vat.md b/docs/book/programs/gov/hmrc/firm-vat.md new file mode 100644 index 0000000000..9ae9e79977 --- /dev/null +++ b/docs/book/programs/gov/hmrc/firm-vat.md @@ -0,0 +1,203 @@ +# Firm-level VAT (planned) + +```{warning} +**Not yet modelled.** PolicyEngine UK models VAT only on *household +consumption*: the `vat` variable applies `gov.hmrc.vat.standard_rate` and +`gov.hmrc.vat.reduced_rate` to each household's standard- and reduced-rate +spending (`full_rate_vat_consumption`, `reduced_rate_vat_consumption`), then +grosses the result up to VAT receipts. There is no business side. This page +sets out the scope for adding firm-level VAT and a `Firm` entity, tracked in +[#1320](https://github.com/PolicyEngine/policyengine-uk/issues/1320) and +[#1333](https://github.com/PolicyEngine/policyengine-uk/issues/1333). Both +issues are still open (October 2026). +``` + +## Why firm-level VAT? + +Household-side VAT in PolicyEngine answers the question "what does this +household pay in VAT on the goods and services it buys?" but it can't +answer policy questions framed at the business side: + +- How does changing the VAT registration threshold (£90,000 of taxable + turnover since 1 April 2024) affect the number of firms in scope, and through + them the consumer prices that households face? +- What's the revenue effect of moving a supply between standard, reduced + and zero rates seen from the *supplier* side (input/output netting)? +- How does the flat-rate scheme distort the modelled compliance picture? + +Closing those questions needs a separate entity that *carries* turnover, +sector, and registration status, and variables for input/output VAT and +net liability. + +## Scope + +### Phase 1: minimum viable product + +1. **`Firm` entity** with the bare attributes needed for the threshold + and rate questions: `firm_turnover`, `firm_industry_sector`, + `firm_vat_registered`. Firms are linked to households for sole + traders and partnerships (unincorporated business income flows + already exist in the FRS) and stand alone for incorporated entities. +2. **Registration threshold logic**: a firm must register if its taxable + turnover is more than a new `gov.hmrc.vat.firm.registration_threshold` + parameter, or it can register voluntarily. The threshold has been + £90,000 since 1 April 2024. +3. **Standard / reduced / zero-rated supply split** at the firm level, + driven by sector-specific share parameters. +4. **`firm_vat_on_sales`** (output VAT) and **`firm_vat_on_purchases`** + (input VAT), each computed from share-based decompositions of + turnover and intermediate consumption. +5. **`firm_net_vat_liability`** = output VAT − input VAT. + +### Phase 2: distributional refinements + +- **Flat-rate scheme** for small businesses (a new + `gov.hmrc.vat.firm.flat_rate_scheme` parameter tree). +- **VAT groups** and partial exemption (de minimis rules for + exempt-versus-taxable splits). +- **Cross-border VAT** after Brexit (Northern Ireland under the Windsor + Framework, postponed VAT accounting on imports). + +### Phase 3: incidence + +- **Pass-through** of VAT rate changes to consumer prices, joining up + the household-side `vat` variable and the firm-side + `firm_net_vat_liability` through an explicit incidence assumption + (already modelled in spirit via `vat_change` on the household side; + Phase 3 promotes it to a first-class link). + +## Implementation plan + +### Entity + +A new `Firm` entity in `policyengine_uk/entities.py` next to the +existing `Household`, `BenUnit`, `Person`: + +```python +Firm = build_entity( + key="firm", + plural="firms", + label="Firm", + doc="A business entity that may register for VAT.", + roles=[ + { + "key": "owner", + "plural": "owners", + "label": "Owner", + "doc": "Person or household that owns the firm.", + } + ], + containing_entities=["state"], +) +``` + +The `containing_entities` set deliberately does *not* include +`household` because incorporated firms have no household; the link for +sole traders is via an explicit `firm_owning_household_id` input +variable rather than entity nesting. + +### Variables + +Under a new `variables/gov/hmrc/vat/firm/` tree: + +- `firm_turnover` (input) +- `firm_industry_sector` (input, enum) +- `firm_vat_registered` (derived from turnover and the threshold; can + also be set as input for voluntary registration) +- `firm_standard_rated_share`, `firm_reduced_rated_share`, + `firm_zero_rated_share`, `firm_exempt_share` (derived from sector or + input override) +- `firm_vat_on_sales` (output VAT) +- `firm_vat_on_purchases` (input VAT) +- `firm_net_vat_liability` (output − input) + +The household-side `vat` variable stays untouched; the two sides only +meet in Phase 3. + +### Parameters + +A new tree under `parameters/gov/hmrc/vat/firm/`: + +- `registration_threshold.yaml`: £90,000 from 1 April 2024. +- `deregistration_threshold.yaml`: £88,000 from 1 April 2024. +- Rates: reference the existing `gov/hmrc/vat/standard_rate.yaml` and + `reduced_rate.yaml` rather than duplicating them. Zero-rated supplies + need no rate parameter. +- `sector_supply_split/`: share parameters by industry sector for the + standard/reduced/zero/exempt split (Phase 1.5 refinement; MVP can use + a single national split per sector). +- `flat_rate_scheme/`: Phase 2. + +### Tests + +- Unit tests around the threshold (firm just below, just above, just + above the deregistration threshold). +- Tests of rate-split arithmetic for each sector. +- Integration test asserting that summing `firm_net_vat_liability` + across a representative firm panel reproduces published HMRC VAT + receipts to within calibration tolerance. + +## Data needs + +This is the harder half of the project. Summary of where each Phase 1 +component sits against our current data: + +| Component | Public source | FRS coverage | Datalab source | +|-----------|---------------|--------------|----------------| +| Firm turnover distribution | ONS *Business Demography* | sole-trader / partnership turnover via FRS self-employment income | HMRC Datalab: anonymised VAT returns | +| Industry sector | SIC codes from ONS *Inter-Departmental Business Register (IDBR)* | not in FRS | HMRC Datalab | +| VAT registration share | HMRC *VAT statistics* (annual) | not in FRS | HMRC Datalab | +| Input VAT shares | ONS *Supply and Use tables* | not applicable | HMRC Datalab | + +### Proposed imputation approach + +1. Build a **synthetic firm panel** weighted to ONS Business Demography + counts by SIC sector and turnover band. +2. Attach to each firm a registration status, supply split, and + intermediate-consumption ratio drawn from HMRC VAT statistics and + ONS Supply and Use tables. +3. Calibrate the panel so that summed `firm_net_vat_liability` matches + the HMRC VAT receipts outturn for the year. This mirrors the + second-stage imputation pattern discussed in + [#1621](https://github.com/PolicyEngine/policyengine-uk/issues/1621). +4. For sole traders and partnerships, link the synthetic firm row back + to the FRS household that owns it via self-employment income. + +The companion repo [`policyengine/uk-vatlab`][vatlab] hosts the analysis +files (#1333) used to size each step before they are productionised. + +## Effort estimate + +- Adding the `Firm` entity, `firm_turnover` input, threshold logic, and + a single-rate liability variable: ~1–2 days. +- Phase 1 MVP (full Firm entity, sector-driven supply split, calibrated + synthetic firm panel, threshold/rate tests, integration test against + HMRC outturn): ~3–4 weeks, with most time on the data side. +- Phase 2 (flat-rate scheme, VAT groups, partial exemption): another + 2–3 weeks once the panel is stable. +- Phase 3 (cross-side incidence): tied to the + `consumer_incident_tax_revenue_change` infrastructure already in the + repo; the link itself is small but the elasticity assumptions need + agreement. + +## Open questions + +- Should the synthetic firm panel live in the data pipeline or in this + repo? Recommendation: in the data pipeline + ([Microcosm UK](https://github.com/PolicyEngine/microcosm)), with the + rules variables here. +- Do we need to model Making Tax Digital filing-frequency mechanics, or + is annualised liability sufficient for nowcasting purposes? +- HMRC Datalab access for anonymised VAT returns: pursue, or stick to + ONS / public-aggregate calibration? + +## References + +- HMRC, [VAT rates](https://www.gov.uk/vat-rates) and [VAT registration thresholds](https://www.gov.uk/vat-registration-thresholds). +- [Value Added Tax Act 1994](https://www.legislation.gov.uk/ukpga/1994/23/contents): primary statute. +- HMRC, [VAT statistics](https://www.gov.uk/government/statistics/value-added-tax-vat-annual-statistics): annual outturn used for calibration. +- ONS, [UK Business: Activity, Size and Location](https://www.ons.gov.uk/businessindustryandtrade/business/activitysizeandlocation) (the IDBR-based business demography release). +- ONS, [Supply and Use tables](https://www.ons.gov.uk/economy/nationalaccounts/supplyandusetables): intermediate consumption shares. +- Companion analysis repo: [`policyengine/uk-vatlab`][vatlab] (#1333). + +[vatlab]: https://github.com/policyengine/uk-vatlab diff --git a/docs/book/programs/gov/hmrc/pension-tax-relief.md b/docs/book/programs/gov/hmrc/pension-tax-relief.md new file mode 100644 index 0000000000..bf7c3a7f07 --- /dev/null +++ b/docs/book/programs/gov/hmrc/pension-tax-relief.md @@ -0,0 +1,124 @@ +# Pension tax relief + +This page documents how PolicyEngine UK models tax relief on personal +pension contributions and addresses the confusion reported in +[#704](https://github.com/PolicyEngine/policyengine-uk/issues/704) +about whether higher-rate taxpayers receive the right amount of +relief. #704 is still open (October 2026); the figures below were re-run +on policyengine-uk 2.126.12. + +## What the model does + +`pension_contributions_relief` (defined in +[`variables/gov/hmrc/pensions/pension_contributions_relief.py`](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/variables/gov/hmrc/pensions/pension_contributions_relief.py)) +reduces the person's taxable income by their pension contributions, +limited to: + +- the person's employment plus self-employment income; +- then the higher of `pension_annual_allowance` and + `gov.hmrc.income_tax.reliefs.pension_contribution.basic_amount` (£3,600); +- and only for people under `gov.hmrc.pensions.pension_contributions_relief_age_limit` + (75). + +The law limits relief to the higher of relevant UK earnings and £3,600 +gross ([Finance Act 2004 s. 190](https://www.legislation.gov.uk/ukpga/2004/12/section/190)). +The model applies the earnings limit first, so someone with no earnings gets +no relief. That makes no difference to their income tax, which is zero +anyway, but it means the basic-rate top-up paid into their pension under +relief at source is not modelled. + +`pension_annual_allowance` is the standard annual allowance (£60,000 from +6 April 2023), tapered by £1 for every £2 of adjusted net income above +£260,000 down to a minimum of £10,000 (parameters under +`gov.hmrc.income_tax.allowances.annual_allowance`). The model tapers on +`adjusted_net_income`, which stands in for the statutory "adjusted +income". The money purchase annual allowance is not modelled. + +Contributions **above** the annual allowance trigger a separate charge, +`personal_pension_contributions_tax`, which taxes the excess at the +person's marginal rate and is added to income tax. The amount tested +against the allowance is `pension_contributions_for_annual_allowance`, +which counts employee, personal, employer and salary sacrifice +contributions +([Finance Act 2004 s. 233](https://www.legislation.gov.uk/ukpga/2004/12/section/233)). + +One gap: the relief does not reduce `adjusted_net_income`, so contributions +do not restore personal allowance lost above £100,000, as they do in law +([Income Tax Act 2007 s. 58](https://www.legislation.gov.uk/ukpga/2007/3/section/58)). +An employee on £110,000 keeps a £7,570 personal allowance in 2025-26 whether +or not they contribute £10,000. + +So the headline behaviour is: **within the annual allowance, pension +contributions reduce taxable income £1-for-£1**. The income-tax saving +that the model reports is the contribution × the person's marginal +rate. + +## Worked examples + +The four scenarios reported in #704 (all 2025-26, all self-employed, +single person aged 40, no other income, contributions entered as +`personal_pension_contributions`). The "model" column was re-run in +October 2026 on policyengine-uk 2.126.12: + +| ID | Self-employed income | Pension contribution | Expected income-tax change | What the model returns | Why | +|-------|---------------------|----------------------|-----------------------------|------------------------|-----| +| 11800 | £30,000 | £0 | baseline | baseline | – | +| 11799 | £30,000 | £3,000 | -£600 | -£600 | £3k contribution × 20% basic rate | +| 11817 | £60,000 | £0 | baseline | baseline | – | +| 11816 | £60,000 | £6,000 | -£2,400 | -£2,400 | £6k contribution × 40% higher rate | + +This is exactly the behaviour reported. The user's expectation that +higher-rate cases should only see -£1,200 (£6k × 20%) corresponds to a +misunderstanding of how UK pension tax relief works in aggregate: the +total tax relief is full marginal rate, even though it is paid in two +parts in the real world (relief at source plus higher-rate top-up via +Self Assessment). + +## Relief at source vs marginal-rate relief + +The user-facing UK pension framework has two parallel mechanisms that +add up to the same total relief: + +1. **Relief at source (RAS)**: the pension provider claims the + basic-rate portion (20%) directly from HMRC. The contributor pays + from net income and the pension grows by the gross amount. For a + basic-rate taxpayer, this is the *entire* relief. +2. **Higher-rate top-up**: for taxpayers at higher or additional + rates, the extra band difference (20% for higher rate, 25% for + additional) is reclaimed via Self Assessment. The mechanism is + technically a **basic-rate-band extension**: HMRC widens the basic + rate band by the gross contribution, pushing income that would + otherwise be taxed at the higher rate down into the basic band. + +PolicyEngine computes the **total** relief (both mechanisms together) +because it operates on annual aggregates rather than walking the +cash-flow steps. That matches HMRC's published statistics on the +*fiscal cost of pension tax relief*, which also report the total +across both mechanisms. + +If you want to see the user-visible tax bill reduction separately from +the pension-provider's RAS claim, you'd need to split the existing +relief into two output variables (a basic-rate-portion and a +higher-rate-top-up portion). This is a presentation question rather +than a calculation question: the totals are unchanged. + +## Marginal tax rate display + +#704 also reported a 0% marginal tax rate where 20% or 40% was +expected. The model's `marginal_tax_rate` variable +([`variables/household/marginal_tax_rate.py`](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/variables/household/marginal_tax_rate.py)) +raises each adult's `employment_income` by +`gov.simulation.marginal_tax_rate_delta` (£1,000, or 0.1% of employment +income where that is larger) and measures the share of the rise that +does not reach `household_net_income`. Re-run in October 2026, it returns +20% for both £30,000 cases and 40% for both £60,000 cases, so the 0% +reported in #704 no longer reproduces in the model. + +## References + +- HMRC, [Tax on your private pension contributions: Tax relief](https://www.gov.uk/tax-on-your-private-pension/pension-tax-relief): user-facing guide to relief at source and higher-rate relief. +- [Finance Act 2004 s. 188-194](https://www.legislation.gov.uk/ukpga/2004/12/section/188): primary statute for relief at source. +- [Finance Act 2004 s. 227](https://www.legislation.gov.uk/ukpga/2004/12/section/227): annual allowance excess charge. +- HMRC, [Personal pensions statistics](https://www.gov.uk/government/collections/personal-pensions-statistics): including the published cost of pension tax relief. +- [Finance Act 2004 s. 190](https://www.legislation.gov.uk/ukpga/2004/12/section/190): the earnings-or-£3,600 limit on relief. +- Model variables: [`pension_contributions_relief`](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/variables/gov/hmrc/pensions/pension_contributions_relief.py) and [`personal_pension_contributions_tax`](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/variables/gov/hmrc/pensions/private_pension_contributions_tax.py). diff --git a/docs/book/programs/gov/hmrc/pensions-comparison-stbm.md b/docs/book/programs/gov/hmrc/pensions-comparison-stbm.md new file mode 100644 index 0000000000..39f18ab016 --- /dev/null +++ b/docs/book/programs/gov/hmrc/pensions-comparison-stbm.md @@ -0,0 +1,96 @@ +# Pension treatment compared with the Scottish Tax-Benefit Model + +This page compares how PolicyEngine UK and the +[Scottish Tax-Benefit Model (STBM)][stbm] treat personal pension +contributions in income tax, for +[#672](https://github.com/PolicyEngine/policyengine-uk/issues/672) (still open, +October 2026). + +The STBM column is taken from its source code at commit +[`f534ca1`][stbm-it] (`src/IncomeTaxCalculations.jl`, +`calculate_pension_taxation!`, and `src/Pensions.jl`). The +[STBM blog post on pension contributions][stbm-blog] that #672 links to is a +short note, and its author says they are unsure of it. It does not describe +the method, so this page does not rely on it. + +[How PolicyEngine UK models pension tax relief](./pension-tax-relief.md) has +more detail on the PolicyEngine side. + +## Summary + +| | PolicyEngine UK | STBM | +|---|---|---| +| How relief is given | Reduces taxable income by the contribution (`pension_contributions_relief`) | Treats contributions as paid net of basic rate: grosses them up by 1/(1 − basic rate) and widens every rate band above the starting band by the gross amount, as under relief at source | +| Contributions that get relief | The person's own contributions | Employee contributions, additional voluntary contributions **and employer contributions** | +| Basic-rate relief added to the pot | Not shown separately | Recorded separately (`pension_relief_at_source`) | +| Earnings limit | Relief capped at employment plus self-employment income | No earnings cap; £3,600 limit when total income is below it | +| Annual allowance | £60,000, tapered on adjusted net income above £260,000 (`pension_annual_allowance`) | Tapered on total income above a threshold-income parameter | +| Contributions above the allowance | Charged at the marginal rate (`personal_pension_contributions_tax`) | Relief stops at the allowance; no separate charge | +| Employer contributions in the data | Read from the data (`employer_pension_contributions`) | Imputed when missing from the FRS, using the ONS ASHE employer-contribution bands (`src/Pensions.jl`) | +| Carry-forward of unused allowance | Not modelled | Not modelled | + +## Where the two models differ + +### 1. Reducing taxable income versus widening the bands + +For an income-tax payer whose contribution stays inside one band, the two +methods give the same total relief. They can differ when the person pays +little or no income tax: STBM still records the basic-rate addition to the +pot, which PolicyEngine UK does not show. + +**Shared gap: the personal allowance taper.** In law, gross pension +contributions reduce adjusted net income +([Income Tax Act 2007 s. 58](https://www.legislation.gov.uk/ukpga/2007/3/section/58)), +so contributing can restore personal allowance lost above £100,000. +Neither model does this. In PolicyEngine UK, `adjusted_net_income` adds up +the taxable income components without deducting `pension_contributions_relief`: +an employee on £110,000 has adjusted net income of £110,000 and a personal +allowance of £7,570 in 2025-26 whether or not they contribute £10,000. +STBM widens the bands but leaves the taper income unchanged, and its code +flags this as unchecked. + +### 2. Employer contributions + +STBM counts employer contributions in the relief calculation. In UK law +employer contributions are not taxed as the employee's income, so they get +no personal relief. They are simply outside taxable pay, which is how +PolicyEngine UK treats them. Including them in STBM's relief widens the +employee's bands further than the law does. + +### 3. Annual allowance taper + +The legal taper uses "adjusted income", which adds employer contributions, +and applies only when "threshold income" is also above £200,000. + +- PolicyEngine UK tapers on adjusted net income, with no threshold-income + test. +- STBM tapers on total income above a threshold parameter. + +Neither follows the statute exactly. Both affect only very high earners. + +### 4. Lifetime and lump sum allowances + +The lifetime allowance was abolished from 6 April 2024 and replaced by the +lump sum allowance and the lump sum and death benefit allowance. +PolicyEngine UK has no parameters for any of these. They apply when pensions +are drawn, not to contributions. + +## What neither model does + +- Carry-forward of unused annual allowance from the previous three years. +- Behavioural responses: contributions are held fixed when tax rates change. +- Decumulation: crystallisation, the tax-free lump sum, and annuity versus + drawdown choices. + +## References + +- STBM source: [`IncomeTaxCalculations.jl`][stbm-it] and [`Pensions.jl`][stbm-pen] at `f534ca1`. +- [STBM blog: pension contributions][stbm-blog]. +- PolicyEngine UK: [pension tax relief](./pension-tax-relief.md); variables [`pension_contributions_relief`](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/variables/gov/hmrc/pensions/pension_contributions_relief.py), [`pension_annual_allowance`](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/variables/gov/hmrc/income_tax/allowances/pension_annual_allowance.py) and [`personal_pension_contributions_tax`](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/variables/gov/hmrc/pensions/private_pension_contributions_tax.py). +- HMRC, [Pensions Tax Manual: annual allowance taper](https://www.gov.uk/hmrc-internal-manuals/pensions-tax-manual/ptm057100). +- Issue: [#672](https://github.com/PolicyEngine/policyengine-uk/issues/672). + +[stbm]: https://github.com/grahamstark/ScottishTaxBenefitModel.jl +[stbm-it]: https://github.com/grahamstark/ScottishTaxBenefitModel.jl/blob/f534ca1277260a6b4f24c9ab5ebfe9c015e1484d/src/IncomeTaxCalculations.jl +[stbm-pen]: https://github.com/grahamstark/ScottishTaxBenefitModel.jl/blob/f534ca1277260a6b4f24c9ab5ebfe9c015e1484d/src/Pensions.jl +[stbm-blog]: https://stb-blog.virtual-worlds.scot/articles/2022/01/01/pension-contributions.html diff --git a/docs/book/programs/gov/ofgem/warm-home-discount-plan.md b/docs/book/programs/gov/ofgem/warm-home-discount-plan.md new file mode 100644 index 0000000000..151871ce26 --- /dev/null +++ b/docs/book/programs/gov/ofgem/warm-home-discount-plan.md @@ -0,0 +1,136 @@ +# Warm Home Discount (planned) + +```{warning} +**Not yet modelled.** The Warm Home Discount (WHD) is a £150 rebate off +a household's electricity bill each winter, paid by energy suppliers +under schemes that Ofgem administers. PolicyEngine UK doesn't model it. +This page sets out the proposed scope, tracked in +[#502](https://github.com/PolicyEngine/policyengine-uk/issues/502). +Updated October 2026: in England and Wales eligibility is now based on +benefit receipt alone, which makes the scheme much easier to model than +when this page was first drafted. +``` + +## The scheme now + +### England and Wales + +[The Warm Home Discount (England and Wales) Regulations 2026](https://www.legislation.gov.uk/uksi/2026/389/contents) +(SI 2026/389) continue the scheme from 1 April 2026 to 31 March 2031. +They replace the +[2022 Regulations](https://www.legislation.gov.uk/uksi/2022/772/contents) +(SI 2022/772), which applied to both England and Wales. + +Under the 2022 Regulations, the means-tested group had to pass a +"high cost to heat" test based on property data. For winter 2025-26 +that test was removed, so every household on a qualifying means-tested +benefit became eligible. The government estimated this would bring +2.7 million more households into the scheme, taking it to more than +6 million households in Great Britain +([government response, June 2025](https://assets.publishing.service.gov.uk/media/6852e6e9679778c74ec15e82/expanding-the-warm-home-discount-scheme-2025-to-2026-government-response.pdf)). + +For winter 2026-27, GOV.UK +([eligibility in England and Wales](https://www.gov.uk/the-warm-home-discount-scheme/if-you-live-in-england-and-wales)) +says a household qualifies if, on the qualifying date of 23 August +2026, its electricity supplier is in the scheme, the person's (or +their partner's) name is on the electricity bill, and the person or +their partner gets one of: + +- Universal Credit; +- Housing Benefit; +- income-related Employment and Support Allowance; +- Pension Credit. + +The rebate is applied automatically. + +### Scotland + +The scheme runs under separate Scottish regulations. +[The Warm Home Discount (Scotland) Regulations 2026](https://www.legislation.gov.uk/uksi/2026/488/contents) +(SI 2026/488) came into force on 1 May 2026 and run the scheme to +31 March 2031, replacing the +[2022 Regulations](https://www.legislation.gov.uk/uksi/2022/1073/contents) +(SI 2022/1073). Both 2026 Regulations set the rebate at £150. For winter 2026-27 +([eligibility in Scotland](https://www.gov.uk/the-warm-home-discount-scheme/if-you-live-in-scotland)): + +- **Core group, automatic:** Pension Credit; Universal Credit with an + extra amount for a disability or health condition; income-related ESA + with a disability or pensioner premium; or a Support for Mortgage + Interest loan with one of those premiums. Also people responsible for + a child under 5 who are unemployed and on Universal Credit or + income-related ESA, or have a Support for Mortgage Interest loan. +- **Broader group, by application** to the energy supplier, which can + set its own extra criteria. + +The qualifying date is also 23 August 2026. + +### Northern Ireland + +The scheme doesn't operate in Northern Ireland: its primary legislation +extends to Great Britain only. + +## Proposed scope + +### Phase 1: England and Wales + +Now a rule on benefit receipt: + +- Parameter `gov.ofgem.warm_home_discount.amount`: £150, the + "prescribed rebate" in both sets of 2026 Regulations (£140 before + winter 2022-23). +- Parameter listing the qualifying benefits, so a reform can widen or + narrow the list. +- Variable `warm_home_discount` (Household, YEAR): the amount if the + household is in England or Wales and any benefit unit receives a + qualifying benefit. Read receipt from the modelled benefits, for + example `universal_credit`, `housing_benefit`, `esa_income` and + `pension_credit`, so that take-up carries through. +- No separate take-up flag, since the rebate is paid without a claim. + +### Phase 2: Scotland core group + +The core group can be built from the same benefit variables plus the +Universal Credit disability elements, the ESA premiums and the age of +the youngest child. + +### Phase 3: Scotland broader group + +Application-based, so it needs a take-up rate calibrated to Ofgem's +published recipient numbers for Scotland, on top of the eligible group. + +## Data and calibration + +- Ofgem's [Warm Home Discount pages](https://www.ofgem.gov.uk/environmental-and-social-schemes/warm-home-discount-whd): + recipients and spend by group and country. +- DWP benefit caseloads, already used to calibrate the qualifying + benefits. +- The 2025-26 impact assessment and government response give the + expected number of recipients after the expansion. Use them to check + Phase 1's aggregate. + +## Open questions + +- **Timing.** The scheme year runs from October to March, with the + qualifying date in August. A model year (April to March) contains one + winter's rebate, so pay the rebate in the year that contains the + winter. +- **Who counts as the household.** The rebate attaches to an + electricity account. In multi-benefit-unit households, decide whether + one qualifying benefit unit is enough (simplest, and likely right for + most households) and say so. +- **Electricity VAT interaction.** The rebate reduces the bill. If the + model ever nets it off energy spending, it would interact with the + domestic electricity VAT work in + [#2189](https://github.com/PolicyEngine/policyengine-uk/pull/2189). + Modelled as a benefit, it doesn't. + +## References + +- [The Warm Home Discount (England and Wales) Regulations 2026 (SI 2026/389)](https://www.legislation.gov.uk/uksi/2026/389/contents). +- [The Warm Home Discount (England and Wales) Regulations 2022 (SI 2022/772)](https://www.legislation.gov.uk/uksi/2022/772/contents). +- [The Warm Home Discount (Scotland) Regulations 2026 (SI 2026/488)](https://www.legislation.gov.uk/uksi/2026/488/contents). +- [The Warm Home Discount (Scotland) Regulations 2022 (SI 2022/1073)](https://www.legislation.gov.uk/uksi/2022/1073/contents). +- DESNZ, [Expanding the Warm Home Discount scheme, 2025 to 2026: government response](https://assets.publishing.service.gov.uk/media/6852e6e9679778c74ec15e82/expanding-the-warm-home-discount-scheme-2025-to-2026-government-response.pdf). +- GOV.UK, [Warm Home Discount Scheme](https://www.gov.uk/the-warm-home-discount-scheme). +- Ofgem, [Warm Home Discount scheme pages and reports](https://www.ofgem.gov.uk/environmental-and-social-schemes/warm-home-discount-whd). +- Issue: [#502](https://github.com/PolicyEngine/policyengine-uk/issues/502). diff --git a/docs/book/validation/child-poverty-tcl.md b/docs/book/validation/child-poverty-tcl.md new file mode 100644 index 0000000000..8948ebf2bd --- /dev/null +++ b/docs/book/validation/child-poverty-tcl.md @@ -0,0 +1,120 @@ +# Child poverty validation: removing the two-child limit (2029-30) + +This page compares PolicyEngine UK's estimate of the child-poverty +impact and fiscal cost of removing the [Universal Credit two-child +limit][gov-2cl] against the published estimates from the Department for +Work and Pensions, the Office for Budget Responsibility and the Resolution +Foundation. Budget 2025 announced the removal with effect from 6 April +2026, which the +`gov.dwp.universal_credit.elements.child.limit.child_count` parameter +encodes (`.inf` from `2026-04-06`). + +Tracks [#1398](https://github.com/PolicyEngine/policyengine-uk/issues/1398). + +## Results + +| Source | Children lifted from poverty (2029-30) | Treasury cost (2029-30) | +|--------|----------------------------------------:|------------------------:| +| [DWP poverty impact assessment][gov-2cl] (children); [Budget 2025, Table 1.2][budget-2025] (cost, OBR-certified) | 450,000 (AHC); 300,000 (BHC) | £3.1 bn | +| [Resolution Foundation *No half measures*][rf-nhm] | 480,000 (AHC) | £3.5 bn | +| **PolicyEngine UK, Microcosm UK 2024-25 (AHC)** | **~463,000** | **£3.26 bn** | +| **PolicyEngine UK, Microcosm UK 2024-25 (BHC)** | **~336,000** | (same) | +| PolicyEngine UK, Enhanced FRS 2022-23 (AHC), May 2026 | ~501,000 | £3.40 bn | +| PolicyEngine UK, Enhanced FRS 2022-23 (BHC), May 2026 | ~415,000 | (same) | + +The October 2026 run uses the Microcosm UK 2024-25 national release +(SHA-256 `aa31bdf6…`) on policyengine-uk 2.123.7. On that dataset, +PolicyEngine's after-housing-costs headcount sits between the DWP and +Resolution Foundation figures, its before-housing-costs headcount is +above DWP's, and its cost sits between the Budget 2025 and Resolution +Foundation figures. The May 2026 Enhanced FRS run, kept for comparison, +was above both on headcount. + +## How the estimate was produced + +```python +from policyengine_uk import Microsimulation + +# Local path to the Microcosm UK 2024-25 national release file. +DATASET = "microcosm_uk_2024_25.h5" +YEAR = 2029 + +# Current law baseline (TCL removed from 2026-04-06 per Budget 2025) +sim_baseline = Microsimulation(dataset=DATASET) + +# Counterfactual where the TCL is held in place at 2 children +sim_counter = Microsimulation( + dataset=DATASET, + reform={ + "gov.dwp.universal_credit.elements.child.limit.child_count": { + f"{YEAR}-01-01": 2, + }, + }, +) + +is_child = sim_baseline.calculate("is_child", YEAR) +in_poverty_baseline = sim_baseline.calculate( + "in_poverty_ahc", YEAR, map_to="person" +).astype(float) +in_poverty_counter = sim_counter.calculate( + "in_poverty_ahc", YEAR, map_to="person" +).astype(float) + +children_lifted = ((in_poverty_counter - in_poverty_baseline) * is_child).sum() +fiscal_cost = ( + sim_counter.calculate("gov_balance", YEAR).sum() + - sim_baseline.calculate("gov_balance", YEAR).sum() +) + +print(f"AHC children lifted: {children_lifted / 1e3:.0f}k") +print(f"Treasury cost: £{fiscal_cost / 1e9:.2f}bn") +``` + +This is a *reverse* comparison: PolicyEngine's baseline already includes +the TCL removal (the parameter flips to infinity from 2026-04-06), so the +counterfactual *re-imposes* the limit at 2 children to compute the +2029-30 effect. The signs cancel out: the children-lifted figure is +positive (baseline has fewer in poverty than counterfactual) and the +Treasury cost is positive (baseline collects less revenue than the +counterfactual would). + +## Why the estimates differ + +Three plausible drivers of the remaining differences, and of the move +between the Enhanced FRS and Microcosm runs: + +1. **Nowcasting methodology**. RF and DWP/OBR use somewhat different + earnings, rent, and benefit-uprating paths through 2029-30. See the + [PolicyEngine vs RF comparison](../assumptions/nowcasting-comparison.md) + and the [RF methodology detail](../assumptions/rf-nowcasting-methodology.md) + for the specific divergence points. +2. **Take-up assumptions**. PolicyEngine applies the same Universal + Credit take-up (`would_claim_uc`, set in the data) to families gaining + entitlement from the removal as to existing claimants. A source that + assumes lower take-up among newly entitled families would show a + smaller headcount and cost. +3. **Data and calibration vintage**. The DWP, OBR and RF estimates were + finalised against November 2025 assumptions; PolicyEngine's parameters tick + over with each EFO release. The Microcosm release is built from FRS + 2024-25 rather than 2022-23, with take-up anchored on reported receipt. + +## How to keep this validation alive + +- Re-run the snippet on each new Microcosm UK release, and when the + remaining `would_claim_*` conversions (#1621) land. Both could move the + AHC poverty rate by a non-trivial amount. +- After each new EFO release, update the per-year poverty headcounts and + the fiscal cost table. + +## References + +- HM Treasury, [Budget 2025][budget-2025], Table 1.2: the OBR-certified cost of removing the two-child limit (£3.1 billion in 2029-30). +- OBR, [Economic and Fiscal Outlook November 2025][obr-efo]. +- Resolution Foundation, [No half measures: ending the two-child limit][rf-nhm]. +- DWP, [Removing the two-child limit on Universal Credit: impact on low income poverty levels in the United Kingdom][gov-2cl], Tables 1 and 2 (financial year ending 2030). +- Issue: [#1398](https://github.com/PolicyEngine/policyengine-uk/issues/1398). Related: [#1171](https://github.com/PolicyEngine/policyengine-uk/issues/1171), [#1388](https://github.com/PolicyEngine/policyengine-uk/issues/1388). + +[obr-efo]: https://obr.uk/efo/economic-and-fiscal-outlook-november-2025/ +[budget-2025]: https://www.gov.uk/government/publications/budget-2025-document/budget-2025-html +[rf-nhm]: https://www.resolutionfoundation.org/publications/no-half-measures/ +[gov-2cl]: https://www.gov.uk/government/publications/poverty-impacts-of-social-security-changes-at-budget-2025/removing-the-two-child-limit-on-universal-credit-impact-on-low-income-poverty-levels-in-the-united-kingdom diff --git a/docs/book/validation/policyengine-py-household-impact.md b/docs/book/validation/policyengine-py-household-impact.md new file mode 100644 index 0000000000..1926c110cc --- /dev/null +++ b/docs/book/validation/policyengine-py-household-impact.md @@ -0,0 +1,119 @@ +# Household-impact tests returning 0 (resolved) + +```{note} +**Resolved.** [#1628](https://github.com/PolicyEngine/policyengine-uk/issues/1628) +reported that two `policyengine.py` household-impact integration tests +started asserting `0.0` for expected-positive tax and child-benefit +values after bumping the UK extra to `policyengine-uk==2.88.0`. This +page documents the diagnosis from the PolicyEngine UK side and the +guidance for the upstream `policyengine.py` repo. It is not a model +bug: the UK model produces the expected non-zero values when called +correctly. + +Re-checked in October 2026 on policyengine-uk 2.123.7: both scenarios +below still return £7,486 and £1,354.60. The upstream reproducer +(policyengine.py#280) was closed without merging. policyengine.py has +since replaced the `TestUKHouseholdImpact` tests with +`TestUKCalculateHousehold` in `tests/test_household_impact.py`, which +passes an explicit `year` to every case and pins policyengine-uk +2.125.1. #1628 can be closed once those tests are confirmed green. +``` + +## Reproduction from the UK side + +Running the two scenarios directly against `policyengine-uk` main: + +```python +from policyengine_uk import Simulation + +YEAR = 2025 + +# Scenario 1: single adult with employment income. +s1 = { + "people": {"adult": {"age": {YEAR: 30}, "employment_income": {YEAR: 50_000}}}, + "benunits": {"benunit": {"members": ["adult"]}}, + "households": {"household": {"members": ["adult"]}}, +} +sim = Simulation(situation=s1) +print("income_tax:", sim.calculate("income_tax", YEAR).sum()) +# -> 7486.00 + +# Scenario 2: couple with one child. +s2 = { + "people": { + "a1": {"age": {YEAR: 35}, "employment_income": {YEAR: 30_000}}, + "a2": {"age": {YEAR: 33}}, + "c1": {"age": {YEAR: 5}}, + }, + "benunits": {"benunit": {"members": ["a1", "a2", "c1"]}}, + "households": {"household": {"members": ["a1", "a2", "c1"]}}, +} +sim = Simulation(situation=s2) +print("child_benefit:", sim.calculate("child_benefit", YEAR).sum()) +# -> 1354.60 +``` + +Both return the expected non-zero values across the range 2022-2025 +(the income-tax figure is invariant because the personal allowance is +£12,570 throughout this window). So **the underlying model is healthy**. + +## What changed between 2.74 and 2.88 + +The change in that window that most plausibly explains the upstream +failure is the **dataset-required** invariant, added in 2.76.0 +("Stop defaulting to private dataset"): `Simulation()` with no +`situation=`, no explicit `dataset=` and no +`POLICYENGINE_UK_DEFAULT_DATASET` environment variable raises +`ValueError: Simulation() requires an explicit dataset when no +situation is provided`. If the `policyengine.py` harness was catching +that error and falling through to a degenerate code path (a +zero-filled `Microsimulation`), the tests would see zeros. + +The **default-period** contract is a second thing to rule out, although +it did not change in that window (both 2.74.0 and 2.88.0 default to +2025): + +```python +# policyengine_uk/simulation.py +class Simulation(CoreSimulation): + default_input_period: int = 2025 + default_calculation_period: int = 2025 +``` + +The failing tests built 2024 households. Any calculation that relied on +the default period would compute 2025, and any tax or benefit input +populated only for 2024 would be missing for that year. + +## Guidance for the upstream fix + +The `policyengine.py` household-impact harness should: + +1. Pass an **explicit `period`** to every `calculate(...)` call + matching the year encoded in the situation YAML, rather than relying + on the model default. +2. Build single-household cases with **`Simulation(situation=...)`** + rather than a dataset-backed simulation. They don't need a dataset + and shouldn't bring in the default-dataset env-var dependency. +3. **Assert against the year encoded in the test fixture**, not the + model's `default_input_period`. The fixture-encoded year is what the + test author intended. + +The two failing tests should produce ~£7,486 income tax for a +£50k-earning single adult and ~£1,354.60 of child benefit for a +couple-with-one-child benefit unit at 2025 rates. + +## Why this is documented in `policyengine-uk` rather than fixed there + +The actual fix lives in `policyengine.py`'s test harness and is +explicitly out of scope for this repo (sister-repo edits aren't +made from `policyengine-uk`). This page exists so the next reviewer +who sees `#1628` and is unfamiliar with the policyengine.py setup has +a one-stop diagnosis instead of having to re-derive it. + +## References + +- Issue: [#1628](https://github.com/PolicyEngine/policyengine-uk/issues/1628). +- Reproducer: [`PolicyEngine/policyengine.py#280`](https://github.com/PolicyEngine/policyengine.py/pull/280). +- Default-period contract: [`policyengine_uk/simulation.py`](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/simulation.py) (`default_input_period`, `default_calculation_period`). +- Default-dataset env var contract: `POLICYENGINE_UK_DEFAULT_DATASET`, + defined in [`policyengine_uk/tax_benefit_system.py`](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/tax_benefit_system.py). diff --git a/docs/book/validation/reform-impacts-dashboard-plan.md b/docs/book/validation/reform-impacts-dashboard-plan.md new file mode 100644 index 0000000000..316ace197c --- /dev/null +++ b/docs/book/validation/reform-impacts-dashboard-plan.md @@ -0,0 +1,174 @@ +# Reform impacts dashboard (planned) + +```{note} +**Planning page.** Tracks +[#806](https://github.com/PolicyEngine/policyengine-uk/issues/806), +which asks for a dashboard tracking the budgetary impact of +high-importance reforms over time and across model / data versions. +The repo already has the **fixtures**; what's missing is a published +view onto them. Updated October 2026: still not built, and the planned +dataset move to Microcosm UK (below) makes it more useful. +``` + +## What exists today + +[`policyengine_uk/tests/microsimulation/reforms_config.yaml`](../../../policyengine_uk/tests/microsimulation/reforms_config.yaml) +is a list of canonical reform-impact regression fixtures with the +shape: + +```yaml +- name: Raise basic rate by 1pp + expected_impact: 7.8 # £bn fiscal impact at the reference year + parameters: + gov.hmrc.income_tax.rates.uk[0].rate: 0.21 +- name: Reduce Universal Credit taper rate to 20% + expected_impact: -18.1 + tolerance: 3.0 + parameters: + gov.dwp.universal_credit.means_test.reduction_rate: 0.2 +``` + +Each fixture is run by +[`tests/microsimulation/test_reform_impacts.py`](../../../policyengine_uk/tests/microsimulation/test_reform_impacts.py), +which compares the change in `gov_balance` in 2029 with +`expected_impact` (£bn), within `tolerance` (default £1bn). The tests +are marked `microsimulation`, so they run in CI's test suite only when a +dataset is available: `policyengine_uk/tests/conftest.py` points +`POLICYENGINE_UK_DEFAULT_DATASET` at the Enhanced FRS 2023-24 file +(`enhanced_frs_2023_24.h5@1.40.3`) when a Hugging Face token is set. +The suite requires that data in the merge queue and on main, and skips +these tests on pull requests without credentials. So CI already +catches *unintended* impact changes before they reach main. But the +signal is only +pass or fail, and when a change is intended, someone reruns +[`update_reform_impacts.py`](../../../policyengine_uk/tests/microsimulation/update_reform_impacts.py) +and the old value is overwritten. The fixtures are regression values +taken from the model's own output, not external costings (the file says +so at the top). + +What's missing is a **time series**: the same eight reforms run on +every commit / version, plotted against the published OBR/RF +benchmark for the same reform, with annotations for the relevant +infrastructure changes (model versions, dataset refreshes, EFO +updates). + +## Proposed shape + +### Storage + +Each scheduled run writes a row per fixture to a CSV / Parquet on +HuggingFace Hub or a small Postgres instance: + +``` +timestamp, policyengine_uk_version, policyengine_uk_data_version, +dataset_url, reform_name, expected_impact, observed_impact, gap +``` + +The table is append-only, with no overwrites, so the history survives. + +### Computation + +A new entry point under `tests/microsimulation/` (or, better, a +GitHub Actions workflow that doesn't run inside the test suite) +walks `reforms_config.yaml`, runs each reform, and writes a row +per fixture. Schedule: weekly + on every release tag. + +### Publication + +Three publication paths, in order of effort: + +1. **Static page in the docs site**: the simplest. Convert the CSV + to a Plotly chart (matching the existing `validation/*.ipynb` + pattern) and render it under `docs/book/validation/`. Updates + when the docs site rebuilds. +2. **Living JSON endpoint**: push the latest aggregate JSON to a + stable URL (HuggingFace dataset or a `gh-pages` artefact) so the + policyengine.org app can render it without needing the docs build. +3. **policyengine.org dashboard tile**: a tile on the marketing site + that shows the latest impact alongside the OBR/RF benchmark. Most + work; right answer once the static page proves the data is stable. + +### What to put on the dashboard + +For each reform: + +- **Latest observed impact** (£bn, current model). +- **Published external benchmark** (OBR EFO costing where one + exists; RF *No half measures*-style costing as a comparator). Annotate + the publication date. +- **Time series** of observed impact across the last N model releases. +- **Annotations** for each known regime shift: EFO release, dataset + refresh, methodology PR. The reforms_config history is the natural + source of "what changed" annotations. + +## Initial reform list + +Use the existing `reforms_config.yaml` entries as the v1 dashboard +content: + +1. Raise basic rate by 1pp. +2. Raise higher rate by 1pp. +3. Raise the personal allowance to £13,000 (the fixture's name says + "by ~800GBP/year", but the allowance is frozen at £12,570 to + 2030-31, so in 2029 the rise is £430). +4. Set the child benefit rate for each additional child to £25 a week. +5. Reduce Universal Credit taper to 20%. +6. Raise Class 1 main employee NI rate to 10%. +7. Raise VAT standard rate by 2pp. +8. Raise additional rate by 3pp. + +These cover three of the four big revenue heads (income tax, NI, VAT) +plus a UC parameter that matters disproportionately for distributional +analysis. + +Add to v2 over time as published OBR / RF reforms become available +benchmarks: + +- Remove the two-child limit (an external comparison is drafted in + [#1724](https://github.com/PolicyEngine/policyengine-uk/pull/1724)). +- Freeze the Personal Allowance for an additional year. +- 1pp employer NI change. + +## Why now + +The UK data pipeline is moving from the Enhanced FRS to +[Microcosm UK](https://github.com/PolicyEngine/microcosm), while the +fixtures still run on the Enhanced FRS 2023-24. Moving the +fixtures' dataset will shift every expected impact at once, and model +changes to VAT, fuel duty and other bases are landing at the same time. +A history that records the dataset alongside each value is the easiest +way to show which change moved which number. + +## What this doesn't replace + +This is a **regression-tracking** dashboard, not a forecast. The +numbers it plots are PolicyEngine UK's reading of a reform under +current model + data + EFO assumptions; they will move when any of +those move, by design. The whole point of plotting the time series is +to make those movements legible rather than hidden. + +For comparison with external nowcasters (RF, IFS, OBR scoring), the +relevant pages are: + +- [nowcasting-comparison.md](../assumptions/nowcasting-comparison.md) (PolicyEngine vs RF). +- [rf-nowcasting-methodology.md](../assumptions/rf-nowcasting-methodology.md) (RF methodology detail). +- [#1724](https://github.com/PolicyEngine/policyengine-uk/pull/1724) (child poverty against OBR and Resolution Foundation benchmarks, in review). + +## Open questions + +- Storage substrate: HuggingFace dataset CSV vs a small managed + database. CSV is simplest; database lets the policyengine.org app + query directly. +- Scheduling: every commit to main, every weekly tag, or both? + Recommend weekly + on tag, since per-commit runs cost + microsim time without proportional information gain. +- Should the dashboard include reform variants from the policyengine + blog series, or stick to the parametric fixtures? Recommend + parametric fixtures only for v1 to keep maintenance burden low. + +## References + +- Issue: [#806](https://github.com/PolicyEngine/policyengine-uk/issues/806). +- Existing fixture list: [`reforms_config.yaml`](../../../policyengine_uk/tests/microsimulation/reforms_config.yaml). +- Existing regression runner: [`test_reform_impacts.py`](../../../policyengine_uk/tests/microsimulation/test_reform_impacts.py). +- Script that rewrites the expected values: [`update_reform_impacts.py`](../../../policyengine_uk/tests/microsimulation/update_reform_impacts.py). diff --git a/docs/book/validation/ubuntu-vs-macos.md b/docs/book/validation/ubuntu-vs-macos.md new file mode 100644 index 0000000000..8f2d867ad2 --- /dev/null +++ b/docs/book/validation/ubuntu-vs-macos.md @@ -0,0 +1,98 @@ +# Ubuntu vs macOS reproducibility + +```{note} +Issue [#1209](https://github.com/PolicyEngine/policyengine-uk/issues/1209) +reported that PolicyEngine UK produced different aggregates on +`ubuntu-latest` and `macos-latest` for the same dataset and the same +`policyengine-uk` version. The cause was in parameter uprating. As of +October 2026 the parameter tree is byte-identical across macOS and +Linux, and CI has moved back to Linux. This page records the diagnosis, +the fix and how to check for a regression. +``` + +## Symptom + +The divergence was first spotted in +[#1205](https://github.com/PolicyEngine/policyengine-uk/pull/1205), +whose results differed between the two runners. A diagnostic +repository that ran the same code on both runners and stored the full +entity dataset as artifacts showed that: + +- the input data were identical on both operating systems; +- a limited subset of parameters was uprated on macOS but not on + Ubuntu (the parameter-tree dumps are attached to + [#1209](https://github.com/PolicyEngine/policyengine-uk/issues/1209#issuecomment-3062418736)). + +The effect on weighted aggregates was small, but it broke +reproducibility. + +## Cause + +[#1223](https://github.com/PolicyEngine/policyengine-uk/pull/1223) +(closed without merging) traced it to an **import-time ordering +problem**: the uprating growth-factor table +(`BASELINE_GROWFACTORS = create_policyengine_uprating_factors_table()`) +was computed when its module was imported, before the parameter tree +had been fully processed. Whether the tree was ready by then depended +on the order in which modules and parameter files were loaded, and that +order differed between the two operating systems. + +## How it was resolved + +- [#1092](https://github.com/PolicyEngine/policyengine-uk/pull/1092) + and [#1207](https://github.com/PolicyEngine/policyengine-uk/pull/1207) + ran the Test job on `macos-latest` in 2025, so that CI matched the + platform that gave the expected results. +- [#1254](https://github.com/PolicyEngine/policyengine-uk/pull/1254) + (multi-year datasets, with uprating as a separate step) removed the + import-time `BASELINE_GROWFACTORS` table. Uprating is now applied by + `apply_uprating` in + [`policyengine_uk/data/economic_assumptions.py`](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/data/economic_assumptions.py). +- Later fixes to the same path: + [#1523](https://github.com/PolicyEngine/policyengine-uk/pull/1523) + (no redundant dataset copy in `apply_uprating`), + [#1543](https://github.com/PolicyEngine/policyengine-uk/pull/1543) and + [#1544](https://github.com/PolicyEngine/policyengine-uk/pull/1544) + (`uprate_rent` lookups). +- policyengine-core now sorts each directory listing when it loads the + parameter tree, so the load order no longer depends on the file + system. + +On 3 October 2026 the Test job moved from macOS to `ubuntu-24.04-arm` +(commit +[`67c63861`](https://github.com/PolicyEngine/policyengine-uk/commit/67c638613)). +That change verified, on main at `3c48247e`, that: + +- the full parameter tree (2,706 parameter objects and 43,666 dated + values, including the baseline copies) is byte-identical on macOS + arm64, `ubuntu-latest` and `ubuntu-24.04-arm`; +- the YAML tests pass on both Linux runners; +- every pytest outcome matches the macOS run of the same code, test by + test. + +#1209 can be closed on that evidence. + +## Checking for a regression + +CI now runs the test suite (`.github/workflows/test-suite.yaml`: the +YAML tests and the Python tests, split over four shards) on Linux arm64 +(`ubuntu-24.04-arm`), and the install and smoke-import checks on x86 +`ubuntu-latest`. There is no macOS job. If an OS difference is +suspected again: + +1. Dump the processed parameter tree on each platform (every parameter + and every dated value, including `parameters.baseline`) and compare + checksums. This is the check the October 2026 move relied on. +2. If the trees differ, look first at anything that runs at import time + or depends on file or dictionary order. +3. If the trees match but results differ, compare the uprated dataset + variables next. #1209 showed the input data themselves were the same + on both systems. + +## References + +- Tracking issue: [#1209](https://github.com/PolicyEngine/policyengine-uk/issues/1209). +- Where the difference was first seen: [#1205](https://github.com/PolicyEngine/policyengine-uk/pull/1205). +- Diagnosis: [#1223](https://github.com/PolicyEngine/policyengine-uk/pull/1223) (closed). +- CI moved to macOS: #1092, #1207; moved back to Linux: [`67c63861`](https://github.com/PolicyEngine/policyengine-uk/commit/67c638613). +- Uprating rebuild and follow-ups: #1254, #1523, #1543, #1544. From 1cee5b318b5ed16dc2df8a65fe5345c0b29d032a Mon Sep 17 00:00:00 2001 From: vahid-ahmadi Date: Sun, 18 Oct 2026 16:18:11 +0200 Subject: [PATCH 2/7] Bring the pension tax relief page in line with main's relief rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The page said the earnings limit applies first, so someone with no earnings gets no relief, and that the annual allowance caps relief. On main, relief at source can use the £3,600 basic amount above earnings (FA 2004 s. 191(7)), the annual allowance does not limit relief, the taper tests adjusted and threshold income, the charge is sliced across the rate bands, and relieved contributions reduce adjusted net income. Mentions #2464 (SSP and SMP in relevant earnings) and #2488 (ATI uplift) as pending. Figures re-run on policyengine-uk 2.135.6. Co-Authored-By: Claude Opus 5.5 --- .../programs/gov/hmrc/pension-tax-relief.md | 98 ++++++++++++------- 1 file changed, 60 insertions(+), 38 deletions(-) diff --git a/docs/book/programs/gov/hmrc/pension-tax-relief.md b/docs/book/programs/gov/hmrc/pension-tax-relief.md index bf7c3a7f07..9db4c64e2b 100644 --- a/docs/book/programs/gov/hmrc/pension-tax-relief.md +++ b/docs/book/programs/gov/hmrc/pension-tax-relief.md @@ -5,60 +5,82 @@ pension contributions and addresses the confusion reported in [#704](https://github.com/PolicyEngine/policyengine-uk/issues/704) about whether higher-rate taxpayers receive the right amount of relief. #704 is still open (October 2026); the figures below were re-run -on policyengine-uk 2.126.12. +on policyengine-uk 2.135.6. ## What the model does `pension_contributions_relief` (defined in -[`variables/gov/hmrc/pensions/pension_contributions_relief.py`](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/variables/gov/hmrc/pensions/pension_contributions_relief.py)) -reduces the person's taxable income by their pension contributions, -limited to: - -- the person's employment plus self-employment income; -- then the higher of `pension_annual_allowance` and - `gov.hmrc.income_tax.reliefs.pension_contribution.basic_amount` (£3,600); +[`variables/gov/hmrc/pensions/pension_contributions_relief.py`](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/variables/gov/hmrc/pensions/pension_contributions_relief.py), +with the limit in `utils/pensions.py`) reduces the person's taxable income +by their pension contributions, up to the annual limit for relief in +[Finance Act 2004 s. 190](https://www.legislation.gov.uk/ukpga/2004/12/section/190): + +- the person's relevant UK earnings, which the model takes as employment + income plus self-employment profit (a trading loss counts as zero, and + does not reduce employment income); +- raised towards `gov.hmrc.income_tax.reliefs.pension_contribution.basic_amount` + (£3,600) where earnings are lower, but only by relief-at-source + contributions ([s. 191(7)](https://www.legislation.gov.uk/ukpga/2004/12/section/191)). + The model treats `personal_pension_contributions` as relief at source and + employee contributions as a net pay arrangement, which cannot use the + basic amount; - and only for people under `gov.hmrc.pensions.pension_contributions_relief_age_limit` (75). -The law limits relief to the higher of relevant UK earnings and £3,600 -gross ([Finance Act 2004 s. 190](https://www.legislation.gov.uk/ukpga/2004/12/section/190)). -The model applies the earnings limit first, so someone with no earnings gets -no relief. That makes no difference to their income tax, which is zero -anyway, but it means the basic-rate top-up paid into their pension under -relief at source is not modelled. - -`pension_annual_allowance` is the standard annual allowance (£60,000 from -6 April 2023), tapered by £1 for every £2 of adjusted net income above -£260,000 down to a minimum of £10,000 (parameters under -`gov.hmrc.income_tax.allowances.annual_allowance`). The model tapers on -`adjusted_net_income`, which stands in for the statutory "adjusted -income". The money purchase annual allowance is not modelled. - -Contributions **above** the annual allowance trigger a separate charge, -`personal_pension_contributions_tax`, which taxes the excess at the -person's marginal rate and is added to income tax. The amount tested -against the allowance is `pension_contributions_for_annual_allowance`, -which counts employee, personal, employer and salary sacrifice -contributions -([Finance Act 2004 s. 233](https://www.legislation.gov.uk/ukpga/2004/12/section/233)). - -One gap: the relief does not reduce `adjusted_net_income`, so contributions -do not restore personal allowance lost above £100,000, as they do in law +So someone with no earnings who pays £3,600 gross into a personal pension +gets £3,600 of relief, the basic-rate top-up paid into their pension under +relief at source, while the same payment as an employee contribution gets +none. Two open pull requests would change this section once merged: +[#2464](https://github.com/PolicyEngine/policyengine-uk/pull/2464) would add +Statutory Sick Pay and Statutory Maternity Pay to relevant earnings, and +[#2488](https://github.com/PolicyEngine/policyengine-uk/pull/2488) would apply +the same relief-at-source-only rule to the pension uplift in adjusted total +income. + +The annual allowance does not limit the relief. `pension_annual_allowance` +is the standard annual allowance (£60,000 from 6 April 2023). From 2016-17 it +is tapered only when both the statutory adjusted income +(`pension_annual_allowance_adjusted_income`, above £260,000 from 2023-24) and +threshold income (`pension_annual_allowance_threshold_income`, above the +taper level less the standard allowance, £200,000 from 2023-24) are over +their limits, by £1 for every £2 of adjusted income above the taper level, +rounded down to whole pounds, to a minimum of £10,000 +([Finance Act 2004 s. 228ZA](https://www.legislation.gov.uk/ukpga/2004/12/section/228ZA); +parameters under `gov.hmrc.income_tax.allowances.annual_allowance`). See +`docs/engineering/pension-annual-allowance.md` for the income conventions. +The money purchase annual allowance is not modelled. + +Contributions **above** the annual allowance can still be relieved and +separately attract the annual allowance charge, +`personal_pension_contributions_tax`, which is added to income tax. It charges +the excess at the basic, higher and additional rates on the slices that fall +in each band when it is added on top of the person's income after allowances +([Finance Act 2004 s. 227](https://www.legislation.gov.uk/ukpga/2004/12/section/227)). +The amount tested against the allowance is +`pension_contributions_for_annual_allowance`, which counts member +contributions made before age 75, employer contributions and salary +sacrifice ([Finance Act 2004 s. 233](https://www.legislation.gov.uk/ukpga/2004/12/section/233)). +For example, an employee on £100,000 who pays £70,000 into a personal pension +in 2025-26 gets £70,000 of relief and a £2,000 charge on the £10,000 excess, +which falls in the basic rate band. + +Relieved contributions also reduce `adjusted_net_income`, so they restore +personal allowance lost above £100,000, as in law ([Income Tax Act 2007 s. 58](https://www.legislation.gov.uk/ukpga/2007/3/section/58)). -An employee on £110,000 keeps a £7,570 personal allowance in 2025-26 whether -or not they contribute £10,000. +An employee on £110,000 has a £7,570 personal allowance in 2025-26, and the +full £12,570 if they pay £10,000 gross into a personal pension. -So the headline behaviour is: **within the annual allowance, pension +So the headline behaviour is: **up to the relief limit, pension contributions reduce taxable income £1-for-£1**. The income-tax saving that the model reports is the contribution × the person's marginal -rate. +rate, plus any personal allowance restored. ## Worked examples The four scenarios reported in #704 (all 2025-26, all self-employed, single person aged 40, no other income, contributions entered as `personal_pension_contributions`). The "model" column was re-run in -October 2026 on policyengine-uk 2.126.12: +October 2026 on policyengine-uk 2.135.6: | ID | Self-employed income | Pension contribution | Expected income-tax change | What the model returns | Why | |-------|---------------------|----------------------|-----------------------------|------------------------|-----| @@ -120,5 +142,5 @@ reported in #704 no longer reproduces in the model. - [Finance Act 2004 s. 188-194](https://www.legislation.gov.uk/ukpga/2004/12/section/188): primary statute for relief at source. - [Finance Act 2004 s. 227](https://www.legislation.gov.uk/ukpga/2004/12/section/227): annual allowance excess charge. - HMRC, [Personal pensions statistics](https://www.gov.uk/government/collections/personal-pensions-statistics): including the published cost of pension tax relief. -- [Finance Act 2004 s. 190](https://www.legislation.gov.uk/ukpga/2004/12/section/190): the earnings-or-£3,600 limit on relief. +- [Finance Act 2004 s. 190](https://www.legislation.gov.uk/ukpga/2004/12/section/190) and [s. 191(7)](https://www.legislation.gov.uk/ukpga/2004/12/section/191): the earnings-or-£3,600 limit on relief, with the £3,600 basic amount for relief at source only. - Model variables: [`pension_contributions_relief`](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/variables/gov/hmrc/pensions/pension_contributions_relief.py) and [`personal_pension_contributions_tax`](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/variables/gov/hmrc/pensions/private_pension_contributions_tax.py). From d7ed2889a7bc94d69c6ca10aa8f1b1296643293c Mon Sep 17 00:00:00 2001 From: vahid-ahmadi Date: Mon, 19 Oct 2026 13:13:31 +0200 Subject: [PATCH 3/7] Correct three planning formulas and a broken link from review - Take-up: count only eligible units in the numerator and treat a zero denominator as undefined. - Future-dated announcements: a reversal removes or replaces the cancelled row at its own effective date; a new row at the reversal date leaves the future row in force. - BSP: pay the single initial payment from a claim-year input, so an 18-month bereavement window can't pay it twice; keep the 18 months for the instalments. - PIP points plan: close the DWP statistics link. Co-Authored-By: Claude Opus 5.5 --- docs/book/assumptions/takeup-analysis-plan.md | 9 +++++-- docs/book/policy/future-date-announcements.md | 16 ++++++++---- .../gov/dwp/bereavement-support-plan.md | 26 ++++++++++++------- docs/book/programs/gov/dwp/pip-points-plan.md | 2 +- 4 files changed, 36 insertions(+), 17 deletions(-) diff --git a/docs/book/assumptions/takeup-analysis-plan.md b/docs/book/assumptions/takeup-analysis-plan.md index d4836b0de7..64cd8e5679 100644 --- a/docs/book/assumptions/takeup-analysis-plan.md +++ b/docs/book/assumptions/takeup-analysis-plan.md @@ -93,8 +93,13 @@ For each programme above, the analysis should produce: analysis year, plus the source (HMRC, DWP or an academic study) and vintage. 2. **Effective take-up rate post-pipeline:** counted from the built - dataset as `sum(would_claim_X * weight) / sum(eligible_X * weight)`, - where `eligible_X` is computed from the runtime formulas. + dataset as + `sum(would_claim_X * eligible_X * weight) / sum(eligible_X * weight)`, + where `eligible_X` is computed from the runtime formulas. The + numerator counts only eligible units, because `would_claim_X` can be + true for ineligible units too (otherwise one eligible unit and two + willing units would give 200%). Where no unit is eligible the + denominator is zero; report the rate as undefined (n/a), not 0%. 3. **Published outturn share** for the same year: DWP and HMRC statistical publications giving claimant counts and either eligible population counts (where DWP estimates them) or a published take-up diff --git a/docs/book/policy/future-date-announcements.md b/docs/book/policy/future-date-announcements.md index b0649e838b..8e91ec8ce7 100644 --- a/docs/book/policy/future-date-announcements.md +++ b/docs/book/policy/future-date-announcements.md @@ -64,11 +64,17 @@ Truss "growth plan" basic-rate cut to 19% (announced September 2022, reversed October 2022) is the canonical example. To represent this honestly: -1. Add the announced value at the announced effective date with a - reference to the announcement. -2. Add a *second* row at the reversal date with the original value (or - the actual landing value) and a reference to the reversal - announcement. +1. When the announcement is made, add the announced value at the + announced effective date with a reference to the announcement (for + the Truss case, 19% from 6 April 2023). +2. When it is reversed, remove that row, or replace its value with the + value that actually applies from that date (20% from 6 April 2023), + and cite the reversal announcement. A new row at the reversal date + does not cancel the future row: parameter rows apply from their own + dates, so a 20% row dated October 2022 is followed by the 19% row in + April 2023, and fiscal-year conversion still gives 19% for 2023. +3. Keep the announcement history in the row's references and in version + control rather than in a live value. The baseline then follows what was finally enacted. PolicyEngine UK has no "policy as announced on date X" switch, so to score an announcement as diff --git a/docs/book/programs/gov/dwp/bereavement-support-plan.md b/docs/book/programs/gov/dwp/bereavement-support-plan.md index 86da510bd1..a8160f9926 100644 --- a/docs/book/programs/gov/dwp/bereavement-support-plan.md +++ b/docs/book/programs/gov/dwp/bereavement-support-plan.md @@ -69,22 +69,30 @@ children, with effect back to 30 August 2018, after the Supreme Court's To model this we need: -- `is_bereaved_recently` (Person, YEAR): a new input flagging - whether the person has been bereaved in the last 18 months. +- `bsp_initial_payment_due` (Person, YEAR): a new input that is true + only in the year the BSP claim is made after the partner's death, so + the [single initial payment](https://www.gov.uk/bereavement-support-payment/what-youll-get) + is paid once. A flag for "bereaved in the last 18 months" would stay + true across two consecutive annual periods and pay the lump sum twice. - `bsp_higher_rate_eligible` (Person, YEAR): whether they have a dependent child. - `bsp_months_remaining` (Person, YEAR): how many of the 18 monthly - instalments fall within the simulation year. + instalments fall within the simulation year (0 to 12). This is where + the 18-month window belongs. The BSP formula then becomes: ``` bsp = ( - lump_sum_amount * is_bereaved_recently + lump_sum_amount * bsp_initial_payment_due + monthly_amount * bsp_months_remaining ) ``` +An alternative is a single date-of-death (or claim-date) input from +which both the initial-payment year and the months in each year are +derived. Either way the lump sum is tied to one year only. + Both `lump_sum_amount` and `monthly_amount` parameter trees split on `bsp_higher_rate_eligible` (£3,500 / £350 vs £2,500 / £100). @@ -113,11 +121,11 @@ the current data they sit inside `bsp_reported` (see above), so they are already counted in income; the gap is that they can't be told apart from BSP or reformed separately. -### Phase 3: data-side bereavement flag +### Phase 3: data-side bereavement inputs -`is_bereaved_recently` doesn't exist in the FRS. The FRS is a -cross-section, not a panel, and doesn't record when a partner died, so -the flag would need imputing in the data (Microcosm UK, as the +Neither `bsp_initial_payment_due` nor `bsp_months_remaining` exists in +the FRS. The FRS is a cross-section, not a panel, and doesn't record +when a partner died, so these inputs would need imputing in the data (Microcosm UK, as the replacement dataset): - start from FRS respondents who report BSP (once it is separated from @@ -126,7 +134,7 @@ replacement dataset): and region. This is the heaviest of the three phases; without it the new -variables work for the household calculator (which can input the flag +variables work for the household calculator (which can input them directly) but not for microsim aggregates. ## Open questions diff --git a/docs/book/programs/gov/dwp/pip-points-plan.md b/docs/book/programs/gov/dwp/pip-points-plan.md index 33afd2ef86..67a20e8091 100644 --- a/docs/book/programs/gov/dwp/pip-points-plan.md +++ b/docs/book/programs/gov/dwp/pip-points-plan.md @@ -160,7 +160,7 @@ machinery (for reform analysis) get the activity-level surface. - [The Social Security (Personal Independence Payment) Regulations 2013 (SI 2013/377)][si-377]: Schedule 1, Parts 2 and 3 define the point system. - DWP, [Personal Independence Payment: Assessment guide][assess-guide]. -- DWP, [PIP statistics](https://www.gov.uk/government/collections/personal-independence-payment-statistics: caseload by component and band. +- DWP, [PIP statistics](https://www.gov.uk/government/collections/personal-independence-payment-statistics): caseload by component and band. - DWP, [Modernising support for independent living: the health and disability green paper (April 2024)][green-paper]: proposals on the descriptors. - DWP, [Pathways to Work: Reforming Benefits and Support to Get Britain Working (March 2025)](https://www.gov.uk/government/consultations/pathways-to-work-reforming-benefits-and-support-to-get-britain-working-green-paper): the four-point proposal. - Issue: [#1069](https://github.com/PolicyEngine/policyengine-uk/issues/1069). From c19ce75734e474c6e0ec6794ab765df6a525c077 Mon Sep 17 00:00:00 2001 From: vahid-ahmadi Date: Mon, 19 Oct 2026 15:54:54 +0200 Subject: [PATCH 4/7] =?UTF-8?q?Separate=20the=20=C2=A33,600=20relief=20ded?= =?UTF-8?q?uction=20from=20the=20=C2=A3720=20provider=20claim?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The no-earnings example called the £3,600 pension_contributions_relief the basic-rate top-up. It is a deduction from taxable income. Under relief at source a £3,600 gross contribution is £2,880 paid by the contributor and £720 claimed by the provider from HMRC (FA 2004 s. 192), which the model does not represent as a payment. Co-Authored-By: Claude Opus 5.5 --- docs/book/programs/gov/hmrc/pension-tax-relief.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/book/programs/gov/hmrc/pension-tax-relief.md b/docs/book/programs/gov/hmrc/pension-tax-relief.md index 9db4c64e2b..4cb4b40702 100644 --- a/docs/book/programs/gov/hmrc/pension-tax-relief.md +++ b/docs/book/programs/gov/hmrc/pension-tax-relief.md @@ -27,10 +27,16 @@ by their pension contributions, up to the annual limit for relief in - and only for people under `gov.hmrc.pensions.pension_contributions_relief_age_limit` (75). -So someone with no earnings who pays £3,600 gross into a personal pension -gets £3,600 of relief, the basic-rate top-up paid into their pension under -relief at source, while the same payment as an employee contribution gets -none. Two open pull requests would change this section once merged: +So someone with no earnings who makes a £3,600 gross contribution to a +personal pension gets a £3,600 `pension_contributions_relief`, while the same +payment as an employee contribution gets none. That £3,600 is the deduction +from taxable income, not a cash top-up. Under relief at source the £3,600 +gross contribution is £2,880 paid by the contributor plus £720 (20%) that the +pension provider claims from HMRC and adds to the pension +([Finance Act 2004 s. 192](https://www.legislation.gov.uk/ukpga/2004/12/section/192)). +PolicyEngine does not represent the £720 claim as a payment, and with no +taxable income the deduction saves no income tax. Two open pull requests +would change this section once merged: [#2464](https://github.com/PolicyEngine/policyengine-uk/pull/2464) would add Statutory Sick Pay and Statutory Maternity Pay to relevant earnings, and [#2488](https://github.com/PolicyEngine/policyengine-uk/pull/2488) would apply From 0163a613dd117622c4545faa3e6ea8d63d9c31db Mon Sep 17 00:00:00 2001 From: vahid-ahmadi Date: Tue, 20 Oct 2026 09:23:04 +0200 Subject: [PATCH 5/7] Count SSP and SMP in relevant UK earnings in the pension docs #2464 added employer-paid Statutory Sick Pay and Statutory Maternity Pay (employment_benefits) to relevant UK earnings for pension tax relief, in line with FA 2004 s. 189(2) and HMRC PTM044100. Update the pension tax relief page and the STBM comparison table to match, and note that Statutory Paternity Pay still waits on #2368. Co-Authored-By: Claude Opus 5.5 --- .../programs/gov/hmrc/pension-tax-relief.md | 22 +++++++++++++------ .../gov/hmrc/pensions-comparison-stbm.md | 2 +- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/docs/book/programs/gov/hmrc/pension-tax-relief.md b/docs/book/programs/gov/hmrc/pension-tax-relief.md index 4cb4b40702..925c9aa865 100644 --- a/docs/book/programs/gov/hmrc/pension-tax-relief.md +++ b/docs/book/programs/gov/hmrc/pension-tax-relief.md @@ -15,9 +15,17 @@ with the limit in `utils/pensions.py`) reduces the person's taxable income by their pension contributions, up to the annual limit for relief in [Finance Act 2004 s. 190](https://www.legislation.gov.uk/ukpga/2004/12/section/190): -- the person's relevant UK earnings, which the model takes as employment - income plus self-employment profit (a trading loss counts as zero, and - does not reduce employment income); +- the person's relevant UK earnings + ([s. 189(2)](https://www.legislation.gov.uk/ukpga/2004/12/section/189)), + which the model takes as employment income, plus employer-paid Statutory + Sick Pay and Statutory Maternity Pay (`employment_benefits`, listed as + relevant UK earnings in HMRC's + [PTM044100](https://www.gov.uk/hmrc-internal-manuals/pensions-tax-manual/ptm044100)), + plus self-employment profit (a trading loss counts as zero, and does not + reduce employment earnings). Statutory Paternity Pay is not yet counted; + open pull request + [#2368](https://github.com/PolicyEngine/policyengine-uk/pull/2368) would + add it to taxable employment pay; - raised towards `gov.hmrc.income_tax.reliefs.pension_contribution.basic_amount` (£3,600) where earnings are lower, but only by relief-at-source contributions ([s. 191(7)](https://www.legislation.gov.uk/ukpga/2004/12/section/191)). @@ -35,10 +43,10 @@ gross contribution is £2,880 paid by the contributor plus £720 (20%) that the pension provider claims from HMRC and adds to the pension ([Finance Act 2004 s. 192](https://www.legislation.gov.uk/ukpga/2004/12/section/192)). PolicyEngine does not represent the £720 claim as a payment, and with no -taxable income the deduction saves no income tax. Two open pull requests -would change this section once merged: -[#2464](https://github.com/PolicyEngine/policyengine-uk/pull/2464) would add -Statutory Sick Pay and Statutory Maternity Pay to relevant earnings, and +taxable income the deduction saves no income tax. Statutory Sick Pay and +Statutory Maternity Pay were added to relevant earnings by +[#2464](https://github.com/PolicyEngine/policyengine-uk/pull/2464), merged on +19 October 2026. One open pull request would change this section once merged: [#2488](https://github.com/PolicyEngine/policyengine-uk/pull/2488) would apply the same relief-at-source-only rule to the pension uplift in adjusted total income. diff --git a/docs/book/programs/gov/hmrc/pensions-comparison-stbm.md b/docs/book/programs/gov/hmrc/pensions-comparison-stbm.md index 39f18ab016..be51378fe7 100644 --- a/docs/book/programs/gov/hmrc/pensions-comparison-stbm.md +++ b/docs/book/programs/gov/hmrc/pensions-comparison-stbm.md @@ -23,7 +23,7 @@ more detail on the PolicyEngine side. | How relief is given | Reduces taxable income by the contribution (`pension_contributions_relief`) | Treats contributions as paid net of basic rate: grosses them up by 1/(1 − basic rate) and widens every rate band above the starting band by the gross amount, as under relief at source | | Contributions that get relief | The person's own contributions | Employee contributions, additional voluntary contributions **and employer contributions** | | Basic-rate relief added to the pot | Not shown separately | Recorded separately (`pension_relief_at_source`) | -| Earnings limit | Relief capped at employment plus self-employment income | No earnings cap; £3,600 limit when total income is below it | +| Earnings limit | Relief capped at relevant UK earnings: employment income, employer-paid Statutory Sick Pay and Statutory Maternity Pay, and self-employment profit | No earnings cap; £3,600 limit when total income is below it | | Annual allowance | £60,000, tapered on adjusted net income above £260,000 (`pension_annual_allowance`) | Tapered on total income above a threshold-income parameter | | Contributions above the allowance | Charged at the marginal rate (`personal_pension_contributions_tax`) | Relief stops at the allowance; no separate charge | | Employer contributions in the data | Read from the data (`employer_pension_contributions`) | Imputed when missing from the FRS, using the ONS ASHE employer-contribution bands (`src/Pensions.jl`) | From 6659ebfec11c2229378253d88c2223f1231ac93d Mon Sep 17 00:00:00 2001 From: vahid-ahmadi Date: Tue, 20 Oct 2026 09:31:49 +0200 Subject: [PATCH 6/7] Update pension docs for the statutory annual allowance taper and carry-forward The STBM comparison said the taper uses adjusted net income with no threshold-income test, that carry-forward is not modelled and that adjusted net income ignores pension relief. Main now applies both s. 228ZA tests, takes unused allowance (s. 228A) as an input defaulting to nil, and deducts relief in adjusted net income. State the remaining limits (no MPAA, cash-proxy pension input) and re-run the examples on 2.140.0. Co-Authored-By: Claude Opus 5.5 --- .../programs/gov/hmrc/pension-tax-relief.md | 19 +++++-- .../gov/hmrc/pensions-comparison-stbm.md | 53 ++++++++++++------- 2 files changed, 50 insertions(+), 22 deletions(-) diff --git a/docs/book/programs/gov/hmrc/pension-tax-relief.md b/docs/book/programs/gov/hmrc/pension-tax-relief.md index 925c9aa865..1762ca37f6 100644 --- a/docs/book/programs/gov/hmrc/pension-tax-relief.md +++ b/docs/book/programs/gov/hmrc/pension-tax-relief.md @@ -5,7 +5,7 @@ pension contributions and addresses the confusion reported in [#704](https://github.com/PolicyEngine/policyengine-uk/issues/704) about whether higher-rate taxpayers receive the right amount of relief. #704 is still open (October 2026); the figures below were re-run -on policyengine-uk 2.135.6. +on policyengine-uk 2.140.0. ## What the model does @@ -62,7 +62,15 @@ rounded down to whole pounds, to a minimum of £10,000 ([Finance Act 2004 s. 228ZA](https://www.legislation.gov.uk/ukpga/2004/12/section/228ZA); parameters under `gov.hmrc.income_tax.allowances.annual_allowance`). See `docs/engineering/pension-annual-allowance.md` for the income conventions. -The money purchase annual allowance is not modelled. +Unused allowance carried forward from the previous three tax years +([s. 228A](https://www.legislation.gov.uk/ukpga/2004/12/section/228A)) is +the input `unused_pension_annual_allowance`, which is added to the allowance +when the charge is worked out. It defaults to nil, because survey data does +not record past pension input amounts, and the model does not derive it from +earlier years. The money purchase annual allowance, the £10,000 limit on +money purchase savings after pensions are first flexibly accessed +([s. 227ZA](https://www.legislation.gov.uk/ukpga/2004/12/section/227ZA)), +is not modelled. Contributions **above** the annual allowance can still be relieved and separately attract the annual allowance charge, @@ -76,7 +84,10 @@ contributions made before age 75, employer contributions and salary sacrifice ([Finance Act 2004 s. 233](https://www.legislation.gov.uk/ukpga/2004/12/section/233)). For example, an employee on £100,000 who pays £70,000 into a personal pension in 2025-26 gets £70,000 of relief and a £2,000 charge on the £10,000 excess, -which falls in the basic rate band. +which falls in the basic rate band. With £10,000 of unused allowance +brought forward, there is no charge. By default the pension input amount is +cash contributions: defined-benefit accrual counts only if supplied through +`pension_contributions_for_annual_allowance`. Relieved contributions also reduce `adjusted_net_income`, so they restore personal allowance lost above £100,000, as in law @@ -94,7 +105,7 @@ rate, plus any personal allowance restored. The four scenarios reported in #704 (all 2025-26, all self-employed, single person aged 40, no other income, contributions entered as `personal_pension_contributions`). The "model" column was re-run in -October 2026 on policyengine-uk 2.135.6: +October 2026 on policyengine-uk 2.140.0: | ID | Self-employed income | Pension contribution | Expected income-tax change | What the model returns | Why | |-------|---------------------|----------------------|-----------------------------|------------------------|-----| diff --git a/docs/book/programs/gov/hmrc/pensions-comparison-stbm.md b/docs/book/programs/gov/hmrc/pensions-comparison-stbm.md index be51378fe7..67ce942385 100644 --- a/docs/book/programs/gov/hmrc/pensions-comparison-stbm.md +++ b/docs/book/programs/gov/hmrc/pensions-comparison-stbm.md @@ -24,10 +24,10 @@ more detail on the PolicyEngine side. | Contributions that get relief | The person's own contributions | Employee contributions, additional voluntary contributions **and employer contributions** | | Basic-rate relief added to the pot | Not shown separately | Recorded separately (`pension_relief_at_source`) | | Earnings limit | Relief capped at relevant UK earnings: employment income, employer-paid Statutory Sick Pay and Statutory Maternity Pay, and self-employment profit | No earnings cap; £3,600 limit when total income is below it | -| Annual allowance | £60,000, tapered on adjusted net income above £260,000 (`pension_annual_allowance`) | Tapered on total income above a threshold-income parameter | -| Contributions above the allowance | Charged at the marginal rate (`personal_pension_contributions_tax`) | Relief stops at the allowance; no separate charge | +| Annual allowance | £60,000 (`pension_annual_allowance`), tapered only when adjusted income is above £260,000 and threshold income is above £200,000, as in FA 2004 s. 228ZA | Tapered on total income above a threshold-income parameter | +| Contributions above the allowance | Annual allowance charge on the excess, taxed as the top slice of income (`personal_pension_contributions_tax`) | Relief stops at the allowance; no separate charge | | Employer contributions in the data | Read from the data (`employer_pension_contributions`) | Imputed when missing from the FRS, using the ONS ASHE employer-contribution bands (`src/Pensions.jl`) | -| Carry-forward of unused allowance | Not modelled | Not modelled | +| Carry-forward of unused allowance | Input `unused_pension_annual_allowance` (FA 2004 s. 228A) adds to the allowance; defaults to nil, as the survey data has no past pension input amounts | Not modelled | ## Where the two models differ @@ -38,16 +38,15 @@ methods give the same total relief. They can differ when the person pays little or no income tax: STBM still records the basic-rate addition to the pot, which PolicyEngine UK does not show. -**Shared gap: the personal allowance taper.** In law, gross pension -contributions reduce adjusted net income +**The personal allowance taper.** In law, gross pension contributions +reduce adjusted net income ([Income Tax Act 2007 s. 58](https://www.legislation.gov.uk/ukpga/2007/3/section/58)), so contributing can restore personal allowance lost above £100,000. -Neither model does this. In PolicyEngine UK, `adjusted_net_income` adds up -the taxable income components without deducting `pension_contributions_relief`: -an employee on £110,000 has adjusted net income of £110,000 and a personal -allowance of £7,570 in 2025-26 whether or not they contribute £10,000. -STBM widens the bands but leaves the taper income unchanged, and its code -flags this as unchecked. +PolicyEngine UK does this: `adjusted_net_income` deducts +`pension_contributions_relief`, so an employee on £110,000 has a personal +allowance of £7,570 in 2025-26, and the full £12,570 if they pay £10,000 +gross into a personal pension. STBM widens the bands but leaves the taper +income unchanged, and its code flags this as unchecked. ### 2. Employer contributions @@ -59,14 +58,29 @@ employee's bands further than the law does. ### 3. Annual allowance taper -The legal taper uses "adjusted income", which adds employer contributions, -and applies only when "threshold income" is also above £200,000. - -- PolicyEngine UK tapers on adjusted net income, with no threshold-income - test. +The legal taper +([FA 2004 s. 228ZA](https://www.legislation.gov.uk/ukpga/2004/12/section/228ZA)) +uses "adjusted income", which adds employer contributions, and applies only +when "threshold income" is also above £200,000 (from 2023-24). + +- PolicyEngine UK applies both tests + (`pension_annual_allowance_adjusted_income` and + `pension_annual_allowance_threshold_income`). An employee on £300,000 + who pays £100,000 gross into a personal pension in 2025-26 has adjusted + income of £300,000 but threshold income of £200,000, so keeps the full + £60,000 allowance. Its limits: by default the pension input amount is + cash contributions, so defined-benefit accrual is counted only if supplied + as an input, and all salary sacrifice is treated as made on or after + 9 July 2015. - STBM tapers on total income above a threshold parameter. -Neither follows the statute exactly. Both affect only very high earners. +Both affect only very high earners. + +PolicyEngine UK does not model the money purchase annual allowance, the +£10,000 limit on money purchase savings after pensions are first flexibly +accessed +([FA 2004 s. 227ZA](https://www.legislation.gov.uk/ukpga/2004/12/section/227ZA)). +It always uses the default chargeable amount. ### 4. Lifetime and lump sum allowances @@ -77,7 +91,9 @@ are drawn, not to contributions. ## What neither model does -- Carry-forward of unused annual allowance from the previous three years. +- Working out carry-forward of unused annual allowance from the previous + three years. PolicyEngine UK accepts the result as an input but does not + derive it from past years. - Behavioural responses: contributions are held fixed when tax rates change. - Decumulation: crystallisation, the tax-free lump sum, and annuity versus drawdown choices. @@ -87,6 +103,7 @@ are drawn, not to contributions. - STBM source: [`IncomeTaxCalculations.jl`][stbm-it] and [`Pensions.jl`][stbm-pen] at `f534ca1`. - [STBM blog: pension contributions][stbm-blog]. - PolicyEngine UK: [pension tax relief](./pension-tax-relief.md); variables [`pension_contributions_relief`](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/variables/gov/hmrc/pensions/pension_contributions_relief.py), [`pension_annual_allowance`](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/variables/gov/hmrc/income_tax/allowances/pension_annual_allowance.py) and [`personal_pension_contributions_tax`](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/variables/gov/hmrc/pensions/private_pension_contributions_tax.py). +- [Finance Act 2004 s. 228ZA](https://www.legislation.gov.uk/ukpga/2004/12/section/228ZA) (taper) and [s. 228A](https://www.legislation.gov.uk/ukpga/2004/12/section/228A) (carry-forward). - HMRC, [Pensions Tax Manual: annual allowance taper](https://www.gov.uk/hmrc-internal-manuals/pensions-tax-manual/ptm057100). - Issue: [#672](https://github.com/PolicyEngine/policyengine-uk/issues/672). From 1d9405370acd1a577384fcadec1445c17d0f199e Mon Sep 17 00:00:00 2001 From: vahid-ahmadi Date: Tue, 20 Oct 2026 13:51:34 +0100 Subject: [PATCH 7/7] State that BSP rates are held flat and note the War Widow's Pension split SI 2026/148 art 19 keeps the four BSP amounts in SI 2017/410 reg 3 unchanged, so the planned parameters carry no uprating. Note that #2382 gave War Widow's Pension its own variables and that bsp_reported keeps CPI uprating only until the datasets move FRS code 9 out (#2381). Co-Authored-By: Claude Opus 5.5 --- .../gov/dwp/bereavement-support-plan.md | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/docs/book/programs/gov/dwp/bereavement-support-plan.md b/docs/book/programs/gov/dwp/bereavement-support-plan.md index a8160f9926..9499139048 100644 --- a/docs/book/programs/gov/dwp/bereavement-support-plan.md +++ b/docs/book/programs/gov/dwp/bereavement-support-plan.md @@ -44,6 +44,14 @@ That means: [source notes](https://github.com/PolicyEngine/microcosm/blob/d1fda720/packages/microcosm-build/src/microcosm/build/uk/spec/sources.yaml#L1870) say code 6 mixes benefits and code 9 is tax-free War Widow's Pension. So the reported "BSP" also carries legacy widow's benefits and War Widow's Pension. +- Since [#2382](https://github.com/PolicyEngine/policyengine-uk/pull/2382) + (2.135.7), War Widow's Pension has its own variables, + `war_widows_pension_reported` and `war_widows_pension`. `bsp_reported` + keeps CPI uprating only until the datasets move FRS code 9 out of it + ([policyengine-uk-data#578](https://github.com/PolicyEngine/policyengine-uk-data/issues/578), + [microcosm#1222](https://github.com/PolicyEngine/microcosm/issues/1222)); + [#2381](https://github.com/PolicyEngine/policyengine-uk/issues/2381) + tracks freezing it after that. ## Proposed scope @@ -96,6 +104,12 @@ derived. Either way the lump sum is tied to one year only. Both `lump_sum_amount` and `monthly_amount` parameter trees split on `bsp_higher_rate_eligible` (£3,500 / £350 vs £2,500 / £100). +These parameters should be held flat, with no `uprating` key. BSP is +not uprated: the four amounts in [SI 2017/410 reg 3][bsp-reg-3] have +been £350, £3,500, £100 and £2,500 since April 2017, and the +[Social Security Benefits Up-rating Order 2026][uprating-2026] +(SI 2026/148), art 19, says each of them "remains unchanged". + ### Phase 2: historical WPA under the same formula Widowed Parent's Allowance (under [Social Security Contributions and @@ -139,14 +153,6 @@ directly) but not for microsim aggregates. ## Open questions -- BSP isn't uprated each year. The rates have been £2,500/£3,500 and - £100/£350 since 2017. The model's `bsp_reported` is nonetheless - uprated with CPI, which suits the War Widow's Pension and legacy - widow's benefits it also contains but not BSP itself. Should the new - parameter tree carry an explicit - `uprating: gov.economic_assumptions.indices.obr.consumer_price_index` - to mirror what other DWP rates do, or hold flat to match the - historical reality? - WPA recipients who would have been on BSP under the new rules: do we want a reform-side parameter "treat all WPA receipts as if they were BSP" for counterfactual analysis? @@ -159,6 +165,7 @@ directly) but not for microsim aggregates. - [Pensions Act 2014, Part 5](https://www.legislation.gov.uk/ukpga/2014/19/part/5): primary statute for BSP. - [Bereavement Support Payment Regulations 2017 (SI 2017/410)][bsp-regs]. +- [Social Security Benefits Up-rating Order 2026 (SI 2026/148), art 19][uprating-2026]: holds the four BSP amounts unchanged. - [Bereavement Benefits (Remedial) Order 2023 (SI 2023/134)](https://www.legislation.gov.uk/uksi/2023/134/contents): cohabiting-parent extension, effective from 30 August 2018. - [SSCBA 1992 s. 39A][sscba-39a]: legacy WPA basis. - gov.uk, [Bereavement Support Payment](https://www.gov.uk/bereavement-support-payment) and [Widowed Parent's Allowance](https://www.gov.uk/widowed-parents-allowance). @@ -167,4 +174,6 @@ directly) but not for microsim aggregates. [pa-2014-5]: https://www.legislation.gov.uk/ukpga/2014/19/part/5 [bsp-regs]: https://www.legislation.gov.uk/uksi/2017/410/contents +[bsp-reg-3]: https://www.legislation.gov.uk/uksi/2017/410/regulation/3 +[uprating-2026]: https://www.legislation.gov.uk/uksi/2026/148/article/19 [sscba-39a]: https://www.legislation.gov.uk/ukpga/1992/4/section/39A