Skip to content

Refuse disclosure codes that match nothing, and key UK regions on GSS codes - #638

Merged
juaristi22 merged 8 commits into
mainfrom
uk-disclosure-625
Oct 16, 2026
Merged

juaristi22 merged 8 commits into
mainfrom
uk-disclosure-625

Conversation

@juaristi22

@juaristi22 juaristi22 commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #625.

Summary

The release check matches areas by their code strings. Two of its inputs were never checked against the areas the release holds, so a mismatch made the nesting check and the prior-publication check find nothing, without an error. UK regions also came out under different spellings depending on the dataset. This PR makes each of these an error or a single scheme.

  • parent_of. For a weight-matrix level, Release now raises DisclosureError when a child area maps to a code that is not an area of the parent level. Before, every parent group came out empty and the check passed.

  • Prior publications.

    • Release.check() raises for any prior_public cell at a level the release holds whose area code is not an area of that level. When the release holds the cell's reform and year, the area must be one of those estimates'; otherwise any of the level's areas will do. This runs at the start of check(), so the order estimates are added in doesn't matter. It also covers levels held only as references.
    • Release() raises for a prior cell whose outcome is not one of the six outcomes.
    • Prior cells at levels the release doesn't hold stay allowed, since they belong to other publications. A cell at a held level must still name one of that level's areas, whatever its reform and year. So a prior on older boundaries for another year now raises, and has to be filtered out first.
    • compute_uk_geography_disclosure raises for a prior cell whose level is neither a UK level (national, region, constituency, local_authority) nor one given in area_weights=. A release can't tell a misspelt level from another publication's, but the adapter knows every UK level.
  • UK regions are keyed on ONS GSS codes. This holds whether they come from region_code_oa or from the engine's region enum, as strings, bytes or enum members:

    • England's nine regions use their E12 codes;
    • Wales, Scotland and Northern Ireland use their country codes, W92000004, S92000003 and N92000002;
    • Microcosm's pseudo codes (W/S/N99999999) map to those country codes;
    • UNKNOWN and blank values are unassigned;
    • anything else raises.

    The table is UK_REGION_GSS_CODES. A regional release from a national dataset and a seat release from a local file now publish and match the same codes. The Autumn Budget dashboard uses the same scheme (Publish results by region and constituency through the disclosure layer uk-autumn-budget-dashboard-2026#48).

  • Empty area columns. A column that gives no household an area is skipped, so an empty region_code_oa falls back to the stored region. A requested level whose columns give no household an area raises a clear error, instead of a KeyError from the check.

  • Docs. docs/disclosure.md has a new section on UK region codes. It explains that prior cells and parent_of match by code, and that region weights passed with area_weights= must use GSS codes too.

Messages give counts and level names, never codes or values.

Behaviour changes

  • Region rows from national datasets now carry GSS codes instead of enum names.
  • A dataset with a region spelling outside the table now raises in the adapter.
  • Two existing tests relied on the old behaviour (a region named "R", and enum names in parent_of), and now use GSS codes.
  • A prior on older boundaries raises, even for a year the release doesn't hold.
  • Version: minor (6.6.0). The added fragment for UK_REGION_GSS_CODES sets it.
    • Strictly, this is breaking: region codes change on national datasets, and enum names in parent_of and prior_public now raise.
    • Against that, the layer shipped in 6.4.0 on 14 October, and its one known consumer, the dashboard's Request HF token with getpass if not set in env #48, already uses GSS codes.
    • This should merge before the first regional release, so nothing is published under the old codes.

Still not caught

  • A prior whose reform label or year differs from the release's is still ignored. Those cells legitimately belong to other publications, and the docs now say that releases meant to check against each other must use the same reform label and year.
  • A misspelt level name is caught only on the UK adapter path. Release can't tell it from another publication's level.
  • A parent_of that uses valid codes but maps areas wrongly still passes. For example, a parent can be left with no children. That can't be an error, because a seat matrix may legitimately cover only part of the parent level.
  • Extra keys in parent_of are ignored. That is harmless, because missing child areas still raise.

Tests (synthetic data only)

  • The issue's probe. It uses 400 records in two regions and a 4 × 400 seat matrix, with one seat withheld.
    • With parent_of using the region level's own codes, check() returns three blocking recoverable_remainder findings.
    • With the regions named differently, it now raises. On the old code it returned no findings.
  • A prior cell with an unknown area raises, and so does one whose area only another year holds. A prior cell for a reform or year the release doesn't hold is accepted. An unknown outcome raises.
  • The adapter maps enum names, Microcosm pseudo codes and GSS codes to the same codes, and rejects unknown values.
  • The adapter raises for a prior cell at a misspelt level, and accepts one at a level given in area_weights=.
  • An empty region_code_oa (blank, whitespace or missing) falls back to region. A requested level whose column is empty raises.

Checks run locally

  • tests/test_disclosure_release.py, tests/test_uk_geography_disclosure.py and tests/test_disclosure_schema.py: 82 passed. CI runs the full suite.
  • ruff format --check . and ruff check . with ruff 0.17.1, the version CI installs: clean. The locked ruff (0.12.11) formats these files the same way.
  • .github/check-changelog.sh origin/main: passes (fixed, changed and added fragments).

🤖 Generated with Claude Code

Maria Juaristi and others added 4 commits October 16, 2026 09:59
For a weight-matrix level, Release._groups checked that every child area had
an entry in parent_of, but not that the entry was an area of the parent level.
A mapping that spelt the parent codes another way left every group empty, so
the nesting check passed with no finding. On the synthetic probe from the
issue, a seat withheld because three records carry it went unflagged as soon
as the regions were named differently.

The check now raises DisclosureError and gives the count of unknown codes,
never the codes. It checks the parent codes of the child areas the estimates
hold; entries for other areas are ignored, as before. The probe is a test.

Part of #625.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The release used PriorPublic cells as given. A cell whose area code matched no
area of its level was ignored, so a cell that an earlier publication showed,
and that this release withholds, still counted as hidden, and the checks that
depend on it never ran. Regions published under one spelling and passed to a
release that holds them under another matched nothing, without an error.

check() now raises DisclosureError for a prior cell at a level the release
holds whose area code is not an area of that level, whatever the cell's
reform and year. It runs in check(), so the order in which estimates are added
does not matter. Release() also refuses a prior cell with an unknown outcome.
Cells at levels the release does not hold, or for reforms or years it does not
hold, belong to other publications and stay allowed. The messages give the
prior's name and the count of cells, never codes or values.

Part of #625.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The adapter keyed regions on region_code_oa when the household output had it,
else on the stored region. On a Microcosm local file that gave GSS codes such
as E12000007; on a national dataset, the engine's enum names such as LONDON.
Regional results published from the national dataset and passed as
prior_public to a seat release from a local file therefore matched nothing.

Regions are now always keyed on ONS GSS codes: E12000001 to E12000009 for
England's nine regions, and the country codes W92000004, S92000003 and
N92000002 for Wales, Scotland and Northern Ireland. The table is one constant,
UK_REGION_GSS_CODES. It reads GSS codes, the pseudo codes W99999999,
S99999999 and N99999999 that Microcosm writes for the nations, and the enum
names, as strings, bytes or enum members. UNKNOWN and missing values are
unassigned, so those households count in the nation but in no region. Any
other value raises, with a count and never the values.

Publishers must use the same GSS codes in parent_of and in the region cells of
prior_public; the release check now raises on codes that match nothing. The
docs set out the codes.

Part of #625.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A fixed fragment for the release check, which now raises on parent_of codes
and prior cells that match nothing instead of passing in silence, and a
changed fragment for UK regions, which the adapter now keys on ONS GSS codes
whatever the dataset stores.

Part of #625.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vahid-ahmadi

Copy link
Copy Markdown
Contributor

@juaristi22 thanks, this is my review at 4eb6dd531.

Good to merge once CI is green. One should, the rest are nits.

All three parts of #625 are fixed. On main, the issue's probe with renamed parents returns no findings. At this head it raises. The region table matches ONS, and every new check is killed by its test when I revert it.

Findings

  1. should, src/policyengine/outputs/uk_geography_disclosure.py:217. A prior cell at a misspelt level is still dropped without an error, and that is the same failure as Disclosure release: area codes that don't match skip the nesting and prior-public checks without an error #625. Release can't catch it, because cells at levels it doesn't hold rightly belong to other publications. The adapter can catch it cheaply, because it knows every valid level: national plus the keys of UK_DISCLOSURE_LEVEL_COLUMNS. Synthetic probe: a seat release where M1 is withheld, with the prior cell ("constituencies", "M1", ...). It returns the three recoverable_remainder findings and no already_public finding, with no error. With "constituency" the already_public finding appears. Suggest raising in compute_uk_geography_disclosure for any prior_public cell whose level is not one of those names. The message would give the count and the level names, and the PR body's "still not caught" list could then drop that item for the UK path.

  2. nit, src/policyengine/disclosure/release.py:292-294. _require_prior_areas merges area codes per level, across every reform and year. So a prior cell at a held (level, reform, year) passes if its area exists only under another year of that level, and then matches nothing in _known_cells. Synthetic probe: constituency estimates for 2026 (A1, A2) and 2027 (B1, B2), and the prior cell ("constituency", "B1", "reform", 2026, ...). check() returns []. That is rare in the adapter, since each run has one reform and one year. The fix is small: when the release holds the cell's (level, reform, year), check against that key's areas, and fall back to the level's merged set only otherwise.

  3. nit, the PR body and the changelog. These say prior cells "for reforms or years it doesn't hold stay allowed". In the code, which I think is the safer choice, a cell at a held level must still name an area of that level, whatever its reform and year. The docs say this correctly. One case may surprise publishers: a prior on older constituency boundaries for another year now raises, so it has to be filtered out first. A line in the body or changelog would avoid that surprise.

  4. nit, semver, a release-policy call rather than a code point. changed gives a patch bump (.github/bump_version.py:84-88). The change alters the published area_code values for regions on national datasets. It also makes parent_of and prior_public inputs in enum names, which used to be accepted, now raise. By strict semver that is breaking. Against that, the layer shipped in 6.4.0 two days ago, and the only consumer I found (the dashboard's Request HF token with getpass if not set in env #48) already uses GSS codes. Patch or minor seems defensible. I'd just make the choice deliberately, and merge before the first regional release, so nothing gets published under the old codes.

  5. nit, uk_geography_disclosure.py:200. A region level passed through area_weights= skips the normalisation, so its codes are whatever the caller gives. The docs say regions are keyed on GSS codes "whichever column the household output carries", which is true. It might be worth one sentence saying that custom weights for regions must use GSS codes too.

  6. nit, not in this diff, and the same on main. If the output carries a region_code_oa column that is entirely blank, the adapter does not fall back to region. The region level then has no areas, and check() fails with KeyError: 'outcome' from estimate.py:102. That is loud but unhelpful. Something to consider for a follow-up.

Still not caught (beyond the PR's own list)

  • A parent_of mapping that uses valid codes but is wrong still passes. For example, every seat mapped to R1 leaves R2 with no child areas, and the check runs only on R1. A parent with no children could be a warning. It can't be an error, because a seat matrix may legitimately cover only part of the parent level.
  • Extra keys in parent_of are ignored. That is harmless, because missing child areas still raise.
  • A misspelt child level in add_nesting is caught indirectly: the release still raises a blocking unchecked_overlap.

What looks right

  • The parent_of check (release.py:385) raises for renamed, padded or lower-case parent codes, and also when the seat level is reference-only. A missing child area still raises "lacks". The messages give counts and level names only. I checked that no codes appear in any of them.
  • The prior area check runs at the start of check(), so the order the estimates are added in doesn't matter, and it covers levels held only as references. Padded or lower-case codes raise. Integer area codes are refused by pydantic. A string year is coerced. An unknown outcome raises in Release().
  • UK_REGION_GSS_CODES matches ONS: E12000001 to E12000009 run North East, North West, Yorkshire and The Humber, East Midlands, West Midlands, East of England, London, South East, South West. W92000004, S92000003 and N92000002 are the country codes. Every member of the engine's Region enum is in the table (policyengine-uk 2.130.1). Enum members, decode_to_str() output, bytes, np.bytes_, padded values, None, NaN and pd.NA all behave as documented. The pseudo codes match what Microcosm writes (FRS_REGION_TO_REGION_CODE in rowwise_geography.py).
  • Raising on enum display values such as London or Yorkshire and the Humber is fine. I found no path in this package that writes them, and the failure is loud.
  • The cross-dataset test really does cross spellings: enum names on the national side, Microcosm codes on the local side.
  • No real or microdata-derived numbers in the code, tests, docs or changelog. All the test data is synthetic.

What I checked

  • Fresh worktree at 4eb6dd531 and a fresh uv venv (Python 3.13, .[uk,dev]).
  • The touched tests: 76 passed. The full suite: 2750 passed, 11 skipped. ruff format --check and ruff check (0.17.1) are clean.
  • mypy on disclosure/ and the adapter gives the same 6 union-attr errors as main, at shifted lines, so nothing new.
  • I ran the issue's probe on main and at this head: main returns [] for renamed parents, and this head raises. The new release tests also fail against main's source.
  • Mutation checks: removing each of the parent_of check, the prior area check, the outcome check, region normalisation, the unknown-value raise, enum handling, bytes decoding, the UNKNOWN mapping or the Wales pseudo code makes at least one test fail. So does skipping reference levels in the prior check, or running it at add() time.
  • Synthetic probes for every case above.
  • CI: at the time of writing, Lint, Mypy, the docs build, smoke imports and the changelog check pass, and the four Test jobs are still running.
  • Not checked: how region is stored on a real national dataset file. I used no survey data, so I relied on the passthrough code and the engine's enum.

🤖 Generated with Claude Code

Maria Juaristi and others added 3 commits October 16, 2026 11:33
A prior cell at a held (level, reform, year) must name an area of those
estimates; only a cell for a reform or year the release does not hold may
name an area of any of the level's estimates. Before, an area that only
another year held passed and then matched nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- compute_uk_geography_disclosure raises for a prior cell at a level that
  is neither a UK level nor one given in area_weights=. A release accepts
  such cells, since they may belong to other publications, so a misspelt
  level would leave a public cell counted as hidden.
- A column that gives no household an area is skipped, so an empty
  region_code_oa falls back to the stored region. A requested level whose
  columns give no household an area raises a clear error instead of a
  KeyError from the check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The docs and changelog now say a prior on older boundaries raises even
for a year the release does not hold, that the adapter refuses unknown
levels, and that region weights passed with area_weights= must use GSS
codes. An added fragment for UK_REGION_GSS_CODES makes the release a
minor one, since region codes change and enum names now raise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@juaristi22

Copy link
Copy Markdown
Contributor Author

Thanks Vahid. All six are in 863634c, d419e46 and 79f6b3a.

  1. Misspelt levels (should). compute_uk_geography_disclosure now raises for a prior_public cell whose level is neither a UK level (national or a key of UK_DISCLOSURE_LEVEL_COLUMNS) nor one given in area_weights=. The message gives the count and the level names. Your probe is a test: "constituencies" raises, and a level given through area_weights= is accepted. The body's "still not caught" item now says this is caught only on the adapter path.
  2. Per-key areas (nit). When the release holds the cell's (level, reform, year), _require_prior_areas checks against those estimates' areas. It falls back to the level's merged set only otherwise. Your two-year probe is a test, along with the fallback for a year the release doesn't hold.
  3. Wording (nit). The fixed fragment and the body now say that a cell at a held level must name one of that level's areas whatever its reform and year. They also say that a prior on older boundaries for another year now raises and has to be filtered out first. The docs say the same.
  4. Semver (nit). I went with minor (6.6.0), through an added fragment for UK_REGION_GSS_CODES, which is a new public table. Strictly the change is breaking, but the layer is two days old and its one known consumer already uses GSS codes. The body says so, along with the plan to merge before the first regional release.
  5. Custom region weights (nit). The docs now say that region weights passed with area_weights= are used as given, so they must carry GSS codes.
  6. Empty region_code_oa (nit). I fixed this here rather than in a follow-up. A column that gives no household an area is skipped, so an empty region_code_oa falls back to region. A requested level whose columns are empty raises a clear error instead of the KeyError.

I added your parent_of points to "still not caught": a parent left with no children, and the ignored extra keys.

Checks: the three touched suites (82 passed), ruff format --check, and the changelog check with all three fragments. CI runs the full suite.

…at alike

The lambda's layout differed between the locked ruff and the one CI
installs, so the format check failed in CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Disclosure release: area codes that don't match skip the nesting and prior-public checks without an error

2 participants