3.8 KiB
| title | module | date | problem_type | component | severity | tags |
|---|---|---|---|---|---|---|
| SVD theme divergence from actual party positions | analysis | 2026-04-05 | logic_error | analysis | medium | [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
- Pole labels (
left_pole/right_pole) described parties that weren't actually on those sides after flip - The flip mechanism (
compute_flip_direction) worked correctly, but theme text was stale
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:
- Motion sponsors ≠ voting patterns (a motion sponsored by FVD/DENK may be voted on differently by all parties)
- The "long tail" of motions also loads on each component and can shift party positions
- 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:
# 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
Added left_pole and right_pole fields to all 10 SVD_THEMES entries. These describe what's on the left and right sides AFTER flip, decoupling label text from raw SVD math. Updated 4 rendering locations in explorer.py to use these semantic labels with backward compat fallback.
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:
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.pyafter 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 - Consider automating theme generation from party positions + motion analysis
Related Files
analysis/config.py— SVD_THEMES with left_pole/right_pole fieldsexplorer.py— rendering functions using semantic pole labelsanalysis/svd_labels.py— compute_flip_direction() functionscripts/validate_svd_themes.py— validation hook