@ -5,6 +5,19 @@ import sys
import pytest
# ---------------------------------------------------------------------------
# Helpers shared by orientation tests
# ---------------------------------------------------------------------------
def _make_fake_traj ( aligned ) :
fake = types . SimpleNamespace ( )
fake . _load_window_ids = lambda db : list ( aligned . keys ( ) )
fake . _load_mp_vectors_for_window = lambda db , w : aligned . get ( w , { } )
fake . _procrustes_align_windows = lambda x : aligned
return fake
def test_compute_2d_axes_pca_synthetic ( monkeypatch ) :
""" Synthetic test for compute_2d_axes using patched alignment helper. """
@ -14,15 +27,15 @@ def test_compute_2d_axes_pca_synthetic(monkeypatch):
# _load_window_ids should return ordered windows
fake_traj . _load_window_ids = lambda db : [ " w1 " , " w2 " ]
# _load_mp_vectors_for_window is not used because we patch _procrustes_align_windows
fake_traj . _load_mp_vectors_for_window = lambda db , w : { }
# Provide aligned vectors directly
aligned = {
" w1 " : { " Alice " : np . array ( [ 1.0 , 0.0 , 0.0 ] ) , " Bob " : np . array ( [ 0.0 , 1.0 , 0.0 ] ) } ,
" w2 " : { " Alice " : np . array ( [ 0.8 , 0.2 , 0.0 ] ) , " Bob " : np . array ( [ 0.1 , 0.9 , 0.0 ] ) } ,
}
# _load_mp_vectors_for_window returns the pre-aligned vectors (needed for padding step)
fake_traj . _load_mp_vectors_for_window = lambda db , w : aligned . get ( w , { } )
fake_traj . _procrustes_align_windows = lambda x : aligned
# Insert fake module into sys.modules for import by analysis.political_axis
@ -42,3 +55,95 @@ def test_compute_2d_axes_pca_synthetic(monkeypatch):
assert np . isfinite ( coord [ 0 ] ) and np . isfinite ( coord [ 1 ] )
assert axis_def . get ( " method " ) == " pca "
def test_pca_axis_orientation ( monkeypatch ) :
""" PCA axes must be oriented so right parties score higher on X and
progressive parties score higher on Y than their respective opposites .
We construct a minimal vote - matrix world where :
- Right MPs ( PVV , VVD members ) cluster in one direction on dim - 0.
- Left MPs ( SP , GroenLinks - PvdA members ) cluster in the opposite direction .
- Progressive MPs cluster on dim - 1 ; conservative MPs on the opposite side .
The orientation logic in compute_2d_axes should flip axis signs so that
right_x > left_x and prog_y > cons_y regardless of the raw SVD sign .
"""
# Build vectors so that right parties are at +1 on dim-0 and
# progressive parties are at +1 on dim-1.
# We deliberately negate them to test that auto-orient flips them back.
# Right/left use magnitude 3, prog/cons use magnitude 1 so that dim-0
# dominates PCA variance — ensuring PC1 = left-right axis, PC2 = prog-cons.
right_vec = np . array ( [ - 3.0 , 0.0 , 0.0 ] ) # intentionally negative on dim-0
left_vec = np . array ( [ 3.0 , 0.0 , 0.0 ] ) # intentionally positive on dim-0
prog_vec = np . array ( [ 0.0 , - 1.0 , 0.0 ] ) # intentionally negative on dim-1
cons_vec = np . array ( [ 0.0 , 1.0 , 0.0 ] ) # intentionally positive on dim-1
aligned = {
" w1 " : {
# Right-leaning MPs
" Wilders, G. " : right_vec ,
" Rutte, M. " : right_vec + np . array ( [ 0.0 , 0.0 , 0.05 ] ) ,
# Left-leaning MPs
" Marijnissen, L. " : left_vec ,
" Klever, A. " : left_vec + np . array ( [ 0.0 , 0.0 , 0.05 ] ) ,
# Progressive MPs
" Bromet, L. " : prog_vec ,
" Nijboer, H. " : prog_vec + np . array ( [ 0.0 , 0.0 , - 0.05 ] ) ,
# Conservative MPs
" Segers, G. " : cons_vec ,
" Omtzigt, P. " : cons_vec + np . array ( [ 0.0 , 0.0 , - 0.05 ] ) ,
}
}
# mp_metadata rows used by the orientation code (party affiliation)
mp_metadata = [
( " Wilders, G. " , " PVV " ) ,
( " Rutte, M. " , " VVD " ) ,
( " Marijnissen, L. " , " SP " ) ,
( " Klever, A. " , " GroenLinks-PvdA " ) ,
( " Bromet, L. " , " GroenLinks-PvdA " ) ,
( " Nijboer, H. " , " SP " ) ,
( " Segers, G. " , " CDA " ) ,
( " Omtzigt, P. " , " Nieuw Sociaal Contract " ) ,
]
fake_traj = _make_fake_traj ( aligned )
monkeypatch . setitem ( sys . modules , " analysis.trajectory " , fake_traj )
# Patch duckdb so the orientation helper can fetch mp_metadata
import types as _types
fake_conn = _types . SimpleNamespace (
execute = lambda q : _types . SimpleNamespace ( fetchall = lambda : mp_metadata ) ,
close = lambda : None ,
)
import duckdb as _duckdb
monkeypatch . setattr ( _duckdb , " connect " , lambda db_path , * * kw : fake_conn )
# Need to reload the module so monkeypatched sys.modules takes effect
import importlib , analysis . political_axis as _ax
importlib . reload ( _ax )
from analysis . political_axis import compute_2d_axes
positions_by_window , axis_def = compute_2d_axes (
db_path = " dummy " , window_ids = [ " w1 " ] , method = " pca "
)
pos = positions_by_window [ " w1 " ]
# X-axis: right parties should score higher than left parties
right_x = np . mean ( [ pos [ " Wilders, G. " ] [ 0 ] , pos [ " Rutte, M. " ] [ 0 ] ] )
left_x = np . mean ( [ pos [ " Marijnissen, L. " ] [ 0 ] , pos [ " Klever, A. " ] [ 0 ] ] )
assert right_x > left_x , (
f " Expected right parties (x= { right_x : .3f } ) > left parties (x= { left_x : .3f } ) on X-axis "
)
# Y-axis: progressive parties should score higher than conservative parties
prog_y = np . mean ( [ pos [ " Bromet, L. " ] [ 1 ] , pos [ " Nijboer, H. " ] [ 1 ] ] )
cons_y = np . mean ( [ pos [ " Segers, G. " ] [ 1 ] , pos [ " Omtzigt, P. " ] [ 1 ] ] )
assert prog_y > cons_y , (
f " Expected progressive parties (y= { prog_y : .3f } ) > conservative parties (y= { cons_y : .3f } ) on Y-axis "
)