Board: surface the newly-nested municipals in county drill-downs

build_viz.py was resolving officials to counties without the place→county
crosswalk, so only FL's 67 counties had rosters. Factor the resolver out of
build.py into a shared build.place_resolver() and use it in both, so the
Board and the tree agree by construction.

- scripts/build.py — extract place_resolver(); main() now calls it (tree
  output byte-identical, refactor is behavior-preserving)
- scripts/build_viz.py — resolve via the shared resolver
- viz/county_data.json, viz/county_detail.json — regenerated

County drill-downs: 67 → 1049 counties with rosters; officials mapped
5184/5226 (42 county-unresolvable, tree-only). Deterministic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Fabio
2026-07-04 20:36:42 -04:00
parent 199d8b37d3
commit 662ccecb26
4 changed files with 33 additions and 23 deletions
+24 -15
View File
@@ -74,6 +74,29 @@ def acs_cslug(row):
return slugify(norm_county(re.sub(r"\s+County$", "", base))) return slugify(norm_county(re.sub(r"\s+County$", "", base)))
def place_resolver(acs_county, crosswalk):
"""(normalized place name, state) -> county dir slug, from the crosswalk.
county_geoid is the stable key; the slug is taken from the ACS county node
(the same source that names county dirs) so municipals nest correctly. Only
unique name matches whose county has an ACS node are included — ambiguous
names, label-less rows, and counties with no ACS node fall through.
Shared by build.py and build_viz.py so the tree and the Board agree.
"""
fips_to_cslug = {canonical_fips(r["county_fips"]): acs_cslug(r) for r in acs_county}
geoids = {}
for x in crosswalk:
geoids.setdefault(
(norm_place(x["place_name"]), x["state_code"]), set()).add(x["county_geoid"])
out = {}
for key, geos in geoids.items():
if len(geos) == 1:
cf = canonical_fips(next(iter(geos)))
if cf in fips_to_cslug:
out[key] = fips_to_cslug[cf]
return out
# committee_title (source) -> normalized role on the committee # committee_title (source) -> normalized role on the committee
ROLE_MAP = { ROLE_MAP = {
"member": "member", "Chair": "chair", "Chairman": "chair", "member": "member", "Chair": "chair", "Chairman": "chair",
@@ -572,21 +595,7 @@ def main():
acs_county = load("acs_county.jsonl") acs_county = load("acs_county.jsonl")
acs_cd = load("acs_cd.jsonl") acs_cd = load("acs_cd.jsonl")
crosswalk = load("place_county_crosswalk.jsonl") crosswalk = load("place_county_crosswalk.jsonl")
place_to_cslug = place_resolver(acs_county, crosswalk)
# place -> county resolver: (normalized place name, state) -> county dir slug.
# county_geoid is the stable key; the slug is taken from the ACS county node
# (the same source that names county dirs) so municipals nest correctly.
fips_to_cslug = {canonical_fips(r["county_fips"]): acs_cslug(r) for r in acs_county}
_place_geoids = {}
for x in crosswalk:
_place_geoids.setdefault(
(norm_place(x["place_name"]), x["state_code"]), set()).add(x["county_geoid"])
place_to_cslug = {}
for key, geos in _place_geoids.items():
if len(geos) == 1:
cf = canonical_fips(next(iter(geos)))
if cf in fips_to_cslug:
place_to_cslug[key] = fips_to_cslug[cf]
bodies_by_code = {b["code"]: b for b in bodies} bodies_by_code = {b["code"]: b for b in bodies}
comm_codes = committee_codes(bodies) comm_codes = committee_codes(bodies)
+7 -6
View File
@@ -11,11 +11,11 @@ source. This regenerates its two companions deterministically:
— per-county drill-down roster. — per-county drill-down roster.
County/municipal officials are mapped to a county the same way build.py places County/municipal officials are mapped to a county the same way build.py places
them in the tree (county_slug / city helpers), so the viz and the tree agree by them in the tree (shared county_slug + place_resolver), so the viz and the tree
construction. Officials whose county can't be resolved to an ACS FIPS (e.g. the agree by construction. Officials whose county still can't be resolved to an ACS
non-FL municipal rows still blocked on the place->county crosswalk) are counted FIPS (ambiguous names, county-level rows mislabeled municipal, CT planning
in the tree but do not appear in the county-keyed viz — an honest gap, not a regions, label-less rows) are counted in the tree but do not appear in the
silent drop; the tally is printed at the end. county-keyed viz — an honest gap, not a silent drop; the tally is printed.
Deterministic: sorted iteration, fixed key order. Two runs are byte-identical. Deterministic: sorted iteration, fixed key order. Two runs are byte-identical.
""" """
@@ -47,6 +47,7 @@ def city_name(rec):
def main(): def main():
officeholders = build.load("officeholders-v3.jsonl") officeholders = build.load("officeholders-v3.jsonl")
acs_county = build.load("acs_county.jsonl") acs_county = build.load("acs_county.jsonl")
place_to_cslug = build.place_resolver(acs_county, build.load("place_county_crosswalk.jsonl"))
# slug -> fips, and fips -> (name, state, demographics), deduped by fips # slug -> fips, and fips -> (name, state, demographics), deduped by fips
slug_to_fips = {} slug_to_fips = {}
@@ -72,7 +73,7 @@ def main():
for rec in officeholders: for rec in officeholders:
if rec["level"] not in ("county", "municipal"): if rec["level"] not in ("county", "municipal"):
continue continue
key = ((rec.get("state_abbr") or "").lower(), build.county_slug(rec)) key = ((rec.get("state_abbr") or "").lower(), build.county_slug(rec, place_to_cslug))
oh_count[key] += 1 oh_count[key] += 1
name = rec.get("full_name") or rec.get("title") or "Unknown" name = rec.get("full_name") or rec.get("title") or "Unknown"
role = rec.get("title") or "" role = rec.get("title") or ""
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long