Repository navigation
viz: a region NAME is not a unique identity — two same-named regions with equal values silently merge #4548
Description
Split out of #4526, where this was handed over from local review of #4534 (PR #4536). It is a distinct question from that issue's coarse-geography denominator hole and blocks on a different decision, so it gets its own number.
Problem
viz smart keys its region-level dedupe on the owning-region cell, and is_region_concept deliberately accepts city and county NAMES (geo.city, geo.county, town, municipality) alongside the code concepts — #4529 widened it on purpose, since an event-level dataset keyed by CITY repeats its regional values exactly like one keyed by county FIPS.
A name is not unique. Two Springfield rows can be different cities in different states:
| values for the duplicated name | outcome | safe? |
|---|---|---|
| differ (Aurora CO 386,261 vs Aurora IL 180,542) | registers as a conflict; the KPI tile is omitted | yes — closed by PR #4536 |
| exactly equal | the two regions merge into one key and the total silently undercounts | no — measured 4,260,700 against a true 4,560,700 |
Not introduced by #4534: measure_by_dim_panel keys its own dedupe the same way, so this is a property of the merged #4529 design. #4534 makes it numerically visible, because that path now prints a figure where it previously printed nothing.
Why this is blocked, and on what
The obvious fix — a composite identity qualifying the name by an enclosing column — requires choosing that enclosing column. Cardinality cannot make that choice: two cardinality-only ownership rules have already shipped and both were reviewed as defects, in opposite directions (see the record on #4526). Building a third at the consumption site would re-open that oscillation.
Note also what does not work: qualifying by any other region column. A city + zip_code dataset has many zips per city, so a legitimate per-city population would be flagged ambiguous and suppressed. The qualifier has to be an enclosing column, and picking it is the open problem.
Prior art already in the tree
The choropleth path solved a closely related problem for region matching in #4481 — qualifier_spec / canonicalize_qualified / qual_raws_by_cand key aliases by (name, qualifier) because "a region spelling alone no longer identifies one feature". That mechanism is a plausible starting point, and it shows the project has already accepted that a name needs qualifying; what it does not settle is which column qualifies when nothing declares it.
Current state
Recorded as a KNOWN LIMITATION on region_deduped_totals in src/cmd/viz.rs, which currently points at #4526 — that pointer should be re-aimed here now that the halves are split. The unsafe-but-detectable half (differing values) is closed; only the silent-merge half remains.
Anchors
region_deduped_totals,RegionDedupe,is_region_concept, and the dedupe inmeasure_by_dim_panel—src/cmd/viz.rs- Sibling issues from the same split: viz: reject a dictionary
x-qsv.denominatorfrom a COARSER geography than the region key #4526 (coarse-geography denominator), viz: skip the rate panel when anx-qsv.denominatortakes ONE distinct value across >=2 regions #4547 (degenerate single-value denominator)
Activity
describegpt: propose a per-field UCUM `x-qsv.unit` for numeric measur…
jqnatividad commentedon Sep 9, 2026
Investigation
The claim checks out, but three things change the shape of this issue.
1. The exposed surface is narrower than the issue implies
is_region_conceptacceptsREGION_CODE_LEAVES ∪ CITY_NAME_LEAVES. Of those, only these can carry a non-unique value:geo.city/geo.town/geo.municipality— always names, ambiguous nationally.geo.county— ambiguous only when it holds names.COUNTY_NAME_LEAVES' own doc notes a county-tagged column "may hold either county FIPS CODES or county NAMES", told apart by VALUE SHAPE at resolution time.Not exposed:
zip_code/zip/postal_code/zcta/county_fips/state_fips/place_fips/fips(nationally unique codes), andstate/country(unique as code or name). Minor caveat: a barecensus_tractnumber is not nationally unique — a full 11-digit GEOID is. Low priority, since a tract column is an unlikely KPI owning region.2. The blocking premise is already half-answered in the tree
The issue says the fix requires choosing the enclosing column, and that cardinality cannot choose. The second half is right — two cardinality rules shipped and both were reviewed as defects, in opposite directions. But the tree already chose, and not from cardinality:
STATE_LEAVES = ["state", "state_fips", "state_code"]— the choice is made by concept. The function is unconditional (no#[cfg(feature = "geocode")]), takes onlycol_sems, andcol_semsis already in scope at bothowning_region_idxcall sites (viz.rs:17877 and 35250).QualifierSpec::StateFipsthen normalizes viastate_fips_for_any, soPA,42andPennsylvaniaall yield"42".So a
(region cell, state FIPS)composite identity needs zero new selection rules — it reuses the qualifier #4481 already picked for this same ambiguity, at a second consumption site. That is precisely what keeps it out of the cardinality oscillation this issue is worried about.Useful property: qualifying is a no-op when the owning column holds codes (
42125keys one region whatever the state cell says), so there is no need to shape-detect names vs FIPS — it could apply unconditionally.3. Two traps that would make a clean-looking fix spawn a second review round
(a) Do not reuse
RegionDedupe::conflictfor an "identity may be ambiguous" signal.measure_by_dim_paneltreatsconflictas abandon the collapse → fall back to the ordinary row-wise verb. Setting it on an ambiguity signal converts a silent undercount into an inflated total — the same shape roborev 4542 killed on the KPI side. A detection-only variant needs its own field that the KPI caller reads andmeasure_by_dim_paneldoes not.(b) The blank qualifier cell is the real open question. A blank state cell yields qualifier
""while its siblings yield"42", splitting one region into two keys and double-counting it — the opposite error from the one being fixed. This has to be decided explicitly rather than left to fall out.RegionDedupe::unattributed_rowsis the natural home, noting the settled asymmetry that blanks are reported beside the tile, not carried at full weight (that rule belongs to the grouped bar only).Residual hole after a state-qualifier fix
A name-keyed dataset with no enclosing region column at all, holding two same-named regions with exactly equal values. Undetectable from the data — nothing in the file distinguishes the rows. That is a genuine permanent limitation, and a more accurate subject for the
KNOWN LIMITATIONcomment than "the whole question is open".On frequency
The headline figure (4,260,700 vs a true 4,560,700) is synthetic. The real Aurora values differ (CO 386,261 vs IL 180,542), which is the already-closed safe case. Two same-named places with exactly equal
measure.populationis rare; roundedmeasure.areais the more plausible collision,REGION_LEVEL_CONCEPTSbeing exactly those two. The merge is real and silent, so this is not a phantom — but it is not a live data-corruption bug either. Worth fixing on merit, not urgency.Two corrections to the issue body
KNOWN LIMITATIONpointer was already re-aimed from viz: reject a dictionaryx-qsv.denominatorfrom a COARSER geography than the region key #4526 to viz: a region NAME is not a unique identity — two same-named regions with equal values silently merge #4548 in3aa7ad678; no edit is needed there.smart_state_columnavenue, the blank-qualifier open question, and the "must not read an ambiguity flag" constraint onmeasure_by_dim_panel, so the next reader does not re-derive them.If a test is written:
geo.county_namefailsis_region_concept— assert ongeo.county, and mutation-test the assertion.jqnatividad commentedon Sep 9, 2026
Recommendation: defer — no code change, keep this open
Following on from the investigation above. The merge is real, but every available fix costs more than it buys on realistic inputs.
The asymmetry that decides it
There are two viable fix shapes and neither is free:
unattributed_rowsBlank state cells are far more common in real data than two same-named regions carrying exactly equal values. So the unconditional version probably makes aggregate accuracy worse, and the careful version is a three-part change guarding a narrow conjunction (name-keyed owning region and two same-named regions in one file and exactly equal values).
Meanwhile the dangerous half is already closed: when the values DIFFER,
conflictfires and the tile is omitted (#4536). What remains is a silent undercount in the rare exactly-equal case.Exit condition
Deferring is only defensible with a trigger attached, so: reopen the moment a real dataset or a gallery fixture publishes a materially wrong number from this path. That evidence would also settle which of the two shapes to build, which is currently the thing no one can decide from first principles.
If closing is preferred over deferring
The smallest safe change is detection-only, not composite identity: if the owning column is name-valued and an enclosing-concept column varies (as distinct from being blank) within a name key, mark the identity ambiguous and omit the tile. It produces no new arithmetic and reuses the settled omit disposition, so it cannot introduce an inflation path.
Caveats if that route is taken:
RegionDedupefield, neverconflict—measure_by_dim_panelreadsconflictas abandon-the-collapse and would fall back to an inflated row-wise total.One correction to the investigation comment above
looks_like_county_names(viz.rs:5688) covers county names only. The exposed surface isgeo.city/town/municipalityplus name-valuedgeo.county, so that helper is not a drop-in shape test for a name-valued-only variant.viz(gallery): add a real-geography denominator-aware rate map