fix: use aligned PCA positions for SVD components 1-2 (consistent with compass)

Previously the SVD components tab used raw SVD scores while the compass
used Procrustes-aligned PCA positions. This caused party orderings to
differ between the two visualizations.

Changes:
- Components 1-2 now use aligned positions from load_positions()
  (same as compass) for consistent party ordering
- Components 3-10 continue to use raw SVD scores
- Added _get_aligned_party_coords() helper to convert aligned MP
  positions to party centroids
main
Sven Geboers 3 weeks ago
parent 4d6c777d54
commit 12936c52c1
  1. 71
      explorer.py

@ -2410,6 +2410,9 @@ def build_svd_components_tab(db_path: str) -> None:
Reads thoughts/explorer/top_svd_top_motions.json and displays a selector
for components 1..10 with theme labels/explanations and a detail pane per motion.
Components 1-2 use aligned PCA positions (consistent with compass).
Components 3-10 use raw SVD scores.
"""
st.subheader("🔬 SVD Assen — politieke polarisatiethema's")
st.markdown(
@ -2556,7 +2559,7 @@ def build_svd_components_tab(db_path: str) -> None:
key=f"svd_window_{comp_sel}",
)
# Load party scores for the selected window
# Load party scores for the selected window (used for components 3-10)
if svd_window == "current_parliament":
party_scores = party_scores_default
else:
@ -2567,6 +2570,55 @@ def build_svd_components_tab(db_path: str) -> None:
{p: len(v) for p, v in party_mp_vectors.items()} if party_mp_vectors else {}
)
# For components 1-2, use aligned positions from load_positions (same as compass)
# for consistency. For components 3-10, use raw SVD scores.
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
}
# Extract 1D scores for this component
party_1d_coords: dict = {}
if comp_sel <= 2:
# Components 1-2: use aligned PCA positions from load_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 # Convert to 0-indexed
for party, scores in party_scores.items():
try:
if scores and len(scores) > idx:
party_1d_coords[party] = (float(scores[idx]),)
except Exception:
continue
# Auto-compute flip directions for ALL components 1-10 based on party centroids.
# Each window's SVD has arbitrary sign orientation, so we compute flip per component
# to ensure canonical right parties (PVV, FVD, JA21, SGP) appear on the RIGHT.
@ -2587,16 +2639,6 @@ def build_svd_components_tab(db_path: str) -> None:
"flip": computed_flips.get(comp_sel, theme.get("flip", False)),
}
# Extract 1D scores for this component (ALL components use raw SVD values)
party_1d_coords: dict = {}
idx = comp_sel - 1 # Convert to 0-indexed
for party, scores in party_scores.items():
try:
if scores and len(scores) > idx:
party_1d_coords[party] = (float(scores[idx]),)
except Exception:
continue
# Filter parties by minimum MP count
if min_mps > 1 and party_mp_counts:
valid_parties = {p for p, count in party_mp_counts.items() if count >= min_mps}
@ -2615,10 +2657,9 @@ def build_svd_components_tab(db_path: str) -> None:
has_current = "current_parliament" in available_windows
all_windows = year_windows + (["current_parliament"] if has_current else [])
# ALL components use raw (non-aligned) SVD vectors.
# Procrustes alignment rotates the full vector space which makes scores
# incomparable with the single-window view. Per-window flip computation
# handles orientation alignment for the trajectory.
# For components 1-2, use aligned PCA positions for consistency with compass.
# For components 3-10, use raw SVD scores.
# Per-window flip computation handles orientation alignment for the trajectory.
party_scores_by_window = load_party_scores_all_windows(db_path, all_windows)
_render_svd_time_trajectory(

Loading…
Cancel
Save