UI improvements + add axis orientation test
- Rename app to 'Motief: de stematlas' in Home.py - Remove PCA variance caption from compass tab - Hardcode db_path and window_size; remove sidebar inputs - Change trajectories default to [CDA, D66, VVD] - Move quiz to pages/1_Stemwijzer.py; wrap in st.form - Remove quiz tab from main explorer - Add pytest dev dep + fix test fixtures (_load_mp_vectors_for_window) - Add test_pca_axis_orientation with proper PCA variance dominance
This commit is contained in:
@@ -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"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user