Merge pull request #3 from Influence-Tools/board-partisan-lens
Board: partisan-lean lens
This commit is contained in:
+29
-3
@@ -104,22 +104,46 @@ def main():
|
|||||||
|
|
||||||
# ---- district representation per county (fips -> {cd, ss, sh}) ----
|
# ---- district representation per county (fips -> {cd, ss, sh}) ----
|
||||||
rep_by_node = defaultdict(list) # district node id -> current rep name(s)
|
rep_by_node = defaultdict(list) # district node id -> current rep name(s)
|
||||||
|
rep_party_by_node = defaultdict(list) # district node id -> current rep party code(s)
|
||||||
for rec in officeholders:
|
for rec in officeholders:
|
||||||
if not rec.get("is_current"):
|
if not rec.get("is_current"):
|
||||||
continue
|
continue
|
||||||
nid = build.person_district_node(rec)
|
nid = build.person_district_node(rec)
|
||||||
if nid:
|
if nid:
|
||||||
rep_by_node[nid].append(rec.get("full_name") or "?")
|
rep_by_node[nid].append(rec.get("full_name") or "?")
|
||||||
|
rep_party_by_node[nid].append(rec.get("party"))
|
||||||
KEYMAP = {"CD": "cd", "SS": "ss", "SH": "sh"}
|
KEYMAP = {"CD": "cd", "SS": "ss", "SH": "sh"}
|
||||||
districts_by_fips = defaultdict(lambda: {"cd": [], "ss": [], "sh": []})
|
districts_by_fips = defaultdict(lambda: {"cd": [], "ss": [], "sh": []})
|
||||||
|
party_ct = defaultdict(lambda: {"D": 0, "R": 0}) # fips -> rep head-count by party (>=5% of county)
|
||||||
for e in sorted(build.load("county_district_edges.jsonl"),
|
for e in sorted(build.load("county_district_edges.jsonl"),
|
||||||
key=lambda x: (x["county_geoid"], build.DTYPE_RANK[x["type"]], -x["area_weight"])):
|
key=lambda x: (x["county_geoid"], build.DTYPE_RANK[x["type"]], -x["area_weight"])):
|
||||||
dt = e["type"]
|
dt = e["type"]
|
||||||
ident = build.cd_ident(e["district_label"]) if dt == "CD" else build.leg_ident(e["district_label"])
|
ident = build.cd_ident(e["district_label"]) if dt == "CD" else build.leg_ident(e["district_label"])
|
||||||
reps = sorted(set(rep_by_node.get(build.edge_node_id(e), [])))
|
node = build.edge_node_id(e)
|
||||||
districts_by_fips[build.canonical_fips(e["county_geoid"])][KEYMAP[dt]].append(
|
reps = sorted(set(rep_by_node.get(node, [])))
|
||||||
|
cf = build.canonical_fips(e["county_geoid"])
|
||||||
|
w = e["area_weight"]
|
||||||
|
dparties = set()
|
||||||
|
for p in rep_party_by_node.get(node, []):
|
||||||
|
if p in ("D", "R"):
|
||||||
|
dparties.add(p)
|
||||||
|
if w >= 0.05: # ignore sliver overlaps in the county tally
|
||||||
|
party_ct[cf][p] += 1
|
||||||
|
districts_by_fips[cf][KEYMAP[dt]].append(
|
||||||
{"d": build.district_title(dt, e["district_state"], ident),
|
{"d": build.district_title(dt, e["district_state"], ident),
|
||||||
"w": round(e["area_weight"], 3), "rep": ", ".join(reps) or None})
|
"w": round(e["area_weight"], 3), "rep": ", ".join(reps) or None,
|
||||||
|
"p": (next(iter(dparties)) if len(dparties) == 1 else None)})
|
||||||
|
|
||||||
|
# ---- partisan representation lean per county (from state + federal reps) ----
|
||||||
|
# lean = (R - D) / (R + D) by rep head-count; -1 all-D .. +1 all-R. Head-count (not
|
||||||
|
# area) so it matches the "N D · M R" readout and isn't skewed by large rural
|
||||||
|
# districts; districts covering <5% of the county are treated as slivers, excluded.
|
||||||
|
for cf, entry in county_data.items():
|
||||||
|
ct = party_ct.get(cf, {"D": 0, "R": 0})
|
||||||
|
tot = ct["D"] + ct["R"]
|
||||||
|
entry["lean"] = round((ct["R"] - ct["D"]) / tot, 3) if tot > 0 else None
|
||||||
|
entry["repD"] = ct["D"]
|
||||||
|
entry["repR"] = ct["R"]
|
||||||
|
|
||||||
# ---- county_detail.json (any county with officials OR districts) ----
|
# ---- county_detail.json (any county with officials OR districts) ----
|
||||||
fips_to_key = {cf: key for key, cf in slug_to_fips.items()}
|
fips_to_key = {cf: key for key, cf in slug_to_fips.items()}
|
||||||
@@ -165,6 +189,8 @@ def main():
|
|||||||
f"({len(districts_by_fips)} with district representation)")
|
f"({len(districts_by_fips)} with district representation)")
|
||||||
print(f"officials mapped: {total_oh - unresolved}/{total_oh} "
|
print(f"officials mapped: {total_oh - unresolved}/{total_oh} "
|
||||||
f"(county-unresolvable, tree-only: {unresolved})")
|
f"(county-unresolvable, tree-only: {unresolved})")
|
||||||
|
leaned = sum(1 for v in county_data.values() if v.get("lean") is not None)
|
||||||
|
print(f"partisan lean: {leaned}/{len(county_data)} counties have D/R representation")
|
||||||
print(f"board.html injected: totals={totals}")
|
print(f"board.html injected: totals={totals}")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+26
-6
File diff suppressed because one or more lines are too long
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