fix(overton): strict centrist definition + left support analysis
- Reclassified centrist to {D66, CDA, CU, NSC} — removing VVD/BBB
which are center-right coalition partners
- Added centrist_support_strict (0.251→0.507, d=+0.65), center_right_support,
and left_support_mp columns via migration script
- Figure 1 now shows center-right (VVD/BBB) support as orange dashed line
- New Figure 3: bar chart of left-party support for right-wing motions
(0.268→0.202, left opposition hardened)
- New report Section 6 covering left-wing support trends
- All analysis now uses strict centrist definition throughout
This commit is contained in:
@@ -1,15 +1,32 @@
|
||||
"""Add MP-weighted centrist_support column to right_wing_motions.
|
||||
"""Add MP-weighted support columns to right_wing_motions.
|
||||
|
||||
The existing centrist_support is party-bloc-level (fraction of centrist
|
||||
parties where >=50% of MPs voted voor). This adds centrist_support_mp which
|
||||
is the fraction of individual centrist MPs who voted voor, weighted by party
|
||||
size.
|
||||
Adds centrist_support_mp, centrist_support_strict, center_right_support,
|
||||
and left_support_mp — all computed as the fraction of individual MPs
|
||||
within each party set who voted 'voor'.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import duckdb
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).parent.parent.parent.resolve()
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
import duckdb
|
||||
|
||||
from analysis.config import CANONICAL_LEFT
|
||||
|
||||
CANONICAL_CENTRIST = frozenset({"VVD", "D66", "CDA", "NSC", "BBB", "CU"})
|
||||
CANONICAL_CENTRIST_STRICT = frozenset({"D66", "CDA", "CU", "NSC"})
|
||||
CANONICAL_CENTER_RIGHT = frozenset({"VVD", "BBB"})
|
||||
|
||||
COLUMNS = [
|
||||
("centrist_support_mp", CANONICAL_CENTRIST),
|
||||
("centrist_support_strict", CANONICAL_CENTRIST_STRICT),
|
||||
("center_right_support", CANONICAL_CENTER_RIGHT),
|
||||
("left_support_mp", CANONICAL_LEFT),
|
||||
]
|
||||
|
||||
|
||||
def compute_mp_support(
|
||||
@@ -51,16 +68,18 @@ def main(db_path: str = "data/motions.db"):
|
||||
pv = mv.setdefault(party, {"voor": 0, "tegen": 0, "afwezig": 0})
|
||||
pv[vote] = pv.get(vote, 0) + n
|
||||
|
||||
# Add column
|
||||
# Add columns if missing
|
||||
for col_name, _party_set in COLUMNS:
|
||||
col_check = con.execute(
|
||||
"SELECT column_name FROM information_schema.columns "
|
||||
"WHERE table_name = 'right_wing_motions' AND column_name = 'centrist_support_mp'"
|
||||
"WHERE table_name = 'right_wing_motions' AND column_name = ?",
|
||||
[col_name],
|
||||
).fetchone()
|
||||
if col_check is None:
|
||||
con.execute(
|
||||
"ALTER TABLE right_wing_motions ADD COLUMN centrist_support_mp DOUBLE"
|
||||
f"ALTER TABLE right_wing_motions ADD COLUMN {col_name} DOUBLE"
|
||||
)
|
||||
print("Added centrist_support_mp column")
|
||||
print(f"Added {col_name} column")
|
||||
|
||||
# Update rows
|
||||
rows = con.execute(
|
||||
@@ -74,10 +93,11 @@ def main(db_path: str = "data/motions.db"):
|
||||
if votes is None:
|
||||
skipped += 1
|
||||
continue
|
||||
cs_mp = compute_mp_support(votes, CANONICAL_CENTRIST)
|
||||
for col_name, party_set in COLUMNS:
|
||||
val = compute_mp_support(votes, party_set)
|
||||
con.execute(
|
||||
"UPDATE right_wing_motions SET centrist_support_mp = ? WHERE motion_id = ?",
|
||||
[cs_mp, motion_id],
|
||||
f"UPDATE right_wing_motions SET {col_name} = ? WHERE motion_id = ?",
|
||||
[val, motion_id],
|
||||
)
|
||||
updated += 1
|
||||
|
||||
|
||||
@@ -100,7 +100,8 @@ def compute_yearly_rw_metrics(con: duckdb.DuckDBPyConnection) -> dict[int, dict]
|
||||
r.motion_id,
|
||||
r.year,
|
||||
r.title,
|
||||
r.centrist_support,
|
||||
r.centrist_support_strict,
|
||||
r.center_right_support,
|
||||
r.right_support,
|
||||
r.left_opposition,
|
||||
r.category,
|
||||
@@ -118,7 +119,8 @@ def compute_yearly_rw_metrics(con: duckdb.DuckDBPyConnection) -> dict[int, dict]
|
||||
yearly: dict[int, dict[str, Any]] = {}
|
||||
for year in range(YEAR_MIN, YEAR_MAX + 1):
|
||||
yearly[year] = {
|
||||
"centrist_support": [],
|
||||
"centrist_support_strict": [],
|
||||
"center_right_support": [],
|
||||
"right_support": [],
|
||||
"left_opposition": [],
|
||||
"extremity": [],
|
||||
@@ -128,10 +130,11 @@ def compute_yearly_rw_metrics(con: duckdb.DuckDBPyConnection) -> dict[int, dict]
|
||||
"motion_ids": [],
|
||||
}
|
||||
|
||||
for mid, year, title, cs, rs, lo, cat, ext, vr_json, wm in rows:
|
||||
for mid, year, title, cst, crs, rs, lo, cat, ext, vr_json, wm in rows:
|
||||
if year is None or year < YEAR_MIN or year > YEAR_MAX:
|
||||
continue
|
||||
yearly[year]["centrist_support"].append(cs if cs is not None else np.nan)
|
||||
yearly[year]["centrist_support_strict"].append(cst if cst is not None else np.nan)
|
||||
yearly[year]["center_right_support"].append(crs if crs is not None else np.nan)
|
||||
yearly[year]["right_support"].append(rs if rs is not None else np.nan)
|
||||
yearly[year]["left_opposition"].append(lo if lo is not None else np.nan)
|
||||
yearly[year]["extremity"].append(ext if ext is not None else np.nan)
|
||||
@@ -286,7 +289,7 @@ def compute_opposition_metrics(
|
||||
opp: dict[int, dict[str, list]] = {}
|
||||
for year in range(YEAR_MIN, YEAR_MAX + 1):
|
||||
opp[year] = {
|
||||
"centrist_support": [],
|
||||
"centrist_support_strict": [],
|
||||
"extremity": [],
|
||||
"passed": [],
|
||||
"n": 0,
|
||||
@@ -310,7 +313,7 @@ def compute_opposition_metrics(
|
||||
if submitter_party in coal:
|
||||
continue
|
||||
|
||||
opp[year]["centrist_support"].append(d["centrist_support"][idx])
|
||||
opp[year]["centrist_support_strict"].append(d["centrist_support_strict"][idx])
|
||||
opp[year]["extremity"].append(d["extremity"][idx])
|
||||
opp[year]["passed"].append(d["passed"][idx])
|
||||
opp[year]["n"] += 1
|
||||
@@ -326,14 +329,14 @@ def compute_domain_metrics(
|
||||
non_mig: dict[int, dict[str, list]] = {}
|
||||
|
||||
for year in range(YEAR_MIN, YEAR_MAX + 1):
|
||||
mig[year] = {"centrist_support": [], "extremity": [], "passed": [], "n": 0}
|
||||
non_mig[year] = {"centrist_support": [], "extremity": [], "passed": [], "n": 0}
|
||||
mig[year] = {"centrist_support_strict": [], "extremity": [], "passed": [], "n": 0}
|
||||
non_mig[year] = {"centrist_support_strict": [], "extremity": [], "passed": [], "n": 0}
|
||||
|
||||
for year, d in yearly_raw.items():
|
||||
for idx in range(len(d["titles"])):
|
||||
cat = d["categories"][idx]
|
||||
target = mig if cat == "asiel/vreemdelingen" else non_mig
|
||||
target[year]["centrist_support"].append(d["centrist_support"][idx])
|
||||
target[year]["centrist_support_strict"].append(d["centrist_support_strict"][idx])
|
||||
target[year]["extremity"].append(d["extremity"][idx])
|
||||
target[year]["passed"].append(d["passed"][idx])
|
||||
target[year]["n"] += 1
|
||||
@@ -361,7 +364,7 @@ def compute_extremity_stratified(
|
||||
period = "pre-2024" if year < BREAK_YEAR else "post-2024"
|
||||
for idx in range(len(d["titles"])):
|
||||
ext = d["extremity"][idx]
|
||||
cs = d["centrist_support"][idx]
|
||||
cs = d["centrist_support_strict"][idx]
|
||||
if np.isnan(ext) or cs is None or (isinstance(cs, float) and np.isnan(cs)):
|
||||
continue
|
||||
if ext < 2:
|
||||
@@ -377,17 +380,33 @@ def compute_extremity_stratified(
|
||||
return pre_post
|
||||
|
||||
|
||||
def compute_left_support_yearly(con: duckdb.DuckDBPyConnection) -> dict[int, dict]:
|
||||
"""Query left_support_mp yearly averages from right_wing_motions."""
|
||||
rows = con.execute("""
|
||||
SELECT year, AVG(left_support_mp), COUNT(*)
|
||||
FROM right_wing_motions
|
||||
WHERE classified = TRUE AND left_support_mp IS NOT NULL
|
||||
GROUP BY year ORDER BY year
|
||||
""").fetchall()
|
||||
|
||||
result: dict[int, dict] = {}
|
||||
for year, avg, n in rows:
|
||||
year = int(year)
|
||||
result[year] = {"mean_left_support": avg, "n": n}
|
||||
return result
|
||||
|
||||
|
||||
def yearly_summary(yearly: dict[int, dict]) -> dict[int, dict]:
|
||||
"""Compute mean values from raw lists."""
|
||||
summary: dict[int, dict] = {}
|
||||
for year, d in yearly.items():
|
||||
s: dict[str, Any] = {}
|
||||
for key in ["centrist_support", "right_support", "left_opposition", "extremity"]:
|
||||
for key in ["centrist_support_strict", "center_right_support", "right_support", "left_opposition", "extremity"]:
|
||||
vals = [v for v in d.get(key, []) if not (isinstance(v, float) and np.isnan(v))]
|
||||
s[f"mean_{key}"] = np.mean(vals) if vals else float("nan")
|
||||
passes = [p for p in d.get("passed", []) if p is not None]
|
||||
s["pass_rate"] = sum(passes) / len(passes) if passes else float("nan")
|
||||
s["n"] = len(d.get("motion_ids", d.get("centrist_support", [])))
|
||||
s["n"] = len(d.get("motion_ids", d.get("centrist_support_strict", [])))
|
||||
summary[year] = s
|
||||
return summary
|
||||
|
||||
@@ -486,16 +505,18 @@ def create_figure_1(
|
||||
colour_non_mig = "#4CAF50"
|
||||
colour_baseline = "#9E9E9E"
|
||||
|
||||
ax.plot(years_arr, _vals(yearly_sum, "mean_centrist_support"),
|
||||
ax.plot(years_arr, _vals(yearly_sum, "mean_centrist_support_strict"),
|
||||
marker="o", color=colour_rw, linewidth=2, label="All right-wing", zorder=5)
|
||||
ax.plot(years_arr, _vals(opp_sum, "mean_centrist_support"),
|
||||
ax.plot(years_arr, _vals(opp_sum, "mean_centrist_support_strict"),
|
||||
marker="s", color=colour_opp, linewidth=1.5, linestyle="--", label="Opposition-only", zorder=4)
|
||||
ax.plot(years_arr, _vals(mig_sum, "mean_centrist_support"),
|
||||
ax.plot(years_arr, _vals(mig_sum, "mean_centrist_support_strict"),
|
||||
marker="^", color=colour_mig, linewidth=1.5, linestyle=":", label="Migration", zorder=3)
|
||||
ax.plot(years_arr, _vals(non_mig_sum, "mean_centrist_support"),
|
||||
ax.plot(years_arr, _vals(non_mig_sum, "mean_centrist_support_strict"),
|
||||
marker="v", color=colour_non_mig, linewidth=1.5, linestyle="-.", label="Non-migration", zorder=2)
|
||||
ax.plot(years_arr, _vals(baseline_sum, "mean_centrist_support"),
|
||||
color=colour_baseline, linewidth=1, linestyle="dashed", alpha=0.7, zorder=1, label="All motions (baseline)")
|
||||
ax.plot(years_arr, _vals(yearly_sum, "mean_center_right_support"),
|
||||
marker="D", color="#FF8F00", linewidth=1.5, linestyle="--", label="Center-right (VVD/BBB)", zorder=3)
|
||||
|
||||
ax.axvline(x=BREAK_YEAR - 0.5, color="black", linestyle=":", alpha=0.5, linewidth=1)
|
||||
ax.annotate("2024", xy=(BREAK_YEAR - 0.3, ax.get_ylim()[1] * 0.95 if ax.get_ylim()[1] > 0 else 0.95),
|
||||
@@ -506,8 +527,8 @@ def create_figure_1(
|
||||
bbox=dict(boxstyle="round", facecolor="white", alpha=0.8))
|
||||
|
||||
ax.set_xlabel("Year")
|
||||
ax.set_ylabel("Centrist support (fraction of parties)")
|
||||
ax.set_title("Centrist Support for Right-Wing Motions Over Time", fontweight="bold")
|
||||
ax.set_ylabel("Centrist support (strict — fraction of parties)")
|
||||
ax.set_title("Centrist Support (Strict) for Right-Wing Motions Over Time", fontweight="bold")
|
||||
ax.legend(loc="lower right", fontsize=8, ncol=2)
|
||||
ax.set_ylim(0, 1.05)
|
||||
ax.grid(True, alpha=0.3)
|
||||
@@ -594,10 +615,10 @@ def create_figure_2(
|
||||
|
||||
pre_means_a = np.array(pre_means)
|
||||
post_means_a = np.array(post_means)
|
||||
pre_lower = pre_means_a - np.array(pre_p25s)
|
||||
pre_upper = np.array(pre_p75s) - pre_means_a
|
||||
post_lower = post_means_a - np.array(post_p25s)
|
||||
post_upper = np.array(post_p75s) - post_means_a
|
||||
pre_lower = np.maximum(pre_means_a - np.array(pre_p25s), 0)
|
||||
pre_upper = np.maximum(np.array(pre_p75s) - pre_means_a, 0)
|
||||
post_lower = np.maximum(post_means_a - np.array(post_p25s), 0)
|
||||
post_upper = np.maximum(np.array(post_p75s) - post_means_a, 0)
|
||||
pre_yerr = np.vstack([pre_lower, pre_upper])
|
||||
post_yerr = np.vstack([post_lower, post_upper])
|
||||
|
||||
@@ -616,7 +637,7 @@ def create_figure_2(
|
||||
f"N={n}", ha="center", va="bottom", fontsize=8, fontweight="bold")
|
||||
|
||||
overall_cs_mean = np.average(
|
||||
_vals(yearly_sum, "mean_centrist_support"),
|
||||
_vals(yearly_sum, "mean_centrist_support_strict"),
|
||||
weights=_vals(yearly_sum, "n"),
|
||||
)
|
||||
ax2.axhline(y=overall_cs_mean, color="grey", linestyle="--", alpha=0.7, linewidth=1,
|
||||
@@ -638,6 +659,47 @@ def create_figure_2(
|
||||
return path
|
||||
|
||||
|
||||
def create_figure_3(
|
||||
left_yearly: dict[int, dict],
|
||||
) -> str:
|
||||
"""Figure 3: Left-party support for right-wing motions (bar chart)."""
|
||||
years = sorted(left_yearly.keys())
|
||||
years_arr = np.array(years)
|
||||
|
||||
means = np.array([left_yearly[y]["mean_left_support"] for y in years])
|
||||
ns = np.array([left_yearly[y]["n"] for y in years])
|
||||
|
||||
# Weighted all-years mean
|
||||
overall_mean = np.average(means, weights=ns) if ns.sum() > 0 else 0.0
|
||||
|
||||
fig, ax = plt.subplots(figsize=(12, 6))
|
||||
bars = ax.bar(years_arr, means, color="#1565C0", edgecolor="white", alpha=0.9)
|
||||
for bar, n in zip(bars, ns):
|
||||
ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.005,
|
||||
f"N={int(n)}", ha="center", va="bottom", fontsize=8)
|
||||
|
||||
ax.axhline(y=overall_mean, color="#D32F2F", linestyle="--", alpha=0.8, linewidth=1,
|
||||
label=f"Weighted mean ({overall_mean:.3f})")
|
||||
ax.axvline(x=BREAK_YEAR - 0.5, color="black", linestyle=":", alpha=0.5, linewidth=1)
|
||||
ax.annotate("2024", xy=(BREAK_YEAR - 0.3, ax.get_ylim()[1] * 0.95),
|
||||
fontsize=9, color="black", alpha=0.7)
|
||||
|
||||
ax.set_xlabel("Year")
|
||||
ax.set_ylabel("Mean left_support_mp")
|
||||
ax.set_title("Left-wing party support for right-wing motions", fontweight="bold")
|
||||
ax.legend(fontsize=9)
|
||||
ax.set_xticks(years_arr)
|
||||
ax.set_xticklabels([str(y) for y in years], rotation=45)
|
||||
ax.grid(True, alpha=0.3, axis="y")
|
||||
|
||||
plt.tight_layout()
|
||||
path = str(REPORTS_DIR / "breakpoint_figure_3.png")
|
||||
fig.savefig(path, dpi=150, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
logger.info("Saved Figure 3 to %s", path)
|
||||
return path
|
||||
|
||||
|
||||
def generate_report(
|
||||
yearly_sum: dict[int, dict],
|
||||
opp_sum: dict[int, dict],
|
||||
@@ -647,8 +709,10 @@ def generate_report(
|
||||
ext_stratified: dict[str, dict[str, list]],
|
||||
yearly_raw: dict[int, dict],
|
||||
opp_raw: dict[int, dict],
|
||||
left_yearly: dict[int, dict],
|
||||
fig1_path: str,
|
||||
fig2_path: str,
|
||||
fig3_path: str,
|
||||
audit_sample: list[dict],
|
||||
audit_notes: str = "",
|
||||
) -> str:
|
||||
@@ -674,8 +738,8 @@ def generate_report(
|
||||
opp_post_ext = []
|
||||
|
||||
for y, d in yearly_raw.items():
|
||||
for idx in range(len(d.get("centrist_support", []))):
|
||||
cs = d["centrist_support"][idx]
|
||||
for idx in range(len(d.get("centrist_support_strict", []))):
|
||||
cs = d["centrist_support_strict"][idx]
|
||||
ext = d["extremity"][idx]
|
||||
if not (isinstance(cs, float) and np.isnan(cs)):
|
||||
if y < BREAK_YEAR:
|
||||
@@ -689,8 +753,8 @@ def generate_report(
|
||||
rw_post_ext.append(ext)
|
||||
|
||||
for y, d in opp_raw.items():
|
||||
for idx in range(len(d.get("centrist_support", []))):
|
||||
cs = d["centrist_support"][idx]
|
||||
for idx in range(len(d.get("centrist_support_strict", []))):
|
||||
cs = d["centrist_support_strict"][idx]
|
||||
ext = d["extremity"][idx]
|
||||
if not (isinstance(cs, float) and np.isnan(cs)):
|
||||
if y < BREAK_YEAR:
|
||||
@@ -710,11 +774,11 @@ def generate_report(
|
||||
d_opp_ext = cohens_d(np.array(opp_pre_ext), np.array(opp_post_ext)) if opp_pre_ext and opp_post_ext else float("nan")
|
||||
|
||||
# Yearly summary table
|
||||
yearly_table = "| Year | N (RW) | Centrist Support | Extremity | Right Support | Left Opp. |\n"
|
||||
yearly_table += "|------|--------|-----------------|-----------|---------------|----------|\n"
|
||||
yearly_table = "| Year | N (RW) | Centrist Support (Strict) | Extremity | Right Support | Left Opp. |\n"
|
||||
yearly_table += "|------|--------|---------------------------|-----------|---------------|----------|\n"
|
||||
for y in years:
|
||||
n = _val(yearly_sum, y, "n")
|
||||
cs = _val(yearly_sum, y, "mean_centrist_support")
|
||||
cs = _val(yearly_sum, y, "mean_centrist_support_strict")
|
||||
ext = _val(yearly_sum, y, "mean_extremity")
|
||||
rs = _val(yearly_sum, y, "mean_right_support")
|
||||
lo = _val(yearly_sum, y, "mean_left_opposition")
|
||||
@@ -817,8 +881,8 @@ def generate_report(
|
||||
]
|
||||
|
||||
for domain_name, domain_sum in [("Migration", mig_sum), ("Non-migration", non_mig_sum)]:
|
||||
pre_cs = np.nanmean([_val(domain_sum, y, "mean_centrist_support") for y in pre_years])
|
||||
post_cs = np.nanmean([_val(domain_sum, y, "mean_centrist_support") for y in post_years])
|
||||
pre_cs = np.nanmean([_val(domain_sum, y, "mean_centrist_support_strict") for y in pre_years])
|
||||
post_cs = np.nanmean([_val(domain_sum, y, "mean_centrist_support_strict") for y in post_years])
|
||||
lines.append(
|
||||
f"| {domain_name} | {pre_cs:.3f} | {post_cs:.3f} | {post_cs - pre_cs:+.3f} |"
|
||||
)
|
||||
@@ -835,19 +899,55 @@ def generate_report(
|
||||
"If centrist support rose uniformly across all buckets, the shift is about volume",
|
||||
"(more motions) rather than tolerance. If only the 1-2 bucket rose, right-wing",
|
||||
"parties filed milder motions post-2024 and the 'shift' is illusory.",
|
||||
]
|
||||
|
||||
# Section 6: Left support for right-wing motions
|
||||
left_years_sorted = sorted(left_yearly.keys())
|
||||
left_pre_years_list = [y for y in pre_years if y in left_yearly]
|
||||
left_post_years_list = [y for y in post_years if y in left_yearly]
|
||||
|
||||
left_pre_vals = [left_yearly[y]["mean_left_support"] for y in left_pre_years_list]
|
||||
left_post_vals = [left_yearly[y]["mean_left_support"] for y in left_post_years_list]
|
||||
left_pre_mean = np.mean(left_pre_vals) if left_pre_vals else float("nan")
|
||||
left_post_mean = np.mean(left_post_vals) if left_post_vals else float("nan")
|
||||
left_delta = left_post_mean - left_pre_mean
|
||||
|
||||
left_table = "| Year | N | Mean left_support_mp |\n"
|
||||
left_table += "|------|---|---------------------|\n"
|
||||
for y in left_years_sorted:
|
||||
ls = left_yearly[y]["mean_left_support"]
|
||||
n = left_yearly[y]["n"]
|
||||
left_table += f"| {y} | {int(n)} | {ls:.4f} |\n"
|
||||
|
||||
lines += [
|
||||
"",
|
||||
"## 6. Manual Extremity Audit",
|
||||
"## 6. Left-wing support for right-wing motions",
|
||||
"",
|
||||
left_table,
|
||||
"",
|
||||
f"| Metric | Pre-2024 Mean | Post-2024 Mean | Δ |",
|
||||
f"|--------|--------------|---------------|-----|",
|
||||
f"| Left Support (MP) | {left_pre_mean:.4f} | {left_post_mean:.4f} | {left_delta:+.4f} |",
|
||||
"",
|
||||
f"**Interpretation:** Left parties moved from {left_pre_mean:.1%} to {left_post_mean:.1%} "
|
||||
f"support — a {abs(left_delta):.1f} point shift. "
|
||||
"Whether this represents leftward Overton expansion depends on whether left parties "
|
||||
"are tolerating or actively supporting right-wing positions.",
|
||||
"",
|
||||
f".name})",
|
||||
"",
|
||||
"## 7. Manual Extremity Audit",
|
||||
"",
|
||||
audit_notes,
|
||||
"",
|
||||
audit_table,
|
||||
"",
|
||||
"## 7. Limitations",
|
||||
"## 8. Limitations",
|
||||
"",
|
||||
"- **Small-N time series:** 8 pre-2024 years and at most 3 post-2024 years (2026 is partial).",
|
||||
" Effect sizes are descriptive, not confirmatory.",
|
||||
"- **LLM extremity scores:** Content-based, not independently validated beyond the",
|
||||
" manual audit above. See §6 for agreement rate and noted biases.",
|
||||
" manual audit above. See §7 for agreement rate and noted biases.",
|
||||
"- **Coalition composition:** Hardcoded per year. 2024 is ambiguous (Rutte IV until July,",
|
||||
" Schoof thereafter). Early 2024 motions may be miscoded as Schoof-era.",
|
||||
"- **Submitter party identification:** Parsed from motion title prefixes (e.g.,",
|
||||
@@ -856,12 +956,13 @@ def generate_report(
|
||||
"- **Keyword penetration not analyzed:** The right-wing keyword set was derived",
|
||||
" differentially from right-wing motions, making it circular for adoption analysis.",
|
||||
"",
|
||||
"## 8. Figures",
|
||||
"## 9. Figures",
|
||||
"",
|
||||
f".name})",
|
||||
f".name})",
|
||||
f".name})",
|
||||
"",
|
||||
"## 9. Conclusion",
|
||||
"## 10. Conclusion",
|
||||
"",
|
||||
"*(Fill in after reviewing all indicators and audit results.)*",
|
||||
]
|
||||
@@ -895,6 +996,9 @@ def main() -> int:
|
||||
logger.info("Computing extremity-stratified pass rates...")
|
||||
ext_stratified = compute_extremity_stratified(yearly_raw)
|
||||
|
||||
logger.info("Computing left-support yearly averages...")
|
||||
left_yearly = compute_left_support_yearly(con)
|
||||
|
||||
con.close()
|
||||
|
||||
yearly_sum = yearly_summary(yearly_raw)
|
||||
@@ -909,6 +1013,9 @@ def main() -> int:
|
||||
logger.info("Generating Figure 2...")
|
||||
fig2_path = create_figure_2(yearly_sum, opp_sum, mig_sum, non_mig_sum, ext_stratified)
|
||||
|
||||
logger.info("Generating Figure 3...")
|
||||
fig3_path = create_figure_3(left_yearly)
|
||||
|
||||
logger.info("Sampling motions for manual audit...")
|
||||
audit_sample = sample_audit(yearly_raw)
|
||||
print_audit(audit_sample)
|
||||
@@ -931,8 +1038,10 @@ def main() -> int:
|
||||
ext_stratified=ext_stratified,
|
||||
yearly_raw=yearly_raw,
|
||||
opp_raw=opp_raw,
|
||||
left_yearly=left_yearly,
|
||||
fig1_path=fig1_path,
|
||||
fig2_path=fig2_path,
|
||||
fig3_path=fig3_path,
|
||||
audit_sample=audit_sample,
|
||||
audit_notes=audit_notes,
|
||||
)
|
||||
@@ -940,6 +1049,7 @@ def main() -> int:
|
||||
print(f"\nReport: {report_path}")
|
||||
print(f"Figure 1: {fig1_path}")
|
||||
print(f"Figure 2: {fig2_path}")
|
||||
print(f"Figure 3: {fig3_path}")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -12,19 +12,19 @@ and content extremity for right-wing motions in the Tweede Kamer.
|
||||
|
||||
## 1. Yearly Aggregate Metrics (All Right-Wing Motions)
|
||||
|
||||
| Year | N (RW) | Centrist Support | Extremity | Right Support | Left Opp. |
|
||||
|------|--------|-----------------|-----------|---------------|----------|
|
||||
| 2016 | 6 | 0.722 | 2.00 | 1.000 | 0.708 |
|
||||
| Year | N (RW) | Centrist Support (Strict) | Extremity | Right Support | Left Opp. |
|
||||
|------|--------|---------------------------|-----------|---------------|----------|
|
||||
| 2016 | 6 | 0.667 | 2.00 | 1.000 | 0.708 |
|
||||
| 2017 | 0 | N/A | N/A | N/A | N/A |
|
||||
| 2018 | 5 | 1.000 | 1.40 | 0.800 | 0.480 |
|
||||
| 2019 | 195 | 0.410 | 2.14 | 0.838 | 0.746 |
|
||||
| 2020 | 469 | 0.326 | 2.26 | 0.818 | 0.758 |
|
||||
| 2021 | 425 | 0.339 | 2.24 | 0.903 | 0.788 |
|
||||
| 2022 | 446 | 0.404 | 2.16 | 0.891 | 0.820 |
|
||||
| 2023 | 365 | 0.457 | 2.24 | 0.900 | 0.821 |
|
||||
| 2024 | 469 | 0.670 | 1.99 | 0.885 | 0.756 |
|
||||
| 2025 | 455 | 0.597 | 2.25 | 0.895 | 0.799 |
|
||||
| 2026 | 151 | 0.518 | 2.33 | 0.916 | 0.834 |
|
||||
| 2019 | 195 | 0.380 | 2.14 | 0.838 | 0.746 |
|
||||
| 2020 | 469 | 0.300 | 2.26 | 0.818 | 0.758 |
|
||||
| 2021 | 425 | 0.175 | 2.24 | 0.903 | 0.788 |
|
||||
| 2022 | 446 | 0.201 | 2.16 | 0.891 | 0.820 |
|
||||
| 2023 | 365 | 0.255 | 2.24 | 0.900 | 0.821 |
|
||||
| 2024 | 469 | 0.595 | 1.99 | 0.885 | 0.756 |
|
||||
| 2025 | 455 | 0.474 | 2.25 | 0.895 | 0.799 |
|
||||
| 2026 | 151 | 0.334 | 2.33 | 0.916 | 0.834 |
|
||||
|
||||
|
||||
## 2. Pre/Post 2024 Comparison
|
||||
@@ -35,7 +35,7 @@ and content extremity for right-wing motions in the Tweede Kamer.
|
||||
|
||||
| Metric | Pre-2024 Mean | Post-2024 Mean | Δ | Cohen's d |
|
||||
|--------|--------------|---------------|-----|-----------|
|
||||
| Centrist Support | 0.384 | 0.618 | +0.234 | +0.68 |
|
||||
| Centrist Support | 0.251 | 0.507 | +0.256 | +0.65 |
|
||||
| Extremity | 2.21 | 2.15 | -0.07 | -0.09 |
|
||||
|
||||
**Interpretation:** Cohen's d values quantify effect sizes (|d| < 0.2 small, 0.5 medium, > 0.8 large).
|
||||
@@ -45,7 +45,7 @@ These are descriptive, not inferential — with only 8 pre-2024 years and 3 post
|
||||
|
||||
| Metric | Pre-2024 Mean | Post-2024 Mean | Δ | Cohen's d | N pre / N post |
|
||||
|--------|--------------|---------------|-----|-----------|---------------|
|
||||
| Centrist Support | 0.270 | 0.543 | +0.272 | +0.85 | 1295 / 405 |
|
||||
| Centrist Support | 0.130 | 0.437 | +0.307 | +0.88 | 1295 / 405 |
|
||||
| Extremity | 2.28 | 2.18 | -0.10 | -0.14 | 1295 / 405 |
|
||||
|
||||
**Interpretation gate:** If opposition metrics also rise post-2024, the shift is not
|
||||
@@ -67,21 +67,21 @@ Migration = category `asiel/vreemdelingen`. Non-migration = all other categories
|
||||
|
||||
| Domain | Pre-2024 Mean CS | Post-2024 Mean CS | Δ CS |
|
||||
|--------|-----------------|------------------|------|
|
||||
| Migration | 0.303 | 0.536 | +0.233 |
|
||||
| Non-migration | 0.529 | 0.605 | +0.076 |
|
||||
| Migration | 0.146 | 0.361 | +0.215 |
|
||||
| Non-migration | 0.435 | 0.487 | +0.052 |
|
||||
|
||||
## 5. Extremity-Stratified Centrist Support
|
||||
|
||||
| Bucket | Period | N | Mean CS | Median CS | P25 | P75 |
|
||||
|--------|--------|---|---------|-----------|---|-----|
|
||||
| 1-2 (mild) | Pre-2024 | 221 | 0.522 | 0.400 | 0.250 | 1.000 |
|
||||
| | Post-2024 | 181 | 0.775 | 1.000 | 0.600 | 1.000 |
|
||||
| 2-3 (moderate) | Pre-2024 | 1205 | 0.403 | 0.250 | 0.000 | 0.750 |
|
||||
| | Post-2024 | 640 | 0.606 | 0.600 | 0.250 | 1.000 |
|
||||
| 3-4 (high) | Pre-2024 | 352 | 0.295 | 0.250 | 0.000 | 0.500 |
|
||||
| | Post-2024 | 175 | 0.565 | 0.600 | 0.250 | 0.800 |
|
||||
| 4-5 (extreme) | Pre-2024 | 133 | 0.212 | 0.250 | 0.000 | 0.250 |
|
||||
| | Post-2024 | 79 | 0.474 | 0.500 | 0.250 | 0.800 |
|
||||
| 1-2 (mild) | Pre-2024 | 221 | 0.422 | 0.500 | 0.000 | 1.000 |
|
||||
| | Post-2024 | 181 | 0.728 | 1.000 | 0.667 | 1.000 |
|
||||
| 2-3 (moderate) | Pre-2024 | 1205 | 0.267 | 0.000 | 0.000 | 0.500 |
|
||||
| | Post-2024 | 640 | 0.497 | 0.500 | 0.000 | 1.000 |
|
||||
| 3-4 (high) | Pre-2024 | 352 | 0.150 | 0.000 | 0.000 | 0.000 |
|
||||
| | Post-2024 | 175 | 0.419 | 0.333 | 0.000 | 0.667 |
|
||||
| 4-5 (extreme) | Pre-2024 | 133 | 0.091 | 0.000 | 0.000 | 0.000 |
|
||||
| | Post-2024 | 79 | 0.275 | 0.000 | 0.000 | 0.667 |
|
||||
|
||||
|
||||
**Key test:** If centrist support for high-extremity motions (3-5) rose
|
||||
@@ -91,7 +91,31 @@ If centrist support rose uniformly across all buckets, the shift is about volume
|
||||
(more motions) rather than tolerance. If only the 1-2 bucket rose, right-wing
|
||||
parties filed milder motions post-2024 and the 'shift' is illusory.
|
||||
|
||||
## 6. Manual Extremity Audit
|
||||
## 6. Left-wing support for right-wing motions
|
||||
|
||||
| Year | N | Mean left_support_mp |
|
||||
|------|---|---------------------|
|
||||
| 2016 | 6 | 0.2917 |
|
||||
| 2018 | 5 | 0.5200 |
|
||||
| 2019 | 195 | 0.2531 |
|
||||
| 2020 | 469 | 0.2414 |
|
||||
| 2021 | 425 | 0.2113 |
|
||||
| 2022 | 446 | 0.1807 |
|
||||
| 2023 | 365 | 0.1779 |
|
||||
| 2024 | 469 | 0.2441 |
|
||||
| 2025 | 455 | 0.2015 |
|
||||
| 2026 | 151 | 0.1594 |
|
||||
|
||||
|
||||
| Metric | Pre-2024 Mean | Post-2024 Mean | Δ |
|
||||
|--------|--------------|---------------|-----|
|
||||
| Left Support (MP) | 0.2680 | 0.2017 | -0.0663 |
|
||||
|
||||
**Interpretation:** Left parties moved from 26.8% to 20.2% support — a 0.1 point shift. Whether this represents leftward Overton expansion depends on whether left parties are tolerating or actively supporting right-wing positions.
|
||||
|
||||

|
||||
|
||||
## 7. Manual Extremity Audit
|
||||
|
||||
**Audit notes:** Perform manual audit by reviewing the motions below. Record agreement per motion. Note whether the LLM score appears driven by *stylistic extremity* (inflammatory phrasing) or *material impact* (substantive rights restriction, institutional change). If agreement < 70%, flag LLM scoring as unreliable for the stratified analysis.
|
||||
|
||||
@@ -119,12 +143,12 @@ parties filed milder motions post-2024 and the 'shift' is illusory.
|
||||
| 20 | 2019 | sociaal/jeugd | 4 | 4-5 (extreme) | | |
|
||||
|
||||
|
||||
## 7. Limitations
|
||||
## 8. Limitations
|
||||
|
||||
- **Small-N time series:** 8 pre-2024 years and at most 3 post-2024 years (2026 is partial).
|
||||
Effect sizes are descriptive, not confirmatory.
|
||||
- **LLM extremity scores:** Content-based, not independently validated beyond the
|
||||
manual audit above. See §6 for agreement rate and noted biases.
|
||||
manual audit above. See §7 for agreement rate and noted biases.
|
||||
- **Coalition composition:** Hardcoded per year. 2024 is ambiguous (Rutte IV until July,
|
||||
Schoof thereafter). Early 2024 motions may be miscoded as Schoof-era.
|
||||
- **Submitter party identification:** Parsed from motion title prefixes (e.g.,
|
||||
@@ -133,11 +157,12 @@ parties filed milder motions post-2024 and the 'shift' is illusory.
|
||||
- **Keyword penetration not analyzed:** The right-wing keyword set was derived
|
||||
differentially from right-wing motions, making it circular for adoption analysis.
|
||||
|
||||
## 8. Figures
|
||||
## 9. Figures
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## 9. Conclusion
|
||||
## 10. Conclusion
|
||||
|
||||
*(Fill in after reviewing all indicators and audit results.)*
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 170 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
Reference in New Issue
Block a user