feat: add axis classifier with party ideology reference data
classify_axes() correlates per-party PCA positions against party_ideologies.csv to assign honest dynamic labels (Links-Rechts, Coalitie-Oppositie, etc.) instead of always assuming the first PCA axis is left-right.
This commit is contained in:
@@ -365,3 +365,107 @@ def test_compute_party_discipline_empty_range(monkeypatch):
|
||||
)
|
||||
|
||||
assert df.empty
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests for analysis.axis_classifier
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
import importlib
|
||||
|
||||
|
||||
def _fresh_classifier(monkeypatch):
|
||||
"""Import axis_classifier with cleared module-level caches."""
|
||||
import analysis.axis_classifier as _cls
|
||||
|
||||
monkeypatch.setattr(_cls, "_ideology_cache", None)
|
||||
monkeypatch.setattr(_cls, "_coalition_cache", None)
|
||||
return _cls
|
||||
|
||||
|
||||
def test_axis_label_left_right(tmp_path, monkeypatch):
|
||||
"""Positions that closely correlate with left_right scores → label 'Links–Rechts'."""
|
||||
_cls = _fresh_classifier(monkeypatch)
|
||||
|
||||
(tmp_path / "party_ideologies.csv").write_text(
|
||||
"party,left_right,progressive\n"
|
||||
"VVD,0.65,0.10\n"
|
||||
"PvdA,-0.70,0.75\n"
|
||||
"SP,-0.90,0.50\n"
|
||||
"PVV,0.90,-0.50\n"
|
||||
"D66,-0.10,0.85\n"
|
||||
"CDA,0.25,-0.45\n"
|
||||
)
|
||||
(tmp_path / "coalition_membership.csv").write_text("window_id,party\n")
|
||||
|
||||
# X values are the party's left_right scores — perfect correlation
|
||||
positions_by_window = {
|
||||
"2022": {
|
||||
"VVD": (0.65, 0.10),
|
||||
"PvdA": (-0.70, 0.20),
|
||||
"SP": (-0.90, 0.30),
|
||||
"PVV": (0.90, -0.10),
|
||||
"D66": (-0.10, 0.40),
|
||||
"CDA": (0.25, -0.20),
|
||||
}
|
||||
}
|
||||
axes = {"x_axis": None, "y_axis": None, "method": "pca"}
|
||||
|
||||
result = _cls.classify_axes(positions_by_window, axes, str(tmp_path / "motions.db"))
|
||||
|
||||
assert result["x_label"] == "Links\u2013Rechts"
|
||||
assert result["x_quality"]["2022"] >= 0.65
|
||||
|
||||
|
||||
def test_axis_label_coalition_dominant(tmp_path, monkeypatch):
|
||||
"""Positions that match coalition pattern but NOT left-right → 'Coalitie–Oppositie'."""
|
||||
_cls = _fresh_classifier(monkeypatch)
|
||||
|
||||
(tmp_path / "party_ideologies.csv").write_text(
|
||||
"party,left_right,progressive\n"
|
||||
"VVD,0.65,0.10\n"
|
||||
"PvdA,-0.70,0.75\n"
|
||||
"SP,-0.90,0.50\n"
|
||||
"PVV,0.90,-0.50\n"
|
||||
"D66,-0.10,0.85\n"
|
||||
"CDA,0.25,-0.45\n"
|
||||
)
|
||||
# 2016: Rutte II coalition = VVD + PvdA
|
||||
(tmp_path / "coalition_membership.csv").write_text(
|
||||
"window_id,party\n2016,VVD\n2016,PvdA\n"
|
||||
)
|
||||
|
||||
# Coalition parties (VVD + PvdA) at x ≈ +1, opposition at x ≈ -1.
|
||||
# VVD (right) and PvdA (left) are both near +1 → low left_right correlation
|
||||
# but high coalition correlation.
|
||||
positions_by_window = {
|
||||
"2016": {
|
||||
"VVD": (0.95, 0.10),
|
||||
"PvdA": (0.90, 0.20),
|
||||
"SP": (-0.85, 0.30),
|
||||
"PVV": (-0.95, -0.10),
|
||||
"D66": (-0.80, 0.40),
|
||||
"CDA": (-0.75, -0.20),
|
||||
}
|
||||
}
|
||||
axes = {"x_axis": None, "y_axis": None, "method": "pca"}
|
||||
|
||||
result = _cls.classify_axes(positions_by_window, axes, str(tmp_path / "motions.db"))
|
||||
|
||||
assert result["x_label"] == "Coalitie\u2013Oppositie"
|
||||
assert "coalitie" in result["x_interpretation"]["2016"].lower()
|
||||
|
||||
|
||||
def test_axis_classifier_missing_csv(tmp_path, monkeypatch):
|
||||
"""Missing party_ideologies.csv → returns axes dict unchanged, no exception."""
|
||||
_cls = _fresh_classifier(monkeypatch)
|
||||
|
||||
# No CSVs written — directory exists but files do not
|
||||
positions_by_window = {"2022": {"VVD": (1.0, 0.5), "PvdA": (-1.0, 0.3)}}
|
||||
axes = {"x_axis": None, "y_axis": None, "method": "pca"}
|
||||
|
||||
result = _cls.classify_axes(positions_by_window, axes, str(tmp_path / "motions.db"))
|
||||
|
||||
# Must not crash and must return the original axes dict unchanged
|
||||
assert result is axes
|
||||
assert "x_label" not in result
|
||||
|
||||
Reference in New Issue
Block a user