@ -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 (