ONS effects of taxes and benefits (mode 1, distributional): the decile-incidence population HMT publishes only as charts (#90) - #92
Conversation
The lane #61/#68 went looking for and could not have. HM Treasury publishes its decile impacts as unlabeled chart bars — 132 marks, zero digitizable values, which that lane declares rather than digitizes. ONS publishes observations, so this one carries numbers: 2,640 claims across the five income concepts, one per stage of the tax-benefit system (original -> gross -> disposable -> post-tax -> final). Source reconciliation, asserted by the build: 5,280 published observations = 2,640 claims + 2,640 deliberate drops The dropped half is the `Deflated value` series. Its PRICE BASE is stated nowhere — not in the dataset, not in its CSVW metadata, not in the `value-deflation` code list — and a real-terms figure whose base year is unknown cannot be reproduced or compared with anything PolicyEngine computes. The 2024 bulletin does name CPIH excluding Council Tax, but that is a different release from this version-3 dataset, and attributing its deflator here would launder one publication's methodology onto another's numbers. Two identity facts read out of the data rather than assumed: - The time dimension is genuinely mixed. ONS's own code list is named `financial-and-calendar-years`: 1977-1993 are CALENDAR years and 1994-95 onward are FINANCIAL years. Each row carries the basis its label implies and a financial year keys its END year, so the two never share one time basis. - The five income concepts are five quantities, not one with a qualifier. The same quintile, statistic and period under two concepts are two different claims, and the test asserts their claim ids differ. Coverage rides on the claim, not on a reader's memory. Each row carries `pe_expressibility`: `final` income is NOT_EXPRESSIBLE because benefits in kind (health, education, housing subsidy) have no PE-UK counterpart at all, and `post_tax` is PARTIAL because PE-UK exposes VAT and the duties but no indirect-tax aggregate. The not_expressible rows carry an action_link (gate #9), so a `final`-income divergence can never be reported as an engine defect when it is a coverage gap — the same call the HMT lane made about public services. Provenance is the API dataset, pinned to version 3 rather than following latest_version: ONS's bulletin series runs later than the dataset, so a silent follow would change the observations under the claims. Both the CSV and its CSVW metadata are vendored with SHA-256 pins. raw/README.md records what was surveyed and NOT kept — including the decile table this lane originally wanted, which is a 2015-saved .xls from a discontinued series; quintiles are the honest current grain and the lane says so rather than implying decile coverage it does not have. ONS ranks by equivalised DISPOSABLE income, which is not HBAI's BHC/AHC ranking nor UKMOD's nor RF's, so the quintiles are registered per source and recorded DISTINCT from all three. Suite 307 passed, two builds agree on content_hash, no-drift clean, ruff format clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016HuXJFVme8HRbnke2Ey3Me
DTrim99
left a comment
There was a problem hiding this comment.
Reviewed closely — checked out the branch, recomputed the pins, and ran tests/test_ons_etb.py (21 pass) against the real vendored CSV. Same discipline as #91; the "reconciliation asserted by the build" approach holds up. Stacked on #91 — this targets uk/thinktank-ingest, so it should merge after #91 lands. Approving, with one small tidy and two hygiene notes.
Verified against the real data
- Accounting identity
5,280 = 2,640 claims + 2,640 dropsis computed and enforced (raises if it doesn't close), and reconciles from the CSV. The dropped half is exactly theDeflated valueseries, dropped because the price base is stated nowhere — and the code correctly does not launder the 2024 bulletin's CPIH deflator onto a v3 dataset. Good call. - Time basis — calendar (1977–1993) vs financial (1994-95+) is read from ONS's own
financial-and-calendar-yearscode list; a financial year keys its END year, andparse_periodvalidatessuffix == start + 1and raises otherwise. 17 calendar + 27 financial = 44 periods checks out; a calendar-1990 and a financial-2020-21 row never share a basis. - 5 concepts = 5 quantities — claim_ids are value-invariant and differ across concepts, with a collision-raising uniqueness gate. Tested.
- Expressibility — original/gross/disposable
expressible, post_taxpartial(no PE-UK indirect-tax aggregate), finalnot_expressible(benefits in kind), and thenot_expressiblerows carry theaction_linkso afinal-income divergence can't be read as an engine defect. Correct. - Provenance — pinned to dataset v3 (no
/latestanywhere; tested), and both the CSV and its CSVW metadata carry SHA-256 pins that are recompute-and-checked before parsing (I recomputed both — they match). Adapter parses by column name (not order), with closed registries that raise on any unknown concept/quintile/statistic/deflation/geography. - Determinism — content_hash/claim_id are
sort_keyscanonical (dict-order invariant), no timestamps into the hash, anddata/lanes.jsonis byte-identical to theapp/publicmirror. - Quintile-not-decile honesty — quintiles registered, and
raw/README.mdrecords the discontinued 2015.xlsdecile table as surveyed-and-not-kept, with no implied decile coverage. Nicely honest.
One thing to tidy (non-blocking)
The DISTINCT prose overclaims. source.json, adapter.py, and uk_aliases.py comments all say ONS quintiles are distinct from "HBAI's… not UKMOD's, not RF's" (and IFS), but the only DISTINCT edges (and tests) that actually exist are ons_etb ↔ ukmod and ons_etb ↔ resolution_foundation — there's no ons_etb ↔ hbai or ons_etb ↔ ifs edge. It isn't a silent-aliasing bug (the canon key is per-(source, axis), so cross-source values can't unify without an explicit alias, and none exist), but if the DISTINCT set is meant as the assertion registry that proves the claim, it currently reads as half-populated. Either add the ons_etb↔hbai/ifs edges (+ extend the DISTINCT test) or soften the comments to name only the pairs you actually register.
Hygiene notes
adapter.py'sopen(..., newline="")has no explicitencoding="utf-8"— benign here because the SHA-256 pin fixes the bytes, but worth pinning the encoding so a platform default can't ever change how it reads.- Consider a
.gitattributesforcing LF (-text/text eol=lf) on the vendored.csv/.csv-metadata.json— I hit a spurious SHA-pin mismatch locally from Windows autocrlf converting line endings on checkout; the committed blob is correct under LF, but a Windows contributor running the suite would see it fail confusingly.
Strong lane — the "ONS publishes observations, HMT publishes chart bars, so this one carries numbers" framing is exactly the right complement to #68.
Reviewed with Claude Code assistance.
1. The DISTINCT prose overclaimed. It named HBAI (and the adapter named HMT) among the groupings ONS quintiles are distinct from, while no such edges existed. Corrected in the direction the evidence points: the ons_etb edges are now exhaustive against UKMOD (all five), Resolution Foundation and IFS — and HBAI is named as a DELIBERATE absence, because it registers no decile or quantile vocabulary at all (its subgroups are children/pensioners/working_age/total), so there is nothing on that side to assert against. Padding the ledger with hbai edges would have been the same overclaiming in reverse. 2. The adapter pins encoding="utf-8" on its read. The SHA-256 gate fixes the bytes; a platform default could still change how they are decoded. 3. .gitattributes forces -text on the vendored .csv, .csv-metadata.json and .jsonl artifacts, so a Windows checkout with autocrlf cannot rewrite line endings and break every SHA pin confusingly. Thank you for hitting that locally rather than leaving it for a contributor. Also carries the #91 review fixes via merge. Suite 310 passed, two builds agree on content_hash, ruff format clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016HuXJFVme8HRbnke2Ey3Me
|
Thank you — all three addressed, and the The DISTINCT prose overclaim. Correct, and I fixed it in the direction the evidence points rather than by padding the ledger. The But not against HBAI, deliberately, and the prose now says why: HBAI registers no decile or quantile vocabulary at all (its subgroups are Hygiene 1 — encoding. Pinned Hygiene 2 — line endings. Added This branch also carries the #91 review fixes via merge, so it stays mergeable behind it. Suite 310 passed, two builds agree on |
Closes #90. Stacked on #91 (
uk/thinktank-ingest) — both touchuk_aliases,relationshipsandbuild_db, so this targets that branch rather than main; review #91 first.The lane #61/#68 went looking for and could not have. HM Treasury publishes its decile impacts as unlabeled chart bars — 132 marks, zero digitizable values, which that lane declares rather than digitizes. ONS publishes observations, so this one carries numbers: 2,640 claims across the five income concepts, one per stage of the tax-benefit system.
Source reconciliation, asserted by the build
The dropped half is the
Deflated valueseries. Its price base is stated nowhere — not in the dataset, not in its CSVW metadata, not in thevalue-deflationcode list — and a real-terms figure whose base year is unknown cannot be reproduced or compared with anything PolicyEngine computes.The 2024 bulletin does name CPIH excluding Council Tax. But that is a different release from this version-3 dataset, and attributing its deflator here would launder one publication's methodology onto another's numbers. So the nominal half is emitted and the real-terms half is a declared, tallied drop.
Two identity facts, read out of the data rather than assumed
The time dimension is genuinely mixed. ONS's own code list is named
financial-and-calendar-yearsand it means it: 1977–1993 are calendar years, 1994-95 onward are financial. Each row carries the basis its label implies, and a financial year keys its END year — so a calendar-1990 observation and a financial-2020-21 one never share a time basis.Five income concepts are five quantities, not one with a qualifier. The same quintile, statistic and period under two concepts are two different claims; a test asserts their claim ids differ.
Coverage rides on the claim
Each row carries
pe_expressibility:expressiblepartialnot_expressibleThe
not_expressiblerows carry anaction_link(gate #9), so afinal-income divergence can never be reported as an engine defect when it is a coverage gap — the same call the HMT lane made about public services.Provenance
Pinned to dataset version 3, not
latest_version: ONS's bulletin series runs later than the dataset, so a silent follow would change the observations under the claims. The CSV and its CSVW metadata are both vendored with SHA-256 pins.raw/README.mdrecords what was surveyed and not kept — including the decile table this issue originally asked for, which turns out to be a 2015-saved.xlsfrom a discontinued series. Quintiles are the honest current grain, and the lane says so rather than implying decile coverage it does not have.ONS ranks by equivalised disposable income — not HBAI's BHC/AHC ranking, not UKMOD's, not RF's — so the quintiles are registered per source and recorded
DISTINCTfrom all three.Verification
content_hashruff format --checkcleanReviewers
@MaxGhenis @DTrim99 — the call I'd most like challenged is dropping the deflated half. The alternative is to carry it with the price base recorded as unknown, but I think an unreproducible real-terms number is worse than an absent one. Second:
quintiles, not decilescloses this issue with less granularity than it asked for; I'd rather state that than vendor a decade-stale file.