--- title: "SVD theme divergence from actual party positions" module: analysis date: 2026-04-05 problem_type: logic_error component: analysis severity: medium tags: [svd, themes, party-positions, validation, data-drift] --- # SVD Theme Divergence from Actual Party Positions ## Problem SVD axis themes in `analysis/config.py` can drift from actual party positions in `svd_vectors`. Themes are derived from subagent summaries of top motions, but party positions reflect voting on ALL motions. When the SVD is recomputed or voting patterns shift, themes may no longer match the data. ## Symptoms - Axis 4 theme said "Mainstreampartijen versus FVD/DENK-oppositie" but actual party positions showed NSC (-24.47) and BBB (-4.58) on the left extreme, D66 (10.53)/CDA (10.11)/JA21 (9.90) on the right extreme, and FVD/DENK in the middle - The flip mechanism (`compute_flip_direction`) worked correctly, but theme text was stale - **NOTE (2026-04-12): The `left_pole`/`right_pole` static fields added here caused the same bug — when runtime flip differed from static config flip, labels pointed to wrong sides. These fields were removed. See `docs/solutions/ui-bugs/svd-axis-pole-labels-incorrect-after-flip.md` for the corrected approach.** ## Root Cause Themes were written manually by subagents summarizing top 20 motions per component. This captures what motions drive each axis, but party positions come from how parties voted on ALL 8,732 motions. The divergence occurs because: 1. Motion sponsors ≠ voting patterns (a motion sponsored by FVD/DENK may be voted on differently by all parties) 2. The "long tail" of motions also loads on each component and can shift party positions 3. No automated validation existed to detect when themes drift from actual data ## Solution ### 1. Fixed axis 4 theme to match actual data Updated `analysis/config.py` component 4: ```python # Before (wrong): "label": "Mainstreampartijen versus FVD/DENK-oppositie", "left_pole": "FVD en DENK: oppositieposities buiten de mainstream", "right_pole": "Mainstreampartijen: D66, CDA, VVD, PVV, GL-PvdA, SP, Volt, 50PLUS", # After (matches actual positions): "label": "NSC/BBB versus D66/CDA/JA21 (indicatief)", "left_pole": "NSC, BBB — moties met andere focus", "right_pole": "D66, CDA, JA21 — moties met brede steun", ``` ### 2. Added semantic left_pole/right_pole labels — SUPERSEDED (2026-04-12) **This approach caused the same bug.** The static `left_pole`/`right_pole` fields assumed a fixed flip direction, but `compute_flip_direction` determines flip at runtime. When runtime flip differed from static config, labels pointed to wrong sides. These fields were removed. See `docs/solutions/ui-bugs/svd-axis-pole-labels-incorrect-after-flip.md` for the corrected approach. ### 3. Created validation hook Created `scripts/validate_svd_themes.py` that validates: - Canonical right-wing parties (PVV, FVD, JA21, SGP) appear on the right side after flip - Theme pole labels match actual party positions - Uses full vectors for flip computation (not single-component scores) Usage: ```bash uv run python scripts/validate_svd_themes.py --db data/motions.db ``` Returns exit code 1 if any divergence found — suitable for CI integration. ## Why This Works The flip mechanism (`compute_flip_direction`) correctly positions canonical right parties on the right side by comparing mean scores. The validation hook uses the same function with full average vectors to verify post-flip positions. Theme pole labels are now pre-computed semantic descriptions that match the flipped orientation, not raw SVD positive/negative poles. ## Prevention - Run `scripts/validate_svd_themes.py` after any SVD recomputation - Add to CI pipeline: `uv run python scripts/validate_svd_themes.py --db data/motions.db` - When updating themes, verify against actual party positions from `svd_vectors`, not just motion sponsors - **NEVER add static `left_pole`/`right_pole` fields** — derive labels at render time from runtime flip (see corrected approach in `svd-axis-pole-labels-incorrect-after-flip.md`) - Run `tests/test_svd_axis_alignment.py` to validate alignment after SVD recomputation ## Related Files - `analysis/config.py` — SVD_THEMES (no `left_pole`/`right_pole`) - `explorer.py` — label derivation and component 3-10 scoring - `analysis/svd_labels.py` — compute_flip_direction() function - `scripts/validate_svd_themes.py` — validation hook - `tests/test_svd_axis_alignment.py` — alignment tests (added 2026-04-12)