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
This commit is contained in:
+56
-15
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user