"""Test that SVD component 1 scores match compass x positions for current_parliament. Bug: The compass filters current_parliament to active MPs only (still seated) at explorer.py line 1473. The SVD components tab previously did NOT do this filter, causing party means to differ (e.g. VVD: ~0.108 vs ~0.335). Fix: get_aligned_party_scores() now accepts an active_mps parameter and filters current_parliament when provided. """ import numpy as np def test_svd_comp1_matches_compass_for_current_parliament_with_active_filter(): """VVD comp1 from get_aligned_party_scores should match compass when active_mps provided. REGRESSION TEST: Without the active_mps filter, VVD comp1 ≈ 0.108 (wrong). With the active_mps filter, VVD comp1 ≈ 0.335 (matching compass). """ from explorer import get_aligned_party_scores, load_active_mps, load_party_map from analysis.political_axis import compute_2d_axes, compute_nd_axes from analysis.explorer_data import get_uniform_dim_windows db_path = "data/motions.db" windows = get_uniform_dim_windows(db_path) active_mps = load_active_mps(db_path) party_map = load_party_map(db_path) # --- Compass reference: VVD mean x position --- # Compass uses compute_2d_axes + filters to active_mps for current_parliament pos2d, _ = compute_2d_axes(db_path, window_ids=windows) cp_pos2d_active = { mp: xy for mp, xy in pos2d.get("current_parliament", {}).items() if mp in active_mps } vvd_mps_2d = [mp for mp in cp_pos2d_active if party_map.get(mp) == "VVD"] compass_vvd_mean = float(np.mean([cp_pos2d_active[mp][0] for mp in vvd_mps_2d])) # --- SVD tab: WITH active_mps filter --- svd_with_filter = get_aligned_party_scores( db_path, "current_parliament", active_mps ) vvd_comp1_with = float(svd_with_filter["VVD"][0]) # These MUST match (within tolerance) diff = abs(compass_vvd_mean - vvd_comp1_with) assert diff < 0.001, ( f"VVD comp1 mismatch: compass={compass_vvd_mean:.6f}, " f"SVD with filter={vvd_comp1_with:.6f}, diff={diff:.6f}" ) def test_without_active_filter_gives_wrong_mean(): """Without active_mps filter, get_aligned_party_scores gives a different VVD mean. The unfiltered mean includes all historical VVD MPs, while the filtered mean includes only currently-seated MPs. These must differ significantly. The exact values drift with the database; only the delta is asserted here. The compass-match assertion lives in the test above. """ from explorer import get_aligned_party_scores, load_active_mps from analysis.explorer_data import get_uniform_dim_windows db_path = "data/motions.db" windows = get_uniform_dim_windows(db_path) active_mps = load_active_mps(db_path) # Without filter (BUGGY) svd_no_filter = get_aligned_party_scores( db_path, "current_parliament", active_mps=None ) vvd_no_filter = float(svd_no_filter["VVD"][0]) # With filter (CORRECT) svd_with_filter = get_aligned_party_scores( db_path, "current_parliament", active_mps=active_mps ) vvd_with_filter = float(svd_with_filter["VVD"][0]) # The two values must differ significantly diff = abs(vvd_no_filter - vvd_with_filter) assert diff > 0.1, ( f"Expected large diff between unfiltered ({vvd_no_filter:.4f}) and " f"filtered ({vvd_with_filter:.4f}), got diff={diff:.4f}" ) def test_historical_window_unchanged(): """Historical windows (e.g. '2025') should NOT be affected by active_mps filter.""" from explorer import get_aligned_party_scores db_path = "data/motions.db" svd_no_filter = get_aligned_party_scores(db_path, "2025", active_mps=None) svd_with_filter = get_aligned_party_scores(db_path, "2025", active_mps={"fake_mp"}) # For historical windows, the output should be identical regardless of active_mps # (the filter is only applied for current_parliament) for party in svd_no_filter: if party in svd_with_filter: diff = abs(svd_no_filter[party][0] - svd_with_filter[party][0]) assert diff < 1e-6, ( f"Historical window changed with active_mps filter for {party}" )