You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
motief/docs/solutions/ui-bugs/svd-compass-components-posi...

4.4 KiB

title date module problem_type component symptoms root_cause resolution_type severity tags
SVD compass vs components tab party ordering inconsistency 2026-04-13 analysis ui_bug analysis [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] logic_error code_fix medium [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
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:

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
  • explorer.py_get_aligned_party_coords() helper, component 1-2 data loading
  • analysis/political_axis.pyload_positions() and PCA alignment logic
  • analysis/explorer_data.pyload_party_scores_all_windows() for components 3-10
  • This fix builds on the earlier SVD axis label alignment fix (docs/solutions/ui-bugs/svd-axis-pole-labels-incorrect-after-flip.md)