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:
+24
-15
@@ -74,6 +74,29 @@ def acs_cslug(row):
|
||||
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
|
||||
ROLE_MAP = {
|
||||
"member": "member", "Chair": "chair", "Chairman": "chair",
|
||||
@@ -572,21 +595,7 @@ def main():
|
||||
acs_county = load("acs_county.jsonl")
|
||||
acs_cd = load("acs_cd.jsonl")
|
||||
crosswalk = load("place_county_crosswalk.jsonl")
|
||||
|
||||
# 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]
|
||||
place_to_cslug = place_resolver(acs_county, crosswalk)
|
||||
|
||||
bodies_by_code = {b["code"]: b for b in bodies}
|
||||
comm_codes = committee_codes(bodies)
|
||||
|
||||
@@ -11,11 +11,11 @@ source. This regenerates its two companions deterministically:
|
||||
— per-county drill-down roster.
|
||||
|
||||
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
|
||||
construction. Officials whose county can't be resolved to an ACS FIPS (e.g. the
|
||||
non-FL municipal rows still blocked on the place->county crosswalk) are counted
|
||||
in the tree but do not appear in the county-keyed viz — an honest gap, not a
|
||||
silent drop; the tally is printed at the end.
|
||||
them in the tree (shared county_slug + place_resolver), so the viz and the tree
|
||||
agree by construction. Officials whose county still can't be resolved to an ACS
|
||||
FIPS (ambiguous names, county-level rows mislabeled municipal, CT planning
|
||||
regions, label-less rows) are counted in the tree but do not appear in the
|
||||
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.
|
||||
"""
|
||||
@@ -47,6 +47,7 @@ def city_name(rec):
|
||||
def main():
|
||||
officeholders = build.load("officeholders-v3.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_to_fips = {}
|
||||
@@ -72,7 +73,7 @@ def main():
|
||||
for rec in officeholders:
|
||||
if rec["level"] not in ("county", "municipal"):
|
||||
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
|
||||
name = rec.get("full_name") or rec.get("title") or "Unknown"
|
||||
role = rec.get("title") or ""
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user