docs: add improvement roadmap, research notes, and solution docs
- Add 2026-04-24 ROADMAP with 5 phases / 17 items - Add detailed implementation plans for P1-001 through P4-005 - Add research artifacts and solution docs from ledger merge - Add test for SVD component 1 compass alignment
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
"""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 wrong VVD mean.
|
||||
|
||||
This documents the original bug: without filtering, VVD comp1 ≈ 0.108
|
||||
(average of all historical VVD MPs). With filter, VVD comp1 ≈ 0.335
|
||||
(only currently-seated VVD MPs, matching compass).
|
||||
"""
|
||||
from explorer import get_aligned_party_scores, load_active_mps
|
||||
from analysis.political_axis import 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)
|
||||
|
||||
# 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 buggy value should be significantly lower than the correct one
|
||||
# (historical MPs have lower scores, dragging the mean down)
|
||||
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}"
|
||||
)
|
||||
|
||||
# The correct value should be ~0.33 (matching compass)
|
||||
assert 0.30 < vvd_with_filter < 0.40, (
|
||||
f"Active-filtered VVD comp1 ({vvd_with_filter:.4f}) should be ~0.335"
|
||||
)
|
||||
|
||||
|
||||
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}"
|
||||
)
|
||||
Reference in New Issue
Block a user