feat(overton): coalition coding fix + regenerate breakpoint analysis
This commit is contained in:
@@ -16,6 +16,7 @@ Output:
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
@@ -24,6 +25,9 @@ import sys
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
ROOT = Path(__file__).parent.parent.parent.resolve()
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
import duckdb
|
import duckdb
|
||||||
import matplotlib
|
import matplotlib
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -58,6 +62,9 @@ def _extremity_bucket(score: float) -> str:
|
|||||||
CANONICAL_LEFT_SET = set(CANONICAL_LEFT)
|
CANONICAL_LEFT_SET = set(CANONICAL_LEFT)
|
||||||
CANONICAL_RIGHT_SET = set(CANONICAL_RIGHT)
|
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]] = {
|
COALITION: dict[int, set[str]] = {
|
||||||
2016: {"VVD", "PvdA"},
|
2016: {"VVD", "PvdA"},
|
||||||
2017: {"VVD", "PvdA"},
|
2017: {"VVD", "PvdA"},
|
||||||
@@ -67,18 +74,21 @@ COALITION: dict[int, set[str]] = {
|
|||||||
2021: {"VVD", "CDA", "D66", "CU"},
|
2021: {"VVD", "CDA", "D66", "CU"},
|
||||||
2022: {"VVD", "D66", "CDA", "CU"},
|
2022: {"VVD", "D66", "CDA", "CU"},
|
||||||
2023: {"VVD", "D66", "CDA", "CU"},
|
2023: {"VVD", "D66", "CDA", "CU"},
|
||||||
2024: {"PVV", "VVD", "NSC", "BBB"},
|
2024: SCHOOF_COALITION,
|
||||||
2025: {"PVV", "VVD", "NSC", "BBB"},
|
2025: SCHOOF_COALITION,
|
||||||
2026: {"PVV", "VVD", "NSC", "BBB"},
|
2026: SCHOOF_COALITION,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SCHOOF_START_DATE = "2024-07-01"
|
||||||
|
|
||||||
COALITION_NOTE = (
|
COALITION_NOTE = (
|
||||||
"2016-2017: Rutte II (VVD/PvdA). "
|
"2016-2017: Rutte II (VVD/PvdA). "
|
||||||
"2018-2021: Rutte III (VVD/CDA/D66/CU). "
|
"2018-2021: Rutte III (VVD/CDA/D66/CU). "
|
||||||
"2022-2023: Rutte IV (VVD/D66/CDA/CU). "
|
"2022-2023: Rutte IV (VVD/D66/CDA/CU). "
|
||||||
"2024-2026: Schoof (PVV/VVD/NSC/BBB). "
|
"2024 split: Rutte IV (VVD/D66/CDA/CU) for Jan-Jun 2024, "
|
||||||
"2024 ambiguous: Schoof cabinet started July 2024; all 2024 motions are coded "
|
"Schoof (PVV/VVD/NSC/BBB) for Jul-Dec 2024. "
|
||||||
"to the Schoof coalition. Coalition effect may be overestimated for early 2024."
|
"2025-2026: Schoof (PVV/VVD/NSC/BBB). "
|
||||||
|
"Period detection uses motion date, not just year."
|
||||||
)
|
)
|
||||||
|
|
||||||
YEAR_MIN, YEAR_MAX = 2016, 2026
|
YEAR_MIN, YEAR_MAX = 2016, 2026
|
||||||
@@ -114,7 +124,8 @@ def compute_yearly_rw_metrics(con: duckdb.DuckDBPyConnection) -> dict[int, dict]
|
|||||||
r.category,
|
r.category,
|
||||||
e.text_score AS extremity_score,
|
e.text_score AS extremity_score,
|
||||||
m.voting_results,
|
m.voting_results,
|
||||||
m.winning_margin
|
m.winning_margin,
|
||||||
|
m.date
|
||||||
FROM right_wing_motions r
|
FROM right_wing_motions r
|
||||||
JOIN extremity_scores e ON r.motion_id = e.motion_id
|
JOIN extremity_scores e ON r.motion_id = e.motion_id
|
||||||
JOIN motions m ON r.motion_id = m.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": [],
|
"categories": [],
|
||||||
"titles": [],
|
"titles": [],
|
||||||
"motion_ids": [],
|
"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:
|
if year is None or year < YEAR_MIN or year > YEAR_MAX:
|
||||||
continue
|
continue
|
||||||
yearly[year]["centrist_support_strict"].append(cst if cst is not None else np.nan)
|
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]["categories"].append(cat or "other")
|
||||||
yearly[year]["titles"].append(title or "")
|
yearly[year]["titles"].append(title or "")
|
||||||
yearly[year]["motion_ids"].append(mid)
|
yearly[year]["motion_ids"].append(mid)
|
||||||
|
yearly[year]["dates"].append(motion_date)
|
||||||
|
|
||||||
if vr_json is not None:
|
if vr_json is not None:
|
||||||
voting = json.loads(vr_json) if isinstance(vr_json, str) else vr_json
|
voting = json.loads(vr_json) if isinstance(vr_json, str) else vr_json
|
||||||
@@ -299,9 +312,9 @@ def compute_opposition_metrics(
|
|||||||
}
|
}
|
||||||
|
|
||||||
coalition = COALITION
|
coalition = COALITION
|
||||||
|
schoof_cutoff = datetime.date(2024, 7, 1)
|
||||||
|
|
||||||
for year, d in yearly_raw.items():
|
for year, d in yearly_raw.items():
|
||||||
coal = coalition.get(year, set())
|
|
||||||
for idx in range(len(d["titles"])):
|
for idx in range(len(d["titles"])):
|
||||||
title = d["titles"][idx]
|
title = d["titles"][idx]
|
||||||
submitter_name, submitter_party = parse_lead_submitter(title, name_party_map)
|
submitter_name, submitter_party = parse_lead_submitter(title, name_party_map)
|
||||||
@@ -309,6 +322,13 @@ def compute_opposition_metrics(
|
|||||||
if submitter_party is None:
|
if submitter_party is None:
|
||||||
continue
|
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:
|
if submitter_party in coal:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,309 @@
|
|||||||
|
---
|
||||||
|
title: "Address Critical Gaps in Overton Window Analysis"
|
||||||
|
type: feat
|
||||||
|
status: active
|
||||||
|
date: 2026-05-25
|
||||||
|
---
|
||||||
|
|
||||||
|
# Address Critical Gaps in Overton Window Analysis
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
The current Overton window synthesis identifies a structural break in centrist voting behavior post-2024 but leaves critical analytical gaps unresolved. This plan addresses the seven most important gaps: temporal trajectory analysis, 2D extremity decomposition, systematic mechanism classification, causal mechanism exploration, left-wing response patterns, motion success correlation, and quarterly granularity. The goal is to transform the current "what happened" analysis into a "how and why" explanation.
|
||||||
|
|
||||||
|
## Problem Frame
|
||||||
|
|
||||||
|
The synthesis report establishes that centrist support for right-wing motions surged from 0.251 to 0.507 (d=+0.65) and that right-wing parties moderated their proposals (material impact 2.78→2.43). However, the analysis relies on a binary pre/post-2024 split that obscures the actual dynamics. We don't know whether the shift was immediate (post-election shock) or gradual (learning curve), whether the 2D extremity trends diverge over time, whether the 24-motion mechanism sample generalizes, or what actually caused the behavioral change. These gaps prevent us from distinguishing between competing explanations: strategic adaptation by right-wing parties, genuine ideological convergence by centrists, coalition dynamics, or external shocks.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- R1. Replace binary pre/post-2024 analysis with continuous temporal trajectories showing when and how the shift occurred
|
||||||
|
- R2. Decompose 2D extremity scores into separate stylistic and material trend lines to test whether the "flat single-dimension trend" masks diverging trajectories
|
||||||
|
- R3. Systematically classify mechanisms across a representative sample (not just 24 top motions) to validate the consensus framing hypothesis
|
||||||
|
- R4. Identify causal mechanisms by correlating the timing of the shift with political events (Schoof cabinet formation, European rightward shift, specific policy crises)
|
||||||
|
- R5. Analyze left-wing voting patterns to determine whether the shift reflects right-wing moderation, centrist acceptance, or left-wing opposition hardening
|
||||||
|
- R6. Correlate centrist support with actual motion passage to test whether high-support motions passed at higher rates
|
||||||
|
- R7. Provide quarterly or monthly granularity to distinguish immediate post-election effects from gradual adaptation
|
||||||
|
|
||||||
|
## Scope Boundaries
|
||||||
|
|
||||||
|
- In scope: Quantitative analysis of existing data (motions, votes, 2D scores, SVD positions). No new data collection.
|
||||||
|
- Out of scope: Qualitative interviews, media analysis, public opinion data, comparative analysis with other countries.
|
||||||
|
- Deferred: Full causal inference modeling (diff-in-diff, regression discontinuity) — requires more sophisticated statistical framework than current descriptive approach.
|
||||||
|
|
||||||
|
## Key Technical Decisions
|
||||||
|
|
||||||
|
- **Temporal unit**: Use quarterly aggregation (Q1 2016 through Q2 2026 = 42 quarters). Monthly would be too noisy; annual loses the 2024 breakpoint resolution.
|
||||||
|
- **2D extremity analysis**: Compute separate yearly means for stylistic and material scores, then test for divergence using paired t-tests or Wilcoxon signed-rank tests.
|
||||||
|
- **Mechanism classification**: Use the existing 24-motion taxonomy (consensus framing, institutional, welfare, procedural, local, coalition, symbolic, targeted restriction, system dismantling, crisis response) and apply it to a stratified sample of 200 motions (50 pre-2024, 150 post-2024) using LLM classification with manual validation of 20%.
|
||||||
|
- **Causal timing**: Identify the exact quarter when centrist support crossed the 0.4 threshold (midpoint between pre and post means) and correlate with political events.
|
||||||
|
- **Left-wing analysis**: Compute left_support_mp (already exists) and analyze whether left-wing opposition hardened (decreased support) or remained stable.
|
||||||
|
- **Success correlation**: Compute pass_rate for motions binned by centrist_support quartiles (0-0.25, 0.25-0.5, 0.5-0.75, 0.75-1.0) and test for monotonic relationship.
|
||||||
|
|
||||||
|
## Implementation Units
|
||||||
|
|
||||||
|
### U1. Temporal Trajectory Analysis
|
||||||
|
|
||||||
|
**Goal:** Replace binary pre/post analysis with continuous quarterly trajectories showing the exact timing and shape of the centrist support shift.
|
||||||
|
|
||||||
|
**Requirements:** R1, R7
|
||||||
|
|
||||||
|
**Dependencies:** None
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `analysis/right_wing/temporal_trajectory.py`
|
||||||
|
- Output: `reports/overton_window/temporal_trajectory.md`
|
||||||
|
- Output: `reports/overton_window/temporal_trajectory_figure.png`
|
||||||
|
|
||||||
|
**Approach:**
|
||||||
|
- Aggregate centrist_support_strict by quarter (2016-Q1 through 2026-Q2)
|
||||||
|
- Compute rolling 3-quarter moving average to smooth noise
|
||||||
|
- Identify the inflection point: first quarter where centrist_support > 0.4
|
||||||
|
- Plot trajectory with confidence intervals (bootstrap resampling, 1000 iterations)
|
||||||
|
- Annotate political events: 2021 election, 2023 election, July 2024 Schoof cabinet formation
|
||||||
|
- Compute quarterly motion counts to show volume surge timing
|
||||||
|
|
||||||
|
**Patterns to follow:**
|
||||||
|
- `analysis/right_wing/temporal_analysis.py` — yearly aggregation pattern
|
||||||
|
- `analysis/right_wing/overton_breakpoint_analysis.py` — matplotlib chart patterns
|
||||||
|
|
||||||
|
**Test scenarios:**
|
||||||
|
- Happy path: Script produces quarterly aggregates for all 42 quarters, identifies inflection point, generates figure with 5 lines (overall, opposition-only, migration, non-migration, all-motions baseline)
|
||||||
|
- Edge case: Quarters with <10 motions should show wider confidence intervals
|
||||||
|
- Edge case: 2026-Q2 (partial year) should be flagged as incomplete
|
||||||
|
|
||||||
|
**Verification:**
|
||||||
|
- `temporal_trajectory.md` contains a table with quarterly centrist_support, motion counts, and confidence intervals
|
||||||
|
- Figure shows the exact quarter when the shift began and whether it was immediate or gradual
|
||||||
|
- Inflection point is explicitly identified and correlated with political events
|
||||||
|
|
||||||
|
### U2. 2D Extremity Temporal Decomposition
|
||||||
|
|
||||||
|
**Goal:** Test whether the "flat single-dimension trend" masks diverging trajectories when stylistic and material scores are analyzed separately.
|
||||||
|
|
||||||
|
**Requirements:** R2
|
||||||
|
|
||||||
|
**Dependencies:** U1 (uses same temporal framework)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `analysis/right_wing/extremity_2d_temporal.py`
|
||||||
|
- Output: `reports/overton_window/extremity_2d_temporal.md`
|
||||||
|
- Output: `reports/overton_window/extremity_2d_temporal_figure.png`
|
||||||
|
|
||||||
|
**Approach:**
|
||||||
|
- Join extremity_scores_2d with right_wing_motions to get year for each motion
|
||||||
|
- Compute yearly means for stylistic_score and material_score separately
|
||||||
|
- Plot both trajectories on the same figure with the original single-dimension score for comparison
|
||||||
|
- Test for divergence: paired Wilcoxon signed-rank test on yearly (stylistic, material) pairs
|
||||||
|
- Compute the gap (material - stylistic) over time to see if it's widening, narrowing, or stable
|
||||||
|
- Stratify by domain (migration vs non-migration) to test whether the gap differs by policy area
|
||||||
|
|
||||||
|
**Patterns to follow:**
|
||||||
|
- `analysis/right_wing/extremity_rescore_2d.py` — 2D score structure
|
||||||
|
- `analysis/right_wing/temporal_analysis.py` — yearly aggregation
|
||||||
|
|
||||||
|
**Test scenarios:**
|
||||||
|
- Happy path: Script produces yearly means for both dimensions, generates figure with 3 lines (stylistic, material, original), computes divergence test statistic
|
||||||
|
- Edge case: Years with <50 scored motions should be flagged as low-confidence
|
||||||
|
- Integration: Results should be consistent with the aggregate findings (material > stylistic, r≈0.47)
|
||||||
|
|
||||||
|
**Verification:**
|
||||||
|
- `extremity_2d_temporal.md` contains a table with yearly stylistic and material means
|
||||||
|
- Figure shows whether the two dimensions diverged over time or moved in parallel
|
||||||
|
- Divergence test result is reported (p-value or effect size)
|
||||||
|
|
||||||
|
### U3. Systematic Mechanism Classification
|
||||||
|
|
||||||
|
**Goal:** Validate the consensus framing hypothesis by classifying mechanisms across a representative sample of 200 motions, not just the 24 highest-support motions.
|
||||||
|
|
||||||
|
**Requirements:** R3
|
||||||
|
|
||||||
|
**Dependencies:** None
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `analysis/right_wing/mechanism_classification.py`
|
||||||
|
- Output: `reports/overton_window/mechanism_classification.md`
|
||||||
|
|
||||||
|
**Approach:**
|
||||||
|
- Stratified sampling: 50 pre-2024 motions (25 high centrist support, 25 low), 150 post-2024 motions (75 high, 75 low)
|
||||||
|
- Use LLM classification with the 10-mechanism taxonomy from the synthesis report
|
||||||
|
- Prompt template: "Classify this motion's primary mechanism for gaining centrist support: [taxonomy with definitions]"
|
||||||
|
- Manual validation: randomly sample 40 motions (20%) and have a human reviewer confirm or correct the classification
|
||||||
|
- Compute mechanism distribution by period (pre vs post) and by centrist support level (high vs low)
|
||||||
|
- Test whether consensus framing is more common in high-support post-2024 motions than in other groups
|
||||||
|
|
||||||
|
**Patterns to follow:**
|
||||||
|
- `analysis/right_wing/derive_categories.py` — LLM classification pattern
|
||||||
|
- `analysis/right_wing/extremity_rescore_2d.py` — batch processing with validation
|
||||||
|
|
||||||
|
**Test scenarios:**
|
||||||
|
- Happy path: Script classifies 200 motions, produces mechanism distribution table, computes chi-squared test for mechanism × period × support interaction
|
||||||
|
- Edge case: LLM returns invalid mechanism labels should be caught and re-prompted
|
||||||
|
- Integration: Manual validation should achieve >80% agreement with LLM classifications
|
||||||
|
|
||||||
|
**Verification:**
|
||||||
|
- `mechanism_classification.md` contains a table showing mechanism distribution across 4 groups (pre-high, pre-low, post-high, post-low)
|
||||||
|
- Chi-squared test result is reported
|
||||||
|
- Manual validation agreement rate is reported
|
||||||
|
|
||||||
|
### U4. Causal Timing Analysis
|
||||||
|
|
||||||
|
**Goal:** Identify the exact timing of the centrist support shift and correlate it with political events to distinguish between competing causal explanations.
|
||||||
|
|
||||||
|
**Requirements:** R4, R7
|
||||||
|
|
||||||
|
**Dependencies:** U1 (uses quarterly trajectory data)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `analysis/right_wing/causal_timing.py`
|
||||||
|
- Output: `reports/overton_window/causal_timing.md`
|
||||||
|
|
||||||
|
**Approach:**
|
||||||
|
- Use the quarterly trajectory from U1
|
||||||
|
- Identify the inflection point: first quarter where centrist_support > 0.4 (midpoint between pre=0.25 and post=0.51)
|
||||||
|
- Compute the "shift velocity": change in centrist_support per quarter in the 4 quarters before and after the inflection point
|
||||||
|
- Correlate with political events timeline:
|
||||||
|
- March 2021: Rutte IV election
|
||||||
|
- November 2023: Schoof election (PVV victory)
|
||||||
|
- July 2024: Schoof cabinet formation
|
||||||
|
- Ongoing: European rightward shift (Meloni 2022, Sweden 2022, Finland 2023)
|
||||||
|
- Test whether the shift was immediate (single-quarter jump) or gradual (multi-quarter ramp)
|
||||||
|
- Compute "event proximity": did the shift begin before or after the Schoof cabinet formation?
|
||||||
|
|
||||||
|
**Patterns to follow:**
|
||||||
|
- `analysis/right_wing/overton_breakpoint_analysis.py` — breakpoint detection logic
|
||||||
|
|
||||||
|
**Test scenarios:**
|
||||||
|
- Happy path: Script identifies inflection point quarter, computes shift velocity, generates timeline figure with annotated events
|
||||||
|
- Edge case: If no clear inflection point (gradual shift), report the quarter with the steepest slope
|
||||||
|
- Integration: Results should be consistent with U1 trajectory analysis
|
||||||
|
|
||||||
|
**Verification:**
|
||||||
|
- `causal_timing.md` explicitly states which quarter the shift began
|
||||||
|
- Shift velocity is reported (quarters to reach 80% of the total shift)
|
||||||
|
- Timeline figure shows the relationship between the shift and political events
|
||||||
|
|
||||||
|
### U5. Left-Wing Response Analysis
|
||||||
|
|
||||||
|
**Goal:** Determine whether the centrist support surge reflects right-wing moderation, centrist acceptance, or left-wing opposition hardening.
|
||||||
|
|
||||||
|
**Requirements:** R5
|
||||||
|
|
||||||
|
**Dependencies:** None
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `analysis/right_wing/left_wing_response.py`
|
||||||
|
- Output: `reports/overton_window/left_wing_response.md`
|
||||||
|
- Output: `reports/overton_window/left_wing_response_figure.png`
|
||||||
|
|
||||||
|
**Approach:**
|
||||||
|
- Compute left_support_mp (already exists in right_wing_motions) for pre and post-2024
|
||||||
|
- Stratify by left party: SP, PvdA, GroenLinks, PvdD, Volt, DENK
|
||||||
|
- Test whether left-wing opposition hardened (decreased support) or remained stable
|
||||||
|
- Compute the "polarization gap": (centrist_support - left_support) over time
|
||||||
|
- If the gap widened, it could reflect centrist acceptance OR left-wing hardening OR both
|
||||||
|
- Stratify by domain to see if left-wing hardening is concentrated in migration (where centrist acceptance is highest)
|
||||||
|
|
||||||
|
**Patterns to follow:**
|
||||||
|
- `analysis/right_wing/overton_breakpoint_analysis.py` — party-level vote analysis
|
||||||
|
- `analysis/right_wing/migrate_mp_level_metrics.py` — left_support_mp computation
|
||||||
|
|
||||||
|
**Test scenarios:**
|
||||||
|
- Happy path: Script computes pre/post left_support_mp by party, generates figure showing left-wing trajectory vs centrist trajectory
|
||||||
|
- Edge case: Parties with <5 MPs in a given year should be excluded from party-level analysis
|
||||||
|
- Integration: Results should be consistent with the synthesis report's claim that "left opposition hardened"
|
||||||
|
|
||||||
|
**Verification:**
|
||||||
|
- `left_wing_response.md` contains a table with pre/post left_support_mp by party
|
||||||
|
- Figure shows whether left-wing opposition hardened, softened, or remained stable
|
||||||
|
- Polarization gap trajectory is reported
|
||||||
|
|
||||||
|
### U6. Motion Success Correlation
|
||||||
|
|
||||||
|
**Goal:** Test whether motions with high centrist support actually passed at higher rates, validating that centrist support translates to legislative success.
|
||||||
|
|
||||||
|
**Requirements:** R6
|
||||||
|
|
||||||
|
**Dependencies:** None
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `analysis/right_wing/success_correlation.py`
|
||||||
|
- Output: `reports/overton_window/success_correlation.md`
|
||||||
|
|
||||||
|
**Approach:**
|
||||||
|
- Compute pass_rate for right-wing motions binned by centrist_support quartiles: [0-0.25], (0.25-0.5], (0.5-0.75], (0.75-1.0]
|
||||||
|
- Test for monotonic relationship using Cochran-Armitage trend test
|
||||||
|
- Stratify by period (pre vs post-2024) to see if the relationship strengthened after the shift
|
||||||
|
- Control for motion type: government motions (from coalition parties) vs opposition motions
|
||||||
|
- Compute "success premium": pass_rate(high support) - pass_rate(low support)
|
||||||
|
|
||||||
|
**Patterns to follow:**
|
||||||
|
- `analysis/right_wing/overton_breakpoint_analysis.py` — pass rate computation (even though it's 96%+, we're testing for variation within that 4%)
|
||||||
|
|
||||||
|
**Test scenarios:**
|
||||||
|
- Happy path: Script computes pass_rate by centrist_support quartile, performs trend test, generates table
|
||||||
|
- Edge case: Quartiles with <50 motions should be flagged as low-confidence
|
||||||
|
- Integration: Results should show whether the 96%+ pass rate is uniform or varies by centrist support level
|
||||||
|
|
||||||
|
**Verification:**
|
||||||
|
- `success_correlation.md` contains a table with pass_rate by centrist_support quartile
|
||||||
|
- Trend test result is reported (p-value)
|
||||||
|
- Success premium is computed and interpreted
|
||||||
|
|
||||||
|
### U7. Synthesis Update
|
||||||
|
|
||||||
|
**Goal:** Integrate all new findings into the synthesis report, updating the verdict and uncertainty hierarchy.
|
||||||
|
|
||||||
|
**Requirements:** R1-R7
|
||||||
|
|
||||||
|
**Dependencies:** U1, U2, U3, U4, U5, U6
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `reports/overton_window/overton_window_synthesis.md`
|
||||||
|
|
||||||
|
**Approach:**
|
||||||
|
- Update the "Three Indicators at a Glance" table with new temporal and 2D findings
|
||||||
|
- Add a new section "Temporal Dynamics" summarizing U1 and U4 findings (when the shift happened, how fast)
|
||||||
|
- Add a new section "2D Extremity Trajectories" summarizing U2 findings (whether stylistic and material diverged)
|
||||||
|
- Update the "Mechanisms of Influence" section with U3 systematic classification results
|
||||||
|
- Add a new section "Causal Mechanisms" summarizing U4 timing analysis and event correlation
|
||||||
|
- Add a new section "Left-Wing Response" summarizing U5 findings
|
||||||
|
- Update the "Uncertainty Hierarchy" table to reflect which gaps are now resolved
|
||||||
|
- Revise the verdict if new evidence changes the interpretation
|
||||||
|
|
||||||
|
**Patterns to follow:**
|
||||||
|
- Existing synthesis report structure
|
||||||
|
|
||||||
|
**Test scenarios:**
|
||||||
|
- Happy path: All U1-U6 outputs are integrated, uncertainty hierarchy is updated, verdict is revised if needed
|
||||||
|
- Integration: Report remains internally consistent after updates
|
||||||
|
|
||||||
|
**Verification:**
|
||||||
|
- Synthesis report contains new sections for temporal dynamics, 2D trajectories, causal mechanisms, and left-wing response
|
||||||
|
- Uncertainty hierarchy table reflects the current state of knowledge
|
||||||
|
- Verdict is supported by all available evidence
|
||||||
|
|
||||||
|
## System-Wide Impact
|
||||||
|
|
||||||
|
- **No database changes:** All analysis uses existing tables (right_wing_motions, extremity_scores_2d, mp_votes, motions)
|
||||||
|
- **No UI changes:** All outputs are markdown reports and PNG figures
|
||||||
|
- **No agent_tools changes:** Analysis scripts are standalone
|
||||||
|
- **Reproducibility:** All scripts are deterministic given the same database state
|
||||||
|
|
||||||
|
## Risks & Dependencies
|
||||||
|
|
||||||
|
| Risk | Mitigation |
|
||||||
|
|------|------------|
|
||||||
|
| Quarterly aggregation produces noisy estimates for low-volume quarters | Use 3-quarter moving average and bootstrap confidence intervals |
|
||||||
|
| LLM mechanism classification may be inconsistent | Manual validation of 20% sample, re-prompt invalid classifications |
|
||||||
|
| Causal timing analysis may be ambiguous (gradual vs immediate shift) | Report both the inflection point and the shift velocity; let the data speak |
|
||||||
|
| Left-wing analysis may be underpowered for small parties | Exclude parties with <5 MPs in a given year from party-level analysis |
|
||||||
|
| Pass rate analysis may find no variation (96%+ ceiling) | Report the result honestly; if no correlation exists, say so |
|
||||||
|
|
||||||
|
## Sources & References
|
||||||
|
|
||||||
|
- **Current synthesis:** `reports/overton_window/overton_window_synthesis.md`
|
||||||
|
- **2D extremity data:** `extremity_scores_2d` table (2,869 motions scored)
|
||||||
|
- **Temporal framework:** `analysis/right_wing/temporal_analysis.py`
|
||||||
|
- **Mechanism taxonomy:** Synthesis report Section "Mechanisms of Influence"
|
||||||
|
- **Left-wing data:** `left_support_mp` column in `right_wing_motions` table
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
---
|
||||||
|
title: "Overton Window Analysis: Improvements and Extensions"
|
||||||
|
type: feat
|
||||||
|
status: active
|
||||||
|
date: 2026-05-26
|
||||||
|
origin: docs/plans/2026-05-25-001-overton-window-analysis-gaps-plan.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# Overton Window Analysis: Improvements and Extensions
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
The current Overton window analysis is methodologically strong — multi-indicator, 2D extremity decomposition, causal timing, mechanism classification. But it has structural gaps that limit interpretability. This plan addresses six gaps: (1) right-wing party differentiation (PVV vs FVD vs JA21 vs SGP — who filed the motions?), (2) coalition coding fix (split 2024 into Rutte IV / Schoof periods), (3) voting margin analysis (the 96% ceiling makes pass rate useless — use actual voor/tegen percentages instead), (4) SVD temporal trajectory (plot the spatial drift over 10 annual windows), (5) mechanism classification validation (second classifier for inter-rater reliability), and (6) predictive modeling (what motion features predict centrist support?). Each unit is independent and can be executed in parallel.
|
||||||
|
|
||||||
|
## Problem Frame
|
||||||
|
|
||||||
|
The synthesis report establishes that the Overton window did not shift right — right-wing parties moderated toward it. But the analysis treats right-wing parties as a bloc, uses a binary coalition coding that misattributes early 2024 motions, relies on pass rate as a success metric despite its 96% ceiling, and has no SVD visualization of the spatial drift. The mechanism classification (200 motions, single classifier) lacks inter-rater validation. Most critically, we have no predictive model: we can describe *what* happened but not *what features* predict which motions will gain centrist support.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- R1. Break down centrist support, extremity, and mechanism patterns by right-wing party (PVV, FVD, JA21, SGP) to identify which party drives the moderation effect
|
||||||
|
- R2. Fix coalition coding by splitting 2024 into pre-Schoof (Rutte IV, Jan-Jun) and post-Schoof (Schoof, Jul-Dec) periods
|
||||||
|
- R3. Replace pass rate with voting margin analysis (actual voor/tegen percentages) as the primary success metric
|
||||||
|
- R4. Visualize SVD spatial drift over 10 annual windows showing centrist and right-wing trajectories
|
||||||
|
- R5. Validate mechanism classification with a second classifier and compute inter-rater reliability (Cohen's kappa)
|
||||||
|
- R6. Build a predictive model for centrist support using motion features (category, extremity scores, submitter party, mechanism, text features)
|
||||||
|
|
||||||
|
## Scope Boundaries
|
||||||
|
|
||||||
|
- In scope: Quantitative analysis of existing data, new visualizations, predictive modeling
|
||||||
|
- Out of scope: Qualitative interviews, media analysis, public opinion data, international comparison
|
||||||
|
- Deferred: Cross-domain interaction analysis (migration × security), network/gateway motion analysis, submitter-level MP analysis
|
||||||
|
|
||||||
|
## Key Technical Decisions
|
||||||
|
|
||||||
|
- **Party differentiation:** Use `voting_results` JSON from motions table to extract per-party vote counts. Compute party-specific centrist support separately for PVV, FVD, JA21, SGP motions.
|
||||||
|
- **Coalition coding:** Split 2024 at July 1, 2024 (Schoof cabinet formation). Motions dated before July 2024 use Rutte IV coalition; after use Schoof coalition.
|
||||||
|
- **Voting margin:** Compute `margin = (voor - tegen) / (voor + tegen + afwezig)` per motion. This gives a continuous [-1, 1] scale instead of binary pass/fail.
|
||||||
|
- **SVD trajectory:** Use existing `load_party_scores_all_windows_aligned()` to get 2D positions for all parties across 10 windows. Plot as trajectory arrows.
|
||||||
|
- **Mechanism validation:** Use a second LLM (different model or different prompt) to classify the same 200 motions. Compute Cohen's kappa.
|
||||||
|
- **Predictive model:** Use logistic regression or random forest with features: category, stijl_extremiteit, materiele_impact, submitter_party, mechanism, text_length, keyword_count.
|
||||||
|
|
||||||
|
## Implementation Units
|
||||||
|
|
||||||
|
### U1. Right-Wing Party Differentiation
|
||||||
|
|
||||||
|
**Goal:** Break down all key metrics by right-wing party to identify which party drives the moderation effect.
|
||||||
|
|
||||||
|
**Requirements:** R1
|
||||||
|
|
||||||
|
**Dependencies:** None
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `analysis/right_wing/party_differentiation.py`
|
||||||
|
- Output: `reports/overton_window/party_differentiation.md`
|
||||||
|
- Output: `reports/overton_window/party_differentiation_figure.png`
|
||||||
|
|
||||||
|
**Approach:**
|
||||||
|
- Parse `voting_results` JSON from motions table to identify the submitter party for each right-wing motion
|
||||||
|
- Compute per-party: motion volume, mean centrist_support, mean extremity (2D), mechanism distribution
|
||||||
|
- Stratify by period (pre vs post-2024)
|
||||||
|
- Test whether PVV's moderation is distinct from FVD/JA21/SGP
|
||||||
|
- Plot: 4-panel figure with (a) volume over time, (b) centrist support over time, (c) extremity over time, (d) mechanism distribution
|
||||||
|
|
||||||
|
**Patterns to follow:**
|
||||||
|
- `analysis/right_wing/overton_breakpoint_analysis.py` — party-level analysis patterns
|
||||||
|
- `analysis/right_wing/classify_motions.py` — submitter parsing from title
|
||||||
|
|
||||||
|
**Test scenarios:**
|
||||||
|
- Happy path: Script produces per-party metrics for PVV, FVD, JA21, SGP across all years
|
||||||
|
- Edge case: Multi-submitter motions (use first submitter)
|
||||||
|
- Edge case: Parties with <10 motions in a year → exclude from party-level analysis
|
||||||
|
|
||||||
|
**Verification:**
|
||||||
|
- Report contains per-party tables for volume, centrist support, extremity, mechanisms
|
||||||
|
- Figure shows whether moderation is PVV-specific or party-general
|
||||||
|
|
||||||
|
### U2. Coalition Coding Fix
|
||||||
|
|
||||||
|
**Goal:** Split 2024 into pre-Schoof (Rutte IV) and post-Schoof (Schoof) periods to eliminate coalition coding ambiguity.
|
||||||
|
|
||||||
|
**Requirements:** R2
|
||||||
|
|
||||||
|
**Dependencies:** None
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `analysis/right_wing/overton_breakpoint_analysis.py` (coalition coding logic)
|
||||||
|
- Modify: `analysis/right_wing/temporal_trajectory.py` (quarterly analysis)
|
||||||
|
- Output: Updated reports with corrected coalition coding
|
||||||
|
|
||||||
|
**Approach:**
|
||||||
|
- Define coalition periods: Rutte IV (2022-Oct to 2024-Jul), Schoof (2024-Jul to present)
|
||||||
|
- Update `is_opposition` logic to use motion date for period detection
|
||||||
|
- Re-run opposition-only analysis with corrected coding
|
||||||
|
- Compare results with original binary coding
|
||||||
|
|
||||||
|
**Patterns to follow:**
|
||||||
|
- `analysis/right_wing/overton_breakpoint_analysis.py` — existing coalition coding at line ~200
|
||||||
|
|
||||||
|
**Test scenarios:**
|
||||||
|
- Happy path: Opposition-only analysis shows corrected centrist support trajectory
|
||||||
|
- Edge case: Motions in July 2024 (transition month) → assign to Schoof
|
||||||
|
- Integration: Results should be consistent with temporal trajectory findings
|
||||||
|
|
||||||
|
**Verification:**
|
||||||
|
- Opposition-only centrist support trajectory is recalculated with corrected coding
|
||||||
|
- Report explicitly states the coding change and its impact on findings
|
||||||
|
|
||||||
|
### U3. Voting Margin Analysis
|
||||||
|
|
||||||
|
**Goal:** Replace binary pass/fail with continuous voting margin as the primary success metric.
|
||||||
|
|
||||||
|
**Requirements:** R3
|
||||||
|
|
||||||
|
**Dependencies:** None
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `analysis/right_wing/voting_margin.py`
|
||||||
|
- Output: `reports/overton_window/voting_margin.md`
|
||||||
|
- Output: `reports/overton_window/voting_margin_figure.png`
|
||||||
|
|
||||||
|
**Approach:**
|
||||||
|
- Compute `margin = (voor - tegen) / (voor + tegen + afwezig)` for each right-wing motion
|
||||||
|
- Analyze margin distribution by centrist support quartile
|
||||||
|
- Test whether higher centrist support → higher margin (not just pass/fail)
|
||||||
|
- Stratify by period (pre vs post-2024)
|
||||||
|
- Plot: margin distribution by centrist support quartile, with period comparison
|
||||||
|
|
||||||
|
**Patterns to follow:**
|
||||||
|
- `analysis/right_wing/success_correlation.py` — existing pass rate analysis
|
||||||
|
|
||||||
|
**Test scenarios:**
|
||||||
|
- Happy path: Script computes margins for all right-wing motions, produces distribution figure
|
||||||
|
- Edge case: Motions with 0 votes → exclude
|
||||||
|
- Edge case: Motions with unanimous support → margin = 1.0
|
||||||
|
|
||||||
|
**Verification:**
|
||||||
|
- Report contains margin distribution by centrist support quartile
|
||||||
|
- Figure shows whether centrist support predicts voting margin (continuous) better than pass rate (binary)
|
||||||
|
|
||||||
|
### U4. SVD Temporal Trajectory Visualization
|
||||||
|
|
||||||
|
**Goal:** Visualize SVD spatial drift over 10 annual windows showing centrist and right-wing party trajectories.
|
||||||
|
|
||||||
|
**Requirements:** R4
|
||||||
|
|
||||||
|
**Dependencies:** None
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `analysis/right_wing/svd_trajectory_viz.py`
|
||||||
|
- Output: `reports/overton_window/svd_trajectory_figure.png`
|
||||||
|
|
||||||
|
**Approach:**
|
||||||
|
- Use `load_party_scores_all_windows_aligned()` to get 2D positions for all parties across 10 windows
|
||||||
|
- Plot: 2D compass with trajectory arrows for centrist parties (VVD, D66, CDA, NSC, BBB, CU) and right-wing parties (PVV, FVD, JA21, SGP)
|
||||||
|
- Color by party, arrow direction shows temporal progression
|
||||||
|
- Annotate windows with year labels
|
||||||
|
|
||||||
|
**Patterns to follow:**
|
||||||
|
- `analysis/right_wing/overton_svd_drift.py` — existing SVD drift analysis
|
||||||
|
- `explorer.py` — compass plotting with PARTY_COLOURS
|
||||||
|
|
||||||
|
**Test scenarios:**
|
||||||
|
- Happy path: Figure shows clear trajectory arrows for all parties
|
||||||
|
- Edge case: Missing party in a window → skip that arrow segment
|
||||||
|
|
||||||
|
**Verification:**
|
||||||
|
- Figure shows whether centrist parties moved left while right-wing parties moved right
|
||||||
|
- Trajectory arrows are clearly labeled with year markers
|
||||||
|
|
||||||
|
### U5. Mechanism Classification Validation
|
||||||
|
|
||||||
|
**Goal:** Validate mechanism classification with a second classifier and compute inter-rater reliability.
|
||||||
|
|
||||||
|
**Requirements:** R5
|
||||||
|
|
||||||
|
**Dependencies:** None
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `analysis/right_wing/mechanism_validation.py`
|
||||||
|
- Output: `reports/overton_window/mechanism_validation.md`
|
||||||
|
|
||||||
|
**Approach:**
|
||||||
|
- Use a second LLM (different model or different prompt) to classify the same 200 motions
|
||||||
|
- Compute Cohen's kappa for inter-rater reliability
|
||||||
|
- Report disagreements and resolve them
|
||||||
|
- Update mechanism classification with validated results
|
||||||
|
|
||||||
|
**Patterns to follow:**
|
||||||
|
- `analysis/right_wing/mechanism_classification.py` — existing classification
|
||||||
|
|
||||||
|
**Test scenarios:**
|
||||||
|
- Happy path: Second classifier produces classifications for all 200 motions
|
||||||
|
- Edge case: Disagreements → report and resolve
|
||||||
|
|
||||||
|
**Verification:**
|
||||||
|
- Report contains Cohen's kappa score
|
||||||
|
- Disagreements are documented and resolved
|
||||||
|
|
||||||
|
### U6. Predictive Modeling
|
||||||
|
|
||||||
|
**Goal:** Build a predictive model for centrist support using motion features.
|
||||||
|
|
||||||
|
**Requirements:** R6
|
||||||
|
|
||||||
|
**Dependencies:** U1 (party differentiation), U3 (voting margin)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `analysis/right_wing/predictive_model.py`
|
||||||
|
- Output: `reports/overton_window/predictive_model.md`
|
||||||
|
- Output: `reports/overton_window/predictive_model_figure.png`
|
||||||
|
|
||||||
|
**Approach:**
|
||||||
|
- Features: category, stijl_extremiteit, materiele_impact, submitter_party, mechanism, text_length, keyword_count
|
||||||
|
- Target: centrist_support (binary: >0.5 = high, <=0.5 = low)
|
||||||
|
- Models: logistic regression (interpretable), random forest (accuracy)
|
||||||
|
- Evaluate: accuracy, precision, recall, AUC-ROC
|
||||||
|
- Feature importance: which features best predict centrist support?
|
||||||
|
|
||||||
|
**Patterns to follow:**
|
||||||
|
- `analysis/right_wing/extremity_rescore_2d.py` — batch processing patterns
|
||||||
|
|
||||||
|
**Test scenarios:**
|
||||||
|
- Happy path: Model achieves AUC-ROC > 0.7
|
||||||
|
- Edge case: Missing features → impute with median/mode
|
||||||
|
|
||||||
|
**Verification:**
|
||||||
|
- Report contains model performance metrics and feature importance
|
||||||
|
- Figure shows ROC curve and feature importance plot
|
||||||
|
|
||||||
|
## System-Wide Impact
|
||||||
|
|
||||||
|
- **No database changes:** All analysis uses existing tables
|
||||||
|
- **No UI changes:** All outputs are markdown reports and PNG figures
|
||||||
|
- **No agent_tools changes:** Analysis scripts are standalone
|
||||||
|
- **Reproducibility:** All scripts are deterministic given the same database state
|
||||||
|
|
||||||
|
## Risks & Dependencies
|
||||||
|
|
||||||
|
| Risk | Mitigation |
|
||||||
|
|------|------------|
|
||||||
|
| Party differentiation may show PVV dominates everything | Report per-party sample sizes; exclude parties with <10 motions |
|
||||||
|
| Coalition coding fix may not change findings | Report both codings and compare |
|
||||||
|
| Voting margin may be correlated with pass rate | Compute correlation; if r>0.95, margin adds no value |
|
||||||
|
| SVD trajectory may be too cluttered | Use separate panels for centrist and right-wing |
|
||||||
|
| Mechanism validation may show low agreement | Report kappa; if <0.6, revise taxonomy |
|
||||||
|
| Predictive model may overfit | Use cross-validation; report train/test split |
|
||||||
|
|
||||||
|
## Sources & References
|
||||||
|
|
||||||
|
- **Current synthesis:** `reports/overton_window/overton_window_synthesis.md`
|
||||||
|
- **Temporal trajectory:** `reports/overton_window/temporal_trajectory.md`
|
||||||
|
- **Mechanism classification:** `reports/overton_window/mechanism_classification.md`
|
||||||
|
- **SVD drift:** `analysis/right_wing/overton_svd_drift.py`
|
||||||
|
- **Party positions:** `analysis/explorer_data.py` — `load_party_scores_all_windows_aligned()`
|
||||||
@@ -45,8 +45,8 @@ 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 |
|
| Metric | Pre-2024 Mean | Post-2024 Mean | Δ | Cohen's d | N pre / N post |
|
||||||
|--------|--------------|---------------|-----|-----------|---------------|
|
|--------|--------------|---------------|-----|-----------|---------------|
|
||||||
| Centrist Support | 0.130 | 0.437 | +0.307 | +0.88 | 1295 / 405 |
|
| Centrist Support | 0.130 | 0.423 | +0.293 | +0.85 | 1295 / 437 |
|
||||||
| Extremity | 2.28 | 2.18 | -0.10 | -0.14 | 1295 / 405 |
|
| Extremity | 2.28 | 2.17 | -0.10 | -0.14 | 1295 / 437 |
|
||||||
|
|
||||||
**Interpretation gate:** If opposition metrics also rise post-2024, the shift is not
|
**Interpretation gate:** If opposition metrics also rise post-2024, the shift is not
|
||||||
purely coalition-driven. If opposition metrics stay flat while overall metrics rise,
|
purely coalition-driven. If opposition metrics stay flat while overall metrics rise,
|
||||||
@@ -54,7 +54,7 @@ the shift is coalition-specific.
|
|||||||
|
|
||||||
## 3. Coalition Composition
|
## 3. Coalition Composition
|
||||||
|
|
||||||
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.
|
2016-2017: Rutte II (VVD/PvdA). 2018-2021: Rutte III (VVD/CDA/D66/CU). 2022-2023: Rutte IV (VVD/D66/CDA/CU). 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.
|
||||||
|
|
||||||
Submitter party is parsed from motion title prefixes
|
Submitter party is parsed from motion title prefixes
|
||||||
(e.g., "Motie van het lid Wilders over ..."). Only the lead submitter's party is
|
(e.g., "Motie van het lid Wilders over ..."). Only the lead submitter's party is
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 169 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 153 KiB |
Reference in New Issue
Block a user