Skip to content

Move the VAT survey coverage to 0.69 on Microcosm a6299504 - #2634

Open
MaxGhenis wants to merge 14 commits into
mainfrom
fix/vat-coverage-a133
Open

MaxGhenis wants to merge 14 commits into
mainfrom
fix/vat-coverage-a133

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #2633

Reproducer: policyengine_uk/tests/policy/baseline/gov/hmrc/vat/vat_grossing.yaml, policyengine_uk/tests/test_vat_grossing_legacy.py::test_legacy_default_is_the_product_of_the_two_factors

axiom: n/a: a microsimulation calibration factor for survey under-recording, not law

We move gov.simulation.vat.survey_consumption_coverage from 0.67 to 0.69, the value we derive on a6299504, the Microcosm UK 2024-25 national cut that policyengine.py certifies. The deprecated combined factor gov.simulation.microdata_vat_coverage moves to the new product, 0.483. The PR also adds VAT grossing property tests, a 2024 grossing case, and the cut's provenance and receipts source in docs/engineering/vat-coverage.md.

Before the run, we set a rule. If a run with #2189's formula, on the cut that follows microcosm#1383's rebuild, put 2024-25 VAT within ±2% of receipts at 0.67, 0.67 would stay; otherwise the coverage would move to the value that run gave. On a6299504, whose fit holds obr.vat out, total vat at 0.67 came to £176.06bn in 2024-25, exceeding OBR's £170.994bn by 3.0%, and the 2024-25 coverage came to 0.690. So the coverage moves to 0.69.

What changed and why

We derived gov.simulation.vat.survey_consumption_coverage in #2187 on the Microcosm UK 2024-25 cut f9d1922c (2 October 2026), and re-derived it in #2189 with energy bills split out. Its metadata tells maintainers to re-derive it whenever the dataset it should match changes.

policyengine.py certified 6cba5d71 on 20 October 2026 (commit a66e883c4). A proposal to certify a133f37a, policyengine.py#715, closed unmerged on 27 October 2026, so no policyengine.py release certified that cut. policyengine.py now certifies a6299504: commit 324ecad65 certified it on 26 October 2026, release 6.11.1 shipped it, and release 6.12.0 still certifies it.

On 24 October 2026, before the energy split, we ran the two later cuts. 6cba5d71 and a133f37a record about 10% more standard-rated consumption than f9d1922c (£435.60bn against £396.15bn in 2024-25 on a133f37a), so at 0.67 total vat on a133f37a (main c0252415a) exceeded receipts by 7.5% to 10.3%:

2024-25 2025-26 2026-27
Model vat £188.61bn £196.43bn £201.71bn
OBR receipts (EFO March 2026, table 3.8) £170.994bn £180.169bn £187.704bn
Gap +10.3% +9.0% +7.5%

On 6cba5d71, raising the standard rate from 20% to 21% raised total vat and gov_balance by £9.94bn in 2026-27 under policyengine-uk 2.135.7 and 2.152.6 alike, exceeding HMRC's £8.8bn ready reckoner by 12.9%. These pre-split runs gave the reason to re-derive the value, and none of them sets it.

This branch changes the parameters, tests, docs and changelog:

  • Parameters: survey_consumption_coverage moves from 0.67 to 0.69. Its derivation now describes a6299504 (81.48 / (0.7 x (170.994 - 2.33)) = 0.690 in 2024-25), and its reference now lists the OBR receipts workbook's own URL beside the EFO page. Its 0.06% bound on the private school VAT split now names the run at 0.67 that measured it. microdata_vat_coverage moves from 0.469 to 0.483, with "(0.69 x 0.7)" in its description.
  • Tests: new Hypothesis properties and fixed band-edge cases in test_vat_grossing_properties.py. vat_grossing.yaml gains a 2024 case (2,050 / 0.483 = 4,244.31, the product later years use too). Its 2025 case moves to 2,050 / 0.483 x (1 - 0.008368) = 4,208.79, with main's October 2026 EFO share of receipts. test_vat_grossing_legacy.py pins the deprecated default at 0.483. test_private_school_vat_receipts.py takes FACTOR = 0.69 * 0.7, and its published receipts check uses a6299504's rounded inputs: 0.2 x £407.40bn / 0.483 + £2.33bn against £170.994bn. In test_scenario_baseline_parameters.py, a comment's illustrative figure, 526.32 from the old 0.38 factor, becomes "about 411". The Raise VAT standard rate by 2pp fixture in reforms_config.yaml moves from 27.6 to 27.1.
  • Docs: docs/engineering/vat-coverage.md gains "Derivation on a6299504 (November 2026)": the cut's provenance and certification, the obr.vat exclusion, the receipts workbook (URL, SHA-256, archived copy, sheet, row and cells), the chunked method, the results at 0.67 and 0.69, the bound and a reproducing snippet at revision 994cac89. Its value section says why 2024-25 sets the coverage and dates the later-year rows to the March 2026 EFO. "Which datasets" now names the cut policyengine.py certifies and the two before it. The October derivation becomes the earlier derivation on f9d1922c, and the doc dates every statement of 0.67 as the value. The deprecated factor's note now says that baseline_vat, which reads the unreformed parameters, keeps the two factors under a reform to it, as baseline_vat.py and test_private_school_vat_receipts.py show. "Later Microcosm cuts (24 October 2026)" stays as a dated record. docs/book/assumptions/mp30k-uk-switch-plan.md now names a6299504.
  • Changelog: 2633.added.md (the tests and the later-cut record) and 2633.changed.md (the value).

Impact

We ran each dataset on 3 November 2026 at 36bbbf42f, this PR's earlier head (policyengine-uk 2.157.1, policyengine-core 3.33.2), which ships 0.67; the runs at 0.69 set the coverage by reform. Total vat counts the vat variable alone. It leaves out private_school_vat, which the receipts include: nothing in 2024-25, and about £1.5bn a year from 2025-26 on HMRC's costing.

Dataset Fiscal year Total vat at 0.67 Against receipts Total vat at 0.69 Against receipts
Microcosm a6299504 2024-25 £176.06bn +3.0% £171.02bn +0.02%
Microcosm a6299504 2025-26 £181.09bn +0.5% £175.91bn -2.4%
Microcosm a6299504 2026-27 £185.18bn -1.3% £179.86bn -4.2%
Enhanced FRS 1.58.1 2024-25 £242.54bn +41.8% £235.58bn +37.8%
Enhanced FRS 1.58.1 2025-26 £249.37bn +38.4% £242.22bn +34.4%
Enhanced FRS 1.58.1 2026-27 £255.28bn +36.0% £247.93bn +32.1%

Since these runs, main has adopted the October 2026 EFO (#2717). It gives 2025-26 outturn of £179.861bn and a 2026-27 forecast of £186.154bn, and main uprates consumption with its forecasts, so the 2025-26 and 2026-27 rows record policyengine-uk 2.157.1 on the March 2026 EFO. The value rests on 2024-25, the cut's own year: apply_uprating leaves that year as the file gives it, and OBR's figure for it is outturn.

Our Microcosm runs summed ten household chunks, since the whole cut needed more than 8 GiB in one process; the Enhanced FRS run used the whole file.

We also costed a one-point rise at 0.69, setting the standard rate to 21% for 2026-27 alone, on Microcosm a6299504:

  • Over all households, total vat rises from £179.862bn to £188.767bn, by £8.906bn, exceeding HMRC's £8.8bn ready reckoner by 1.2%. The formula's arithmetic, 1% of the standard-rated base times (1 - 0.008311) over 0.483, gives £8,905,600,556, within £8,968 of the run.
  • Over the eight of ten household chunks that ran (two needed more than 8 GiB and stopped), gov_balance rises by £7.181bn, vat by £7.120bn and private_school_vat by £0.061bn. The last comes to 5.0% of those chunks' £1.214bn baseline private_school_vat, since that charge moves in proportion to the rate. In each of the eight chunks, the change in gov_balance equals the change in vat plus the change in private_school_vat to within £400 (float32 rounding on totals of about £0.9bn). The attends_private_school formula ranks household net income within the simulated households, so each chunk ranks its own households.

We estimate that a two-point rise in the VAT standard rate, to 22%, adds £27.07bn to gov_balance in 2029-30 on the certified Enhanced FRS, with af0b602ca merged into main at 1d4d020aa. On main alone, it adds £27.87bn. The Raise VAT standard rate by 2pp fixture in reforms_config.yaml now expects 27.1 and records that costing in its receipts list.

Invariants

These hold for every input:

  1. With the deprecated factor at its declared value, the grossing factor equals survey_consumption_coverage × household_share_of_receipts.
  2. A reform that moves the deprecated factor out of the closeness band around its declared value (1e-8 + 1e-5 × the value, NumPy's default isclose tolerance) overrides the two; a value inside the band counts as untouched.
  3. In 2025, each household's vat with no electricity or gas bills equals (standard rate × full_rate_vat_consumption + reduced rate × reduced_rate_vat_consumption) / shipped product × (1 − private_school_share_of_receipts). It scales linearly with each household's consumption, with no cross-household term.
  4. The deprecated default equals the product of the two factors in every year from 2015 to 2030.
  5. The deprecated factor has a single undated value, so 2024 takes the product like every later year (vat_grossing.yaml, 2024 case). Adding a dated entry to it, which would send earlier years down the legacy path, fails that case.

Hypothesis tests in policyengine_uk/tests/test_vat_grossing_properties.py check invariants 1–3, and test_vat_grossing_legacy.py (main) checks invariant 4. The property oracles read the parameters, so they check the shipped product of 0.483. Dropping the household share from vat_grossing_factor fails invariants 1 and 3. Dropping the school share from vat, applying it twice, or charging either rate's spending at the other rate fails invariant 3 on its fixed example. For invariant 2, fixed cases sit at 0.99 and 1.01 bands either side of the declared value; changing the helper's np.isclose to exact equality fails the inside cases. The helper properties have no Hypothesis deadline, since they check algebra, not speed.

Reverting the parameter change on a copy of 303e65852, the head before the merge with main, shows which tests hold the value. With both parameters back at 0.67 and 0.469, the 2025 and 2024 cases in vat_grossing.yaml fail (vat returns to 4,334.49 and 4,371.00), and so do test_legacy_default_is_the_product_of_the_two_factors, test_published_microcosm_receipts_check_is_unchanged and all six test_energy_bill_vat_bears_no_school_share cases. Reverting the coverage alone fails the same tests. Reverting the deprecated factor alone to 0.469 leaves vat on the product, so the YAML cases pass, but test_legacy_default_is_the_product_of_the_two_factors fails (0.469 against 0.483). The clean head passes all of them.

CI can't run the dataset-level check (total vat within a few percent of receipts on the certified Microcosm cut), because the tests run on Enhanced FRS 1.58.1, their certified dataset. The doc's snippet reproduces it.

Tests

At 303e65852, with HUGGING_FACE_TOKEN and POLICYENGINE_UK_DEFAULT_DATASET unset, in the locked environment (uv sync --frozen --extra dev):

  • policyengine-core test policyengine_uk/tests/policy/baseline/gov/hmrc/vat -c policyengine_uk: 10 passed. private_school_vat.yaml: 15 passed. excise_integration.yaml: 2 passed. gambling_duty/integration.yaml: 7 passed.
  • pytest -n 4 on test_vat_grossing_properties.py, test_vat_grossing_legacy.py, test_private_school_vat_receipts.py, test_private_school_vat_properties.py, test_domestic_electricity_vat.py and test_historic_vat_rates.py: 135 passed.
  • pytest -n 4 on test_scenario_baseline_parameters.py, test_dated_reform_routes.py and test_no_economic_assumptions.py, which the coverage grep found: 132 passed, 1 skipped. test_before_load_spending_scenario_constructs_on_the_dataset needs the dataset.
  • pytest -n 4 test_fiscal_partition.py: 41 passed.
  • pytest -n 4 aggregate_impact/: 306 passed, 9 skipped. All nine skips sit in test_aggregate_impact_certified.py, which needs the dataset.
  • pytest policyengine_uk/tests/code_health -n 4: 2887 passed.
  • uvx ruff@0.17.2 format --check . (1981 files already formatted), uvx ruff@0.17.2 check . and uvx ruff@0.17.2 check --select F811,F821 policyengine_uk: all checks passed. git diff --check 36bbbf42f..HEAD: clean.

On a trial merge of 303e65852 with main at 52b30809f, which settles vat_grossing.yaml as this head does, the vat YAML directory gave 12 passed, and test_vat_grossing_legacy.py, test_private_school_vat_receipts.py and test_vat_grossing_properties.py gave 55 passed (-n 4). CI runs the full suite at this head. The survey-data tests, including the reforms_config.yaml fixtures, run where CI has the dataset, which includes merge-queue and main runs.

Interaction with open work

PolicyEngine/microcosm#1383 (merged) locks policyengine-uk 2.151.0 and holds obr.vat out of the fit as a diagnostic (commit 94d4102fd). A Microcosm maintainer approved that exclusion on 24 October 2026 until 24 January 2027, on the terms that the row rebinds once policyengine-uk re-derives the factor on a file whose fit doesn't bind obr.vat. We re-derive it here on a6299504, which meets that condition, and juaristi22 decides whether and when the row rebinds, and any lock bump. Microcosm's calibration diagnostics for the cut report one warn-only VAT warning, uk_consumption_drift: the total full_rate_vat_base moves by more than 2% of its design value between the design and the final weights. If a later cut binds obr.vat under 0.69, its fit would absorb the factor, so this value pairs only with a cut on which a run supports it.

🤖 Generated with Claude Code

survey_consumption_coverage was derived in #2187 on Microcosm f9d1922c.
The 6cba5d71 and a133f37a cuts record about 10% more standard-rated
consumption, so with 0.67 total vat ran 7.5% to 10.3% above OBR receipts
in 2024-25 to 2026-27. The doc's own calculation on a133f37a gives 0.739
for the latest outturn year; it is used as 0.74 (product 0.518), inside
the 0.720 to 0.756 bound from receipts and the ready reckoner.

Adds Hypothesis properties for the grossing factor and for vat being
statutory liability over the shipped product. Totals at the new product
and the reform-regression fixture wait on runs in the hub's next
reserved window.

Fixes #2633

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The deprecated combined factor has one undated value, so every year,
2024 included, takes the two factors' product. A YAML case pins vat in
2024; adding a dated entry to the deprecated factor makes it fail. The
doc's inputs now give the Hugging Face revisions of the a133f37a and
6cba5d71 tags, and the reproduction snippet pins a133f37a's revision.

Refs #2633

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Aggregate impact on certified data: pass

Base fe1250ea77 against head 779c918e54, both on enhanced_frs_2024_25.h5 (policyengine-uk-data 1.58.1, sha256 056e80fd8e91), fiscal years 2025-26 to 2031-32. Each year is a full microsimulation run on each side.

Checks: 0 broken by this change, 1 failing on both sides, 0 fixed. Moves flagged for review: 49 (thresholds in report_spec.yaml; a flag does not fail the job).

Workflow run and full report

Notes

  • In the years a figure from fewer than 10 records is suppressed (spending.disabled_students_allowance), every total containing it, and the decile incomes, are rebuilt from their other components, so no published total includes it. Those totals are marked †. A change is withheld where the two sides leave out different figures and can't be lined up.

Moves flagged for review

Measure Year Base Head Change
VAT excluding private school fees 2026-27 £256.87bn £249.47bn -£7.39bn
VAT excluding private school fees 2027-28 £266.33bn £258.67bn -£7.66bn
VAT excluding private school fees 2031-32 £291.59bn £283.21bn -£8.38bn
VAT excluding private school fees 2030-31 £285.24bn £277.04bn -£8.20bn
VAT excluding private school fees 2029-30 £278.97bn £270.95bn -£8.02bn
VAT excluding private school fees 2028-29 £272.70bn £264.86bn -£7.83bn
VAT excluding private school fees 2025-26 £249.37bn £242.22bn -£7.16bn
Government balance, each receipt once 2025-26 £363.07bn £355.91bn -£7.16bn
gov_balance 2025-26 £363.07bn £355.91bn -£7.16bn
Government balance, each receipt once 2026-27 £389.57bn £382.17bn -£7.39bn
gov_balance 2026-27 £389.57bn £382.17bn -£7.39bn
Government balance, each receipt once 2027-28 £430.34bn £422.68bn -£7.66bn
gov_balance 2027-28 £430.34bn £422.68bn -£7.66bn
Government balance, each receipt once 2028-29 £457.08bn £449.24bn -£7.83bn
gov_balance 2028-29 £457.08bn £449.24bn -£7.83bn
Government balance, each receipt once 2029-30 £487.75bn £479.74bn -£8.02bn
gov_balance 2029-30 £487.75bn £479.74bn -£8.02bn
Government balance, each receipt once 2030-31 £516.37bn £508.18bn -£8.20bn
gov_balance 2030-31 £516.37bn £508.18bn -£8.20bn
Government balance, each receipt once 2031-32 £542.11bn £533.73bn -£8.38bn
gov_balance 2031-32 £542.11bn £533.73bn -£8.38bn
Government receipts, each component once 2025-26 £892.00bn £884.84bn -£7.16bn
gov_tax 2025-26 £892.00bn £884.84bn -£7.16bn
Government receipts, each component once 2026-27 £934.66bn £927.27bn -£7.39bn
gov_tax 2026-27 £934.66bn £927.27bn -£7.39bn
Government receipts, each component once 2027-28 £980.50bn £972.84bn -£7.66bn
gov_tax 2027-28 £980.50bn £972.84bn -£7.66bn
gov_tax 2028-29 £1,015.93bn £1,008.09bn -£7.83bn
Government receipts, each component once 2028-29 £1,015.93bn £1,008.09bn -£7.83bn
Government receipts, each component once 2029-30 £1,054.24bn £1,046.22bn -£8.02bn
gov_tax 2029-30 £1,054.24bn £1,046.22bn -£8.01bn
Government receipts, each component once 2030-31 £1,091.56bn £1,083.36bn -£8.20bn
gov_tax 2030-31 £1,091.56bn £1,083.36bn -£8.20bn
gov_tax 2031-32 £1,124.89bn £1,116.51bn -£8.38bn
Government receipts, each component once 2031-32 £1,124.89bn £1,116.51bn -£8.38bn
Raise VAT standard rate by 2pp: change in vat 2028-29 £27.03bn £26.24bn -£783.3m
Raise VAT standard rate by 2pp: change in vat 2025-26 £24.69bn £23.98bn -£715.8m
Raise VAT standard rate by 2pp: change in vat 2031-32 £28.91bn £28.07bn -£838.1m
Raise VAT standard rate by 2pp: change in vat 2027-28 £26.42bn £25.66bn -£765.8m
Raise VAT standard rate by 2pp: change in vat 2026-27 £25.51bn £24.77bn -£739.5m

9 more flags are in the report artifact.

Receipts by tax head

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
VAT excluding private school fees -£7.16bn -£7.39bn -£7.66bn -£7.83bn -£8.02bn -£8.20bn -£8.38bn

Spending by benefit and service

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Disabled Students' Allowance suppressed suppressed suppressed suppressed suppressed suppressed suppressed

Caseloads

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Disabled Students' Allowance suppressed suppressed suppressed suppressed suppressed suppressed suppressed

Government balance, each component counted once

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Government receipts, each component once -£7.16bn -£7.39bn -£7.66bn -£7.83bn -£8.02bn -£8.20bn -£8.38bn
Government balance, each receipt once -£7.16bn † -£7.39bn † -£7.66bn † -£7.83bn † -£8.02bn † -£8.20bn † -£8.38bn †

Model totals

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
gov_tax -£7.16bn -£7.39bn -£7.66bn -£7.83bn -£8.01bn -£8.20bn -£8.38bn
gov_balance -£7.16bn † -£7.39bn † -£7.66bn † -£7.83bn † -£8.02bn † -£8.20bn † -£8.38bn †

Standard reform costings (change in gov_balance)

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Raise basic rate by 1pp -£1,560 -£521 -£604 -£1,144 +£2,310 -£591 +£1,261
Raise higher rate by 1pp -£251 -£1,572 +£2,020 +£1,643 +£3,436 -£382 +£1,195
Raise personal allowance by ~800GBP/year -£544 +£128 -£324 +£31 -£291 +£95 +£2,769
Raise child benefit by 25GBP/week per additional child -£7 +£348 -£157 -£118 +£228 -£212 +£233
Reduce Universal Credit taper rate to 20% +£17 +£346 -£69 -£244 +£315 -£34 -£461
Raise Class 1 main employee NICs rate to 10% -£548 -£1,614 +£44 +£707 +£2,533 +£1,881 +£837
Raise VAT standard rate by 2pp -£715.8m -£739.5m -£765.8m -£783.3m -£801.5m -£819.7m -£838.1m
Raise additional rate by 3pp -£281 -£201 -£665 +£1,901 +£1,696 -£145 +£863
Raise the Universal Credit standard allowance by 10% -£180 -£0 +£61 -£153 +£232 +£35 -£106
Raise the State Pension by 10% -£162 +£279 +£1,198 +£384 +£1,005 -£681 +£408

Standard reforms: change in each reform's target

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Raise VAT standard rate by 2pp: change in vat -£715.8m -£739.5m -£765.8m -£783.3m -£801.5m -£819.7m -£838.1m

Household net income by decile

No household's net income changes in any year.

No change in any year: household income totals; population; poverty rates; people in poverty; inequality; mean household net income in each side's own deciles.

Checks failing on both base and head (1)
  • VAT excluding private school fees is never negative (sign:vat): 2025-26: 76 units below zero, a weighted £-52,314,332 in total; 2026-27: 76 units below zero, a weighted £-54,150,722 in total; 2027-28: 76 units below zero, a weighted £-56,043,426 in total
Counted-once check

Each of 92 components was raised by £1 per unit in 2026-27, and every head of its totals was read household by household: its own head must rise by exactly £1, no other tax head may move, no other benefit may rise, and the model's own total must move by what its heads moved. A head that breaks a rule is measured again twice: with every other head held at its baseline value, and with every other head held at its value after the first £1 while the component rises again. It fails where either still shows the response. 0 failed; 0 were skipped.

  • widowed_parents_allowance: pension_credit responds only through other heads in fewer than 10 households (other heads that moved there: carers_allowance; gov_spending; not a failure)
  • widowed_parents_allowance: pension_credit responds only through other heads in fewer than 10 households (other heads that moved there: carers_allowance; household_benefits; not a failure)
Provenance
Side Commit Runtime Environment hash Partition hash Time
base main fe1250ea77 uk 2.174.8 (files 459457be4832), core 3.34.1, py 3.13.18 aarch64 94e3b831d7d0 3bcfb3473960 2490.95s
head #2634 merged into main 779c918e54 uk 2.174.8 (files 766513dc7975), core 3.34.1, py 3.13.18 aarch64 94e3b831d7d0 3bcfb3473960 2458.75s

Dataset: hf://policyengine/policyengine-uk-data-private/enhanced_frs_2024_25.h5@2526edbbb687a3b54dbb11272e5389e4b62d2ac2, sha256 056e80fd8e9151d78f432a25cde863340baee81304c6216a37fb575ce8563784 (verified on both sides), policyengine-uk-data 1.58.1.

Receipts by tax head: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Final charge on cash interest in non-cash ISAs £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Lifetime ISA withdrawal charge £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Income tax £294.46bn £315.23bn £335.82bn £351.28bn £366.96bn £385.36bn £399.99bn
National Insurance (employee and self-employed) £51.70bn £54.14bn £56.13bn £57.73bn £59.76bn £61.64bn £63.51bn
National Insurance (employer) £148.71bn £155.37bn £160.88bn £165.09bn £172.06bn £177.36bn £182.57bn
Employer NI change borne by capital £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Employer NI change borne by consumers £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Capital gains tax £12.60bn £13.15bn £13.60bn £14.11bn £14.67bn £15.24bn £15.80bn
Student loan repayments £7.82bn £8.13bn £8.39bn £8.58bn £8.79bn £9.04bn £9.29bn
VAT excluding private school fees £242.22bn £249.47bn £258.67bn £264.86bn £270.95bn £277.04bn £283.21bn
Change in VAT from its baseline £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
VAT on private school fees £1.95bn £2.01bn £2.09bn £2.14bn £2.20bn £2.26bn £2.31bn
Change in private school VAT from its baseline £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Fuel duties £24.71bn £24.13bn £26.00bn £26.83bn £26.51bn £25.78bn £24.63bn
Alcohol duties £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Tobacco duties £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Vehicle excise duties £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Betting and gaming duties £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
TV licence fee £4.26bn £4.40bn £4.41bn £4.42bn £4.43bn £4.44bn £4.45bn
Council tax £49.21bn £52.16bn £54.89bn £57.95bn £61.16bn £64.47bn £67.91bn
High value council tax surcharge £0.00bn £0.00bn £0.00bn £0.48bn £0.49bn £0.50bn £0.51bn
Northern Ireland domestic rates £0.55bn £0.58bn £0.60bn £0.63bn £0.66bn £0.69bn £0.72bn
Business rates £34.68bn £36.20bn £38.00bn £39.46bn £42.27bn £43.25bn £44.29bn
Stamp duty land tax on the household's own purchases £6.88bn £7.32bn £7.66bn £8.06bn £8.47bn £8.91bn £9.35bn
Stamp duty land tax paid by companies £4.72bn £4.57bn £5.29bn £6.03bn £6.37bn £6.92bn £7.45bn
Land transaction tax (Wales) £0.07bn £0.08bn £0.08bn £0.09bn £0.10bn £0.10bn £0.11bn
Land and buildings transaction tax (Scotland) £0.31bn £0.32bn £0.33bn £0.35bn £0.36bn £0.38bn £0.40bn
Unspecified tax change borne by capital £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Unspecified tax change borne by consumers £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Wealth tax £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Wealth tax excluding the main residence £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Land value tax £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Carbon tax £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Spending by benefit and service: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Lifetime ISA bonus £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
State Pension £121.68bn £125.92bn £126.72bn £129.48bn £132.98bn £136.65bn £140.41bn
Pension Credit £6.21bn £6.46bn £6.68bn £6.82bn £6.89bn £7.07bn £7.22bn
Winter Fuel Payment £1.86bn £1.85bn £1.79bn £1.77bn £1.78bn £1.78bn £1.78bn
Pension Age Winter Heating Payment (Scotland) £0.19bn £0.20bn £0.20bn £0.21bn £0.21bn £0.22bn £0.22bn
Universal Credit £75.48bn £80.52bn £82.95bn £84.96bn £86.11bn £87.61bn £88.21bn
Housing Benefit £8.56bn £8.89bn £9.23bn £9.46bn £9.69bn £9.99bn £10.27bn
Council Tax Reduction £2.07bn £2.20bn £2.32bn £2.45bn £2.59bn £2.73bn £2.87bn
Employment and Support Allowance (income-related) £0.79bn £0.82bn £0.84bn £0.86bn £0.88bn £0.90bn £0.92bn
Employment and Support Allowance (contribution-based) £5.20bn £5.37bn £5.56bn £5.69bn £5.82bn £5.95bn £6.09bn
Jobseeker's Allowance (income-based) £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Jobseeker's Allowance (contribution-based) £0.29bn £0.30bn £0.31bn £0.31bn £0.32bn £0.33bn £0.34bn
Income Support £0.11bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Working Tax Credit £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Child Tax Credit £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Incapacity Benefit £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Personal Independence Payment £26.95bn £28.03bn £29.05bn £30.09bn £30.75bn £31.51bn £32.21bn
Disability Living Allowance £7.49bn £7.79bn £8.07bn £8.36bn £8.55bn £8.76bn £8.95bn
Attendance Allowance £8.68bn £9.03bn £9.36bn £9.69bn £9.90bn £10.15bn £10.38bn
Carer's Allowance £4.48bn £4.66bn £4.83bn £5.00bn £5.11bn £5.27bn £5.42bn
Carer Support Payment (Scotland) £0.31bn £0.32bn £0.33bn £0.34bn £0.35bn £0.36bn £0.37bn
Carer's Allowance Supplement (Scotland) £0.00bn £0.04bn £0.04bn £0.05bn £0.05bn £0.05bn £0.05bn
Severe Disablement Allowance £0.03bn £0.03bn £0.03bn £0.03bn £0.03bn £0.03bn £0.03bn
Industrial Injuries Disablement Benefit £0.50bn £0.52bn £0.54bn £0.55bn £0.56bn £0.58bn £0.59bn
Armed Forces Compensation Scheme £0.61bn £0.63bn £0.65bn £0.66bn £0.68bn £0.69bn £0.71bn
War Widow's or Widower's Pension £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Child Benefit £16.19bn £16.82bn £17.39bn £18.00bn £18.39bn £18.84bn £19.25bn
Scottish Child Payment £0.49bn £0.52bn £0.53bn £0.55bn £0.56bn £0.57bn £0.56bn
Two Child Limit Payment (Scotland, counterfactual) £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Healthy Start vouchers £0.05bn £0.05bn £0.05bn £0.05bn £0.05bn £0.05bn £0.05bn
Maternity Allowance £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Statutory Maternity Pay £3.30bn £3.41bn £3.54bn £3.62bn £3.70bn £3.78bn £3.87bn
Statutory Paternity Pay £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Statutory Sick Pay £1.12bn £1.16bn £1.20bn £1.23bn £1.26bn £1.29bn £1.32bn
Sure Start Maternity Grant £0.01bn £0.01bn £0.01bn £0.01bn £0.01bn £0.01bn £0.01bn
Bereavement Support Payment £0.67bn £0.69bn £0.71bn £0.73bn £0.75bn £0.76bn £0.78bn
Widowed Parent's Allowance £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Tax-Free Childcare £0.56bn £0.57bn £0.57bn £0.58bn £0.60bn £0.61bn £0.59bn
Extended childcare entitlement £2.99bn £3.50bn £3.59bn £3.71bn £3.78bn £3.86bn £3.93bn
Universal childcare entitlement £2.06bn £2.22bn £2.29bn £2.37bn £2.43bn £2.48bn £2.53bn
Targeted childcare entitlement £0.50bn £0.51bn £0.52bn £0.54bn £0.55bn £0.57bn £0.56bn
Childcare Offer for Wales £0.26bn £0.28bn £0.28bn £0.29bn £0.30bn £0.31bn £0.32bn
Care to Learn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Childcare Grant £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Parents' Learning Allowance £0.20bn £0.20bn £0.20bn £0.20bn £0.20bn £0.20bn £0.20bn
Adult Dependants' Grant £0.06bn £0.06bn £0.04bn £0.04bn £0.04bn £0.04bn £0.04bn
Travel Grant £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Disabled Students' Allowance suppressed suppressed suppressed suppressed suppressed suppressed suppressed
16 to 19 Bursary Fund £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
NHS spending £203.89bn £204.30bn £204.56bn £204.91bn £205.34bn £205.85bn £206.35bn
State education spending £0.02bn £0.02bn £0.02bn £0.02bn £0.02bn £0.02bn £0.02bn
Transport subsidy spending £25.05bn £25.10bn £25.14bn £25.18bn £25.23bn £25.30bn £25.36bn
Cost-of-living support payment £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Energy Bills Rebate £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Household Energy Payment £0.00bn £2.11bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Energy Price Guarantee subsidy £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Basic income £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Disability basic income £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Other public spending budget change £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Caseloads: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Lifetime ISA bonus 0k 0k 0k 0k 0k 0k 0k
State Pension 11,264k 11,145k 10,819k 10,700k 10,723k 10,750k 10,776k
Pension Credit 1,489k 1,484k 1,476k 1,454k 1,452k 1,457k 1,466k
Winter Fuel Payment 7,844k 7,783k 7,473k 7,375k 7,391k 7,409k 7,427k
Pension Age Winter Heating Payment (Scotland) 803k 802k 800k 797k 799k 801k 802k
Universal Credit 6,342k 6,347k 6,336k 6,297k 6,292k 6,271k 6,223k
Housing Benefit 1,249k 1,245k 1,244k 1,227k 1,227k 1,229k 1,232k
Council Tax Reduction 3,180k 3,200k 3,206k 3,219k 3,234k 3,245k 3,252k
Employment and Support Allowance (income-related) 71k 71k 71k 71k 71k 71k 71k
Employment and Support Allowance (contribution-based) 719k 720k 721k 722k 724k 726k 728k
Jobseeker's Allowance (income-based) 0k 0k 0k 0k 0k 0k 0k
Jobseeker's Allowance (contribution-based) 67k 67k 67k 67k 67k 68k 68k
Income Support 25k 0k 0k 0k 0k 0k 0k
Working Tax Credit 0k 0k 0k 0k 0k 0k 0k
Child Tax Credit 0k 0k 0k 0k 0k 0k 0k
Incapacity Benefit 0k 0k 0k 0k 0k 0k 0k
Personal Independence Payment 3,567k 3,575k 3,579k 3,585k 3,593k 3,602k 3,611k
Disability Living Allowance 1,291k 1,294k 1,296k 1,298k 1,300k 1,304k 1,307k
Attendance Allowance 1,695k 1,698k 1,700k 1,703k 1,707k 1,711k 1,715k
Carer's Allowance 1,034k 1,037k 1,038k 1,040k 1,042k 1,060k 1,062k
Carer Support Payment (Scotland) 71k 71k 71k 72k 72k 72k 72k
Carer's Allowance Supplement (Scotland) 0k 71k 71k 72k 72k 72k 72k
Severe Disablement Allowance 4k 4k 4k 4k 4k 4k 4k
Industrial Injuries Disablement Benefit 137k 137k 137k 138k 138k 138k 139k
Armed Forces Compensation Scheme 93k 93k 94k 94k 94k 94k 94k
War Widow's or Widower's Pension 0k 0k 0k 0k 0k 0k 0k
Child Benefit 8,123k 8,120k 8,120k 8,130k 8,145k 8,161k 8,178k
Scottish Child Payment 351k 352k 352k 353k 350k 348k 337k
Two Child Limit Payment (Scotland, counterfactual) 0k 0k 0k 0k 0k 0k 0k
Healthy Start vouchers 200k 200k 200k 201k 201k 201k 202k
Maternity Allowance 0k 0k 0k 0k 0k 0k 0k
Statutory Maternity Pay 333k 333k 334k 334k 335k 336k 337k
Statutory Paternity Pay 0k 0k 0k 0k 0k 0k 0k
Statutory Sick Pay 181k 182k 182k 182k 182k 183k 183k
Sure Start Maternity Grant 26k 26k 26k 26k 26k 26k 26k
Bereavement Support Payment 98k 98k 98k 98k 98k 99k 99k
Widowed Parent's Allowance 0k 0k 0k 0k 0k 0k 0k
Tax-Free Childcare 1,043k 1,027k 1,014k 1,009k 1,011k 1,009k 993k
Extended childcare entitlement 795k 800k 796k 797k 798k 798k 792k
Universal childcare entitlement 1,071k 1,072k 1,072k 1,073k 1,076k 1,079k 1,074k
Targeted childcare entitlement 165k 156k 154k 154k 155k 155k 150k
Childcare Offer for Wales 39k 39k 39k 39k 39k 39k 39k
Care to Learn 0k 0k 0k 0k 0k 0k 0k
Childcare Grant 0k 0k 0k 0k 0k 0k 0k
Parents' Learning Allowance 98k 98k 98k 99k 99k 99k 99k
Adult Dependants' Grant 18k 18k 12k 12k 12k 12k 12k
Travel Grant 0k 0k 0k 0k 0k 0k 0k
Disabled Students' Allowance suppressed suppressed suppressed suppressed suppressed suppressed suppressed
16 to 19 Bursary Fund 0k 0k 0k 0k 0k 0k 0k
Cost-of-living support payment 0k 0k 0k 0k 0k 0k 0k
Energy Bills Rebate 0k 0k 0k 0k 0k 0k 0k
Household Energy Payment 0k 21,065k 0k 0k 0k 0k 0k
Energy Price Guarantee subsidy 0k 0k 0k 0k 0k 0k 0k
Basic income 0k 0k 0k 0k 0k 0k 0k
Disability basic income 0k 0k 0k 0k 0k 0k 0k
Government balance, each component counted once: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Government receipts, each component once £884.84bn £927.27bn £972.84bn £1,008.09bn £1,046.22bn £1,083.36bn £1,116.51bn
Government spending £528.93bn † £545.10bn † £550.16bn † £558.85bn † £566.49bn † £575.19bn † £582.78bn †
Government balance, each receipt once £355.91bn † £382.17bn † £422.68bn † £449.24bn † £479.74bn † £508.18bn † £533.73bn †
Benefits in household_benefits but not in gov_spending £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Model totals: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
gov_tax £884.84bn £927.27bn £972.84bn £1,008.09bn £1,046.22bn £1,083.36bn £1,116.51bn
household_tax £491.97bn £520.41bn £551.21bn £576.00bn £601.00bn £626.69bn £648.42bn
pre_budget_change_household_tax £484.15bn £512.28bn £542.82bn £566.95bn £591.72bn £617.16bn £638.61bn
gov_spending £528.93bn † £545.10bn † £550.16bn † £558.85bn † £566.49bn † £575.19bn † £582.78bn †
household_benefits £528.93bn † £545.10bn † £550.16bn † £558.85bn † £566.49bn † £575.19bn † £582.78bn †
gov_balance £355.91bn † £382.17bn † £422.68bn † £449.24bn † £479.74bn † £508.18bn † £533.73bn †
Household income totals: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Household market income £1,637.96bn £1,702.77bn £1,757.35bn £1,806.41bn £1,855.97bn £1,912.43bn £1,970.44bn
Household net income £1,636.24bn † £1,687.25bn † £1,714.85bn † £1,746.83bn † £1,777.98bn † £1,816.26bn † £1,858.87bn †
Household net income (HBAI definition) £1,449.13bn £1,499.27bn £1,530.25bn £1,564.51bn £1,597.18bn £1,634.83bn £1,675.80bn
Population: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
People 69,245k 69,383k 69,473k 69,592k 69,738k 69,912k 70,080k
Benefit units 35,110k 35,181k 35,226k 35,286k 35,360k 35,449k 35,534k
Households 31,046k 31,108k 31,149k 31,201k 31,267k 31,345k 31,420k
Poverty rates: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Absolute poverty, before housing costs: everyone 16.3% 15.5% 16.4% 16.3% 16.5% 16.5% 16.6%
Absolute poverty, before housing costs: children 24.7% 22.1% 23.3% 22.9% 23.1% 23.2% 23.5%
Absolute poverty, before housing costs: working-age adults 14.1% 13.8% 14.7% 14.7% 14.8% 14.8% 15.0%
Absolute poverty, before housing costs: pensioners 13.7% 13.4% 14.0% 13.8% 13.9% 14.0% 13.9%
Absolute poverty, after housing costs: everyone 21.6% 21.0% 22.0% 22.6% 22.9% 23.1% 23.4%
Absolute poverty, after housing costs: children 33.3% 31.3% 32.3% 32.5% 32.9% 33.4% 34.2%
Absolute poverty, after housing costs: working-age adults 19.9% 19.8% 20.8% 21.6% 21.9% 22.0% 22.3%
Absolute poverty, after housing costs: pensioners 12.7% 12.4% 13.1% 13.2% 13.3% 13.4% 13.6%
Relative poverty, before housing costs: everyone 17.0% 16.0% 16.4% 16.3% 16.4% 16.3% 16.6%
Relative poverty, before housing costs: children 25.9% 22.9% 23.3% 22.9% 23.0% 22.9% 23.5%
Relative poverty, before housing costs: working-age adults 14.5% 14.1% 14.6% 14.7% 14.7% 14.7% 15.0%
Relative poverty, before housing costs: pensioners 14.5% 14.0% 13.9% 13.8% 13.8% 13.9% 13.9%
Relative poverty, after housing costs: everyone 21.5% 21.0% 21.3% 21.3% 21.3% 21.5% 21.7%
Relative poverty, after housing costs: children 33.1% 31.1% 31.2% 31.0% 31.1% 31.2% 31.8%
Relative poverty, after housing costs: working-age adults 19.9% 19.8% 20.2% 20.4% 20.5% 20.6% 20.8%
Relative poverty, after housing costs: pensioners 12.5% 12.4% 12.2% 11.7% 11.7% 11.8% 12.0%
People in poverty: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Absolute poverty, before housing costs: everyone 11,277k 10,778k 11,402k 11,365k 11,475k 11,514k 11,650k
Absolute poverty, before housing costs: children 3,688k 3,314k 3,496k 3,445k 3,480k 3,502k 3,549k
Absolute poverty, before housing costs: working-age adults 5,965k 5,893k 6,308k 6,362k 6,420k 6,430k 6,521k
Absolute poverty, before housing costs: pensioners 1,624k 1,571k 1,597k 1,558k 1,575k 1,581k 1,580k
Absolute poverty, after housing costs: everyone 14,944k 14,599k 15,310k 15,721k 15,937k 16,140k 16,426k
Absolute poverty, after housing costs: children 4,975k 4,682k 4,837k 4,888k 4,945k 5,040k 5,179k
Absolute poverty, after housing costs: working-age adults 8,463k 8,461k 8,971k 9,344k 9,491k 9,584k 9,706k
Absolute poverty, after housing costs: pensioners 1,506k 1,456k 1,501k 1,489k 1,501k 1,517k 1,541k
Relative poverty, before housing costs: everyone 11,750k 11,084k 11,387k 11,364k 11,416k 11,416k 11,654k
Relative poverty, before housing costs: children 3,872k 3,423k 3,496k 3,445k 3,466k 3,454k 3,551k
Relative poverty, before housing costs: working-age adults 6,159k 6,013k 6,301k 6,362k 6,389k 6,389k 6,523k
Relative poverty, before housing costs: pensioners 1,719k 1,648k 1,590k 1,558k 1,561k 1,573k 1,580k
Relative poverty, after housing costs: everyone 14,864k 14,552k 14,777k 14,809k 14,872k 15,012k 15,232k
Relative poverty, after housing costs: children 4,944k 4,660k 4,673k 4,663k 4,679k 4,707k 4,806k
Relative poverty, after housing costs: working-age adults 8,431k 8,440k 8,711k 8,823k 8,873k 8,967k 9,062k
Relative poverty, after housing costs: pensioners 1,489k 1,452k 1,393k 1,323k 1,321k 1,338k 1,364k
Inequality: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Gini coefficient, equivalised HBAI income before housing costs 0.3078 0.3050 0.3056 0.3055 0.3060 0.3066 0.3082
Mean household net income in each side's own deciles: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Decile 1: mean household_net_income per household £17,681 † £17,998 † £17,916 † £18,152 † £18,308 † £18,533 † £18,718 †
Decile 2: mean household_net_income per household £27,942 † £29,026 † £28,715 † £29,017 † £29,280 † £29,698 † £30,156 †
Decile 3: mean household_net_income per household £34,316 † £35,481 † £36,069 † £36,606 † £37,195 † £37,732 † £38,182 †
Decile 4: mean household_net_income per household £38,934 † £40,684 † £41,726 † £42,728 † £43,331 † £44,130 † £44,432 †
Decile 5: mean household_net_income per household £43,235 † £45,581 † £46,012 † £46,961 † £47,713 † £48,725 † £50,296 †
Decile 6: mean household_net_income per household £47,931 † £49,202 † £50,891 † £51,936 † £52,936 † £53,820 † £54,663 †
Decile 7: mean household_net_income per household £55,659 † £56,569 † £57,437 † £57,642 † £58,733 † £59,661 † £61,087 †
Decile 8: mean household_net_income per household £68,784 † £70,802 † £71,864 † £73,512 † £73,886 † £75,319 † £77,177 †
Decile 9: mean household_net_income per household £78,889 † £81,012 † £80,859 † £82,645 † £85,463 † £86,945 † £89,508 †
Decile 10: mean household_net_income per household £116,861 † £120,161 † £124,545 † £126,420 † £125,753 † £128,368 † £130,693 †
Standard reform costings (change in gov_balance): head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Raise basic rate by 1pp £6.41bn £6.66bn £6.63bn £6.85bn £7.03bn £7.25bn £7.49bn
Raise higher rate by 1pp £3.61bn £3.87bn £4.00bn £4.22bn £4.44bn £4.71bn £4.83bn
Raise personal allowance by ~800GBP/year £-3.81bn £-3.93bn £-3.99bn £-4.05bn £-4.11bn £-4.24bn £-1.95bn
Raise child benefit by 25GBP/week per additional child £-1.99bn £-1.75bn £-1.55bn £-1.38bn £-1.30bn £-1.18bn £-1.07bn
Reduce Universal Credit taper rate to 20% £-20.77bn £-21.32bn £-21.75bn £-22.00bn £-22.35bn £-22.75bn £-22.94bn
Raise Class 1 main employee NICs rate to 10% £10.89bn £11.30bn £11.71bn £12.01bn £12.31bn £12.62bn £13.00bn
Raise VAT standard rate by 2pp £24.17bn £24.97bn £25.86bn £26.46bn £27.07bn £27.68bn £28.31bn
Raise additional rate by 3pp £2.90bn £3.15bn £3.22bn £3.41bn £3.61bn £3.85bn £4.14bn
Raise the Universal Credit standard allowance by 10% £-3.06bn £-3.18bn £-3.26bn £-3.35bn £-3.44bn £-3.49bn £-3.50bn
Raise the State Pension by 10% £-7.76bn £-7.99bn £-8.22bn £-8.45bn £-8.80bn £-8.53bn £-8.87bn
Standard reforms: change in each reform's target: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Raise basic rate by 1pp: change in income_tax £6.55bn £6.86bn £6.79bn £7.00bn £7.20bn £7.44bn £7.66bn
Raise higher rate by 1pp: change in income_tax £3.61bn £3.92bn £4.01bn £4.23bn £4.45bn £4.72bn £4.84bn
Raise personal allowance by ~800GBP/year: change in income_tax £-3.93bn £-4.07bn £-4.13bn £-4.18bn £-4.22bn £-4.40bn £-1.75bn
Raise child benefit by 25GBP/week per additional child: change in child_benefit £2.33bn £2.14bn £1.94bn £1.76bn £1.65bn £1.52bn £1.40bn
Reduce Universal Credit taper rate to 20%: change in universal_credit £20.74bn £21.31bn £21.79bn £22.05bn £22.37bn £22.78bn £22.99bn
Raise Class 1 main employee NICs rate to 10%: change in national_insurance £11.15bn £11.63bn £12.00bn £12.29bn £12.61bn £12.94bn £13.31bn
Raise VAT standard rate by 2pp: change in vat £23.98bn £24.77bn £25.66bn £26.24bn £26.85bn £27.46bn £28.07bn
Raise additional rate by 3pp: change in income_tax £2.90bn £3.15bn £3.22bn £3.42bn £3.62bn £3.86bn £4.12bn
Raise the Universal Credit standard allowance by 10%: change in universal_credit £3.06bn £3.19bn £3.28bn £3.37bn £3.43bn £3.49bn £3.51bn
Raise the State Pension by 10%: change in state_pension £10.68bn £11.17bn £11.28bn £11.60bn £12.03bn £12.46bn £12.96bn

…p.isclose

Review r1 of #2634 (Sol, REQUEST_CHANGES at b6d8af5) found:

- The £9.94bn one-point figure was described as a reform run without the
  runs being cited. The doc now names them: reform minus baseline `vat` and
  `gov_balance` on 6cba5d71 (revision bb225848), policyengine-uk 2.135.7 and
  2.152.6, £9.94bn in 2026-27 and £9.68bn in 2025-26.
- The moved-legacy property classified closeness with pytest.approx, which
  takes the larger tolerance, while the helper uses np.isclose, which adds
  them. A legacy value of 0.518005185 failed the property. The band is now
  stated once (1e-8 + 1e-5 x the default), the moved property draws values
  outside it, a new property draws values inside it, and fixed cases sit at
  0.99 and 1.01 bands either side. Changing the helper to exact equality
  fails the inside cases.
- The helper properties kept Hypothesis's 200 ms deadline and flaked on a
  loaded host; they now have no deadline.
- The doc attributed both cuts' rows to main and core 3.33.2; it now gives
  each cut's recorded versions.
- The parameter said 6cba5d71 matches a133f37a to three decimal places; it
  now says the 2024-25 coverage (0.739) matches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Review r1 (GPT-6.1 Sol, REQUEST_CHANGES at b6d8af5): all five findings are fixed in e1ff493.

  1. £9.94bn evidence (P2). That figure is a real reform run, but the PR didn't cite it. The doc and body now cite it: on 6cba5d71 (revision bb225848), reform (standard rate 21%) minus baseline gives vat +£9.94bn in 2026-27 and +£9.68bn in 2025-26, with the same change in gov_balance. It ran on PolicyEngine-UK 2.135.7 and again on 2.152.6, and both give the same result. The baseline-only driver the review read is a different script; its static figure is not cited anywhere.
  2. Legacy-override oracle (P2). The band is now stated once: 1e-8 + 1e-5 × the declared value, which is np.isclose's default.
    • The moved-legacy property draws values outside it, and a new property draws values inside it.
    • Fixed cases sit at ±0.99 and ±1.01 bands.
    • The review's counterexample (0.518005185) is now classified as untouched and passes.
    • Mutating the helper to exact equality fails the inside cases.
  3. Deadlines (P2). The two helper properties now have deadline=None.
  4. Version provenance (P3). The a133f37a rows: 2.152.7 / core 3.33.2, and again 2.147.2 / core 3.33.3, with the same totals. The 6cba5d71 rows: 2.147.2 / core 3.33.3.
  5. "Same to 3 dp" (P3). The body now says 6cba5d71 gives products of 0.504 to 0.518, and so gives 0.74 once the 0.739 coverage is rounded. The parameter metadata now says only that the 2024-25 coverage matches.

Local runs at e1ff493:

  • test_vat_grossing_properties.py + test_vat_grossing_legacy.py: 12 passed.
  • vat_grossing.yaml: 5 passed.
  • ruff: clean.

Still PENDING, and the PR stays a draft until they are done:

  • the totals at the new product, and the one-point reform run on a133f37a;
  • the Raise VAT standard rate by 2pp fixture, which is updated only from a measured run.

These run in the hub's next allowed host window. A delta review follows them.

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Plan change (24 Oct): this PR no longer lands on a133f37a's figures. Two changes move what the coverage should be:

  • microcosm#1383, María's rebuild tonight, changes the consumption base: it levels the LCFS COICOP divisions' over-draw.
  • Model the temporary 0% VAT on GB domestic electricity #2189 changes the formula: energy-bill VAT is no longer grossed up, and the coverage is derived against receipts net of energy VAT.

So the value is re-derived on Monday's final cut, after its fit, with the formula on main. The 2pp fixture is updated from a run on the same code, and a delta review follows before this is marked ready. If that run puts VAT within ±2% of receipts at 0.67, this keeps 0.67.

At #1383's head e570d65ff, obr.vat is held out of the fit (calibration_measure_exclusions.json; the exclusion is removed from the calibration registry by apply_uk_calibration_measure_exclusions), so that measurement is not circular.

The figures run today are a reference only, on a133f37a at e1ff493 and before #2189:

Run Result
Total vat at 0.74 vs receipts −0.13% (2024-25), −1.29% (2025-26), −2.70% (2026-27)
1pp reform run, reform minus baseline £8.99bn in 2026-27, +2.2% vs HMRC's £8.8bn
Enhanced FRS 1.58.1, total vat vs receipts +29.0% (2024-25), +27.5% (2025-26)
2pp fixture £24.97bn

Takes main's survey coverage (0.67) and derivation doc, and keeps this
branch's property tests, the 2024 pin test and the record of the later
Microcosm cuts. Test expectations go back to the 0.469 product. The
coverage is re-derived with #2189's formula on the next certified cut.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis MaxGhenis changed the title Re-derive VAT survey coverage on the certified a133f37a Microcosm cut VAT grossing property tests and later-cut provenance; coverage re-derived on the next certified cut Oct 24, 2026
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Merged main (with #2189) at 87911eb58. The branch now ships main's coverage, 0.67 (product 0.469), and #2189's formula. Its own changes are the property tests, the 2024 pin case and the later-cut provenance in the doc; test expectations are back at 2,050 / 0.469 = 4,371.00. The VAT pytest files (38 passed) and the gov/hmrc/vat YAML tests (10 passed) pass locally.

The coverage is re-derived with #2189's formula on the next certified cut, after its fit. Title and body updated to match.

docs/engineering/vat-coverage.md conflicted because both sides added a
section after "Energy bills are not grossed up". Both are kept: main's
private school split first, since it describes the current formula, then the
later-cuts record. vat_grossing.yaml merged cleanly; the 2024 case is
unaffected because the school-fee share is zero in model year 2024.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Picking this up for the UK hub. Reply here if you're mid-change and I'll stop.


Generated by Claude Code

vat takes private school fees' share of VAT receipts out of the grossed-up
amount (from #2319, merged into this branch). The property's oracle divided the
statutory liability by the shipped product without that step, so every example
with more than about £0.56 of liability failed by the share (0.8353% in 2025).
The oracle now reads the share from the parameters, asserts the households
carry no energy-bill VAT, and always runs the minimal case as an explicit
example.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6J9S5H2Fv9MFjnDPCr
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Ready for hub review

Branch: claude/vat-grossing-property-fix-yuk7t1, head 66533369d19501c99f81d69d53d0f7666e641f97 (one commit on b2a3e5169a017e514642a14921e843b79cf3b961). No PR opened, and nothing pushed to fix/vat-coverage-a133. git merge-tree against main 46a60678ff52d829253572393abdcc6a03ceb6a7 merges cleanly.

Finding: the test's oracle is wrong, not the model

test_vat_is_statutory_liability_over_the_shipped_product expected vat = liability / product. Since #2319 was merged into this branch (b2a3e51), vat.py computes vat = liability / product × (1 − s) + domestic_energy_vat, where s is gov.simulation.vat.private_school_share_of_receipts (policyengine_uk/variables/gov/hmrc/vat.py:30-39). docs/engineering/vat-coverage.md ("Private school VAT: splitting the existing receipts total") sets out exactly that formula, vat = G * (1 - s) + E. So the model follows its documented design, and the oracle predates the school share. The "about 0.84%" gap is s itself: 0.008353 for 2025 on the PR head, and 0.008368 on main.

What the spend pair means: (full_rate_vat_consumption, reduced_rate_vat_consumption). The failing cases all have the whole spend on the reduced-rate line.

Energy-bill VAT (#2189): zero in these households. They record no energy bills, so domestic_energy_vat returns [0.]. It plays no part in the failure.

Rounding and float32: vat comes back as float32. Float32 accounts for about 1e-8 relative of the error, against the test's rel=1e-5, so it plays no part either. The traced case below gives 15,489.0029 against the corrected oracle's 15,489.0027.

Minimal case, by hand (one household, one spend line, PR head, 2025)

spend=[(0.0, 15438.0)]:

  • liability = 0.05 × 15,438 = 771.90
  • product = 0.67 × 0.7 = 0.469
  • G = 771.90 / 0.469 = 1,645.8422 (the old oracle's expectation)
  • E = domestic_energy_vat = 0
  • vat = 1,645.8422 × (1 − 0.008353) + 0 = 1,632.0945
  • model: 1632.0946 (float32)

On the merge with main, spend=[(0.0, 146510.0)] gives 7,325.5 / 0.469 = 15,619.4030, then × (1 − 0.008368) = 15,488.6998. The model returns 15488.699, which matches the reported rerun.

I ran each of these through a one-household Simulation. By arithmetic (not a run), any example with liability above about £0.56 fails the old oracle, because s × G then exceeds the test's abs=1e-2 + rel=1e-5 × G tolerance. That is why most draws fail.

Reproduction under uv sync --frozen --extra dev, PR head, seed 320551620663142353140068881523791551106: 1 failed, 7 passed. The case was spend=[(0.0, 146510.64859767928)]: obtained 15489.003, expected 15619.472. Corrected, that expectation is 15,489.0027. On this run Hypothesis stopped shrinking after five minutes, so it reported the larger case rather than the 15438.0 one, which I ran directly instead.

Fix (policyengine_uk/tests/test_vat_grossing_properties.py)

  • expected_vat (lines 95-103) reads the coverage, household share, rates and private_school_share_of_receipts from system.parameters(year). It returns liability / product × (1 − s).
  • household_vat (lines 75-92) builds the vector situation and asserts that domestic_energy_vat is zero. That keeps the property's no-energy premise explicit instead of silently adding E.
  • @example(spend=[(0.0, 15_438.0)]) (line 109) makes the property always run the minimal case. A broken formula therefore fails on every run, not just when the five random draws happen to be non-trivial.
  • The module docstring now names the school share and energy-bill VAT. The test name is unchanged.
  • changelog.d/2633.fixed.md now describes the property as "less private school fees' share of VAT receipts". I also removed its leading - list marker. No model code or parameters changed, and survey_consumption_coverage and microdata_vat_coverage are untouched.

I left out a test with hard-coded 2025 and 2026 values, because main changes s (0.008353 → 0.008368, from the October 2026 EFO) and such a test would break on merge. The oracle reads s from the parameters instead.

Checks

Mutants, each run against the corrected property, applied in place and then restored:

  • A: vat_grossing_factor drops the household share (utils/vat.py: * household_share_of_receipts → * 1.0). Fails: obtained 1,142.4662, expected 1,632.0945.
  • B: vat.py drops the school share (1 - s → 1). Fails: obtained 1,645.8423, expected 1,632.0945.

Property file runs (uv run pytest policyengine_uk/tests/test_vat_grossing_properties.py -n 4):

Run Seed Result
Fixed branch 320551620663142353140068881523791551106 8 passed
Fixed branch 769219029 (random) 8 passed
Fixed branch 12353493 (random) 8 passed
Fixed branch 2995432640 (random) 8 passed
Local merge of the fixed branch with main, after uv sync --frozen 320551620663142353140068881523791551106 8 passed

Other tests:

  • test_vat_grossing_properties.py, test_vat_grossing_legacy.py and test_private_school_vat_receipts.py: 55 passed.
  • policyengine-core test policyengine_uk/tests/policy/baseline/gov/hmrc/vat: 10 passed.
  • code_health: 2887 passed.
  • ruff@0.17.2 format --check, check, check --select F811,F821 and git diff --check: all clean.

Skipped

I ran nothing that needs survey data. shard_durations.toml doesn't exist on this branch, and I renamed no tests.

Proposed PR body changes (not edited)

Invariant 3 is now untrue. I propose replacing it with:

  1. Each household's vat with no energy consumption equals (standard rate × full_rate_vat_consumption + reduced rate × reduced_rate_vat_consumption) / shipped product × (1 − private_school_share_of_receipts). It is linear in each household's consumption, with no cross-household term.

In the paragraph after the invariants, I suggest adding: "Dropping the school share from vat, or the household share from vat_grossing_factor, fails invariant 3 on its fixed example."

Judgment calls

  • I fixed the oracle, not the model, because the doc's allocation section and vat.py agree on G × (1 − s) + E.
  • I kept the test's name.
  • I asserted that E is zero rather than adding domestic_energy_vat to the oracle.
  • The fragment is typed fixed, but the body's Reproducer: reads n/a. The Fix evidence check may want Reproducer: policyengine_uk/tests/test_vat_grossing_properties.py::test_vat_is_statutory_liability_over_the_shipped_product. I've left that, and the fragment type, to the PR owner.

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Ready for hub review

Verdict: REQUEST_CHANGES at 66533369d19501c99f81d69d53d0f7666e641f97 (claude/vat-grossing-property-fix-yuk7t1, reviewed as git diff b2a3e5169a017e514642a14921e843b79cf3b961..66533369d19501c99f81d69d53d0f7666e641f97)

The code delta is sound. The corrected oracle matches vat.py and the doc, it passes on every seed I ran and on a merge with main, and it kills every mutant on the formula's grossed-up route. The verdict comes from two P1s that the branch doesn't yet address. First, the PR still fails Fix evidence with its current body. Second, the body's invariant 3 is false once the delta lands. The fragment type and three body lines fix both. The exact text is below.

P1

P1-1. Fix evidence fails with the current body, and would still fail on this branch

.github/check_fix_pr.py (parse_reproducers) splits a Reproducer: value at every unquoted , and ;. The body line Reproducer: n/a: tests and docs only; model source and dependencies are unchanged until the coverage is re-derived therefore gives the checker two ids. The second, model source and dependencies are unchanged until the coverage is re-derived, isn't a test path, so the check fails. CI already fails this way on #2634's head b2a3e51 (job 121058001035):

Fix evidence: FAIL ('VAT grossing property tests and later-cut provenance; coverage re-derived on the next certified cut')
- Invalid reproducer 'model source and dependencies are unchanged until the coverage is re-derived': the path must be an added, modified or type-changed executable test in this PR. ...

I ran main's checker the way .github/workflows/pr_fix_evidence.yaml does. It reads git diff --name-status --no-renames -z <merge-base 7e24c186> 66533369, which gives A changelog.d/2633.fixed.md, M docs/engineering/vat-coverage.md, M .../vat/vat_grossing.yaml and A policyengine_uk/tests/test_vat_grossing_properties.py. The PR has no labels, and I used the live title. Command: uv run --no-project --with pyyaml==6.0.2 python check_fix_pr.py --title-file … --body-file … --labels-file … --changes-file … --repo-root . --head-sha 66533369…

Fragment Reproducer: line Result
2633.fixed.md (as on branch) current body line FAIL (as above)
2633.fixed.md Reproducer: policyengine_uk/tests/test_vat_grossing_properties.py::test_vat_is_statutory_liability_over_the_shipped_product PASS ("Changed executable tests: 2")
2633.fixed.md Reproducer: n/a: this pull request adds tests and docs and leaves model source and dependencies unchanged PASS ("Exemption accepted")
2633.added.md current body line PASS ("Not a fix PR")
2633.added.md Reproducer: n/a: this pull request adds tests and docs and leaves model source and dependencies unchanged PASS ("Not a fix PR")

Proposed: against main, this PR adds tests and docs and changes no model output (its own Impact section says so). The fragment's text also starts "Added". So type it added:

  • rename changelog.d/2633.fixed.md → changelog.d/2633.added.md (content unchanged);
  • replace the body's last-but-one line with:
Reproducer: n/a: this pull request adds tests and docs and leaves model source and dependencies unchanged

That line passes under either type, so it stays valid if the coverage later moves and the fragment becomes fixed or changed. At that point the model source changes, so the line has to name the test instead. Keep commas and semicolons out of any n/a: reason.

P1-2. Invariant 3 in the body is false at this head

Body, "Invariants", item 3: "vat … equals (standard rate × … + reduced rate × …) / shipped product." vat.py:20-39 computes raw_vat / vat_grossing_factor(...) * (1 - private_school_share_of_receipts) + domestic_energy_vat. CI's failing case on b2a3e51 (Test / Python 3/5: assert np.float32(1.2686316) == 1.2793176972281453) has the ratio 0.991647 = 1 − 0.008353, which is exactly that factor. The hand-off's replacement is correct. I've only added the year, because the property runs in 2025 alone, and named what "no energy consumption" means in the test (no electricity or gas bills):

3. In 2025, each household's `vat` with no electricity or gas bills equals (standard rate × `full_rate_vat_consumption` + reduced rate × `reduced_rate_vat_consumption`) / shipped product × (1 − `private_school_share_of_receipts`). It is linear in each household's consumption, with no cross-household term.

In the paragraph after the invariants, replace "A mutation that drops the household share from vat_grossing_factor fails invariant 1." with:

Dropping the household share from `vat_grossing_factor` fails invariants 1 and 3. Dropping the school share from `vat`, applying it twice, or charging reduced-rate spending at the standard rate fails invariant 3 on its fixed example.

Each clause rests on a mutant I ran (see "Mutants" below).

P2

None.

P3

P3-1. The fixed example covers only the reduced rate, so the standard-rate term depends on random draws

The test is policyengine_uk/tests/test_vat_grossing_properties.py:109, @example(spend=[(0.0, 15_438.0)]). The hand-off says this makes "a broken formula … fail on every run". That holds for the shares and the reduced rate, but not for the standard-rate term. With full_rate_consumption * p.hmrc.vat.standard_rate → * reduced_rate in vat.py, running only the explicit phase (phases=[Phase.explicit] on a scratch copy) gives 1 passed, so the mutant survives the example. The random draws killed it on all four seeds I tried (7, 1, 2, 3), but shrinking took 1.5 to 4.8 minutes each time. I ran this proposed example in the explicit phase: it passes on the clean model and kills the mutant (1 failed):

@example(spend=[(0.0, 15_438.0), (10_000.0, 0.0)])

P3-2. The module docstring reads "less the share" as subtraction of an amount

The text is at test_vat_grossing_properties.py:3-7. The model multiplies by (1 − s) and adds energy-bill VAT without grossing it up. Proposed:

`vat` is the statutory liability on recorded consumption divided by one
grossing factor, times one minus private school fees' share of VAT receipts,
plus VAT on energy bills, which is not grossed up. The factor is the survey
coverage times the household share of receipts, unless a reform moves the
deprecated combined parameter, in which case that value is used.

P3-3. The hand-off's lint used a ruff version other than the lock's

uv.lock pins ruff==0.17.0, while the hand-off reports ruff@0.17.2. I reran with 0.17.0 the way code_changes.yaml does. It's clean, so nothing needs to change (see below).

Checked and sound

  • Model and doc agree on vat = G × (1 − s) + E. In vat.py:17-39, raw_vat / vat_grossing_factor(p.simulation), then * (1 - p.simulation.vat.private_school_share_of_receipts), then + household("domestic_energy_vat", period). utils/vat.py returns survey_consumption_coverage × household_share_of_receipts unless the deprecated factor leaves the np.isclose band. The section "Private school VAT: splitting the existing receipts total" in docs/engineering/vat-coverage.md (lines 237-252) sets vat = G * (1 - s) + E, and says the share comes out of G only. The oracle (expected_vat, lines 95-103) reads system.parameters(year), the instant the formula reads.
  • The minimal case spend=[(0.0, 15_438.0)], 2025. By hand: liability 0.05 × 15,438 = 771.90; product 0.67 × 0.7 = 0.469; G = 1,645.8422; s = 0.008353; vat = 1,632.0945. In a one-household Simulation at the head, vat = [1632.0946] and domestic_energy_vat = [0.]. On a local merge with main 47bda0a7, s = 0.008368, by hand 1,632.0698, and Simulation gives [1632.0698]. vat.py and utils/vat.py are identical on the merge.
  • Mutants. I mutated a scratch checkout, ran the tests, then restored it. Unless noted, each ran the property file under --hypothesis-seed=7.
    • A: household share → 1.0 in utils/vat.py. Killed: 5 failed, including the statutory test (1142.4662 vs 1632.0945) and the band's inside cases.
    • B: 1 - s → 1. Killed: test_vat_is_statutory_liability_over_the_shipped_product, 1645.8423 vs 1632.0945.
    • C: school share applied twice. Killed: same test, 1618.4617 vs 1632.0945.
    • D: reduced rate read as the standard rate. Killed: same test, 6528.3784 vs 1632.0945.
    • E: energy-bill VAT grossed up (energy_vat / vat_grossing_factor(...)). Survives the property file, as expected because its households have E = 0. Elsewhere it's killed by 12 tests in test_domestic_electricity_vat.py (for example test_energy_vat_is_not_grossed_up_by_coverage) and test_private_school_vat_receipts.py (for example test_school_share_never_touches_energy_bill_vat), and by vat/domestic_energy_vat.yaml.
    • F: standard rate read as the reduced rate. Killed by random draws on seeds 7, 1, 2 and 3, but it survives the fixed example (P3-1).
    • G: survey coverage → 1.0 in utils/vat.py. Killed: 5 failed, including the statutory test (1093.5034).
  • Zero-energy assertion and the energy route. household_vat asserts domestic_energy_vat == 0. That holds, because the situation sets no electricity or gas consumption. The PR's own diff against main adds no energy test. The route that the body's first paragraph describes (Model the temporary 0% VAT on GB domestic electricity #2189: added after grossing, not grossed up) is covered on main by test_domestic_electricity_vat.py, test_private_school_vat_receipts.py and vat/domestic_energy_vat.yaml, as mutant E shows. The body claims no property test for the energy route: invariant 3 is limited to households without energy consumption, and the proposed text keeps that limit.
  • Runs (uv sync --frozen --extra dev, with HUGGING_FACE_TOKEN and POLICYENGINE_UK_DEFAULT_DATASET unset, -n 4):
    • property file at the head: seed 320551620663142353140068881523791551106 8 passed; random seeds 1591227208, 1530328247 and 280386 8 passed each;
    • property file on the local merge with main, after uv sync, at the fixed seed: 8 passed;
    • test_vat_grossing_legacy.py + test_private_school_vat_receipts.py: 47 passed (55 with the property file, matching the hand-off);
    • policyengine-core test policyengine_uk/tests/policy/baseline/gov/hmrc/vat -c policyengine_uk: 10 passed;
    • policyengine_uk/tests/code_health: 2887 passed;
    • lint with the locked ruff==0.17.0: format --check . (1980 files already formatted), check . and check --select F811,F821 policyengine_uk all clean; git diff --check b2a3e51..66533369 clean.
  • Changelog fragment. The text is accurate, and towncrier adds the list marker itself, as CHANGELOG.md on main shows. Removing the leading - is therefore correct.
  • Merge. git merge-tree --write-tree origin/main 66533369 exits 0 against main 47bda0a7a9f06b59bb262e9b4dffc05ab2428b5d (tree 0c59cc80a55c7f04f460e3bb98a446fb2522e7aa). docs/engineering/vat-coverage.md and vat_grossing.yaml auto-merge.
  • Other body sentences. The delta makes nothing else in the body untrue. "Invariants 1–3 are Hypothesis tests", the band sentence, the deadline sentence and the Impact section still hold. The test name and its other properties are unchanged.

What I couldn't check

  • Anything that needs survey data: total vat on the Microcosm cuts or Enhanced FRS, and the reform figures in the body. None of these numbers comes from the delta.
  • The hand-off's five-minute shrink at the fixed seed on b2a3e51. I didn't rerun the old oracle. The CI log on b2a3e51 shows the same failure mode.
  • Fix evidence as CI would run it on a pushed head. I ran main's checker locally on the branch's diff instead.

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Picking this up for the UK hub. Reply here if you're mid-change and I'll stop.


Generated by Claude Code

The fragment moves to 2633.added.md with its text unchanged: against main
this branch adds tests and docs and changes no model output. The statutory
property's fixed example gains a full-rate household, so a formula that
charges standard-rated spending at another rate fails on every run, not
only on random draws. The module and oracle docstrings say the school share
multiplies the grossed-up VAT by one minus the share.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N2tyHQ6oJEGs3igENBpYcM
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

PR_TITLE.txt

Add VAT grossing property tests and later Microcosm cut provenance

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

PR_BODY.md

Fixes #2633

**Draft. WIP: the coverage value is re-derived on the next certified Microcosm cut before this lands.** This branch now carries main, including #2189 (energy-bill VAT is added after grossing and is not grossed up; the coverage is derived against receipts net of energy VAT). It ships main's coverage, 0.67, plus test, doc and provenance fixes. The value it lands with comes from a run with #2189's formula on the cut that follows microcosm#1383's rebuild, after that cut's fit:
- if that run puts 2024-25 VAT within ±2% of receipts at 0.67, 0.67 stays and this PR lands with only the fixes below;
- otherwise the coverage moves to the value that run gives, and the impact section is filled from runs at that value.

At #1383's merged head, `obr.vat` is held out of the fit (María's ruling of 24 October; `calibration_measure_exclusions.json`), so that measurement is not circular. Every figure marked PENDING comes from a run, not from arithmetic on other runs.

## What changed and why

`gov.simulation.vat.survey_consumption_coverage` was derived in #2187 on the Microcosm UK 2024-25 cut `f9d1922c` (2 Oct) and re-derived with the energy split in #2189. Its metadata says to re-derive it when the dataset it should match changes.

The certified `6cba5d71` (policyengine.py 6.10.2) and `a133f37a` (the current default; policyengine.py#715) cuts record about 10% more standard-rated consumption: £435.60bn against £396.15bn in 2024-25. Before the energy split, with 0.67, total `vat` on `a133f37a` (main `c0252415a`) came out as follows:

| | 2024-25 | 2025-26 | 2026-27 |
|---|---|---|---|
| Model `vat` | £188.61bn | £196.43bn | £201.71bn |
| OBR receipts (EFO March 2026, table 3.8) | £170.994bn | £180.169bn | £187.704bn |
| Gap | +10.3% | +9.0% | +7.5% |

On `6cba5d71`, a reform run that raises the standard rate from 20% to 21% changed total `vat` and `gov_balance` by £9.94bn in 2026-27 (PolicyEngine-UK 2.135.7 and 2.152.6 agree), 12.9% above HMRC's £8.8bn ready reckoner. These pre-split runs are recorded in the doc as the reason to re-derive; they do not set the value.

Changes in this branch against main:
- **Tests:** new Hypothesis properties and fixed band-edge cases in `test_vat_grossing_properties.py`; a 2024 case in `vat_grossing.yaml` (2,050 / 0.469 = 4,371.00, the same product as later years).
- **Docs:** `docs/engineering/vat-coverage.md` gains a "Later Microcosm cuts (24 October 2026)" section with the `a133f37a` and `6cba5d71` inputs (tags, revisions, SHA-256), versions and the pre-split rows, and a pointer from the "Which datasets" paragraph.
- **Parameters:** none yet. `survey_consumption_coverage` stays 0.67 and `microdata_vat_coverage` 0.469 until the run above.

## Impact

With the parameters unchanged, this branch moves no model output: it adds tests and docs. PENDING, if the run above moves the coverage, runs at the new value of:
- total `vat` on the new cut and on Enhanced FRS 1.58.1, 2024-25 to 2026-27;
- a one-point standard-rate reform run on the new cut (reform minus baseline `vat` and `gov_balance`);
- the `Raise VAT standard rate by 2pp` regression fixture (`reforms_config.yaml`), updated from that run.

## Invariants

These hold for every input:
1. With the deprecated factor at its declared value, the grossing factor equals `survey_consumption_coverage × household_share_of_receipts`.
2. A reform that moves the deprecated factor out of the closeness band around its declared value (1e-8 + 1e-5 × the value, NumPy's default `isclose` tolerance) overrides the two; a value inside the band counts as untouched.
3. In 2025, each household's `vat` with no electricity or gas bills equals (standard rate × `full_rate_vat_consumption` + reduced rate × `reduced_rate_vat_consumption`) / shipped product × (1 − `private_school_share_of_receipts`). It is linear in each household's consumption, with no cross-household term.
4. The deprecated default equals the product of the two factors in every year from 2015 to 2030.
5. The deprecated factor has a single undated value, so 2024 takes the product like every later year (`vat_grossing.yaml`, 2024 case). Adding a dated entry to it, which would send earlier years down the legacy path, fails that case.

Invariants 1–3 are Hypothesis tests in `policyengine_uk/tests/test_vat_grossing_properties.py`. Invariant 4 is in `test_vat_grossing_legacy.py` (main). Dropping the household share from `vat_grossing_factor` fails invariants 1 and 3. Dropping the school share from `vat`, applying it twice, or charging reduced-rate spending at the standard rate fails invariant 3 on its fixed example. For invariant 2, fixed cases sit at 0.99 and 1.01 bands either side of the declared value; changing the helper's `np.isclose` to exact equality fails the inside cases. The helper properties have no Hypothesis deadline, since they check algebra, not speed.

The dataset-level check (total `vat` within a few percent of receipts on the certified Microcosm cut) can't run in CI, because the tests' certified dataset is Enhanced FRS 1.58.1. It is reproduced by the doc's snippet.

## Interaction with open work

PolicyEngine/microcosm#1383 (merged) locks policyengine-uk 2.151.0 and holds `obr.vat` out of the fit as a diagnostic (commit `94d4102fd`; exclusion approved 2026-10-24, expiring 2027-01-24). The exclusion says the row rebinds once policyengine-uk re-derives the factor on a file that does not bind it. If a later cut binds `obr.vat` under 0.67, its fit would absorb the factor, so this PR's value is paired only with a cut on which a run supports it. Any lock bump is María's call.

Reproducer: n/a: this pull request adds tests and docs and leaves model source and dependencies unchanged

axiom: n/a: a microsimulation calibration factor for survey under-recording, not law

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Ready for hub review

Branch: claude/vat-oracle-fix-review-t8wimw, head 36bbbf42f34a23968159406d1b6d8942ebcd6e1c. It is one commit on 66533369d19501c99f81d69d53d0f7666e641f97 (claude/vat-grossing-property-fix-yuk7t1). I opened no PR and didn't push to fix/vat-coverage-a133. The new title and body are in the two comments above.

Findings and how I resolved them

P1-1, Fix evidence. I renamed changelog.d/2633.fixed.md to changelog.d/2633.added.md and left the content unchanged (rename only, similarity 100%). In PR_BODY.md the Reproducer: line now reads Reproducer: n/a: this pull request adds tests and docs and leaves model source and dependencies unchanged. I ran main's .github/check_fix_pr.py (47bda0a7) the way pr_fix_evidence.yaml does: --repo-root . in a main checkout, no labels, the new title and body, and git diff --name-status --no-renames -z <merge-base 7e24c186> 36bbbf42. That diff gives A changelog.d/2633.added.md, M docs/engineering/vat-coverage.md, M .../vat/vat_grossing.yaml and A policyengine_uk/tests/test_vat_grossing_properties.py. Result line:

Fix evidence: PASS ('Add VAT grossing property tests and later Microcosm cut provenance'). Not a fix PR: no added/modified/type-changed fixed fragment or bug/bug-fix label.

P1-2, invariant 3. PR_BODY.md uses the review's invariant 3 word for word. It also replaces the mutation sentence after the invariants with the review's two sentences. I checked each clause against policyengine_uk/variables/gov/hmrc/vat.py:21-40 and a mutant I ran (table below).

P3-1, the fixed example. policyengine_uk/tests/test_vat_grossing_properties.py:109 is now @example(spend=[(0.0, 15_438.0), (10_000.0, 0.0)]).

P3-2, the docstring. test_vat_grossing_properties.py:3-7 uses the review's text. It matches vat.py:28-40: raw_vat / vat_grossing_factor(...), then * (1 - private_school_share_of_receipts), then + domestic_energy_vat, which is not grossed up. It also matches utils/vat.py: the product, unless np.isclose to the declared legacy value fails.

P3-3, ruff. I linted with the lock's ruff==0.17.0.

Title. It had 99 characters. The new one has 66: Add VAT grossing property tests and later Microcosm cut provenance.

Main. git merge-tree --write-tree 47bda0a7a9f06b59bb262e9b4dffc05ab2428b5d 36bbbf42 exits 0 with tree 457ebfb3fb4ce289d237c61cf54c750d37726ff2. There's no conflict, so I didn't merge main.

Mutation checks

I ran these in a scratch copy of the property file with phases=[Phase.explicit] (so only the @example runs), restored the source after each, and confirmed with git status.

Mutant Explicit phase only Result
none (clean) new example 1 passed
A: household share → 1.0 in utils/vat.py new example 1 failed (1142.4662 vs 1632.0945)
B: 1 - s → 1 in vat.py new example 1 failed (1645.8423)
C: (1 - s) ** 2 (applied twice) new example 1 failed (1618.4617)
D: reduced-rate spending × standard rate new example 1 failed (6528.3784)
F: full-rate spending × reduced rate new example 1 failed (1057.1929 vs 4228.7719, the second household)
F old example [(0.0, 15_438.0)] 1 passed (the mutant survives; this confirms P3-1)

On the real property file at --hypothesis-seed=7, mutant A fails 5 tests: test_untouched_legacy_factor_uses_the_product_of_the_two and test_legacy_factor_within_the_band_counts_as_untouched (invariant 1), the two inside band cases, and test_vat_is_statutory_liability_over_the_shipped_product (invariant 3). So "fails invariants 1 and 3" holds.

Tests

uv sync --frozen --extra dev, with HUGGING_FACE_TOKEN and POLICYENGINE_UK_DEFAULT_DATASET unset:

  • test_vat_grossing_properties.py at the fixed seed 320551620663142353140068881523791551106: 8 passed. At random seeds 2985727594 and 71524407: 8 passed each.
  • test_vat_grossing_legacy.py + test_private_school_vat_receipts.py: 47 passed (573 s, while code_health ran alongside on the same host).
  • policyengine-core test policyengine_uk/tests/policy/baseline/gov/hmrc/vat -c policyengine_uk: 10 passed.
  • policyengine_uk/tests/code_health: 2887 passed.
  • uvx ruff@0.17.0 format --check .: 1980 files already formatted. check . and check --select F811,F821 policyengine_uk: all checks passed. git diff --check: clean.

I ran the property seeds against the working tree just before committing, and the committed content is identical. I started two more runs of the legacy pair, but my own 500 s timeout stopped them before they finished. They reported no failures, and I don't count them.

Judgment calls

  • expected_vat's docstring (line 96) said "less the school share", which has the same ambiguity as P3-2. I changed it to "times (1 - school share)". The review didn't ask for this.
  • The changelog fragment still says "less private school fees' share of VAT receipts". The review said to keep its content unchanged, so I left it alone.
  • shard_durations.toml: I renamed and removed no tests, so it needs no change.

Skipped

  • Anything that needs survey data (Microcosm cuts, Enhanced FRS): no access.
  • Fix evidence as CI runs it on a pushed PR head. I ran main's checker locally instead.

Generated by Claude Code

@MaxGhenis MaxGhenis changed the title VAT grossing property tests and later-cut provenance; coverage re-derived on the next certified cut Add VAT grossing property tests and later Microcosm cut provenance Nov 2, 2026
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Ready for hub review

Verdict: APPROVE WITH NITS at 36bbbf42f34a23968159406d1b6d8942ebcd6e1c (fix/vat-coverage-a133, reviewed as git diff 66533369d19501c99f81d69d53d0f7666e641f97..36bbbf42f34a23968159406d1b6d8942ebcd6e1c, plus the live title and body)

This commit and the live body close all four findings from the previous round. main's checker passes on the live title, body and labels, and so does the Fix evidence check on this head. Invariant 3 and the mutation sentences match vat.py and utils/vat.py, and every mutant the body names fails. The new example passes on the clean model, and it now kills full-rate spending charged at the reduced rate. The docstring matches the formulas. The three P3s below are wording nits. None of them blocks.

The delta is a rename, changelog.d/2633.fixed.md → changelog.d/2633.added.md (similarity 100%), and three edits in policyengine_uk/tests/test_vat_grossing_properties.py: the module docstring (lines 3-7), the expected_vat docstring (line 96) and the @example (line 109).

Previous findings

Finding Status Evidence
P1-1 Fix evidence Closed Local run of main's checker: PASS. CI's Fix evidence check on this head: success. Details below.
P1-2 Invariant 3 and mutation sentences Closed The body uses the proposed text word for word. Every clause holds against the code and a mutant run (below).
P3-1 Fixed example Closed test_vat_grossing_properties.py:109 is @example(spend=[(0.0, 15_438.0), (10_000.0, 0.0)]). In the explicit phase only, the clean model gives 1 passed. Mutant F gives 1 failed, and it survives the old example.
P3-2 Module docstring Closed Lines 3-7 use the proposed text. They match vat.py:28-40 and utils/vat.py:28-34.
P3-3 ruff version Closed (nothing to change) uvx ruff@0.17.0 is clean (below).

P1

None.

P2

None.

P3

P3-1. The new docstring has two passives

The text is at policyengine_uk/tests/test_vat_grossing_properties.py:5-7: "which is not grossed up" and "in which case that value is used". The previous round proposed this text, so the slip started there, but the delta now carries it. Proposed:

`vat` is the statutory liability on recorded consumption divided by one
grossing factor, times one minus private school fees' share of VAT receipts,
plus VAT on energy bills, which the formula adds without grossing up. The
factor is the survey coverage times the household share of receipts, unless a
reform moves the deprecated combined parameter, in which case the formula uses
that value.

P3-2. The changelog fragment keeps "less private school fees' share"

changelog.d/2633.added.md still reads "VAT as the statutory liability over the shipped product, less private school fees' share of VAT receipts". This has the same ambiguity that P3-2 of the previous round fixed in the docstring. The model multiplies by (1 − share) (vat.py:33-35); it doesn't subtract an amount. The previous round asked for the content to stay unchanged during the rename, so the author was right to leave it. If you touch the fragment again, replace that clause with:

VAT as the statutory liability over the shipped product, times one minus private school fees' share of VAT receipts,

P3-3. The body could claim the mutant that the new example now kills

The example was added to kill full-rate spending charged at the reduced rate (mutant F below), but the body's mutation sentence doesn't list that mutant. Optional replacement for the second mutation sentence:

Dropping the school share from `vat`, applying it twice, or charging either rate's spending at the other rate fails invariant 3 on its fixed example.

Checked and sound

  • P1-1, main's checker on the live PR text. I fetched the title, body and labels from GET /repos/PolicyEngine/policyengine-uk/pulls/2634: no labels, title of 66 characters, draft. They match the brief's PR_TITLE.txt and PR_BODY.md. I wrote them to files the way pr_fix_evidence.yaml does. I ran .github/check_fix_pr.py from a main checkout at 6c6e229a51fbd804dab2062dac1f94af04c609c2 with --repo-root ., on git diff --name-status --no-renames -z 7e24c186 36bbbf42f (7e24c186 is the merge base). That diff gives A changelog.d/2633.added.md, M docs/engineering/vat-coverage.md, M policyengine_uk/tests/policy/baseline/gov/hmrc/vat/vat_grossing.yaml and A policyengine_uk/tests/test_vat_grossing_properties.py.

    uv run --no-project --with pyyaml==6.0.2 python .github/check_fix_pr.py --title-file … --body-file … --labels-file … --changes-file … --repo-root . --head-sha 36bbbf42f34a23968159406d1b6d8942ebcd6e1c --summary-file …
    Fix evidence: PASS ('Add VAT grossing property tests and later Microcosm cut provenance'). Not a fix PR: no added/modified/type-changed fixed fragment or bug/bug-fix label.
    exit=0
    
  • P1-1, CI on this head. GET /commits/36bbbf42f…/check-runs lists Fix evidence as completed/success (check run 125445992937, run 41494558254, event pull_request_target, head 36bbbf42f). Its log line reads Fix evidence: PASS ('Add VAT grossing property tests and later Microcosm cut provenance'). Not a fix PR: …, which matches the local run. A second Fix evidence run, 125445951570, ended cancelled because the workflow cancels in-progress runs, and the later run supersedes it.

  • CI on this head, as I found it. Every other check is success: Test / Python 1/6 to 6/6, Test / Result, Test / YAML, Lint, Changes, Changelog fragments, Documentation build, Test documentation builds, Install + smoke-import (py3.11 to 3.14), Release build records the commit, and Aggregate impact. The combined commit-status endpoint reports pending with no statuses, because every result is a check run.

  • P1-2, invariant 3 against the code. vat.py:21-40: raw_vat = full × standard_rate + reduced × reduced_rate; gross_vat = raw_vat / vat_grossing_factor(p.simulation); general_vat = gross_vat × (1 − private_school_share_of_receipts); return general_vat + domestic_energy_vat. utils/vat.py:28-34 returns the legacy value only when np.isclose fails, and otherwise survey_consumption_coverage × household_share_of_receipts. With no electricity or gas bills, domestic_energy_vat is 0, and the test asserts this at line 90. So invariant 3 holds exactly. The test runs only in 2025, which matches "In 2025". expected_vat (lines 95-103) computes the same expression from system.parameters(year). By hand in 2025: s = 0.008353 and product = 0.67 × 0.7 = 0.469. That gives 0.05 × 15,438 / 0.469 × (1 − s) = 1,632.0945 and 0.2 × 10,000 / 0.469 × (1 − s) = 4,228.7719. Both match the oracle values in the failures below.

  • Mutants, in this commit's scratch checkout, each restored with git checkout and confirmed clean with git status. For the "explicit" rows, a scratch copy of the property file had phases=[Phase.explicit] added to the statutory test's @settings, so only the @example runs (pytest -k statutory -p no:randomly).

    Mutant Run Result
    none explicit, new example 1 passed
    F: full_rate_consumption * reduced_rate explicit, new example killed: 1057.1929 == 4228.771855… ± 0.0423
    F explicit, old example [(0.0, 15_438.0)] survived (1 passed), as the previous round found
    D: reduced_rate_consumption * standard_rate explicit, new example killed: 6528.3784 vs 1632.0945
    B: 1 - s → 1 explicit, new example killed: 1645.8423
    C: (1 - s) ** 2 explicit, new example killed: 1618.4617
    A: household share → 1.0 in utils/vat.py explicit, new example killed: 1142.4662
    A full property file, --hypothesis-seed=7, -n 4 killed: 5 failed (test_untouched_legacy_factor_uses_the_product_of_the_two, test_legacy_factor_within_the_band_counts_as_untouched, both inside band cases, test_vat_is_statutory_liability_over_the_shipped_product)

    So "fails invariants 1 and 3" holds for A. "Fails invariant 3 on its fixed example" holds for B, C and D. Each value matches the author's table.

  • P3-2, docstring against the formulas. "Divided by one grossing factor" matches line 28. "Times one minus … share" matches lines 33-35. "Plus VAT on energy bills, which is not grossed up" matches lines 39-40, where the formula adds domestic_energy_vat after the division. The routing sentence matches utils/vat.py:28-34. The new expected_vat docstring ("times (1 - school share)") matches line 103.

  • Title. Add VAT grossing property tests and later Microcosm cut provenance has 66 characters, uses sentence case and describes the diff.

  • Other body sentences changed this round. The Reproducer: line contains no , or ;, so parse_reproducers reads one n/a: reason, and the checker accepts it. "This pull request adds tests and docs and leaves model source and dependencies unchanged" holds: the diff from the merge base touches only a changelog fragment, a doc, a YAML test and a Python test. Invariant 3 and the two mutation sentences are covered above.

  • Tests at the head (uv sync --frozen --extra dev, with HUGGING_FACE_TOKEN and POLICYENGINE_UK_DEFAULT_DATASET unset):

    • property file at seed 320551620663142353140068881523791551106, -n 4: 8 passed; at a random seed: 8 passed;
    • test_vat_grossing_legacy.py + test_private_school_vat_receipts.py + the property file, -n 4: 55 passed in 469.99 s, which matches the author's 47 + 8;
    • policyengine-core test policyengine_uk/tests/policy/baseline/gov/hmrc/vat -c policyengine_uk: 10 passed;
    • lint: uvx ruff@0.17.0 format --check . (1980 files already formatted), check . and check --select F811,F821 policyengine_uk (all checks passed); git diff --check 66533369d..36bbbf42f is clean.
  • Merge with current main. git merge-tree --write-tree 6c6e229a51fbd804dab2062dac1f94af04c609c2 36bbbf42f exits 0 (tree c312227ff6a342cc9701f76f9898bf43445c0ed4). Since the merge base, main has changed private_school_share_of_receipts.yaml (October 2026 EFO receipts) and nothing in vat.py, utils/vat.py or .github/check_fix_pr.py. On a local merge, 2025's share reads 0.008368, and a two-household Simulation gives vat = [1632.0698, 4228.7075], which matches by hand (2,000 / 0.469 × 0.991632 = 4,228.71). The property file passes at the fixed seed (8 passed), because the oracle reads the parameters rather than hard-coding them. The vat YAML directory gives 12 passed.

What I couldn't check

  • Anything that needs survey data: total vat on the Microcosm cuts or Enhanced FRS, and the reform figures in the body. None of them comes from this delta.
  • Outside this delta: the body's table compares model vat with "OBR receipts (EFO March 2026, table 3.8)". Since the merge base, main has moved the school-share table in docs/engineering/vat-coverage.md and private_school_share_of_receipts.yaml to the October 2026 EFO (2025-26 £179.861bn outturn, 2026-27 £186.154bn). It keeps the March 2026 EFO for the coverage calibration, because that forecast's 2024-25 outturn is unchanged (main's doc, lines 258-261). The branch auto-merges with that change. The body labels its table as pre-split runs against the March figures, so the table still reads correctly. Its 2025-26 and 2026-27 receipt columns are now one forecast behind, which matters only if the re-derivation uses them. I didn't rerun those figures.
  • Hypothesis shrink times under each mutant on the random phase. I ran only the explicit phase, plus seed 7 for mutant A.

Generated by Claude Code

@juaristi22 juaristi22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head 36bbbf42f34a23968159406d1b6d8942ebcd6e1c. PARTIAL overall: no code findings, one nonblocking documentation finding and one precise source gap.

A1 — Refresh the current-default dataset provenance. The body calls a133f37a the current default and cites policyengine.py#715. That PR is closed without merging, and the actual wrapper main now certifies a different cut. Please identify the older cut as historical and refresh the current-default reference, while keeping the historical pre-energy-split comparisons clearly dated. This does not change this head's model outputs.

All 65 focused tests passed: 55 Python tests and all 10 serial YAML cases, with the repeated YAML IDs accounted for individually. Formatting, lint and fix-evidence checks also pass. No formulas, parameters or dependencies change.

The actual Microcosm #1383 merge, model pin and VAT diagnostic exclusion were checked, and HMRC's June 2025 ready reckoner corroborates the £8.8bn one-point benchmark for 2026–27. I could not independently read the exact March 2026 OBR table 3.8 receipt figures after two bounded access attempts. The available archived October 2026 workbook is a different edition and cannot substitute for that evidence; please provide an accessible source artifact for the three March rows.

The declared pre-merge requirement to re-derive coverage on the next certified cut remains outstanding. Approval is withheld while the PR is a draft, that measurement gate remains open, and A1/the source gap are unresolved.

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Picking this up for the UK hub. Reply here if you're mid-change and I'll stop.


Generated by Claude Code

Re-derive gov.simulation.vat.survey_consumption_coverage on the certified
Microcosm UK 2024-25 national cut a6299504, whose fit holds obr.vat out:
81.48 / (0.7 x (170.994 - 2.33)) = 0.690 in 2024-25. The deprecated
combined factor moves to the product, 0.483, and the parameter's
reference adds the OBR receipts workbook's own URL.

Tests that pin the product move with it: the 2024 and 2025 cases in
vat_grossing.yaml, the legacy default in test_vat_grossing_legacy.py,
FACTOR in test_private_school_vat_receipts.py, whose published receipts
check now uses a6299504's rounded inputs, and a stale illustrative
figure in test_scenario_baseline_parameters.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WR8LHapmX36bpbh3SMixXg
…ance

Add a derivation section for the certified Microcosm cut a6299504: the
cut's provenance and certification, the obr.vat exclusion, the OBR
receipts workbook (URL, SHA-256, archived copy, sheet, row and cells),
the chunked method, the results at 0.67 and 0.69 on the cut and on
Enhanced FRS 1.58.1, the bound and a reproducing snippet at revision
994cac89.

Name policyengine.py's certified cuts correctly: a6299504 today,
6cba5d71 on 20 October, and a133f37a only proposed in
policyengine.py#715. Retitle the October derivation as the earlier one
on f9d1922c and date its statements of the value, and point the
switch plan at the new cut.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WR8LHapmX36bpbh3SMixXg
Use the proposed wording for the property module's docstring, take
"times one minus" for the school share in the added fragment, and drop
the fragment's claims that the cuts were certified since f9d1922c and
that the value waits for re-derivation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WR8LHapmX36bpbh3SMixXg
The one-point check 4,337,453,646 x (1 - 0.008311) / 0.483 comes to
8,905,600,556, within 8,968 of the run, and the six-decimal 2026-27
coverage did not follow from the published inputs, so the doc keeps the
three-decimal values. Date the 0.38 factor's and run 40116746457's
figures, qualify the ready reckoner's 0.704 as leaving out the school
share, say that baseline_vat keeps the two factors under a reform to the
deprecated one, and attribute the pre-split division by receipts to the
calculation rather than the model. Say that 0.69 matches 2024-25
receipts, show the 2025 YAML case's full computation, and name the cuts
the added fragment's record covers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WR8LHapmX36bpbh3SMixXg
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

PR_TITLE.txt

Move the VAT survey coverage to 0.69 on Microcosm a6299504

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

PR_BODY.md

Fixes #2633

Reproducer: policyengine_uk/tests/policy/baseline/gov/hmrc/vat/vat_grossing.yaml, policyengine_uk/tests/test_vat_grossing_legacy.py::test_legacy_default_is_the_product_of_the_two_factors

axiom: n/a: a microsimulation calibration factor for survey under-recording, not law

This PR moves `gov.simulation.vat.survey_consumption_coverage` from 0.67 to 0.69, the value a run gives on `a6299504`, the Microcosm UK 2024-25 national cut that policyengine.py certifies, and moves the deprecated combined factor `gov.simulation.microdata_vat_coverage` to the new product, 0.483. It also adds VAT grossing property tests, a 2024 grossing case, and the cut's provenance and receipts source in `docs/engineering/vat-coverage.md`.

Before the run, this PR set a rule: if a run with #2189's formula, on the cut that follows microcosm#1383's rebuild, put 2024-25 VAT within ±2% of receipts at 0.67, 0.67 would stay; otherwise the coverage would move to the value that run gave. On `a6299504`, whose fit holds `obr.vat` out, total `vat` at 0.67 came to £176.06bn in 2024-25, 3.0% above OBR's £170.994bn, and the 2024-25 coverage came to 0.690. So the coverage moves to 0.69.

## What changed and why

#2187 derived `gov.simulation.vat.survey_consumption_coverage` on the Microcosm UK 2024-25 cut `f9d1922c` (2 October 2026), and #2189 re-derived it with energy bills split out. Its metadata says to re-derive it when the dataset it should match changes.

policyengine.py certified `6cba5d71` on 20 October 2026 (commit `a66e883c4`). policyengine.py#715 proposed `a133f37a` and closed unmerged on 27 October 2026, so no policyengine.py release certified it. policyengine.py now certifies `a6299504`: commit `324ecad65` certified it on 26 October 2026, release 6.11.1 shipped it, and release 6.12.0 still certifies it.

Runs on 24 October 2026, before the energy split, showed why the value needed re-deriving. `6cba5d71` and `a133f37a` record about 10% more standard-rated consumption than `f9d1922c`: £435.60bn against £396.15bn in 2024-25 on `a133f37a`. With 0.67, total `vat` on `a133f37a` (main `c0252415a`) came out as follows:

| | 2024-25 | 2025-26 | 2026-27 |
|---|---|---|---|
| Model `vat` | £188.61bn | £196.43bn | £201.71bn |
| OBR receipts (EFO March 2026, table 3.8) | £170.994bn | £180.169bn | £187.704bn |
| Gap | +10.3% | +9.0% | +7.5% |

On `6cba5d71`, a reform run that raised the standard rate from 20% to 21% changed total `vat` and `gov_balance` by £9.94bn in 2026-27 (policyengine-uk 2.135.7 and 2.152.6 agree), 12.9% above HMRC's £8.8bn ready reckoner. The doc records these pre-split runs as the reason to re-derive; they don't set the value.

Changes in this branch against main:
- **Parameters:** `survey_consumption_coverage` moves from 0.67 to 0.69. Its derivation now describes `a6299504` (81.48 / (0.7 x (170.994 - 2.33)) = 0.690 in 2024-25), and its `reference` adds the OBR receipts workbook's own URL beside the EFO page. `microdata_vat_coverage` moves from 0.469 to 0.483, with "(0.69 x 0.7)" in its description.
- **Tests:** new Hypothesis properties and fixed band-edge cases in `test_vat_grossing_properties.py`, whose module docstring now names the formula as the actor. In `vat_grossing.yaml`, a 2024 case (2,050 / 0.483 = 4,244.31, the same product as later years), and the 2025 case moves to 2,050 / 0.483 x (1 - 0.008353) = 4,208.85, with the full computation in its comment. `test_vat_grossing_legacy.py` pins the deprecated default at 0.483. `test_private_school_vat_receipts.py` takes `FACTOR = 0.69 * 0.7`, and its published receipts check uses `a6299504`'s rounded inputs: 0.2 x £407.40bn / 0.483 + £2.33bn against £170.994bn. In `test_scenario_baseline_parameters.py`, a comment's illustrative figure, 526.32 from the old 0.38 factor, becomes "about 411".
- **Docs:** `docs/engineering/vat-coverage.md` gains "Derivation on `a6299504` (November 2026)": the cut's provenance and certification, the `obr.vat` exclusion, the receipts workbook (URL, SHA-256, archived copy, sheet, row and cells), the chunked method, the results at 0.67 and 0.69, the bound and a reproducing snippet at revision `994cac89`. The "Which datasets" section now names the certified cuts correctly. The October derivation becomes the earlier derivation on `f9d1922c`, and the doc dates every statement of 0.67 as the value. The deprecated factor's note now says that `baseline_vat`, which reads the unreformed parameters, keeps the two factors under a reform to it, as `baseline_vat.py` and `test_private_school_vat_receipts.py` show. "Later Microcosm cuts (24 October 2026)" stays as a dated record. `docs/book/assumptions/mp30k-uk-switch-plan.md` now names `a6299504`.
- **Changelog:** `2633.added.md` (the tests and the later-cut record) and `2633.changed.md` (the value).

## Impact

The runs below used this PR's previous head, `36bbbf42f` (policyengine-uk 2.157.1, policyengine-core 3.33.2), on 3 November 2026: the runs at 0.67 took the value that head ships, and the runs at 0.69 set the coverage by reform. This head ships 0.69. Total `vat` counts the `vat` variable alone. It leaves out `private_school_vat`, which the receipts include: nothing in 2024-25, and about £1.5bn a year from 2025-26 on HMRC's costing.

| Dataset | Fiscal year | Total `vat` at 0.67 | Against receipts | Total `vat` at 0.69 | Against receipts |
|---|---|---|---|---|---|
| Microcosm `a6299504` | 2024-25 | £176.06bn | +3.0% | £171.02bn | +0.02% |
| Microcosm `a6299504` | 2025-26 | £181.09bn | +0.5% | £175.91bn | -2.4% |
| Microcosm `a6299504` | 2026-27 | £185.18bn | -1.3% | £179.86bn | -4.2% |
| Enhanced FRS 1.58.1 | 2024-25 | £242.54bn | +41.8% | £235.58bn | +37.8% |
| Enhanced FRS 1.58.1 | 2025-26 | £249.37bn | +38.4% | £242.22bn | +34.4% |
| Enhanced FRS 1.58.1 | 2026-27 | £255.28bn | +36.0% | £247.93bn | +32.1% |

The whole Microcosm cut needed more than 8 GiB in one process, so those runs summed ten household chunks; the Enhanced FRS run used the whole file.

A one-point reform at 0.69 sets the standard rate to 21% for 2026-27 alone. On Microcosm `a6299504` in 2026-27:
- Over all households, total `vat` rises from £179.862bn to £188.767bn, by £8.906bn, exceeding HMRC's £8.8bn ready reckoner by 1.2%. The formula's arithmetic, 1% of the standard-rated base times (1 - 0.008311) over 0.483, gives £8,905,600,556, within £8,968 of the run.
- Over the eight of ten household chunks that ran (two needed more than 8 GiB and stopped), `gov_balance` rises by £7.181bn, `vat` by £7.120bn and `private_school_vat` by £0.061bn. The last comes to 5.0% of those chunks' £1.214bn baseline `private_school_vat`, since that charge moves in proportion to the rate. In each of the eight chunks, the change in `gov_balance` equals the change in `vat` plus the change in `private_school_vat` to within £400 (float32 rounding on totals of about £0.9bn). `attends_private_school` ranks household net income within the simulated households, so each chunk ranks within its own households.

PENDING: the `Raise VAT standard rate by 2pp` fixture in `reforms_config.yaml` waits for this head's Aggregate impact report, which costs the same reform on the certified Enhanced FRS.

## Invariants

These hold for every input:
1. With the deprecated factor at its declared value, the grossing factor equals `survey_consumption_coverage × household_share_of_receipts`.
2. A reform that moves the deprecated factor out of the closeness band around its declared value (1e-8 + 1e-5 × the value, NumPy's default `isclose` tolerance) overrides the two; a value inside the band counts as untouched.
3. In 2025, each household's `vat` with no electricity or gas bills equals (standard rate × `full_rate_vat_consumption` + reduced rate × `reduced_rate_vat_consumption`) / shipped product × (1 − `private_school_share_of_receipts`). It is linear in each household's consumption, with no cross-household term.
4. The deprecated default equals the product of the two factors in every year from 2015 to 2030.
5. The deprecated factor has a single undated value, so 2024 takes the product like every later year (`vat_grossing.yaml`, 2024 case). Adding a dated entry to it, which would send earlier years down the legacy path, fails that case.

Hypothesis tests in `policyengine_uk/tests/test_vat_grossing_properties.py` check invariants 1–3, and `test_vat_grossing_legacy.py` (main) checks invariant 4. The property oracles read the parameters, so they check the shipped product of 0.483. Dropping the household share from `vat_grossing_factor` fails invariants 1 and 3. Dropping the school share from `vat`, applying it twice, or charging either rate's spending at the other rate fails invariant 3 on its fixed example. For invariant 2, fixed cases sit at 0.99 and 1.01 bands either side of the declared value; changing the helper's `np.isclose` to exact equality fails the inside cases. The helper properties have no Hypothesis deadline, since they check algebra, not speed.

Reverting the parameter change in a scratch copy of this head shows which tests hold the value. With both parameters back at 0.67 and 0.469, the 2025 and 2024 cases in `vat_grossing.yaml` fail (`vat` returns to 4,334.49 and 4,371.00), and so do `test_legacy_default_is_the_product_of_the_two_factors`, `test_published_microcosm_receipts_check_is_unchanged` and all six `test_energy_bill_vat_bears_no_school_share` cases. Reverting the coverage alone fails the same tests. Reverting the deprecated factor alone to 0.469 leaves `vat` on the product, so the YAML cases pass, but `test_legacy_default_is_the_product_of_the_two_factors` fails (0.469 against 0.483). The clean head passes all of them.

The dataset-level check (total `vat` within a few percent of receipts on the certified Microcosm cut) can't run in CI, because the tests' certified dataset is Enhanced FRS 1.58.1. The doc's snippet reproduces it.

## Tests

At this head, with `HUGGING_FACE_TOKEN` and `POLICYENGINE_UK_DEFAULT_DATASET` unset, in the locked environment (`uv sync --frozen --extra dev`):
- `policyengine-core test policyengine_uk/tests/policy/baseline/gov/hmrc/vat -c policyengine_uk`: 10 passed. `private_school_vat.yaml`: 15 passed. `excise_integration.yaml`: 2 passed. `gambling_duty/integration.yaml`: 7 passed.
- `pytest -n 4` on `test_vat_grossing_properties.py`, `test_vat_grossing_legacy.py`, `test_private_school_vat_receipts.py`, `test_private_school_vat_properties.py`, `test_domestic_electricity_vat.py` and `test_historic_vat_rates.py`: 135 passed.
- `pytest -n 4` on `test_scenario_baseline_parameters.py`, `test_dated_reform_routes.py` and `test_no_economic_assumptions.py`, which the coverage grep found: 132 passed, 1 skipped. `test_before_load_spending_scenario_constructs_on_the_dataset` needs the dataset.
- `pytest -n 4 test_fiscal_partition.py`: 41 passed.
- `pytest -n 4 aggregate_impact/`: 306 passed, 9 skipped. All nine skips are in `test_aggregate_impact_certified.py`, which needs the dataset.
- `pytest policyengine_uk/tests/code_health -n 4`: 2887 passed.
- `uvx ruff@0.17.2 format --check .` (1981 files already formatted), `uvx ruff@0.17.2 check .` and `uvx ruff@0.17.2 check --select F811,F821 policyengine_uk`: all checks passed. `git diff --check 36bbbf42f..HEAD`: clean.
- `.github/check_fix_pr.py`, main's copy, run as `pr_fix_evidence.yaml` runs it on this title and body: PASS, not a fix PR. With a `bug` label added, it passes on the `Reproducer:` line, with 5 changed executable tests.

The survey-data tests, including the `reforms_config.yaml` fixtures, run where CI has the dataset, which includes merge-queue and `main` runs.

## Interaction with open work

PolicyEngine/microcosm#1383 (merged) locks policyengine-uk 2.151.0 and holds `obr.vat` out of the fit as a diagnostic (commit `94d4102fd`; exclusion approved 2026-10-24, expiring 2027-01-24). The exclusion says the row rebinds once policyengine-uk re-derives the factor on a file that does not bind it. This PR re-derives the factor on `a6299504`, whose fit doesn't bind `obr.vat`: the condition the exclusion names for the row to rebind. juaristi22 decides whether and when the row rebinds, and any lock bump. The cut's calibration diagnostics carry one warn-only VAT warning, `uk_consumption_drift`: the total `full_rate_vat_base` moves by more than 2% of its design value between the design and the final weights. If a later cut binds `obr.vat` under 0.69, its fit would absorb the factor, so this value pairs only with a cut on which a run supports it.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Ready for hub review

Head 303e6585224cd6aa79e65e83059223681c1ec7e4 on branch claude/vat-coverage-rederive-wtsizo: four commits on 36bbbf42f34a23968159406d1b6d8942ebcd6e1c. I opened no PR and pushed nothing to fix/vat-coverage-a133. The two hub documents are in the two comments above this one.

The merge with main conflicts, so I stopped there. git merge-tree --write-tree --name-only HEAD origin/main (main 52b30809ff5681ebfede4d559e72a44f4c893cd2) exits 1 with one conflict, in policyengine_uk/tests/policy/baseline/gov/hmrc/vat/vat_grossing.yaml. Main moved the 2025 case to the October 2026 EFO share (0.008368, 4,334.43); this branch moved the same lines to the product 0.483 (4,208.85 at 0.008353). The hunk also spans this branch's 2024 case, which sits next to them. docs/engineering/vat-coverage.md, test_private_school_vat_receipts.py and test_vat_grossing_legacy.py auto-merge. I didn't merge main. In a scratch merge that I didn't commit or push, I resolved it by keeping both of this branch's cases and taking main's share in the 2025 one: comment (0.008368) and 2,050 / 0.483 x (1 - 0.008368) = 4,208.79, with vat and baseline_vat at 4_208.79. On that merge, the vat YAML directory gives 12 passed (main adds two cases), and test_vat_grossing_legacy.py, test_private_school_vat_receipts.py and test_vat_grossing_properties.py give 55 passed (-n 4).

Findings and how this head resolves them

juaristi22's review (PARTIAL at 36bbbf42f)

  • A1, current-default provenance. In docs/engineering/vat-coverage.md, the "Which datasets the factor applies to" paragraph (lines 53-61) now says policyengine.py certifies a6299504: commit 324ecad65 certified it, 6.11.1 shipped it, and 6.12.0 still certifies it. It says policyengine.py certified 6cba5d71 on 20 October 2026 (a66e883c4), that policyengine.py#715 proposed a133f37a and closed unmerged, and it links the new derivation section. The body's "What changed and why" says the same and dates the pre-split runs to 24 October 2026. Every present-tense statement of the value now says 0.69 (lines 18, 36 and 43, and the new section from line 63). Statements of 0.67 are past tense and tied to their cut: the October section's results and bound, the energy section, the private school section's receipts check, and the Later section, which I left as a dated record apart from its link.
  • Source gap for the March 2026 receipts. The new section's inputs (vat-coverage.md lines 73-104) give the workbook's own URL, SHA-256 f65b6cb7…6a28, 187,899 bytes, last modified 3 March 2026, and the Internet Archive copy with the same bytes. They also give sheet "3.8 Current receipts (on a cash basis)", row 13 "Value added tax", D13 = 170.994, E13 = 180.16893868541794 and F13 = 187.70436725810845, plus the row 66 cash-basis note. survey_consumption_coverage.yaml adds the workbook URL to reference, beside the EFO page.
  • The pre-merge re-derivation. Done: the coverage is 0.69 (product 0.483), taken from run 7's 2024-25 coverage of 0.690 under the pre-declared rule.

The hub's review (APPROVE WITH NITS at 36bbbf42f)

  • P3-1: test_vat_grossing_properties.py lines 3-8 now hold the proposed docstring word for word.
  • P3-2: changelog.d/2633.added.md now says "times one minus private school fees' share of VAT receipts", and no longer says "certified since f9d1922c" or "unchanged pending re-derivation". changelog.d/2633.changed.md is new.
  • P3-3: already in the live body, and unchanged.

Errors in the run record. An independent review of this diff (four reviewers, each checked by a skeptic) found these two. I confirmed both by exact arithmetic.

  • Run 9's arithmetic check: £4,337,453,646 × (1 − 0.008311) / 0.483 = £8,905,600,556 (float64 agrees), not £8,905,599,522. So the run differs from it by £8,968, not £7,934. The doc's Bound section and the body print the corrected arithmetic. Please correct HUB_RUNS.md.
  • Run 7's unrounded 2026-27 coverage: 86,749,072,928 / (0.7 × (187,704,367,258 − 1,749,888,519)) = 0.666439, or 0.666440 with £187.704bn, not 0.666436. The 2024-25 and 2025-26 values reproduce exactly. The doc prints only the three-decimal coverages, which hold either way, as does the 3.4% fall. Please confirm the run's value.

Figures and their runs

Run 7: the Microcosm bases, energy-bill VAT, pre-scaling VAT, coverages and totals at 0.67. Run 9: the Microcosm totals at 0.69, and the one-point rise in vat over all households. Run 10: the Enhanced FRS rows. Run 11: gov_balance, vat and private_school_vat over the eight chunks. The rest comes from HUB_RUNS.md's dataset and source sections: certification, exclusion, workbook and hashes. I derived no dataset figure. The arithmetic I did myself: the YAML cases (2,050 / 0.483 = 4,244.31, and × (1 − 0.008353) = 4,208.85), the receipts check (81.48 / 0.483 + 2.33 = £171.026bn, +0.02%), the corrected one-point check above, and the 3.4% fall.

Grep for coverage-dependent figures

I ran it at 36bbbf42f and again at this head. Lines below are at this head.

  • 0.469
    • microdata_vat_coverage.yaml:11 → 0.483.
    • vat_grossing.yaml:8 and :23 comments → 0.483.
    • test_vat_grossing_legacy.py:34: approx(0.469) → approx(0.483).
    • The doc's intro → 0.483 (line 36).
    • What remains is the dated October section (lines 294 and 324, now past tense).
  • 0.67 as the VAT coverage
    • survey_consumption_coverage.yaml:42 → 0.69.
    • microdata_vat_coverage.yaml:1: "(0.67 x 0.7)" → "(0.69 x 0.7)".
    • vat_grossing.yaml:8 → (0.69 x 0.7).
    • test_private_school_vat_receipts.py:40: FACTOR = 0.67 * 0.7 → 0.69 * 0.7.
    • The doc's intro and "Which datasets" → 0.69 (lines 18 and 43).
    • The brief's four sentences:
      • "The parameter takes …" → "took" (294).
      • The energy section's sentence about the parameter staying at 0.67 → tied to f9d1922c (383-390).
      • "keeping the survey coverage at 0.67" → "which leaves the survey coverage unchanged".
      • The private school receipts check → 81.48 / (0.69 * 0.7) + 2.33 = £171.026bn, 0.02% above £170.994bn, with run 9's £171.02bn on the unrounded inputs. The f9d1922c check stays as a dated sentence (500-507).
    • The Later section's 0.67 rows (544 on) stay as a dated record.
    • I left the non-VAT hits alone: the BHC equivalisation 0.67, JSA take-up, and the "0.67s" timing comment in simulation.py.
  • 4,371/4371
    • vat_grossing.yaml 2024 case → 4,244.31.
    • I left the non-VAT hits alone: a poverty-test constant, a domestic rates value, and ADM para H4371.
  • 4,334.49/4334.49
    • vat_grossing.yaml 2025 case → 4,208.85, with the full computation in its comment (lines 8-13).
  • Expected VAT computed through the grossing factor
    • test_private_school_vat_receipts.py: every FACTOR use (lines 120, 122, 150, 176, 199, 250, 450) follows the new value. test_published_microcosm_receipts_check_is_unchanged now checks a6299504's published rounded inputs: 0.2 × £407.40bn / FACTOR + £2.33bn, within 0.1% of £170.994bn.
    • test_vat_grossing_properties.py: its oracle reads the parameters, so only the docstring changed.
    • These don't depend on the value, so I left them alone: the cases in test_vat_grossing_legacy.py at legacy 0.5 or coverage 1, and the other three vat_grossing.yaml cases (both factors at one, the household share alone, legacy 0.5).
    • test_scenario_baseline_parameters.py:54-55: the comment "(526.32 at the current factor)", left over from the 0.38 factor, → "(about 411 at the shipped factors)". A run with these parameter values gives 410.637; main's 2026 share would give 410.61.
    • These don't depend on the value; I left them alone and ran them all: test_domestic_electricity_vat.py (differences of vat cancel the grossed part), test_dated_reform_routes.py and test_private_school_vat_properties.py (relative comparisons), test_fiscal_partition.py (identities), test_no_economic_assumptions.py, excise_integration.yaml and gambling_duty/integration.yaml (vat: 0 with no consumption), private_school_vat.yaml (no vat output), and domestic_energy_vat.yaml (vat: 100, all from energy bills).
    • microsimulation/reforms_config.yaml, Raise VAT standard rate by 2pp (27.6, tolerance 1.5): untouched, as the brief asks.
  • Other coverage-dependent text
    • docs/book/assumptions/mp30k-uk-switch-plan.md:116-124 named f9d1922c and 38%/44% at 0.67. It now names a6299504 and run 10's 38% at 0.69 on 1.58.1.
    • policyengine_uk/data/fiscal_partition.yaml:258-261: the vat data note's "-£55.7m weighted in 2026 (Aggregate impact run 40116746457, 2026-10-24)", a figure at 0.67 with its run and date. I left it (see judgment calls).
    • survey_consumption_coverage.yaml:29-31: "at most 0.06% below it" comes from that same run at 0.67. I kept it verbatim, since the brief says to keep the private school sentences.

Tests at this head

These are clean runs of head 303e6585, one at a time, each capped at 40 minutes. None stopped. Each ran with HUGGING_FACE_TOKEN and POLICYENGINE_UK_DEFAULT_DATASET unset, in the environment from uv sync --frozen --extra dev.

  • policyengine-core test policyengine_uk/tests/policy/baseline/gov/hmrc/vat -c policyengine_uk: 10 passed. private_school_vat.yaml: 15 passed. excise_integration.yaml: 2 passed. gambling_duty/integration.yaml: 7 passed.
  • pytest -n 4 on test_vat_grossing_properties.py, test_vat_grossing_legacy.py, test_private_school_vat_receipts.py, test_private_school_vat_properties.py, test_domestic_electricity_vat.py and test_historic_vat_rates.py: 135 passed in 25:19.
  • pytest -n 4 on the files the grep found (test_scenario_baseline_parameters.py, test_dated_reform_routes.py, test_no_economic_assumptions.py): 132 passed, 1 skipped in 18:02. The skip needs the dataset.
  • pytest -n 4 test_fiscal_partition.py: 41 passed in 6:58.
  • pytest -n 4 aggregate_impact/: 306 passed, 9 skipped in 1:05. All nine skips are in test_aggregate_impact_certified.py, for lack of the dataset.
  • pytest policyengine_uk/tests/code_health -n 4: 2887 passed in 2:33.
  • uvx ruff@0.17.2 format --check .: 1981 files already formatted. uvx ruff@0.17.2 check . and check --select F811,F821 policyengine_uk: all checks passed. git diff --check 36bbbf42f..HEAD: clean.
  • .github/check_fix_pr.py, main's copy (identical to this branch's), run the way pr_fix_evidence.yaml runs it: changes from merge base 7e24c186 to this head, --head-sha this head, and the final title and body in the comments above. Result: Fix evidence: PASS (…). Not a fix PR. With a bug label added: PASS, "Fix PR has valid reproducer evidence. Changed executable tests: 5."
  • git merge-tree --write-tree --name-only HEAD origin/main: exit 1, one conflict (above).

Revert checks

These ran in a scratch worktree of this head, with imports confirmed to load from it. Each check covered vat_grossing.yaml and 8 selected pytest cases: test_legacy_default_is_the_product_of_the_two_factors, test_published_microcosm_receipts_check_is_unchanged and the six test_energy_bill_vat_bears_no_school_share cases.

  • Clean head: YAML 5 passed; pytest 8 passed.
  • Both parameters back at 0.67 and 0.469: YAML 2 failed, 3 passed, with vat@2025 4334.491 against 4208.85 and vat@2024 4371.002 against 4244.31. Pytest 8 failed.
  • Coverage alone back at 0.67, with the deprecated factor at 0.483: the same, YAML 2 failed and pytest 8 failed.
  • Deprecated factor alone back at 0.469, with the coverage at 0.69: YAML 5 passed, because vat still takes the product. Pytest 1 failed, 7 passed: test_legacy_default_is_the_product_of_the_two_factors gets 0.469 against 0.48299999999999993.

Skipped

  • Anything needing survey data: this session has no Hugging Face token. In the grep-hit group, test_scenario_baseline_parameters.py::test_before_load_spending_scenario_constructs_on_the_dataset (marked microsimulation) skipped. I didn't run the reforms_config.yaml fixtures or other microsimulation tests.
  • I left reforms_config.yaml alone, as the brief asks; the body marks the 2pp fixture PENDING for this head's Aggregate impact report.
  • make documentation. docs/engineering/ isn't in the book; the switch-plan edit is Markdown prose. A script checked that every in-page link in vat-coverage.md resolves to a heading under GitHub's slug rules.
  • The full suite, since CI is the evidence for it.

Judgment calls

  • FACTOR: FACTOR = 0.69 * 0.7 as a literal, the brief's first option. The file pins its other source values (costing, receipts) as literals too, and vat_grossing.yaml and the legacy test pin the value elsewhere.
  • The receipts-check test keeps its name, so nothing that lists test ids needs an edit (this repository has no shard_durations.toml). It now checks the a6299504 inputs that the doc publishes.
  • Files outside the brief's list, found by the grep: the comment in test_scenario_baseline_parameters.py and the switch plan's sentence. Both stated a coverage-dependent figure as current. I reworded the switch-plan sentence to "matches 2024-25 receipts", plus an imperative "Re-derive it", so it has no passive.
  • fiscal_partition.yaml: I didn't touch its vat note. It already dates the figure to its run, and the Aggregate impact report records the partition file's SHA-256, so editing a note would add a partition change to the report that sets the 2pp fixture.
  • "Keep the private school sentences": I kept them verbatim in the YAML. The doc dates the same run's figures (the October formula's -£5.5m to -£142.5m and the private school section's paragraph on run 40116746457) to a coverage of 0.67. I checked that this run ran at 4f352282 with 0.67 (product 0.469).
  • The intro's deprecated-factor note said that vat and baseline_vat both take a reformed legacy value. baseline_vat reads parameters(period).baseline (baseline_vat.py:15), and test_private_school_vat_receipts.py:122-125 expects the two factors there under legacy 0.5. That sentence predates this PR, but it sits in a paragraph I edited, so I corrected it.
  • The ready reckoner's 0.704: the brief gives 0.493 and 0.704 from 1% of the base over £8.8bn, which leaves out (1 − school share). The doc and YAML say "leaving out the school share", so the 0.704 bound and the run's +1.2% at 0.69 reconcile.
  • Reproducing: the snippet moved into the new section, at revision 994cac89…. It subtracts energy-bill VAT and has the memory sentence. The October section keeps a short pointer with f9d1922c's full revision and how to get its pre-split rows. I didn't keep two copies of the code.
  • Range direction: the YAML says "From 2024-25 to 2026-27 the coverage falls from 0.690 to 0.666", and the doc says "38% above receipts in 2024-25, falling to 32% in 2026-27". The brief's figures are the same, with the direction stated.
  • The doc goes past the brief in three places:
    • It adds the uk_consumption_drift diagnostic to the new section's inputs, not only to the body.
    • It says "As of November 2026" rather than "today".
    • It keeps the private school paragraph's £252.5bn (run 40116746457) as that run's figure, now in the past tense, even though run 10 gives £249.37bn for the same file and year at 0.67. I can't tell which measure the £252.5bn was.
  • PR body:
    • The Reproducer: line names vat_grossing.yaml at file level (its case names would trip the passive check) and test_vat_grossing_legacy.py::test_legacy_default_is_the_product_of_the_two_factors. With a bug label on the PR, the old n/a: line fails main's checker, which accepts n/a only when the PR changes no model source; this one passes ("Changed executable tests: 5").
    • The Reproducer: and axiom: lines now sit under Fixes #2633, the order the repository's PR-body rule gives.
    • The body names juaristi22 (no @) for the rebinding call, as the brief words it, and folds "any lock bump" into the same sentence. The old body called that María's call.
    • It labels the runs by commit and date rather than naming who ran them.
  • Title: "Move the VAT survey coverage to 0.69 on Microcosm a6299504" (58 characters).
  • 2633.changed.md: past tense ("Re-derived … and moved …"), like 2633.added.md.
  • Branch: GitHub had no claude/vat-coverage-rederive-wtsizo (only a local tracking ref at main existed), so a plain push created it. No CI ran, because pushes trigger workflows only on main.

Generated by Claude Code

MaxGhenis and others added 2 commits November 3, 2026 07:04
main's October 2026 EFO update moved private school fees' 2025-26 share of
VAT receipts to 0.008368, and this branch moved the product to 0.483. The
2025 vat_grossing case keeps this branch's product and takes main's share:
2,050 / 0.483 x (1 - 0.008368) = 4,208.79.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main's October 2026 EFO gives 2025-26 outturn, so "the latest outturn
year" no longer names 2024-25. The doc now gives the reason that holds
under both EFOs, and says that the 2025-26 and 2026-27 rows come from
policyengine-uk 2.157.1 on the March 2026 EFO.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis MaxGhenis changed the title Add VAT grossing property tests and later Microcosm cut provenance Move the VAT survey coverage to 0.69 on Microcosm a6299504 Nov 3, 2026
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

af0b602ca moves the coverage to 0.69 and adds the provenance and source you asked for.

  • A1. We rewrote the description and docs/engineering/vat-coverage.md around a6299504, the cut policyengine.py certifies: commit 324ecad65 certified it, release 6.11.1 shipped it, and release 6.12.0 still certifies it. policyengine.py certified 6cba5d71 on 20 October, and a proposal to certify a133f37a (policyengine.py#715) closed unmerged on 27 October. The pre-split comparisons keep their 24 October dates.
  • The March 2026 receipts. OBR serves the workbook at https://obr.uk/docs/d055fbf02d5b3g6jq8l2/efo-march-2026-detailed-forecast-tables-receipts.xlsx (187,899 bytes, SHA-256 f65b6cb7f96931d4abd85940794eb60f6f65afca8bbf16c74a4548bf92766a28, last modified 3 March 2026), and the Internet Archive's copy holds the same bytes. In sheet "3.8 Current receipts (on a cash basis)", row 13 gives 170.994 (D13), 180.16893868541794 (E13) and 187.70436725810845 (F13). We added these, with the cut's provenance, in the doc's new section Derivation on a6299504 (November 2026), and the parameter's reference now lists the workbook.
  • The re-derivation. On a6299504, total vat at 0.67 came to £176.06bn in 2024-25 and exceeded OBR's £170.994bn by 3.0%. Under the rule, the coverage moves to that run's 2024-25 value, 0.69 (product 0.483). At 0.69 the cut's 2024-25 total comes to £171.02bn.

The branch also merges main. The model on main now reads private school fees' 2025-26 share of receipts as 0.008368, from the October 2026 EFO (#2717), so the 2025 case in vat_grossing.yaml gives 4,208.79. We dated the doc's 2025-26 and 2026-27 rows to the March 2026 EFO, and the value rests on 2024-25, the cut's own year. The Raise VAT standard rate by 2pp fixture waits for this head's Aggregate impact report.

@juaristi22 juaristi22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed af0b602ca3170417bf306e162adcd130f94f60f6. COMPLETE within the reviewed scope: A1 and the prior OBR source gap are resolved. One nonblocking suggestion remains.

Fresh public wrapper manifests/certification confirm the current a6299504 pin, rather than the unmerged alternative. The official March 2026 OBR workbook's receipt cells and cash basis were checked directly. The published calibration inputs independently give coverage about 0.69012 and product 0.483; the empirical population bases/totals remain author-reported, not a new independent population reproduction.

S1 — Date the private-school accuracy claim. In survey_consumption_coverage.yaml:29, the retained present-tense “at current law” 0.06% bound belongs to the older 0.67/run 40116746457 result. Please date or qualify it as historical, matching the refreshed documentation. This does not establish that 0.69 or household outputs are wrong.

Validation: 92 completed checks passed before the 540-second limit, including all 55 material changed/direct Python checks and 36 VAT/integration YAML cases. The selected 187-item run did not finish; after running the previously unreached native diagnostic alone, 94 ancillary regression nodes remain uncompleted. No full-suite local pass is claimed. Three native configurations passed through final VAT, tax, government balance and household income: the new coverage lowers VAT/tax by £125.64 and raises household income by the same amount, preserving energy VAT and explicit legacy overrides. Hosted checks remain separate.

I am leaving a comment rather than approving while S1 remains open and current-head CI is pending.

juaristi22 added a commit to PolicyEngine/microcosm that referenced this pull request Nov 3, 2026
…e-uk#2634 (microcosm#1359)

The prior-weighted full-rate VAT base falls 1.7%. The coverage that
matches receipts moves by about that ratio, from whatever value the cut
it is re-derived on starts at: 0.69 under PolicyEngine/policyengine-uk#2634,
derived on a6299504, which gives about 0.68.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
juaristi22 added a commit to PolicyEngine/microcosm that referenced this pull request Nov 3, 2026
…e-uk#2634 (microcosm#1359)

The prior-weighted full-rate VAT base falls 1.7%. The coverage that
matches receipts moves by about that ratio, from whatever value the cut
it is re-derived on starts at: 0.69 under PolicyEngine/policyengine-uk#2634,
derived on a6299504, which gives about 0.68.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
juaristi22 added a commit to PolicyEngine/microcosm that referenced this pull request Nov 4, 2026
…e-uk#2634 (microcosm#1359)

The prior-weighted full-rate VAT base falls 1.7%. The coverage that
matches receipts moves by about that ratio, from whatever value the cut
it is re-derived on starts at: 0.69 under PolicyEngine/policyengine-uk#2634,
derived on a6299504, which gives about 0.68.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The coverage description's 0.06% bound comes from the run at a coverage
of 0.67 (Aggregate impact run 40116746457, 24 October 2026), so it now
says so, as docs/engineering/vat-coverage.md does.

The Aggregate impact report for this branch at af0b602, merged into
main at 1d4d020 (run 41633358040, policyengine-uk 2.172.1), costs the
2pp VAT rise at £27.07bn in 2029-30 on Enhanced FRS 2024-25
(policyengine-uk-data 1.58.1), the change in gov_balance that
test_reform_impacts.py measures. The fixture now expects 27.1 and keeps
that costing in its receipts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@juaristi22 juaristi22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incremental review at ffe20fb9165ddcda48ac57531f87847ecc317b9e — COMPLETE; all findings resolved.

S1 resolved: the private-school receipt bound now explicitly names coverage 0.67, run 40116746457 and 24 October 2026. It is a dated result, distinct from the current 0.69 Microcosm derivation. A1’s dataset-provenance correction remains resolved.

The new reform fixture is supported by Aggregate run 41633358040: the 2029–30 gov_balance impact of the two-point VAT rise is £27.070598798bn on certified Enhanced FRS 1.58.1, correctly recorded as 27.071 and rounded to the 27.1 fixture. The verified run merged af0b602ca with main at 1d4d020aa; this costing is separate from the private Microcosm calibration.

Fixture schema, consumer year/metric/units and rounding pass static validation. Numerical parameters, model code, dependencies and existing focused VAT/final-income tests are unchanged; their valid prior proof and timeout limitations are retained. No new model or population run was performed. The PR remains draft, with current-head CI still in progress.

@MaxGhenis
MaxGhenis marked this pull request as ready for review November 5, 2026 02:23
@MaxGhenis
MaxGhenis requested a review from juaristi22 November 5, 2026 02:23

@juaristi22 juaristi22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved at ffe20fb9165ddcda48ac57531f87847ecc317b9e after the readiness and CI follow-up. The completed model/source review remains valid at this unchanged head and description; A1 and S1 remain resolved, with no open findings or material evidence gaps.

All applicable checks now pass at this exact head, including the YAML/Python test result and Aggregate impact. The earlier focused test and public costing evidence is retained with its documented execution limits; this follow-up did not repeat model tests or a private population run.

MaxGhenis added a commit that referenced this pull request Nov 6, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

VAT survey coverage was derived on an older Microcosm cut and puts VAT about 9% above receipts on the current certified cuts

3 participants