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
|
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.
|
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.subheader("🔬 SVD Assen — politieke polarisatiethema's")
|
||||||
st.markdown(
|
st.markdown(
|
||||||
@@ -2556,7 +2559,7 @@ def build_svd_components_tab(db_path: str) -> None:
|
|||||||
key=f"svd_window_{comp_sel}",
|
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":
|
if svd_window == "current_parliament":
|
||||||
party_scores = party_scores_default
|
party_scores = party_scores_default
|
||||||
else:
|
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 {}
|
{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.
|
# 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
|
# 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.
|
# 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)),
|
"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
|
# Filter parties by minimum MP count
|
||||||
if min_mps > 1 and party_mp_counts:
|
if min_mps > 1 and party_mp_counts:
|
||||||
valid_parties = {p for p, count in party_mp_counts.items() if count >= min_mps}
|
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
|
has_current = "current_parliament" in available_windows
|
||||||
all_windows = year_windows + (["current_parliament"] if has_current else [])
|
all_windows = year_windows + (["current_parliament"] if has_current else [])
|
||||||
|
|
||||||
# ALL components use raw (non-aligned) SVD vectors.
|
# For components 1-2, use aligned PCA positions for consistency with compass.
|
||||||
# Procrustes alignment rotates the full vector space which makes scores
|
# For components 3-10, use raw SVD scores.
|
||||||
# incomparable with the single-window view. Per-window flip computation
|
# Per-window flip computation handles orientation alignment for the trajectory.
|
||||||
# handles orientation alignment for the trajectory.
|
|
||||||
party_scores_by_window = load_party_scores_all_windows(db_path, all_windows)
|
party_scores_by_window = load_party_scores_all_windows(db_path, all_windows)
|
||||||
|
|
||||||
_render_svd_time_trajectory(
|
_render_svd_time_trajectory(
|
||||||
|
|||||||
Reference in New Issue
Block a user