Drop ons/council_tax_band_d/{code} target emission (#483) - #484
vahid-ahmadi wants to merge 2 commits into
Conversation
A Band D council tax amount is a rate (£ per Band D dwelling), not an
additive household control. The loss matrix is an additive objective, so
binding a ~£2,500 per-dwelling rate into it is category-wrong — neither
as a sum nor as a mean.
Microcosm adjudicated the same question independently: its
uk_data_target_parity.json concern `local_council_tax_band_d_rate` is
status `reviewed_exclusion`, classification `non_linear`, reason "Band D
currency amounts are per-rate, not additive household controls", to stay
excluded "until a rate-aware non-linear objective exists".
Removed: the per-LA Band D amount Target emission (350 targets, all of
which were silently discarded today because `council_tax_band_d_amount`
does not exist in policyengine-uk) and the now-dead `_year_for_band_d` /
`_ref_for_band_d` helpers and `_ENGLAND_REF` / `_SCOTLAND_REF` /
`_YEAR_BAND_D_WALES` / `_YEAR_BAND_D_SCOTLAND` constants.
Kept: `band_d_amount` in storage/la_council_tax.csv (byte-identical) and
loadable, the `voa/council_tax/{code}/{band}` band-count targets (2,563,
unchanged — counts are additive and Microcosm binds them as
ported_local_declared), and the `housing/council_tax_net/{code}` targets
(318, unchanged).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- The net council-tax target reads total_council_tax_net straight from the CSV; the taxbase x Band D derivation happens upstream at CSV build time. Reword the comment accordingly. - Rename changelog.d/483.md to 483.removed.md so towncrier picks up the fragment type. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
juaristi22
left a comment
There was a problem hiding this comment.
Approving. Dropping the emission is the right shape for the Microcosm register, and it answers the question on #483: no, fencing in place is not needed. The local_council_tax_band_d_rate concern keeps covering targets/sources/la_council_tax either way, and its fence assumed exactly this state. One correction on our side: the fence's origin line says uk-data "did not wire them into the additive LA loss matrix", which was wrong until this PR (the target existed and was silently dropped). We will update that evidence to cite #483 and this PR once it lands, and re-pin the inventory sha256s for la_council_tax.py and test_la_council_tax_targets.py in Microcosm's uk_data_target_inventory.json.
Sequencing: please merge this before any policyengine-uk pin bump that includes PolicyEngine/policyengine-uk#1855.
Nits, none blocking:
- The
uv.lockchange (the package's own version line, 1.56.16 to 1.57.3) is not part of this fix. Keep it only if main's lock is behindpyproject.toml, otherwise drop it from the diff. - The
== 350and== 2563pins will need touching on every CSV refresh. Fine as a deliberate tripwire; the refresh commit should say so. - The conflict with #482 is real (same docstring region, same test file); whichever lands second keeps both paragraphs.
🤖 Generated with Claude Code
Fixes #483.
Decision and provenance
A Band D council tax amount is a rate (£ per Band D dwelling), not an additive household control. The loss matrix is an additive objective — every target is compared against a weighted sum over households. Binding a ~£2,500 per-dwelling rate into it is category-wrong: not as a sum (which scales with the number of households in the LA), and not as a mean (still a rate inside an additive objective).
This is not a fresh judgement call. Microcosm reached the same verdict independently.
uk_data_target_parity.jsonconcernlocal_council_tax_band_d_rateis:non_linearreviewed_exclusionSo the resolution is to drop the emission, not to add a mean-returning compute route.
Why this is latent-bug removal, not a behaviour change today
council_tax_band_d_amountdoes not exist in policyengine-uk, sobuild_loss_matrix.py::_compute_simple_gbpraisesKeyError, returnsNone, and the callercontinues — all 350 of these targets are silently discarded today. The defect activates the moment policyengine-uk gains that variable (PolicyEngine/policyengine-uk#1855 adds it):_compute_simple_gbpwould then sum it against a ~£2,500 level.Removed
Target(name=f"ons/council_tax_band_d/{code}", variable="council_tax_band_d_amount", ...)loop inget_targets(), replaced by a comment block explaining the rate-vs-additive reason, citing Microcosm'slocal_council_tax_band_d_ratereviewed_exclusion and this issue, so it does not get helpfully re-added._year_for_band_d,_ref_for_band_d, and the constants_ENGLAND_REF,_SCOTLAND_REF(the identically-named_SCOTLAND_REFinvoa_council_tax.pyis a separate symbol and is untouched),_YEAR_BAND_D_WALES,_YEAR_BAND_D_SCOTLAND.test_band_d_targets_use_gbp_unit(vacuous once nothing matches).Deliberately kept
band_d_amountinstorage/la_council_tax.csv— needed as data for liability computation.git diff origin/main -- policyengine_uk_data/storage/la_council_tax.csvis empty: the CSV is byte-identical to main. The loader is untouched, and a new test asserts the column is still present and populated for 350 LAs.voa/council_tax/{code}/{band}band-count targets — 2,563, unchanged. These are counts, counts are additive, and Microcosm has themported_local_declaredand actively bound. Not touched.housing/council_tax_net/{code}— 318, unchanged._YEAR_BAND_D_ENGLANDkept: still used, because the England net council-tax requirement derives from the 2026-27 Band D level.Target counts, before and after
ons/council_tax_band_d/*voa/council_tax/*housing/council_tax_net/*Tests
New/changed in
test_la_council_tax_targets.py:test_no_band_d_amount_target_is_emitted— asserts no emitted target name starts withons/council_tax_band_d/(or containscouncil_tax_band_dat all).test_band_d_amount_column_still_loadable— the CSV column survives.test_band_count_target_count_matches_csv— now also pins the count at 2,563 so a future change to the Band D surface cannot quietly disturb the band-count family.Full suite, repo's normal invocation:
ruff check .reports 61 errors on this branch. Measured per-file rather than assumed: bothla_council_tax.pyandtest_la_council_tax_targets.pyreport 0 errors onorigin/mainand 0 on this branch (viagit show origin/main:<f> | ruff check --stdin-filename <f> -), so all 61 are pre-existing and live in files this PR does not touch.Not done / follow-ups
la-council-tax-all-bands) is still open, so this PR is based on currentmainand does not depend on it. Theband_A_amount..band_H_amountcolumns it adds are also rates and must likewise not be wired as additive targets — the follow-up there is "no additive target", not "add eight more". Once Extend LA council tax to all eight bands (MHCLG Table 9, 2026-27) #482 lands, the same comment should be added next to those columns. Flagging for @juaristi22 since this sits on the Microcosm parity surface.band_A_amount..band_H_amountparagraph into the same docstring region where this PR inserts its "No target is emitted…" paragraph, and adds 79 lines to the same test file. Whichever lands second will conflict; the resolution is trivial (keep both paragraphs).== 350and== 2563pins will need updating on any CSV refresh (the Scottish band-count follow-up, VOA 2026 stock). That is deliberate — it makes a silent change to these families impossible — but say so if you would rather keep only the CSV-derived assertion.changelog.d/483.removed.md(towncrier needs the.<type>.segment).build_loss_matrix.py:_compute_columnnever had anons/council_tax_band_d/route, and with the emission gone none is needed.🤖 Generated with Claude Code