--- date: 2026-04-05 topic: right-wing-party-axis-validation --- # Right-Wing Party Axis Validation ## Problem Frame The project convention states that PVV, FVD, JA21, and SGP must appear on the RIGHT side of all axes in visualizations (AGENTS.md). This is the #1 documented convention with zero automated enforcement. A single test prevents regression when SVD labels change or new components are added. ## Requirements **R1. Canonical party sets defined once, imported everywhere** - Define `CANONICAL_RIGHT = frozenset({"PVV", "FVD", "JA21", "SGP"})` in `analysis/config.py` - Define `CANONICAL_LEFT = frozenset({"SP", "PvdA", "GL", "GroenLinks", "GroenLinks-PvdA", "DENK", "PvdD", "Volt"})` in `analysis/config.py` — matches svd_labels.py LEFT_PARTIES exactly - All code that checks political orientation (svd_labels.py, political_axis.py) imports from config instead of defining inline **R2. Validation test loads real data from DuckDB** - Test file: `tests/test_axis_political_orientation.py` - Uses existing data loading functions (`load_party_scores_all_windows_aligned` from `analysis/explorer_data.py`) - No synthetic data — validates against actual `party_axis_scores` table **R3. 2D political compass orientation check (statistical, not per-party)** - `party_axis_scores` table has `x_axis_aligned` (component 1) and `y_axis_aligned` (component 2) - For each window, validate both axes using **mean scores**: - **Axis 1 (x)**: Compute mean of `CANONICAL_RIGHT` x-values and mean of `CANONICAL_LEFT` x-values. Assert `right_mean > left_mean` - **Axis 2 (y)**: Same for y-values. Assert `right_mean > left_mean` - "Right on right" means the **average** right party is right of the **average** left party — individual parties may deviate slightly (e.g., one right party slightly negative is fine) - `compute_flip_direction` already implements this logic (compares group means) — use it - Skips parties not present in a given window (graceful, not a failure) **R4. `compute_flip_direction` consistency check** - After loading data, call `compute_flip_direction(1, party_scores)` and `compute_flip_direction(2, party_scores)` per window - Assert both return `False` (no flip needed) when data is already correctly oriented - If either returns `True`, the data violates the convention and the test fails with a clear message **R5. Clear failure messages** - When orientation check fails, report: window, axis (x/y), right_mean, left_mean, difference - Example: `"Window '2021-2023', x-axis: right_mean=-0.12, left_mean=0.08 (right parties on LEFT side — flip direction=True)"` ## Success Criteria - Test runs as part of `pytest` suite (`.venv/bin/python -m pytest tests/test_axis_political_orientation.py`) - Test passes with current data (convention currently holds — this establishes the baseline) - If convention is violated in future data, test fails with actionable message - Test works for all windows in the database (not just current) - Statistical check (mean-based) — test passes even if individual parties deviate slightly from group mean ## Scope Boundaries - **Not included**: Testing unaligned scores (only aligned scores are validated — these are what users see) - **Not included**: VVD, NSC, BBB, CDA, ChristenUnie — these are center parties, not right-wing per AGENTS.md convention - **Not included**: Per-party strict sign checks (statistical mean check is sufficient and more robust) - **Not included**: Updating `political_axis.py` — R1 only updates `svd_labels.py` to import from config; `political_axis.py` uses different party sets for PCA centroid orientation and is out of scope ## Key Decisions - **Canonical sets match AGENTS.md for right, svd_labels.py for left**: `CANONICAL_RIGHT = {PVV, FVD, JA21, SGP}` matches AGENTS.md exactly. `CANONICAL_LEFT = {SP, PvdA, GL, GroenLinks, GroenLinks-PvdA, DENK, PvdD, Volt}` matches svd_labels.py LEFT_PARTIES exactly. - **Single unified source of truth in config.py**: `CANONICAL_RIGHT` and `CANONICAL_LEFT` frozensets go in `config.py` — it's a prerequisite for the test to work correctly. Only `svd_labels.py` is updated to import from config; `political_axis.py` is out of scope (uses party sets for PCA centroid orientation, not the same usage). - **Aligned scores only**: Unaligned scores may vary across windows due to Procrustes alignment drift; aligned scores are the stable, user-facing representation. - **Statistical (mean-based) validation, not per-party**: The orientation check compares group means, not individual party scores. A single right party being slightly negative is not a failure — the mean right score must exceed the mean left score. ## Dependencies / Assumptions - DuckDB database is populated with `party_axis_scores` table with `x_axis_aligned` and `y_axis_aligned` columns (verified) - `analysis/explorer_data.py` functions work correctly (already tested) - `_PARTY_NORMALIZE` already exists in `config.py` (lines 247-256) — use it for party name alias normalization - `config.py` currently lacks `CANONICAL_RIGHT`/`CANONICAL_LEFT` frozensets — these must be added as part of R1 - `compute_flip_direction()` in `svd_labels.py` currently uses inline `RIGHT_PARTIES`/`LEFT_PARTIES` — must be updated to import from config after R1 ## Outstanding Questions All resolved. Key decisions documented above. ## Next Steps → `/ce:plan` for structured implementation planning