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

main
Sven Geboers 3 weeks ago
parent 7ff3fec992
commit 2d5b28fe1b
  1. 38
      analysis/right_wing/overton_breakpoint_analysis.py
  2. 309
      docs/plans/2026-05-25-001-overton-window-analysis-gaps-plan.md
  3. 255
      docs/plans/2026-05-26-001-overton-improvements-extensions-plan.md
  4. 6
      reports/overton_window/breakpoint_analysis.md
  5. BIN
      reports/overton_window/breakpoint_figure_1.png
  6. BIN
      reports/overton_window/breakpoint_figure_2.png

@ -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

Loading…
Cancel
Save