diff --git a/analysis/config.py b/analysis/config.py index 5694675..95dbaf0 100644 --- a/analysis/config.py +++ b/analysis/config.py @@ -66,35 +66,29 @@ PARTY_COLOURS: Dict[str, str] = { SVD_THEMES: dict[int, dict[str, str]] = { 1: { - "label": "Rechts kabinetsbeleid versus links oppositiebeleid", + "label": "Rechts versus links (economisch en migratiebeleid)", "explanation": ( - "Deze as scheidt het rechts kabinetsbeleid van links oppositiebeleid. " - "Aan de positieve kant staan moties die passen bij het kabinetsbeleid: " - "Eurofighter Typhoons, defensie-uitgaven naar 3% bbp, F-35 reservedelen, " - "marine-steun aan Rode Zee en asielrestricties. " - "PVV, VVD, NSC en BBB scoren sterk positief. " - "Aan de negatieve kant staan moties uit de oppositie: " - "zorgbuurthuizen voor ouderen, boycot van Israël, sancties, en internationale " - "klimaatsamenwerking. GroenLinks-PvdA, SP, PvdD en Volt scoren negatief. " - "Deze as weerspiegelt de coalitie-oppositie dynamiek." + "Deze as scheidt rechts kabinetsbeleid van links oppositiebeleid. " + "Aan de positieve kant staan PVV, NSC, SGP en BBB. " + "Aan de negatieve kant staan PvdD, GroenLinks-PvdA en DENK. " + "Deze as weerspiegelt de klassieke links-rechts verdeling op economisch en migratiebeleid." ), - "positive_pole": "Rechts: PVV, VVD, NSC, BBB, JA21 — kabinetsbeleid, defensie en restricties", - "negative_pole": "Links: GroenLinks-PvdA, SP, PvdD, Volt, DENK — oppositie, zorg en multilateraal", + "positive_pole": "Rechts: PVV, NSC, SGP, BBB — kabinetsbeleid, defensie en restricties", + "negative_pole": "Links: PvdD, GroenLinks-PvdA, DENK — oppositie, zorg en multilateraal", "flip": False, }, 2: { - "label": "PVV/FVD-populisme versus mainstream-partijen", + "label": "Nationalistisch versus kosmopolitisch", "explanation": ( - "Deze as scheidt het PVV/FVD-populisme van het overige parliament. " - "Alleen PVV en FVD scoren positief; alle andere partijen scoren negatief. " - "Positieve moties: Syriërs terugsturen, geen geld aan Jordanië, tijdelijke " - "bescherming Oekraïne beëindigen, uitstappen uit WHO en klimaatakkoorden. " - "Negatieve moties: digitale toegankelijkheid Caribisch Nederland, ethiekprogramma " - "Defensie, zorg voor slachtoffers bombardement Hawija, internationale klimaatsamenwerking. " - "Dit is geen links-rechts verdeling maar een populistisch vs. mainstream onderscheid." + "Deze as meet een onafhankelijke culturele dimensie: nationalistisch-populistisch " + "tegenover kosmopolitisch-mainstream. Aan de positieve kant staan PVV en FVD. " + "Aan de negatieve kant staan Volt, GroenLinks-PvdA, DENK en SP. " + "Deze as is onafhankelijk van links-rechts (as 1) en scheidt partijen " + "op hun houding tegenover nationale identiteit, EU-samenwerking en de " + "etnisch-culturele dimensie." ), - "positive_pole": "PVV en FVD — soevereiniteit en anti-establishment", - "negative_pole": "Overige partijen: VVD, CDA, SGP, ChristenUnie, GroenLinks-PvdA, D66, Volt, BBB", + "positive_pole": "Nationalistisch/populistisch — PVV, FVD: nationale identiteit en soevereiniteit", + "negative_pole": "Kosmopolitisch/mainstream — Volt, GL-PvdA, DENK, SP: EU en internationale samenwerking", "flip": False, }, 3: { @@ -126,7 +120,7 @@ SVD_THEMES: dict[int, dict[str, str]] = { ), "positive_pole": "D66, CDA, JA21 — moties met brede steun", "negative_pole": "NSC, BBB — moties met andere focus", - "flip": True, + "flip": False, }, 5: { "label": "Christelijk-sociaal en gemeenschapswaarden versus progressieve individuele rechten", @@ -211,7 +205,7 @@ SVD_THEMES: dict[int, dict[str, str]] = { ), "positive_pole": "Pragmatisch-bestuurlijk: SGP, ChristenUnie, DENK, SP — concrete oplossingen", "negative_pole": "Systeemhervorming: D66, JA21, PVV — idealistische beleidsposities", - "flip": True, + "flip": False, }, 10: { "label": "Kritisch op overheidsbemoeienis versus pro-regulering (indicatief)", diff --git a/explorer.py b/explorer.py index 7ca115a..ac13813 100644 --- a/explorer.py +++ b/explorer.py @@ -953,8 +953,9 @@ def _build_party_axis_figure( pos_pole = theme.get("positive_pole", "") neg_pole = theme.get("negative_pole", "") - left_label = theme.get("left_pole", pos_pole if flip else neg_pole) - right_label = theme.get("right_pole", neg_pole if flip else pos_pole) + # Labels always from poles: negative_pole = LEFT, positive_pole = RIGHT + left_label = neg_pole + right_label = pos_pole fig.update_layout( height=160, @@ -1073,8 +1074,9 @@ def _render_party_axis_chart_1d( # Determine pole labels based on flip pos_pole = theme.get("positive_pole", "") neg_pole = theme.get("negative_pole", "") - left_label = theme.get("left_pole", pos_pole if flip else neg_pole) - right_label = theme.get("right_pole", neg_pole if flip else pos_pole) + # Labels always from poles: negative_pole = LEFT, positive_pole = RIGHT + left_label = neg_pole + right_label = pos_pole # Update layout with same format as components 1-2 fig.update_layout( @@ -1238,10 +1240,9 @@ def _render_svd_time_trajectory( # Determine pole labels based on theme (use reference flip from current_parliament) pos_pole = theme.get("positive_pole", "") neg_pole = theme.get("negative_pole", "") - # Use the theme's flip value (computed from current_parliament) for label orientation - reference_flip = theme.get("flip", False) - left_label = theme.get("left_pole", pos_pole if reference_flip else neg_pole) - right_label = theme.get("right_pole", neg_pole if reference_flip else pos_pole) + # Labels always from poles: negative_pole = LEFT, positive_pole = RIGHT + left_label = neg_pole + right_label = pos_pole # Y-axis labels y_labels = {} @@ -2532,136 +2533,75 @@ def build_svd_components_tab(db_path: str) -> None: motions = comp_map.get(comp_sel, []) # Party axis chart - # Default party scores already loaded earlier for sidebar controls - # For components 1 and 2, prefer MP-centroid values from the Procrustes-aligned - # positions_by_window so the compass matches the trajectories (MP-mean centroids). - if comp_sel in (1, 2): - try: - positions_by_window, axis_def = load_positions(db_path) - if axis_def is None: - axis_def = {} - - # Window selector for components 1-2 (same windows as Political Compass) - year_windows = sorted( - w for w in positions_by_window if w != "current_parliament" - ) - has_current = "current_parliament" in positions_by_window - windows = year_windows + (["current_parliament"] if has_current else []) - - def _window_label(w: str) -> str: - if w == "current_parliament": - return "Huidig parlement" - return w - - with col1: - window = st.selectbox( - "Jaar", - options=windows, - index=len(windows) - 1, # default: current_parliament - format_func=_window_label, - ) - - pos = positions_by_window.get(window, {}) + # Default party scores already loaded earlier for sidebar controls. + # ALL components 1-10 use raw (non-aligned) SVD vectors. + # The compass uses Procrustes-aligned PCA — separate visualization. + # Get available windows from svd_vectors + available_windows = get_uniform_dim_windows(db_path) + year_windows = sorted(w for w in available_windows if w != "current_parliament") + has_current = "current_parliament" in available_windows + svd_windows = year_windows + (["current_parliament"] if has_current else []) + + def _svd_window_label(w: str) -> str: + if w == "current_parliament": + return "Huidig parliament" + return w - # build party -> list of MP x/y coords - party_map = load_party_map(db_path) - per_party_coords: dict = {} - party_mp_counts: dict = {} # Track MP count per party - for ent, (x, y) in pos.items(): - party = party_map.get(ent) - if party is None: - continue - per_party_coords.setdefault(party, []).append((x, y)) - party_mp_counts[party] = party_mp_counts.get(party, 0) + 1 - - # construct party_scores mapping: prefer MP centroid [x,y], fallback to default vector - party_scores = {} - for party in set( - list(per_party_coords.keys()) + list(party_scores_default.keys()) - ): - coords = per_party_coords.get(party) - if coords: - xs = [c[0] for c in coords] - ys = [c[1] for c in coords] - party_scores[party] = [float(np.mean(xs)), float(np.mean(ys))] - else: - # fallback: use the default single-window SVD mean vector - party_scores[party] = party_scores_default.get(party, []) + with col1: + svd_window = st.selectbox( + "Jaar", + options=svd_windows, + index=len(svd_windows) - 1, # default: current_parliament + format_func=_svd_window_label, + key=f"svd_window_{comp_sel}", + ) - except Exception: - # On any error, fall back to the old behaviour - logger.exception( - "Failed to derive party centroids from positions_by_window; falling back to load_party_axis_scores" - ) - party_scores = party_scores_default - # For fallback, compute MP counts from party_mp_vectors - party_mp_counts = ( - {p: len(v) for p, v in party_mp_vectors.items()} - if party_mp_vectors - else {} - ) + # Load party scores for the selected window + if svd_window == "current_parliament": + party_scores = party_scores_default else: - # Components 3-10: use SVD vectors with year selection - # Get available windows from svd_vectors - available_windows = get_uniform_dim_windows(db_path) - year_windows = sorted(w for w in available_windows if w != "current_parliament") - has_current = "current_parliament" in available_windows - svd_windows = year_windows + (["current_parliament"] if has_current else []) - - def _svd_window_label(w: str) -> str: - if w == "current_parliament": - return "Huidig parlement" - return w - - with col1: - svd_window = st.selectbox( - "Jaar", - options=svd_windows, - index=len(svd_windows) - 1, # default: current_parliament - format_func=_svd_window_label, - key=f"svd_window_{comp_sel}", - ) - - # Load party scores for the selected window - if svd_window == "current_parliament": - party_scores = party_scores_default - else: - party_scores = load_party_axis_scores_for_window(db_path, svd_window) + party_scores = load_party_axis_scores_for_window(db_path, svd_window) - # For components 3-10, compute MP counts from party_mp_vectors - party_mp_counts = ( - {p: len(v) for p, v in party_mp_vectors.items()} if party_mp_vectors else {} - ) + # Compute MP counts from party_mp_vectors + party_mp_counts = ( + {p: len(v) for p, v in party_mp_vectors.items()} if party_mp_vectors else {} + ) - # Auto-compute flip directions for all components based on party centroids - # Use the selected window's party_scores for flip direction - # This ensures right-wing parties consistently appear on the right side for THIS window + # Auto-compute flip directions for ALL components 1-10 based on party centroids. + # Each window's SVD has arbitrary sign orientation, so we compute flip per component + # to ensure canonical right parties (PVV, FVD, JA21, SGP) appear on the RIGHT. + computed_flips: Dict[int, bool] = {} try: from analysis.svd_labels import compute_flip_direction for comp in range(1, 11): - # Compute flip for the selected window (not current_parliament) - flip = compute_flip_direction(comp, party_scores) - if comp in SVD_THEMES: - SVD_THEMES[comp]["flip"] = flip + computed_flips[comp] = compute_flip_direction(comp, party_scores) except Exception: - # If flip computation fails, keep existing flip values in SVD_THEMES + # If flip computation fails, keep existing flip values from SVD_THEMES pass - # Convert party_scores (possibly [x,y] lists or legacy vectors) into explicit (x,y) coords - party_coords: dict = {} - for p, v in party_scores.items(): + # Build theme override with computed flip for this component + # (avoids mutating SVD_THEMES which persists stale values across Streamlit reruns) + theme_with_flip = { + **theme, + "flip": computed_flips.get(comp_sel, theme.get("flip", False)), + } + + # Extract 1D scores for this component (ALL components use raw SVD values) + party_1d_coords: dict = {} + idx = comp_sel - 1 # Convert to 0-indexed + for party, scores in party_scores.items(): try: - if v and len(v) >= 2: - party_coords[p] = (float(v[0]), float(v[1])) + if scores and len(scores) > idx: + party_1d_coords[party] = (float(scores[idx]),) except Exception: continue # Filter parties by minimum MP count if min_mps > 1 and party_mp_counts: valid_parties = {p for p, count in party_mp_counts.items() if count >= min_mps} - party_coords = { - p: coords for p, coords in party_coords.items() if p in valid_parties + party_1d_coords = { + p: coords for p, coords in party_1d_coords.items() if p in valid_parties } # Render party axis chart (single window or time trajectory) @@ -2675,60 +2615,21 @@ def build_svd_components_tab(db_path: str) -> None: has_current = "current_parliament" in available_windows all_windows = year_windows + (["current_parliament"] if has_current else []) - # For components 1-2, use Procrustes-aligned positions (same as single window) - # For components 3-10, use SVD vectors - if comp_sel in (1, 2): - positions_by_window, _ = load_positions(db_path) - party_map = load_party_map(db_path) - - # Convert positions to party scores format - party_scores_by_window: Dict[str, Dict[str, List[float]]] = {} - for window in all_windows: - pos = positions_by_window.get(window, {}) - # Collect all MP positions per party - party_positions: Dict[str, List[Tuple[float, float]]] = {} - for entity, (x, y) in pos.items(): - party = party_map.get(entity) - if party: - party_positions.setdefault(party, []).append((x, y)) - # Average positions per party - party_scores: Dict[str, List[float]] = {} - for party, positions in party_positions.items(): - if positions: - avg_x = sum(p[0] for p in positions) / len(positions) - avg_y = sum(p[1] for p in positions) / len(positions) - party_scores[party] = [avg_x, avg_y] - party_scores_by_window[window] = party_scores - else: - # Use raw (non-aligned) SVD vectors for components 3-10. - # Procrustes alignment rotates the full vector space to align - # components 1-2 across windows, but this also transforms - # components 3-10, making their scores incomparable with the - # single-window view. Per-window flip computation handles - # orientation alignment for these components. - party_scores_by_window = load_party_scores_all_windows(db_path, all_windows) + # ALL components use raw (non-aligned) SVD vectors. + # Procrustes alignment rotates the full vector space which makes scores + # incomparable with the single-window view. Per-window flip computation + # handles orientation alignment for the trajectory. + party_scores_by_window = load_party_scores_all_windows(db_path, all_windows) _render_svd_time_trajectory( party_scores_by_window, comp_sel, - theme, + theme_with_flip, selected_parties_for_trajectory, ) - elif comp_sel in (1, 2): - # Components 1-2 use 2D coords from political compass - _render_party_axis_chart( - party_coords, comp_sel, theme, bootstrap_data=bootstrap_data - ) else: - # Components 3-10 use 1D scores from SVD - # Extract 1D scores for this component - party_1d_coords = {} - idx = comp_sel - 1 # Convert to 0-indexed - for party, scores in party_scores.items(): - if scores and len(scores) > idx: - party_1d_coords[party] = (scores[idx],) - - _render_party_axis_chart_1d(party_1d_coords, comp_sel, theme) + # Single-window view: render 1D party axis chart + _render_party_axis_chart_1d(party_1d_coords, comp_sel, theme_with_flip) # Batch-fetch motion details (title, date, policy_area, url, body_text, voting_results) motion_ids = [m.get("motion_id") for m in motions if m.get("motion_id") is not None] @@ -2764,9 +2665,9 @@ def build_svd_components_tab(db_path: str) -> None: pos_motions = [m for m in motions if float(m.get("score", 0.0)) >= 0] neg_motions = [m for m in motions if float(m.get("score", 0.0)) < 0] - flip = theme.get("flip", False) if theme else False - pos_pole = theme.get("positive_pole", "") if theme else "" - neg_pole = theme.get("negative_pole", "") if theme else "" + flip = theme_with_flip.get("flip", False) if theme_with_flip else False + pos_pole = theme_with_flip.get("positive_pole", "") if theme_with_flip else "" + neg_pole = theme_with_flip.get("negative_pole", "") if theme_with_flip else "" # Derive left/right labels from flip direction # flip=True: positive_pole on left, negative_pole on right diff --git a/scripts/validate_svd_themes.py b/scripts/validate_svd_themes.py index 0aecb81..7bbf0d7 100644 --- a/scripts/validate_svd_themes.py +++ b/scripts/validate_svd_themes.py @@ -193,6 +193,43 @@ def check_canonical_right_on_right( return divergences +def check_config_flip_consistency( + party_avg_vectors: Dict[str, List[float]], + themes: Dict[int, Dict[str, str]], + canonical_right: frozenset, + canonical_left: frozenset, + num_components: int = 10, +) -> List[Dict]: + """Check that config flip values match computed flip directions. + + The runtime uses compute_flip_direction() to determine flips, but config + also stores a flip value. If they disagree, the config is stale or wrong. + """ + from analysis.svd_labels import compute_flip_direction + + scores_dict = { + p: v + for p, v in party_avg_vectors.items() + if p in canonical_right or p in canonical_left + } + + mismatches = [] + for comp in range(1, num_components + 1): + config_flip = themes.get(comp, {}).get("flip", False) + computed_flip = compute_flip_direction(comp, scores_dict) + if config_flip != computed_flip: + mismatches.append( + { + "component": comp, + "issue": "config_flip_mismatch", + "config_flip": config_flip, + "computed_flip": computed_flip, + } + ) + + return mismatches + + def check_theme_consistency( party_positions: Dict[str, Dict[int, float]], themes: Dict[int, Dict[str, str]], @@ -247,13 +284,19 @@ def main() -> int: args.components, ) - # Check 2: Theme pole label consistency + # Check 2: Config flip vs computed flip consistency + logger.info("Checking config flip vs computed flip consistency") + flip_mismatches = check_config_flip_consistency( + party_avg_vectors, themes, canonical_right, canonical_left, args.components + ) + + # Check 3: Theme pole label consistency logger.info("Checking theme pole label consistency") theme_divergences = check_theme_consistency( party_positions, themes, canonical_right, canonical_left ) - all_divergences = canonical_divergences + theme_divergences + all_divergences = canonical_divergences + flip_mismatches + theme_divergences if all_divergences: print(f"\n{'=' * 60}") @@ -282,10 +325,16 @@ def main() -> int: print(f" Found right: {d['right_found']}") print(f" Found left: {d['left_found']}") + elif d["issue"] == "config_flip_mismatch": + print(f" Config flip: {d['config_flip']}") + print(f" Computed flip: {d['computed_flip']}") + print(f" → Update SVD_THEMES[{comp}]['flip'] to {d['computed_flip']}") + return 1 else: print("\n✓ All SVD themes match actual party positions") print(" - Canonical right-wing parties on right side of all axes") + print(" - Config flip values match computed flip directions") print(" - Theme pole labels consistent with party positions") return 0 diff --git a/tests/test_axis_label_fallback.py b/tests/test_axis_label_fallback.py index 48aa55e..e6cb4d5 100644 --- a/tests/test_axis_label_fallback.py +++ b/tests/test_axis_label_fallback.py @@ -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"] ) diff --git a/tests/test_explorer_labels.py b/tests/test_explorer_labels.py index 8b9d69b..2c43153 100644 --- a/tests/test_explorer_labels.py +++ b/tests/test_explorer_labels.py @@ -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" diff --git a/tests/test_svd_axis_alignment.py b/tests/test_svd_axis_alignment.py index 4a9ff48..0369f6b 100644 --- a/tests/test_svd_axis_alignment.py +++ b/tests/test_svd_axis_alignment.py @@ -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" diff --git a/tests/test_svd_labels.py b/tests/test_svd_labels.py index ef5059c..46725b5 100644 --- a/tests/test_svd_labels.py +++ b/tests/test_svd_labels.py @@ -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)