feat(overton): coalition coding fix + regenerate breakpoint analysis

This commit is contained in:
2026-05-31 20:11:03 +02:00
parent 7ff3fec992
commit 2d5b28fe1b
6 changed files with 596 additions and 12 deletions
@@ -16,6 +16,7 @@ Output:
from __future__ import annotations
import datetime
import json
import logging
import random
@@ -24,6 +25,9 @@ import sys
from pathlib import Path
from typing import Any
ROOT = Path(__file__).parent.parent.parent.resolve()
sys.path.insert(0, str(ROOT))
import duckdb
import matplotlib
import numpy as np
@@ -58,6 +62,9 @@ def _extremity_bucket(score: float) -> str:
CANONICAL_LEFT_SET = set(CANONICAL_LEFT)
CANONICAL_RIGHT_SET = set(CANONICAL_RIGHT)
RUTTE_IV_COALITION: set[str] = {"VVD", "D66", "CDA", "CU"}
SCHOOF_COALITION: set[str] = {"PVV", "VVD", "NSC", "BBB"}
COALITION: dict[int, set[str]] = {
2016: {"VVD", "PvdA"},
2017: {"VVD", "PvdA"},
@@ -67,18 +74,21 @@ COALITION: dict[int, set[str]] = {
2021: {"VVD", "CDA", "D66", "CU"},
2022: {"VVD", "D66", "CDA", "CU"},
2023: {"VVD", "D66", "CDA", "CU"},
2024: {"PVV", "VVD", "NSC", "BBB"},
2025: {"PVV", "VVD", "NSC", "BBB"},
2026: {"PVV", "VVD", "NSC", "BBB"},
2024: SCHOOF_COALITION,
2025: SCHOOF_COALITION,
2026: SCHOOF_COALITION,
}
SCHOOF_START_DATE = "2024-07-01"
COALITION_NOTE = (
"2016-2017: Rutte II (VVD/PvdA). "
"2018-2021: Rutte III (VVD/CDA/D66/CU). "
"2022-2023: Rutte IV (VVD/D66/CDA/CU). "
"2024-2026: Schoof (PVV/VVD/NSC/BBB). "
"2024 ambiguous: Schoof cabinet started July 2024; all 2024 motions are coded "
"to the Schoof coalition. Coalition effect may be overestimated for early 2024."
"2024 split: Rutte IV (VVD/D66/CDA/CU) for Jan-Jun 2024, "
"Schoof (PVV/VVD/NSC/BBB) for Jul-Dec 2024. "
"2025-2026: Schoof (PVV/VVD/NSC/BBB). "
"Period detection uses motion date, not just year."
)
YEAR_MIN, YEAR_MAX = 2016, 2026
@@ -114,7 +124,8 @@ def compute_yearly_rw_metrics(con: duckdb.DuckDBPyConnection) -> dict[int, dict]
r.category,
e.text_score AS extremity_score,
m.voting_results,
m.winning_margin
m.winning_margin,
m.date
FROM right_wing_motions r
JOIN extremity_scores e ON r.motion_id = e.motion_id
JOIN motions m ON r.motion_id = m.id
@@ -135,9 +146,10 @@ def compute_yearly_rw_metrics(con: duckdb.DuckDBPyConnection) -> dict[int, dict]
"categories": [],
"titles": [],
"motion_ids": [],
"dates": [],
}
for mid, year, title, cst, crs, rs, lo, cat, ext, vr_json, wm in rows:
for mid, year, title, cst, crs, rs, lo, cat, ext, vr_json, wm, motion_date in rows:
if year is None or year < YEAR_MIN or year > YEAR_MAX:
continue
yearly[year]["centrist_support_strict"].append(cst if cst is not None else np.nan)
@@ -148,6 +160,7 @@ def compute_yearly_rw_metrics(con: duckdb.DuckDBPyConnection) -> dict[int, dict]
yearly[year]["categories"].append(cat or "other")
yearly[year]["titles"].append(title or "")
yearly[year]["motion_ids"].append(mid)
yearly[year]["dates"].append(motion_date)
if vr_json is not None:
voting = json.loads(vr_json) if isinstance(vr_json, str) else vr_json
@@ -299,9 +312,9 @@ def compute_opposition_metrics(
}
coalition = COALITION
schoof_cutoff = datetime.date(2024, 7, 1)
for year, d in yearly_raw.items():
coal = coalition.get(year, set())
for idx in range(len(d["titles"])):
title = d["titles"][idx]
submitter_name, submitter_party = parse_lead_submitter(title, name_party_map)
@@ -309,6 +322,13 @@ def compute_opposition_metrics(
if submitter_party is None:
continue
motion_date = d["dates"][idx] if idx < len(d.get("dates", [])) else None
if year == 2024 and motion_date is not None:
coal = RUTTE_IV_COALITION if motion_date < schoof_cutoff else SCHOOF_COALITION
else:
coal = coalition.get(year, set())
if submitter_party in coal:
continue