Repository navigation
Grow council tax bills by receipts growth net of household weight growth - #2566
Conversation
Each country's council tax series is OBR receipts growth, which already includes growth in the number of Band D equivalent dwellings, while household weights grow with population too. Divide each bill's growth by the growth of the series uprating_indices.yaml projects household_weight with, so each country's weighted total tracks its receipts (#2067). Restate the 2023 and 2024 values, which held Band D level growth, as receipts outturns: MHCLG's council tax requirement (England), the Scottish Government's net amount billed and the Welsh Government's net collectable debit, each net of council tax reduction. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hand-off to the UK hub (sprint-fix-2067 build job)Head: CI at 09:54Z: Lint, Fix evidence, Test / YAML, Documentation build, Test documentation builds, Changes, and Install + smoke-import on 3.11 to 3.14 have passed. Test / Python 1/5 to 5/5 and Aggregate impact are in progress. Local tests (all pass,
Impact:
Open items:
No in-job review was run, per the adopt-common rule. Not merged. |
Aggregate impact on certified data: passBase Checks: 0 broken by this change, 1 failing on both sides, 0 fixed. Moves flagged for review: 97 (thresholds in Notes
Moves flagged for review
57 more flags are in the report artifact. Receipts by tax head
Spending by benefit and service
Caseloads
Government balance, each component counted once
Model totals
Household income totals
Poverty rates
People in poverty
Inequality
Mean household net income in each side's own deciles
Standard reform costings (change in gov_balance)
Household net income by decileMean change in household net income per household, and as a share of the decile's base income; households stay in the base run's deciles.
No change in any year: components the model counts twice; population; standard reforms: change in each reform's target. Checks failing on both base and head (1)
Counted-once checkEach of 84 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.
Provenance
Dataset: Receipts by tax head: head levels
Spending by benefit and service: head levels
Caseloads: head levels
Government balance, each component counted once: head levels
Model totals: head levels
Components the model counts twice: head levels
Household income totals: head levels
Population: head levels
Poverty rates: head levels
People in poverty: head levels
Inequality: head levels
Mean household net income in each side's own deciles: head levels
Standard reform costings (change in gov_balance): head levels
Standard reforms: change in each reform's target: head levels
|
|
Follow-up to the hand-off: the Aggregate impact report has finished and passed. It found 0 checks broken by this change and 1 failing on both sides. It compares main at 6c0feb3 with the merge commit. I quoted it in the body under "Before and after".
Only the Python test shards are still running; the job ends before they finish. Please mark the PR ready once they pass. |
|
Merged by the UK hub (Claude session 8e19f9d5) at head
|
Fixes #2067.
Problem
Projected council tax counted the growth in households twice.
uprate_council_tax(policyengine_uk/data/economic_assumptions.py) multiplied each household'scouncil_taxby 1 +gov.economic_assumptions.yoy_growth.obr.council_tax.<country>.household_weightis projected separately, bygov.economic_assumptions.yoy_growth.ons.population(policyengine_uk/data/uprating_indices.yaml).So each country's weighted total grew by (1 + receipts growth)(1 + population growth), outrunning OBR's receipts by population growth: 0.72% in 2025-26 and 0.37-0.45% a year after, 2.8% by 2030-31 in all three countries.
The series used to mean something else. #1225 defined it as growth "in the level (adjusted for household growth)", and #1246 filled 2023-2025 with Band D level outturns, so bills grew by the level alone. #1514 put receipts growth into 2025-2030 without changing the code. 2023 and 2024 still held Band D level growth.
The law and the sources
The projection keeps every household and scales all weights by population. A dwelling's own bill grows with its Band D amount. But the tax base can grow faster or slower than population, through new homes per person, premiums on second homes, and changes in discounts and council tax reduction. In this model that difference has to go into the bills. Growing bills by the level alone would undershoot receipts whenever the base outgrows population: in 2025-26 England's base grew 1.90% against population growth of 0.72%. Growing them by receipts growth divided by population growth puts the difference into bills, and each country's total then tracks its receipts.
The fix
uprate_council_taxgrows each bill by (1 + receipts growth) / (1 + growth in household weights). The weights' series is the oneuprating_indices.yamllistshousehold_weightunder, found by the newhousehold_weight_growth_path(), so the two cannot drift apart. Northern Ireland has no council tax series and stays flat, as before.These match OBR's own lines:
I used amounts due, not collected, so changes in in-year collection rates do not move the series. Without this restatement the division would cut bills in years that held level growth: Scotland's 2024 value of 0.001 divided by 1.0107 would lower every Scottish bill by 1%.
2025-2030 (OBR's forecasts) are unchanged.
Before and after
One £1,000 bill, 2024-25 to 2025-26:
Country totals against OBR receipts: synthetic weights, 2024-25 to 2030-31, from
test_country_totals_track_obr_receipts.The remaining gap is the series' rounding to 4 decimals, and Wales held at 0.0743 from 2026 where Table 4.1 implies 0.0743 to 0.0744.
Certified dataset: from the Aggregate impact report (run).
6c0feb303a(main, the merge commit's first parent) against headd5f0315f83(the merge commit), on enhanced_frs_2024_25 (uk-data 1.58.1).Council tax falls by the computed ratio: base £49.56bn × (1 − 1/1.0072) = £354m in 2025-26, and base £66.74bn × 2.72% = £1.81bn in 2030-31. 2024-25 is the dataset's base year and is unchanged. The report flags 97 moves for review; the largest are these council tax and balance lines and decile re-rankings.
Invariants
For any households in any regions, with any council tax of zero or more and any positive weights, any base year from 2022 and every projected year:
Tests
policyengine_uk/tests/test_council_tax_uprating.py:test_uprate_council_tax_holds_northern_ireland_flat,test_uprate_council_tax_gives_unknown_region_england_growthandtest_scottish_bills_grow_with_the_amount_billed_in_2024_25: hand-computed cases from the published figures.test_council_tax_growth_matches_published_outturns[...](7 cases): a differential test against the MHCLG, Scottish Government and Welsh Government figures, embedded in the test (invariant 5).test_country_totals_track_obr_receipts: a differential test against OBR Table 4.1 receipts, using synthetic weights.test_each_country_total_grows_by_its_receipts_growth: Hypothesis, invariants 1-3, throughextend_single_year_dataset.test_uprate_council_tax_properties: Hypothesis, invariants 2-4.test_household_weight_growth_path_*(3).policyengine_uk/tests/test_projection_horizon.py:test_rent_and_council_tax_grow_every_projected_yearnow expects the new growth.On origin/main (9d81a5d, a
git archiveexport with the new test file copied in,household_weight_growth_pathstubbed since main has none, Hypothesis shrinking off): 17 of the 21 fail. The 4 that pass there are the three unrecognised-region checks (behaviour this PR keeps) andtest_council_tax_growth_matches_published_outturns[england-2025](OBR's 2025 value is unchanged; the test shows it is also MHCLG's 2025-26 requirement growth).No YAML cases. YAML policy tests cannot reach this code. policyengine-core's runner builds each case from its
inputwithSimulationBuilder().build_from_dict(policyengine_core/tools/test_runner.py), and itsTEST_KEYWORDShas no dataset key. A situation is never projected:_projects_datainsimulation.pyreturns False whenever a situation is given. The hand-computed expectations are in the pytest cases above.On this branch (local,
-n 2, ruff 0.17.0):test_council_tax_uprating.py: 21 passed.test_projection_horizon.py: 14 passed.test_no_economic_assumptions,test_uprating_inputs_reach_data,test_dataset_group_order,test_simulation_dataset_sources,test_rent_uprating,test_domestic_rates_uprating,test_water_uprating,test_residential_property_uprating): 104 passed.test_parameter_metadata,test_parameter_descriptions,test_fiscal_year_parameters,test_import_obr_forecasts,test_scenario_sum_timing,test_dataset_fiscal_year,code_health/test_uprating_declarations,code_health/test_input_variable_definitions): 117 passed.policy/baseline/gov/local_authorities/council_tax,.../council_tax_reduction,.../hmrc/council_tax,consumption/council_tax_less_benefit.yaml,reforms/parametric/hbai_council_tax_lvt.yaml): 299 passed.ruff format --check .andruff checkon the changed files: clean.Interactions checked
simulated_council_tax, gross council tax from LA Band D): no other variable readssimulated_council_tax.gov_tax,household_tax, council tax reduction and net income read the dataset'scouncil_tax, which this PR projects. Compute gross council tax from LA Band D amounts and statutory band ratios #1855's Band D amounts are parameters by year and do not pass through dataset projection, so this PR leaves them unchanged.high_value_council_tax_surchargereadsmain_residence_value,tenure_type,countryand the house price index, notcouncil_tax. It is unaffected, apart from sitting in the same totals.council_tax(council_tax_reduction_maximum_eligible_liability), so simulated CTR awards can fall with the bills.uprate_datasetreadsstorage/uprating_factors.csv, which has nocouncil_taxrow.UKSingleYearDatasetfor 2024, so policyengine-uk projects it and this PR reaches Budget-year council tax. Microcosm also fitsobr.council_tax_{england,scotland,wales}targets at its 2025 solve year, and divides the solved weights by policyengine-uk's own weight projection (uk_runtime/weight_year.py). Its engine pins vendor only thehousehold_weightgrowth, not council tax growth. I did not trace how Microcosm computes solve-year council tax for those targets, so whether a cut built before this PR then sits about 0.7% below its 2025-26 council tax targets is an open item for the Microcosm owners.Not in this PR
council_taxis gross liability, and is grown by the growth in receipts net of CTR. This assumes CTR's share of the gross stays constant.Reproducer: policyengine_uk/tests/test_council_tax_uprating.py::test_country_totals_track_obr_receipts, policyengine_uk/tests/test_council_tax_uprating.py::test_each_country_total_grows_by_its_receipts_growth, policyengine_uk/tests/test_council_tax_uprating.py::test_council_tax_growth_matches_published_outturns
axiom: n/a: dataset projection code and economic-assumption data, no policy rule changes
🤖 Generated with Claude Code