Skip to content

Council tax integration: Nuffield Workstream 2 deliverable tracker #1854

Open

Description

@vahid-ahmadi

Update, 24 Oct 2026: #1855 and #1864 merged on 22 Oct 2026, so gross liability and the statutory discounts are now computed (boxes ticked below). council_tax is still the dataset input, so CTR results have not moved. PolicyEngine/policyengine-uk-data#482 and #484 and TheAxiomFoundation/rulespec-uk#346 are still open.

Tracking issue for the council tax workstream committed in the Nuffield R&D 2025 application (Workstream 2, "Council Tax Integration", £32,000 / 4 person-months, Year 1 Q1 kickoff).

Application text: https://policyengine.github.io/nuffield-rda-2025

What we promised

We will add council tax to microsimulation, enabling combined local and national tax burden analysis. This addresses a significant gap—existing models omit local taxation despite £40bn annual revenue.

Method committed (test-driven, citing the childcare precedent):

  1. Implement council tax bands and local authority rates from MHCLG data
  2. Validation test cases
  3. Compare against administrative totals
  4. Validate distributional impacts

Deliverables committed:

  1. Council tax by band and local authority
  2. National model integration
  3. Validation report
  4. Policy examples

Current state

More exists than the deliverable list suggests — the data layer is substantially built, the liability calculation is not.

Already in place:

  • policyengine-uk-data: policyengine_uk_data/storage/la_council_tax.csv — 360 LAs (England, Scotland, Wales) with band_d_amount, VOA dwelling counts for bands A–I, and net council tax requirement.
  • policyengine-uk-data: band assignment to households in datasets/enhanced_cps.py (_assign_council_tax_bands), using storage/council_tax_bands_2024.csv regional shares.
  • policyengine-uk-data: calibration targets ons/council_tax_band_d/{code} and voa/council_tax/{code}/{band} emitted from targets/sources/la_council_tax.py.
  • policyengine-uk: the full Council Tax Reduction stack under variables/gov/local_authorities/council_tax_reduction/, with bespoke schemes for Kingston upon Thames, Merton, Newham, Oxford and Westminster, plus England pensioner / Scotland / Wales parameters.
  • TheAxiomFoundation/rulespec-uk: the national England pension-age CTR scheme (SI 2012/2885) and 100 local authority CTR schemes encoded with proof citations, plus a 297-row LA registry.

Not in place:

  • Council tax liability is not computed anywhere. variables/input/consumption/property/council_tax.py is an input variable whose own docstring says it is "supplied by the household dataset rather than recomputed from local authority council tax schedules". council_tax_band is likewise an input defaulting to Band D.
  • No statutory band-ratio parameters (A=6/9 … H=18/9 relative to Band D).
  • No council_tax_band_d_amount variable in policyengine-uk, despite the data repo emitting a calibration target against that name (separate issue to follow).
  • No discounts, exemptions or premiums: single-person 25%, student/SMI exemptions, empty-home and second-home premiums.
  • No parish/precept handling.
  • Liability sourcing is England-only — Wales (StatsWales) and Scotland (Scottish Government) band amounts are not sourced.
  • No validation against the MHCLG administrative total.

Work items

Deliverable 1 — council tax by band and local authority

  • Refresh la_council_tax.csv to MHCLG 2026-27 and extend Band D to all eight bands (Table 9 of Tables_1-9_2026-27.ods gives ONS code + Band A–H for 296 English authorities)
  • Alias map for the 12 authorities whose MHCLG name does not exactly match the LocalAuthority enum: Bristol, Cumberland, Durham, Herefordshire, Kings Lynn and West Norfolk, Kingston upon Hull, North Northamptonshire, North Yorkshire, Somerset, St Helens, West Northamptonshire, Westmorland and Furness
  • Source Wales band amounts (StatsWales). Done in Compute gross council tax from LA Band D amounts and statutory band ratios #1855 as Band D amounts in gov.local_authorities.council_tax.band_d.amounts (gov.wales tables), other bands via the statutory ratios
  • Source Scotland band amounts (Scottish Government council tax datasets). Done in Compute gross council tax from LA Band D amounts and statutory band ratios #1855 the same way (gov.scot Band D tables)

Deliverable 2 — national model integration

Deliverable 3 — validation report

  • Aggregate computed liability vs MHCLG administrative total (~£40bn England)
  • Per-LA comparison against total_council_tax_net
  • Band distribution vs VOA dwelling counts
  • Distributional validation (decile incidence) against an external benchmark
  • Write up as a published validation note

Deliverable 4 — policy examples

  • Band revaluation scenario
  • Proportional property tax / council tax replacement
  • Single-person discount reform
  • Band D rate change by LA

Reference data

Notes

Band amounts in MHCLG Table 9 are area figures — they include county, police, fire and parish precepts, not just the billing authority's own requirement. Any comparison against a billing-authority-only total needs to account for that.

Activity

  1. vahid-ahmadi commented on Sep 15, 2026

    @vahid-ahmadi
    CollaboratorAuthor

    Statutory findings from the Axiom side — these change the Wales items above

    Investigated in parallel via TheAxiomFoundation/rulespec-uk (see TheAxiomFoundation/rulespec-uk#344). Three corrections to the scope as originally written in this issue:

    1. Band I for Wales comes from LGFA 1992 s.5(1A), not separate Welsh legislation. s.5(1) prints the England proportions as 6 : 7 : 8 : 9 : 11 : 13 : 15 : 18 for bands A–H. s.5(1A) substitutes, for Wales and for financial years beginning on or after 1 April 2005, 6 : 7 : 8 : 9 : 11 : 13 : 15 : 18 : 21 — so Band I = 21/9. Both sets are printed in the Act as ratios relative to each other; the "relative to Band D" form (A = 6/9 … H = 18/9) is a derivation, not Act text.

    2. The operative provision for gross liability is s.10 (Basic amounts payable), which the original scope above omitted. s.5 gives the proportions; s.10 gives the amount payable.

    3. s.12 was omitted with effect from 1 April 2026 by the Local Government Finance (Wales) Act 2024 (asc 6), ss.18(6) and 23(7). Anything modelling Welsh discounts needs to account for this — and 2026-27 is the first year affected, which is the year the data refresh targets.

    Further detail relevant to the discount/premium work item:

    • s.11(2)'s 50% no-resident discount is derived, not printed — the Act says "twice the appropriate percentage", and s.11(3) sets the appropriate percentage at 25 per cent unless varied by order.
    • ss.11–11D and Schedule 1 are now England-scoped.
    • s.11A's discount floor is "at least 10" per cent.
    • s.11B's "relevant maximum" is year- and duration-dependent (ss.11B(1A)–(1C)).
    • s.11C's cap is 100 per cent; ss.12A/12B Welsh caps are 300 per cent.

    Schedule 1 paragraph numbers for the student and severe-mental-impairment disregards are not stated here deliberately — the headings were confirmed to exist but the numbering was not verified. To be checked against the Act before encoding.

    Axiom will not be the venue for liability

    For the record, since it affects where this work lands: Axiom's omission of council tax liability is deliberate, not a gap. 82 of its 100 encoded CTR modules carry an explicit module.deferred_outputs block, several naming liability deferral directly (Sandwell: "council-tax charging, statutory discounts, joint-liability apportionment, and calendar-day calculation are future outputs"). 68 of 100 use a locally-prefixed liability variable and 30 declare the slot in a module-level inputs: block. Axiom encodes the reduction and expects liability injected by the caller.

    So the liability modelling committed to Nuffield belongs here, in policyengine-uk, not upstream in Axiom. Issue #344 asks them whether they would want an LGFA 1992 Part I charging encoding contributed; that is a separate question from this deliverable.

  2. vahid-ahmadi commented on Sep 18, 2026

    @vahid-ahmadi
    CollaboratorAuthor

    Microcosm compatibility — constraints on everything in this tracker

    PolicyEngine/microcosm consumes both policyengine-uk and policyengine-uk-data, and maintains a formal parity register against uk-data's target inventory. It already has a council tax surface, and in places a richer one than uk-data. Anything built for this deliverable has to hold to that contract, so recording the constraints here.

    1. Band amounts are rates. They must never be wired as additive loss-matrix targets.

    packages/microcosm-build/src/microcosm/build/uk/uk_data_target_parity.json carries local_council_tax_band_d_rate at status reviewed_exclusion, classification non_linear: "Band D currency amounts are per-rate, not additive household controls", with verdict_basis "Keep excluded until a rate-aware non-linear objective exists". Microcosm reached this independently of PolicyEngine/policyengine-uk-data#483.

    This binds the eight new band_A_amount..band_H_amount columns from PolicyEngine/policyengine-uk-data#482 as well — they are amounts, therefore rates, therefore not additive targets. #482 emits no targets from them, which is correct and should stay that way.

    2. Band counts are the surface that legitimately calibrates.

    local_council_tax_band_counts is ported_local_declared and actively bound — Microcosm's council_tax/band_a..i contract rows "bind 2,511 authority cells on the taxbase basis with the 296 English band-H cells and Shetland's band-H cell signed deferred (microcosm#929)". So voa/council_tax/{code}/{band} stays. Counts are additive; amounts are not.

    3. Net council tax at LA grain is blocked upstream, not just here.

    local_council_tax_net sits at blocked_source: "The pinned feed has no comparable 2025 local-authority council-tax net series across the 361-area roster", with an explicit fence against combining unlike country totals, 2026 Welsh budgets, or gross dwelling stock. So retargeting onto total_council_tax_net is not an available escape hatch for #483.

    4. New policyengine-uk variables must be engine-computed, not supplied inputs.

    packages/microcosm-frame/src/microcosm/frame/adapters/policyengine_uk.py classifies variables via _is_engine_computed, which tests for adds/subtracts/formula/formulas. A new variable with a formula is adapter-safe. One that has to be supplied as an input column is not, and the adapter has precedent for requiring explicit input handling (see validate_uc_claimant_input).

    This is a live consideration for #1855: council_tax_band_d_amount reads an LA-keyed parameter rather than being a dataset input, which keeps it computed and therefore adapter-safe. It is also another argument against resolving #1855 by making liability a supplied input.

    5. There is an existing Axiom adapter too.

    adapters/axiom.py sits alongside the policyengine ones, so Microcosm is the point where the rulespec-uk charging encoding (TheAxiomFoundation/rulespec-uk#346) and the policyengine implementation would meet. Worth keeping the two consistent in naming and entity level rather than letting them diverge.

    Action: @juaristi22 added as reviewer on #482, #1855 and TheAxiomFoundation/rulespec-uk#346, and on the two PRs opened against #483 and the discounts work.

    🤖 Generated with Claude Code

  3. vahid-ahmadi commented on Sep 21, 2026

    @vahid-ahmadi
    CollaboratorAuthor

    Council tax workstream — status for review

    @MaxGhenis @juaristi22 — everything open on the Nuffield WS2 council tax line, in one place. Four PRs are reviewable; two items need a decision, one needs an action only Max can take.

    Reviewable now

    PR What
    PolicyEngine/policyengine-uk-data#482 Eight band amounts from MHCLG Table 9 (2026-27), keyed on ONS code
    PolicyEngine/policyengine-uk-data#484 Drops the Band D calibration target
    #1864 LGFA 1992 s.11 statutory discounts
    TheAxiomFoundation/rulespec-uk#346 Five CT-CHG worklist entries (ss.5, 10, 11, 11A, 11B)

    #482 is purely additive — 361 rows unchanged, 16→24 columns, and all 294 directly-matching England rows reconcile to the MHCLG source at £0.00. All 296 authorities obey the statutory proportions (worst deviation £0.0044, per-band rounding). Note band_d_amount was already on 2026-27 figures, so this adds the other seven bands rather than refreshing anything.

    #484 resolves #483. María — your parity register already answered this: local_council_tax_band_d_rate is a reviewed_exclusion, classification non_linear, "Band D currency amounts are per-rate, not additive household controls". So the fix is to drop the target, not to add a mean-returning compute route — a mean would stop the number being wrong but would still bind a rate into an additive objective, which is the thing the fence exists to prevent. Band counts (voa/council_tax/{code}/{band}, 2,563) are untouched and still bound. Retargeting onto total_council_tax_net was the other option and is closed off too, since local_council_tax_net is at blocked_source on your side. Tell me if dropping rather than fencing-in-place is the wrong shape for your register.

    #1864 encodes s.11(1)/(2)/(3) with the 25% as the parameter and the 50% derived as twice it, since "twice the appropriate percentage" is what the Act prints. Microcosm-safe: both new variables define a formula, so _is_engine_computed classifies them as engine-computed and neither can be requested as a supplied input column.

    Decision needed — #1855

    Computed liability gives England £61.21bn against the ~£40bn we cited to Nuffield. Most of that is an artefact, and I think it's a design question rather than a bug: the variable looks up Band D via household("local_authority"), but no dataset builder populates local_authority, so every household decodes to the enum default (Maidstone). LA analysis in uk-data runs through per-LA reweighting (local_authority_weights.h5), not per-household tagging — so a household-level enum lookup may be the wrong mechanism entirely.

    This gates the validation report and policy examples, so I'd rather not build further on it until it's ruled on. council_tax itself is untouched — still the input, so CTR results don't move.

    Action only Max can take — TheAxiomFoundation/rulespec-uk#346

    Blocked on corpus ingest for ss.5, 10, 11, 11A, 11B. Write access to axiom-corpus doesn't carry the ed25519 ingest key: its Actions secrets hold AXIOM_CORPUS_RELEASE_SIGNING_KEY and no ingest private key, the only ingest key in variables is the public verification one, and no workflow signs a manifest. Detail and the three options are on #344.

    Two open questions on #344

    1. Ingest key — provision it, run the five extract-uk-legislation calls yourself, or is there a contributor path I've missed?
    2. Oracle — policyengine-uk-rust does have the band multipliers and the 25% discount, but on unmerged branches (vahid/council-tax and ten others), not main, where council_tax is still an FRS input. Build the axiom-oracles mapping against that branch, wait for it to land, or has this moved to 10ds-microsim?

    Also filed

    Two things I'd like a steer on

    Double-discount risk. FRS-reported council tax is generally the billed amount, already net of any single-person discount, while uk-data documents the input as "gross liability before CTR" — silent on statutory discounts. If that reading is right, applying #1864's discount to the FRS figure would discount twice. This is my inference, not something the repo asserts, and one of you will know outright. It matters for how #1855 and #1864 compose.

    Naming alignment. Microcosm has an adapters/axiom.py alongside the PolicyEngine ones, so Microcosm is where the rulespec charging encoding and the PolicyEngine implementation actually meet. Worth agreeing variable naming and entity levels now, while both are unbuilt, rather than reconciling later.

    Still open against the Nuffield commitments

    Schedule 1 disregards, s.11A determinations, s.11B/11C premiums, exemptions, Wales and Scotland liability, the validation report, and policy examples. All itemised in #1854.

    Three statutory findings that moved scope along the way: Wales Band I comes from LGFA 1992 s.5(1A) itself, not separate Welsh legislation; s.10 is the operative provision for liability and I'd originally missed it; and s.12 was omitted with effect 1 April 2026 by the Local Government Finance (Wales) Act 2024, which matters because 2026-27 is the year our Band D figures come from. Scotland is also not on flat ninths — SSI 2016/123 raised E–H to 473/360, 585/360, 705/360 and 882/360.

    🤖 Generated with Claude Code

  4. vahid-ahmadi commented on Sep 26, 2026

    @vahid-ahmadi
    CollaboratorAuthor

    Correction: I cited the wrong instrument for the Scottish band proportions, in this thread and in several others.

    I wrote that SSI 2016/123 raised the Scottish E–H proportions. That is wrong. SSI 2016/123 is the Local Authority (Capital Finance and Accounting) (Scotland) Regulations 2016 and has nothing to do with council tax.

    The correct instrument is SSI 2016/368 — the Council Tax (Substitution of Proportion) (Scotland) Order 2016. Its article 2 substituted the proportions

    240 : 280 : 320 : 360 : 473 : 585 : 705 : 882
    

    into LGFA 1992 s.74(1) for financial years beginning on or after 1 April 2017.

    Two things that follow, beyond the reference itself:

    • The operative provision for Scottish proportions is s.74(1), in the Scotland Part of the Act — not s.5, which is England and Wales. I had been eliding that.
    • Expressed on the 360 = Band D basis, A–D at 240/280/320/360 are identical to the original s.74(1) proportions, so only E–H changed in 2017. Anything encoding Scottish ratios needs A–D defined from 1993, not from 2017.

    Caught while addressing review feedback on #1855, where the same wrong citation had been carried into the parameter file. Corrected there, and in the bulk/worklist.yaml note on #346 (commit ed3830c).

    Apologies for the noise — a wrong statutory citation is worth more than a footnote in this repo.

    🤖 Generated with Claude Code

  5. vahid-ahmadi commented on Oct 24, 2026

    @vahid-ahmadi
    CollaboratorAuthor

    Status, 24 Oct 2026 (main at 4ed874f):

    🤖 Generated with Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions