PolicyEngine / PolicyEngine/policyengine-uk-data
LA Band D target is silently dropped now, and will compute as a sum-vs-level mismatch once council_tax_band_d_amount exists
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 1
- Forks
- 2
- Avg merge
- 1d 14h
- Merged PRs (30d)
- 4
Description
Summary
targets/sources/la_council_tax.py:182 emits a target whose variable is council_tax_band_d_amount. That variable does not currently exist in policyengine-uk, so the target is silently dropped. Once it exists, the target will activate and be computed incorrectly — as a sum where the target value is a level.
This is currently latent. It becomes a live calibration defect the moment policyengine-uk gains the variable and the pin here is bumped.
Current behaviour: silent drop
build_loss_matrix.py::_compute_column routes voa/council_tax/ and obr/council_tax, but has no route for the ons/council_tax_band_d/ prefix. So it falls through to _compute_simple_gbp, which does:
try:
entity = ctx.sim.tax_benefit_system.variables[variable].entity.key
except KeyError:
return None
KeyError → None → the caller continues. The logger.debug("No compute logic...") branch never fires, so there is no diagnostic either. ~360 LA targets are being discarded with no signal.
The defect that activates on merge
_compute_simple_gbp's docstring is "Sum a variable at household level", and for a household-entity variable it returns ctx.pe(variable) — the weighted sum across households.
But the target value is a level: band_d_amount is a single LA's Band D council tax, on the order of £2,500.
So the loss matrix would compare a national weighted sum (order 1e10) against ~2.5e3. That will distort calibration rather than improve it.
Related architectural note
council_tax_band_d_amount is inherently an LA-level attribute, not a household-level quantity. Note that LA analysis in this repo works by reweighting the national sample per LA (local_authority_weights.h5, one weight vector per code in local_authorities_2021.csv) rather than by tagging households with an LA — so "the Band D amount for this household" is not well-defined outside a specific LA weighting context. Whatever fix is chosen should account for that.
Options
- Drop the target. If Band D amount is an input to liability rather than something to calibrate towards, it may not belong in the loss matrix at all. Band counts (
voa/council_tax/{code}/{band}) and the net requirement are the calibratable quantities. - Add a compute route for
ons/council_tax_band_d/returning a weighted mean rather than a sum. - Retarget onto total council tax revenue per LA, which is a sum and already available as
total_council_tax_netinstorage/la_council_tax.csv.
Option 1 or 3 look more principled than 2, but this is a judgement call for whoever owns the calibration design.
Why now
PolicyEngine/policyengine-uk#1855 adds a council_tax_band_d_amount variable. If that merges and the policyengine-uk pin here is bumped without addressing this, the defect goes live silently — it will not raise, it will just calibrate against a wrong comparison.
Found while working on the council tax deliverable tracked at PolicyEngine/policyengine-uk#1854.
🤖 Generated with Claude Code
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with targets/sources/la_council_tax.py:182 and trace the target through build_loss_matrix.py::_compute_column and _compute_simple_gbp. Review the three proposed options against the LA weighting approach and existing storage/la_council_tax.csv data. Done means a decided calibration design prevents silent target loss and avoids comparing a national sum with a Band D level when the variable becomes available.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- data-engineering
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100