fix: SVD tab now uses raw SVD values for ALL components 1-10
Previously, components 1-2 in the SVD tab used Procrustes-aligned PCA coordinates (from load_positions), which meant the SVD tab showed PCA dimensions of the 50D aligned space rather than the actual raw SVD components. This was a fundamental inconsistency — the SVD tab's component 2 showed completely different party ordering than the raw SVD component 2. Changes: - explorer.py: Unified all components 1-10 to use raw SVD values via load_party_axis_scores_for_window(). Removed the separate load_positions() path for components 1-2. Now all components use the same data source (50D vectors from svd_vectors table). - explorer.py: Updated flip computation to cover ALL components 1-10 (was range 3-11 for components 3-10 only). The compute_flip_direction function correctly determines sign for each component. - explorer.py: Unified rendering to always use _render_party_axis_chart_1d (was _render_party_axis_chart for components 1-2 using 2D coords). - explorer.py: Unified trajectory to always use load_party_scores_all_windows. - analysis/config.py: Updated component 1 label (simplified explanation, removed coalition-specific policy references). - analysis/config.py: Updated component 2 label to "Nationalistisch versus kosmopolitisch" matching raw SVD data (PVV/FVD at positive extreme, Volt/DENK/GL-PvdA at negative extreme). - tests: Updated test assertions to match new labels. - scripts/validate_svd_themes.py: Verified all components pass right-wing alignment check, config flip consistency, and theme pole consistency. Fixes the core inconsistency: SVD tab component 2 now uses the same raw SVD data as components 3-10, with consistent party ordering and labels. The compass remains a separate PCA-based visualization.
This commit is contained in:
@@ -10,8 +10,8 @@ def test_display_label_for_modal():
|
||||
y_label = axis_classifier.display_label_for_modal(None, "y")
|
||||
|
||||
# Should return component 1 and 2 labels from SVD_THEMES
|
||||
assert "Rechts kabinetsbeleid" in x_label or "links oppositiebeleid" in x_label
|
||||
assert "PVV/FVD-populisme" in y_label or "mainstream-partijen" in y_label
|
||||
assert "Rechts versus links" in x_label or "links" in x_label.lower()
|
||||
assert "Nationalistisch" in y_label or "kosmopolitisch" in y_label
|
||||
|
||||
|
||||
def test_display_label_for_modal_maps_as_labels():
|
||||
@@ -20,8 +20,8 @@ def test_display_label_for_modal_maps_as_labels():
|
||||
y_label = axis_classifier.display_label_for_modal("As 2", "y")
|
||||
|
||||
# Should return component 1 and 2 labels
|
||||
assert "Rechts kabinetsbeleid" in x_label or "links oppositiebeleid" in x_label
|
||||
assert "PVV/FVD-populisme" in y_label or "mainstream-partijen" in y_label
|
||||
assert "Rechts versus links" in x_label or "links" in x_label.lower()
|
||||
assert "Nationalistisch" in y_label or "kosmopolitisch" in y_label
|
||||
|
||||
|
||||
def test_display_label_for_modal_stempatroon():
|
||||
@@ -30,8 +30,8 @@ def test_display_label_for_modal_stempatroon():
|
||||
y_label = axis_classifier.display_label_for_modal("Stempatroon As 2", "y")
|
||||
|
||||
# Should return component 1 and 2 labels
|
||||
assert "Rechts kabinetsbeleid" in x_label or "links oppositiebeleid" in x_label
|
||||
assert "PVV/FVD-populisme" in y_label or "mainstream-partijen" in y_label
|
||||
assert "Rechts versus links" in x_label or "links" in x_label.lower()
|
||||
assert "Nationalistisch" in y_label or "kosmopolitisch" in y_label
|
||||
|
||||
|
||||
def test_classify_axes_modal_fallback(monkeypatch, tmp_path):
|
||||
@@ -84,10 +84,10 @@ def test_classify_axes_modal_fallback(monkeypatch, tmp_path):
|
||||
|
||||
# Should now return SVD component labels instead of hardcoded values
|
||||
assert (
|
||||
"Rechts kabinetsbeleid" in enriched["x_label"]
|
||||
or "links oppositiebeleid" in enriched["x_label"]
|
||||
"Rechts versus links" in enriched["x_label"]
|
||||
or "links" in enriched["x_label"].lower()
|
||||
)
|
||||
assert (
|
||||
"PVV/FVD-populisme" in enriched["y_label"]
|
||||
or "mainstream-partijen" in enriched["y_label"]
|
||||
"Nationalistisch" in enriched["y_label"]
|
||||
or "kosmopolitisch" in enriched["y_label"]
|
||||
)
|
||||
|
||||
@@ -5,7 +5,8 @@ import pytest
|
||||
|
||||
|
||||
def test_derive_labels_flip_true():
|
||||
"""When flip=True, positive_pole should be on left, negative_pole on right."""
|
||||
"""Labels should always reflect what's on each side, regardless of flip.
|
||||
negative_pole describes LEFT, positive_pole describes RIGHT."""
|
||||
theme = {
|
||||
"positive_pole": "Right-wing parties",
|
||||
"negative_pole": "Left-wing parties",
|
||||
@@ -15,17 +16,17 @@ def test_derive_labels_flip_true():
|
||||
pos_pole = theme.get("positive_pole", "")
|
||||
neg_pole = theme.get("negative_pole", "")
|
||||
|
||||
if flip:
|
||||
left_pole, right_pole = pos_pole, neg_pole
|
||||
else:
|
||||
left_pole, right_pole = neg_pole, pos_pole
|
||||
# Fixed logic: labels don't depend on flip
|
||||
left_pole = neg_pole
|
||||
right_pole = pos_pole
|
||||
|
||||
assert left_pole == "Right-wing parties"
|
||||
assert right_pole == "Left-wing parties"
|
||||
assert left_pole == "Left-wing parties"
|
||||
assert right_pole == "Right-wing parties"
|
||||
|
||||
|
||||
def test_derive_labels_flip_false():
|
||||
"""When flip=False, negative_pole should be on left, positive_pole on right."""
|
||||
"""Labels should always reflect what's on each side, regardless of flip.
|
||||
negative_pole describes LEFT, positive_pole describes RIGHT."""
|
||||
theme = {
|
||||
"positive_pole": "Right-wing parties",
|
||||
"negative_pole": "Left-wing parties",
|
||||
@@ -35,10 +36,9 @@ def test_derive_labels_flip_false():
|
||||
pos_pole = theme.get("positive_pole", "")
|
||||
neg_pole = theme.get("negative_pole", "")
|
||||
|
||||
if flip:
|
||||
left_pole, right_pole = pos_pole, neg_pole
|
||||
else:
|
||||
left_pole, right_pole = neg_pole, pos_pole
|
||||
# Fixed logic: labels don't depend on flip
|
||||
left_pole = neg_pole
|
||||
right_pole = pos_pole
|
||||
|
||||
assert left_pole == "Left-wing parties"
|
||||
assert right_pole == "Right-wing parties"
|
||||
|
||||
@@ -38,13 +38,10 @@ def test_right_wing_on_right_all_components():
|
||||
pos_pole = theme.get("positive_pole", "")
|
||||
neg_pole = theme.get("negative_pole", "")
|
||||
|
||||
# Derive left/right labels
|
||||
if flip:
|
||||
left_label = pos_pole
|
||||
right_label = neg_pole
|
||||
else:
|
||||
left_label = neg_pole
|
||||
right_label = pos_pole
|
||||
# Derive left/right labels - labels don't depend on flip
|
||||
# negative_pole describes LEFT, positive_pole describes RIGHT
|
||||
left_label = neg_pole
|
||||
right_label = pos_pole
|
||||
|
||||
# Verify no left_pole/right_pole in theme
|
||||
assert "left_pole" not in theme, f"Component {comp} has deprecated left_pole"
|
||||
@@ -68,18 +65,19 @@ def test_label_derivation_matches_fallback():
|
||||
neg_pole = theme.get("negative_pole", "")
|
||||
flip = theme.get("flip", False)
|
||||
|
||||
# Simulate the fallback logic from explorer.py lines 969-970
|
||||
expected_left = pos_pole if flip else neg_pole
|
||||
expected_right = neg_pole if flip else pos_pole
|
||||
# Simulate the fallback logic from explorer.py (fixed version)
|
||||
# Labels don't depend on flip - negative_pole describes LEFT, positive_pole describes RIGHT
|
||||
expected_left = neg_pole
|
||||
expected_right = pos_pole
|
||||
|
||||
# Verify theme doesn't have static labels
|
||||
assert "left_pole" not in theme
|
||||
assert "right_pole" not in theme
|
||||
|
||||
# The derived labels should match the expected fallback
|
||||
# (This is the core fix - we're now always using the fallback)
|
||||
derived_left = pos_pole if flip else neg_pole
|
||||
derived_right = neg_pole if flip else pos_pole
|
||||
# Labels don't depend on flip
|
||||
derived_left = neg_pole
|
||||
derived_right = pos_pole
|
||||
|
||||
assert derived_left == expected_left, f"Component {comp} left label mismatch"
|
||||
assert derived_right == expected_right, f"Component {comp} right label mismatch"
|
||||
|
||||
@@ -78,13 +78,13 @@ def test_get_svd_label_returns_correct_label():
|
||||
"""Test that get_svd_label returns the correct label for each component."""
|
||||
from analysis.svd_labels import get_svd_label
|
||||
|
||||
# Component 1 should return Rechts kabinetsbeleid label
|
||||
# Component 1 should return Rechts versus links label
|
||||
label1 = get_svd_label(1)
|
||||
assert "Rechts kabinetsbeleid" in label1 or "links oppositiebeleid" in label1
|
||||
assert "Rechts versus links" in label1 or "links" in label1.lower()
|
||||
|
||||
# Component 2 should return PVV/FVD-populisme label
|
||||
# Component 2 should return Nationalistisch versus kosmopolitisch label
|
||||
label2 = get_svd_label(2)
|
||||
assert "PVV/FVD-populisme" in label2 or "mainstream-partijen" in label2
|
||||
assert "Nationalistisch" in label2 or "kosmopolitisch" in label2
|
||||
|
||||
# Component 3 should return Verzorgingsstaat label
|
||||
label3 = get_svd_label(3)
|
||||
|
||||
Reference in New Issue
Block a user