This repository was archived by the owner on Jun 19, 2026. It is now read-only.
Commit 604621c
Calibrate LA council tax (band counts + net £) and fix national gross/net (#374)
* Add LA-level council tax calibration targets
Two families of LA-level targets, covering all 360 LAs in
local_authorities_2021.csv, built from four public sources:
- `ons/council_tax_band_d/{code}` (350 targets): average Band D
council tax inclusive of all precepts per billing authority.
Sources: MHCLG *Council Tax levels set by local authorities in
England 2026-27*, Welsh Government *Council Tax levels April 2026
to March 2027*, Scottish Government *Council Tax Assumptions 2025*.
All 296 English + 22 Welsh + 32 Scottish LAs covered.
- `ons/council_tax_band_count/{code}/{band}` (2,541 targets): number
of dwellings per band A-H per LA. Source: VOA *Council Tax: Stock
of Properties, 2025*. Covers England + Wales (318 LAs × ~8 bands,
minus City of London Band A which is VOA-suppressed).
NI is excluded: domestic rates, not council tax. Scotland band
counts are not in VOA; Scottish Assessors publishes them separately
and is a follow-up.
Files
-----
- `storage/la_council_tax.csv` (31 KB, 360 rows): canonical CSV
joining DLUHC Table 10 column 17, Welsh Table 1 "Overall average
band D", Scottish Gov "CT by Band 2025-26" Band D column, and VOA
CTSOP1.0 bands A-H onto the reference LA list.
- Post-2023 South Yorkshire E-codes (E08000038/39) re-mapped to
pre-2023 codes (E08000016/19) to match the reference list.
- Scottish ampersand/double-space naming normalised
("Argyll & Bute" → "Argyll and Bute", etc.).
- `targets/sources/la_council_tax.py`: reads the CSV, emits Target
objects at geographic_level=LOCAL_AUTHORITY with per-country year
tagging and per-country reference URL.
Testing
-------
22 hermetic tests (no network access, no baseline fixture needed):
Structure
- Row count matches local_authorities_2021.csv.
- Every expected column present.
- Four UK country codes represented.
- Every LA code matches the reference list.
Value plausibility (the #371 lesson)
- Band D amount in [£900, £3,500] for every row with a value.
- Total dwellings in [200, 800,000] for every row with a value.
- Explicit Isles of Scilly regression test: total dwellings in
[500, 5,000], not the 2.49M outlier that slipped into #371.
- Band A-H counts sum to total dwellings within 20-property slack
(VOA 10-property suppression allowance).
- Every band-count target value ≤ 500k (largest LA stock).
Coverage expectations
- Every English, Welsh and Scottish LA has a Band D value.
- Northern Ireland has no council tax flagged (has_council_tax=False).
Spot-checks of published facts
- Wandsworth (E09000032) and Westminster (E09000033) are the two
lowest-Band-D English LAs (catches row-swap bugs).
- Scottish average Band D is £500+ below English average.
Target-API invariants
- get_targets() returns a non-empty list without network access.
- Band D target count matches the CSV's non-null Band D count.
- Band count target count matches Σ non-null band columns.
- Every target carries geographic_level=LOCAL_AUTHORITY and a
geo_code.
- Band D targets use Unit.GBP; band count targets use Unit.COUNT
with is_count=True.
- Every target has at least one year of values.
Sources
-------
- MHCLG (England 2026-27):
https://www.gov.uk/government/statistics/council-tax-levels-set-by-local-authorities-in-england-2026-to-2027
- Welsh Government (Wales 2026-27):
https://www.gov.wales/council-tax-levels-april-2026-march-2027-html
- Scottish Government (Scotland 2025-26):
https://www.gov.scot/publications/council-tax-datasets/
- VOA (England + Wales 2025):
https://www.gov.uk/government/statistics/council-tax-stock-of-properties-2025
Out of scope for this PR (follow-ups)
-------------------------------------
- Wiring these targets into
datasets/local_areas/local_authorities/loss.py so the LA
reweighting actually calibrates on them. Planned follow-up PR.
- Scottish Assessors per-LA chargeable-dwellings to fill the Scotland
band-count gap.
- Council Tax Support caseload per LA (DWP StatXplore).
- Single Person Discount rate per LA (CIPFA).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Address review: add Welsh Band I, source totals from VOA, tidy module
Review points addressed:
- Add count_band_I column to la_council_tax.csv, populated for all 22
Welsh LAs (Wales revalued in 2005 and introduced a 9th band). Cardiff
1480, Monmouthshire 670, Vale of Glamorgan 1060, etc. English rows
keep Band I null; VOA marks it [z] (not applicable).
- Re-source total_dwellings from VOA "All properties" column instead
of deriving it as the sum of A-H. Previously Σ(A..H) was used for
both sides of test_band_counts_sum_to_total, making the test
self-referential; now it validates against the published total with
a 20-property slack for VOA rounding.
- Rename count columns symmetrically: band_A..band_H + band_D_count →
count_band_A..count_band_I. Removes the lopsided band_D_count name
that existed only to avoid clashing with band_d_amount.
- Align band-count target names with voa_council_tax.py:
voa/council_tax/{code}/{band} (was ons/council_tax_band_count/...);
variable="council_tax_band" (was council_tax_band_count, which is
not a real PolicyEngine-UK variable); drop breakdown_variable to
match the regional VOA module.
- Cache the CSV read with @lru_cache(maxsize=1), matching voa_council_tax.
- Update module docstring: "A-H in England/Scotland, A-I in Wales".
Tests:
- New: test_welsh_las_have_band_i (all 22 Welsh LAs populated).
- New: test_english_las_have_no_band_i (guard against spurious fills).
- New: test_cardiff_band_i_matches_published_figure (~1,480 per VOA 2025).
Final target counts:
- 350 Band D amount targets (unchanged).
- 2,563 band-count targets, up from 2,541: +22 Welsh Band I plus two
band-H rows that were null due to the earlier truncation.
* Satisfy ruff format on la_council_tax.py
* Wire LA council-tax band-count targets into the calibration loss matrix
The targets registered in la_council_tax.py were inert — the LA target
matrix had no columns for them, so the reweighter could not see them.
This wires the eight VOA Council Tax Stock-of-Properties band-count
targets (A-H) into the LA loss matrix:
- matrix entry: per-household indicator 1[council_tax_band == B] from
policyengine-uk.
- y entry: 360-vector of per-LA dwelling counts from
storage/la_council_tax.csv. For LAs without VOA data — Scottish LAs
(the VOA summary tables don't cover Scotland) and Northern Irish LAs
(no council tax) — the value falls back to
national_count × la_household_share, matching the existing tenure
block's fallback pattern.
Two targets are deliberately not wired in this pass:
- Band I — Wales-only and mostly null in the CSV.
- The Band D £ amount (ons/council_tax_band_d/{code}) — a per-rate
quantity that does not fit the linear matrix-times-weights
aggregation. Wiring it as total council-tax revenue would need
Scotland-specific band ratios (different from England/Wales after
2017) and is worth a separate PR.
New tests in test_la_loss_council_tax.py cover both layers:
- Light: CSV joins to every LA code, the eight count_band_{X} columns
exist, E/W rows are populated, Scotland is null as documented, and
NI has has_council_tax=False.
- Full build (gated on enhanced FRS fixture): all eight columns present
in matrix and y; y vectors length 360, finite and positive; matrix
entries are 0/1 indicators with rows summing to ≤1; y matches the
CSV verbatim for an English LA (Hartlepool); Scotland and NI LAs
receive a positive fallback rather than NaN or zero.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add LA-level net council tax £ target alongside band counts
Wires the second FRS data point into the LA reweighter, addressing
the 28 Apr standup ALIGNED decision: "calibrate the two FRS data
points as the council tax information is provided after deductions."
Both sides of the new constraint are net of CTR:
- matrix col = council_tax_less_benefit (gross − CTR benefit)
- y = directly observed net council tax requirement per LA
Sources (no national-total apportionment, all directly published):
- England (296 LAs): MHCLG Council Taxbase 2025, Table 1.35 "Tax base
after allowance for council tax support" × Band D amount.
Sums to £47.4bn, within 3.4% of the MHCLG Table 1 published England
Council Tax Requirement of £45.86bn (small gap from year mismatch:
2025 taxbase × 2026-27 Band D).
- Wales (22 LAs): Welsh Government "Council Tax Levels April 2026
to March 2027" Table 3 "Council tax income (£m)". Sums to £2.45bn.
- Scotland (32) and NI (10): no source wired; loss.py routes through
the existing national × la_household_share fallback, same pattern
as the band-count target and the rent target.
Mirrors the rent block in loss.py: load CSV → merge into ct_merged →
matrix col / y assignment / has_data mask / national-share fallback.
Files:
- storage/la_council_tax.csv: new column total_council_tax_net.
- targets/sources/la_council_tax.py: load_la_net_council_tax() +
Target objects named housing/council_tax_net/{code}.
- datasets/local_areas/local_authorities/loss.py: housing/council_tax_net
block immediately after the band-count block.
- tests/test_la_loss_council_tax.py: 11 new tests (4 layer-1 +
7 layer-2) covering CSV column presence, country coverage, value
range, England-total ballpark vs MHCLG, matrix-col correctness,
na-fallback behaviour, calibratability sanity check.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix gross/net mismatch in OBR national council tax compute
OBR EFO Table 4.1 reports "Total net council tax receipts" — net of
council tax reduction (CTR). The matching household-level signal is
council_tax_less_benefit (= gross council tax − CTR award), not
council_tax (which is the gross liability before CTR per its
docstring "Gross amount spent on Council Tax, before discounts").
Calibrating gross household values against a net national target
systematically pulls weights down to fit (Σ w × gross > Σ w × net),
leaking bias into adjacent national targets that share the weight
vector.
Order-of-magnitude sanity (UK 2024-25):
Σ w × council_tax (gross) ≈ £55bn
Σ w × council_tax_less_benefit (net) ≈ £47bn
OBR Table 4.1 "Total net council tax" ≈ £44bn
After the fix, the council tax constraint is internally consistent
(both sides net) and aligns with Max's 28 Apr standup decision on
FRS-net-of-CTR alignment. Pairs naturally with the LA-level
housing/council_tax_net target this PR adds — both use the same net
variable.
Adds three regression tests pinning the net-variable contract.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Zero NI council tax targets instead of fabricating fallbacks
Northern Ireland uses domestic rates, not council tax. The CSV's
has_council_tax flag has been False for NI from the original commit,
but loss.py was ignoring it and assigning national × la_household_share
to NI LAs for both band counts and the new net £ column.
Effect: the optimiser was being told "NI households should pay this
much council tax" with a positive target, while every NI household
has council_tax_band == None and council_tax_less_benefit == 0 — an
unsatisfiable constraint that wastes loss the optimiser cannot drive
to zero. Reported by @MaxGhenis in PR review.
Fix: read has_council_tax from the CSV, gate the np.where so NI LAs
get y == 0 for all 9 council-tax columns. Direct-value and fallback
paths unchanged for E/W/S.
Updates two tests that previously asserted positive fallback for NI;
adds explicit zero-NI assertion for housing/council_tax_net.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Document derived/proxy nature + lineage drift for #374 CT targets
Per @MaxGhenis PR review: both council-tax LA targets are derived
proxies, not direct matches for the matrix-side variables. The PR
description and code comments earlier overstated this.
voa/council_tax/{A..H}: target counts VOA dwellings (E&W only,
includes exempt/empty/second homes); matrix counts policyengine-uk
households. Banding ratios differ in Scotland post-2017 and Wales
has Band I.
housing/council_tax_net: target value is MHCLG taxbase × Band D
(taxbase = Band D equivalent dwellings adjusted for ~7 discount/
premium/exemption classes); matrix col is FRS-reported
council_tax_less_benefit (household-reported gross less reported
CTB). Same intent, different construction paths.
Documentation only — no code, data, or test behaviour change.
The la_council_tax.py docstring now has an explicit "Lineage
caveats" section, and loss.py block comments label both targets
as derived/proxy with cross-reference.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Mask unavailable LA council tax targets
* Remove redundant council tax availability gate
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Max Ghenis <mghenis@gmail.com>1 parent 35b5ff4 commit 604621c
9 files changed
Lines changed: 1519 additions & 11 deletions
File tree
- policyengine_uk_data
- datasets/local_areas/local_authorities
- storage
- targets
- compute
- sources
- tests
- utils
Lines changed: 54 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
14 | 17 | | |
15 | 18 | | |
16 | 19 | | |
| |||
252 | 255 | | |
253 | 256 | | |
254 | 257 | | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
255 | 309 | | |
256 | 310 | | |
257 | 311 | | |
| |||
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
22 | | - | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
23 | 32 | | |
24 | | - | |
| 33 | + | |
25 | 34 | | |
26 | 35 | | |
27 | 36 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
120 | 120 | | |
121 | 121 | | |
122 | 122 | | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
0 commit comments