--- title: "SVD compass vs components tab party ordering inconsistency" date: 2026-04-13 module: analysis problem_type: ui_bug component: analysis symptoms: - "SVD components tab and political compass showed different party orderings for the same data" - "Party positions in compass did not match positions in SVD Components tab for components 1-2" root_cause: logic_error resolution_type: code_fix severity: medium tags: - svd - pca - compass - alignment - procrustes --- # SVD Compass vs Components Tab Party Ordering Inconsistency ## Problem The SVD Components tab and the political compass visualization showed different party orderings for the same data. Users would see a party at position X in the compass, but the same party at position Y in the SVD Components tab for components 1-2. ## Symptoms - Same party (e.g., PVV) has different x-coordinate in compass vs SVD Components tab - Party ordering along political axis differs between the two views - Confusing user experience when exploring voting patterns ## What Didn't Work Using raw SVD scores directly in the SVD Components tab. The compass uses Procrustes-aligned PCA positions from `load_positions()`, but components 1-2 in the SVD Components tab were using unaligned raw SVD scores. These are in different coordinate frames. ## Solution For components 1-2 in the SVD Components tab, use aligned PCA positions from `load_positions()` (same data source as compass) instead of raw SVD scores. Components 3-10 continue to use raw SVD scores. Added `_get_aligned_party_coords()` helper function in `explorer.py` that: 1. Calls `load_positions()` to get aligned MP positions 2. Aggregates MP positions to party centroids using `load_party_map()` 3. Returns `{party: (x, y)}` coordinates ```python def _get_aligned_party_coords(window: str) -> Dict[str, Tuple[float, float]]: """Get party (x, y) coordinates from aligned PCA positions for a window.""" positions_by_window, _ = load_positions(db_path, "annual") window_pos = positions_by_window.get(window, {}) if not window_pos: return {} # Load party map to convert MP names to parties _party_map = load_party_map(db_path) # Aggregate MP positions to party centroids party_coords: Dict[str, List[Tuple[float, float]]] = {} for mp_name, (x, y) in window_pos.items(): party = _party_map.get( mp_name, _party_map.get(mp_name.split("(")[0].strip(), None) ) if party: party_coords.setdefault(party, []).append((x, y)) # Compute mean position per party return { party: ( float(np.mean([c[0] for c in coords])), float(np.mean([c[1] for c in coords])), ) for party, coords in party_coords.items() if coords } ``` The rendering code now branches based on component: ```python if comp_sel <= 2: # Components 1-2: use aligned PCA positions (consistent with compass) aligned_coords = _get_aligned_party_coords(svd_window) for party, (x, y) in aligned_coords.items(): party_1d_coords[party] = (x,) if comp_sel == 1 else (y,) else: # Components 3-10: use raw SVD scores idx = comp_sel - 1 for party, scores in party_scores.items(): if scores and len(scores) > idx: party_1d_coords[party] = (float(scores[idx]),) ``` ## Why This Works 1. **Same coordinate frame**: Both visualizations now use Procrustes-aligned PCA positions for components 1-2 2. **Consistent party centroids**: Both aggregate MP positions to party centroids the same way 3. **Clear separation of concerns**: Components 1-2 represent political compass axes (need alignment), while components 3-10 are topic dimensions (use raw SVD scores) ## Prevention - When adding new SVD/PCA visualizations, always check which data source the compass uses and use the same source for consistency - Document coordinate frame requirements: "aligned" vs "raw" SVD scores have different interpretations - Consider adding integration tests that verify compass and SVD Components tab show consistent positions ## Related Files - `explorer.py` — `_get_aligned_party_coords()` helper, component 1-2 data loading - `analysis/political_axis.py` — `load_positions()` and PCA alignment logic - `analysis/explorer_data.py` — `load_party_scores_all_windows()` for components 3-10 ## Related Issues - This fix builds on the earlier SVD axis label alignment fix (`docs/solutions/ui-bugs/svd-axis-pole-labels-incorrect-after-flip.md`)