6.4 KiB
SVD Label Unification Design
Goal
Unify SVD component labels into a single source of truth (SVD_THEMES) and automatically compute axis flip directions so right-wing parties consistently appear on the right side of all SVD component axes.
Background
Currently there are two separate label systems:
SVD_THEMESinexplorer.py- defines labels for all 10 SVD components with detailed explanations_LABELSinaxis_classifier.py- defines short labels for the classifier (lr, eu, pi, co, pc)
This causes duplication and potential inconsistency. Additionally, flip values are hardcoded in SVD_THEMES rather than computed from actual party positions.
Design
Single Source of Truth
SVD_THEMES in explorer.py remains the canonical definition for all SVD component labels. A new shared module analysis/svd_labels.py will:
- Import
SVD_THEMESfrom explorer.py (at runtime to avoid circular imports) - Provide helper functions to derive labels for any component
- Compute flip direction automatically based on party centroids
New Module: analysis/svd_labels.py
"""Unified SVD component labels and automatic flip direction computation."""
# Canonical party sets for orientation
RIGHT_PARTIES = {
"PVV", "VVD", "FVD", "BBB", "JA21",
"Nieuw Sociaal Contract", "SGP", "CDA", "ChristenUnie"
}
LEFT_PARTIES = {
"SP", "PvdA", "GL", "GroenLinks", "GroenLinks-PvdA",
"DENK", "PvdD", "Volt"
}
def get_svd_label(component: int) -> str:
"""Get short label for SVD component (e.g., 'EU-integratie–Nationalisme')."""
def get_svd_theme(component: int) -> dict:
"""Get full theme dict for SVD component."""
def compute_flip_direction(component: int, party_scores: dict) -> bool:
"""
Compute flip so right parties appear on the right side.
Args:
component: SVD component number (1-indexed)
party_scores: {party_name: [score_comp1, score_comp2, ...]}
Returns:
True if axis should be flipped so right parties are on right.
"""
# Get scores for this component (0-indexed internally)
idx = component - 1
right_scores = [scores[idx] for party, scores in party_scores.items()
if party in RIGHT_PARTIES and len(scores) > idx]
left_scores = [scores[idx] for party, scores in party_scores.items()
if party in LEFT_PARTIES and len(scores) > idx]
if not right_scores or not left_scores:
return False # Default: no flip if insufficient data
right_mean = sum(right_scores) / len(right_scores)
left_mean = sum(left_scores) / len(left_scores)
# Flip if right parties have lower mean (they're on the left)
return right_mean < left_mean
def get_fallback_labels() -> tuple[str, str]:
"""Get fallback labels for x and y axes (components 1 and 2)."""
return (get_svd_label(1), get_svd_label(2))
Changes to analysis/axis_classifier.py
- Remove hardcoded
_LABELSdict - Import from
svd_labels.pyat runtime (to avoid circular imports) - Update
display_label_for_modalto derive from SVD_THEMES
# Remove:
_LABELS = {
"lr": "Verzorgingsstaat–Marktwerking",
"eu": "EU-integratie–Nationalisme",
...
}
# Add:
def _get_svd_labels():
"""Lazy import to avoid circular dependency."""
from analysis.svd_labels import get_svd_label, get_fallback_labels
return get_fallback_labels()
def display_label_for_modal(modal_label: Optional[str], axis: str) -> str:
"""Return a user-facing axis label for a modal/internal label."""
from analysis.svd_labels import get_svd_label
if modal_label is None:
# Fallback to component 1 (x) or 2 (y)
comp = 1 if axis == "x" else 2
return get_svd_label(comp)
# Map "As 1" / "As 2" to semantic labels
if axis == "x" and modal_label in ("As 1", "Stempatroon As 1"):
return get_svd_label(1)
if axis == "y" and modal_label in ("As 2", "Stempatroon As 2"):
return get_svd_label(2)
return modal_label
Changes to explorer.py
- Keep
SVD_THEMESas canonical source - Remove hardcoded fallback labels in
_build_political_compass_figure - Use
svd_labels.pyfor all label lookups - Add party position charts for components 3-10 in SVD Components tab
Party Position Charts for All Components
Currently, _render_party_axis_chart only works for components 1 and 2 (which have 2D coords). For components 3-10, we need to show 1D party positions:
def _render_party_axis_chart_1d(party_coords: dict, component: int, theme: dict, bootstrap_data: dict = None):
"""Render a 1D party position chart for a single SVD component."""
# Extract scores for this component
# Show parties on a horizontal axis
# Use theme['label'] for axis title
# Use theme['positive_pole'] and theme['negative_pole'] for annotations
Auto-compute Flip Values
On app startup or when loading SVD data:
- Load party scores for all components
- Compute flip direction for each component using
compute_flip_direction - Update
SVD_THEMES[component]["flip"]dynamically OR store precomputed values
Files Modified
| File | Changes |
|---|---|
analysis/svd_labels.py |
NEW - Unified label system with auto-flip |
analysis/axis_classifier.py |
Remove _LABELS, import from svd_labels |
explorer.py |
Remove fallback labels, add 1D charts for components 3-10 |
tests/test_axis_label_fallback.py |
Update to use new label system |
tests/test_political_compass.py |
Update assertions for new labels |
Test Plan
-
Unit tests for
svd_labels.py:get_svd_label(component)returns correct labelcompute_flip_directionreturns correct flip based on party scoresget_fallback_labels()returns tuple of component 1 and 2 labels
-
Integration tests:
- Compass plot uses correct labels from SVD_THEMES
- Trajectory plot uses correct labels from SVD_THEMES
- SVD Components tab shows party positions for all components
- Right parties appear on right side of all axes
-
Regression tests:
- Existing tests pass with new label system
- No circular import errors
Implementation Order
- Create
analysis/svd_labels.pywith helper functions - Update
axis_classifier.pyto use svd_labels - Update
explorer.pyto use svd_labels for fallback labels - Add 1D party position charts for components 3-10
- Update tests
- Verify flip directions are correct for all components