diff --git a/analysis/axis_classifier.py b/analysis/axis_classifier.py index 828f78c..a26ed70 100644 --- a/analysis/axis_classifier.py +++ b/analysis/axis_classifier.py @@ -13,6 +13,8 @@ import numpy as np import re import json +from analysis.svd_labels import get_svd_label + _logger = logging.getLogger(__name__) # Module-level caches — loaded once per process lifetime. @@ -23,15 +25,15 @@ _coalition_cache: Optional[Dict[str, set]] = None _THRESHOLD = 0.65 _LABELS = { - "lr": "Links\u2013Rechts", - "co": "Coalitie\u2013Oppositie", - "pc": "Conservatief\u2013Progressief", - # When we have no interpretable classifier signal, fall back to numeric SVD - # component names (As 1 / As 2) rather than the vague "Stempatroon As N". - # The UI will still add directional annotations (▲/▼) when rendering the - # vertical axis to make polarity unambiguous. - "fallback_x": "As 1", - "fallback_y": "As 2", + "lr": "Verzorgingsstaat–Marktwerking", + "eu": "EU-integratie–Nationalisme", + "pi": "Populistisch–Institutioneel", + "co": "Coalitie–Oppositie", + "pc": "Conservatief–Progressief", + # When we have no interpretable classifier signal, fall back to the known + # SVD component meanings rather than generic "As N" labels. + "fallback_x": get_svd_label(1), + "fallback_y": get_svd_label(2), } @@ -39,11 +41,29 @@ _LABELS = { # Remove duplicate lower definition (keep the one at the top) +def display_label_for_modal(modal_label: Optional[str], axis: str) -> str: + """Return a user-facing axis label for a modal/internal label. + + Keeps existing behavior: map numeric fallback names 'As 1' / 'Stempatroon As 1' + to the conventional semantic defaults used in the UI. Any other label is + returned unchanged; None is treated as the semantic fallback for the axis. + """ + if modal_label is None: + return "Links\u2013Rechts" if axis == "x" else "Progressief\u2013Conservatief" + if axis == "x" and modal_label in ("As 1", "Stempatroon As 1"): + return "Links\u2013Rechts" + if axis == "y" and modal_label in ("As 2", "Stempatroon As 2"): + return "Progressief\u2013Conservatief" + return modal_label + + _INTERPRETATION_TEMPLATES = { - "lr": "De {orientation} as weerspiegelt de klassieke links-rechts tegenstelling.", + "lr": "De {orientation} as weerspiegelt de economische tegenstelling tussen verzorgingsstaat en marktwerking.", + "eu": "De {orientation} as weerspiegelt de tegenstelling tussen EU-integratie/internationalisme en nationalisme/soevereiniteit.", + "pi": "De {orientation} as scheidt populistisch-nationalistische partijen van het institutioneel-parlementaire establishment.", "co": ( "De {orientation} as weerspiegelt stemgedrag van coalitie- versus " - "oppositiepartijen (r={r:.2f}). Links-rechts is minder dominant dit jaar." + "oppositiepartijen (r={r:.2f}). Ideologische tegenstellingen zijn minder dominant dit jaar." ), "pc": "De {orientation} as weerspiegelt de progressief-conservatieve tegenstelling.", "fallback": ( @@ -55,8 +75,10 @@ _INTERPRETATION_TEMPLATES = { # Maps motion-path keyword labels to _INTERPRETATION_TEMPLATES keys. # Labels not present here fall back to "fallback". _MOTION_LABEL_TEMPLATE_KEY: Dict[str, str] = { - "Links\u2013Rechts": "lr", - "Progressief\u2013Conservatief": "pc", + "Verzorgingsstaat–Marktwerking": "lr", + "EU-integratie–Nationalisme": "eu", + "Populistisch–Institutioneel": "pi", + "Progressief–Conservatief": "pc", } @@ -64,8 +86,8 @@ _MOTION_LABEL_TEMPLATE_KEY: Dict[str, str] = { _KEYWORD_THRESHOLD = 0.4 _KEYWORDS: Dict[str, List[str]] = { - "Links\u2013Rechts": [ - # economic + "Verzorgingsstaat–Marktwerking": [ + # economic / welfare state "belasting", "uitkering", "bijstand", @@ -78,18 +100,57 @@ _KEYWORDS: Dict[str, List[str]] = { "pensioen", "aow", "zorg", - # immigration + "huur", + "woning", + "sociaal", + "werkloos", + "ww", + "arbeidsongeschik", + "wao", + "gemeentefonds", + ], + "EU-integratie–Nationalisme": [ + # EU and international cooperation + "europees", + "europese", + " eu ", + "eu-", + "verdrag", + "intergouvernementeel", + "samenwerking", + "internationaal", + "navo", + "nato", + " vn ", + "vn-", + "sancties", + "israël", + "vluchteling", "asiel", - "asielaanvraag", - "migratie", - "vreemdeling", - "vluchtelingen", - "terugkeer", - "grenzen", - "opvang", - "statushouder", + "soevereiniteit", + "nationaal", + ], + "Populistisch–Institutioneel": [ + # Populist/nationalist themes + "terugsturen", + "syrië", + "syrier", + "grenzen dicht", + "remigratie", + "eigen volk", + "nederland eerst", + "corona", + "vaccin", + "ivermectine", + "hydroxychloroquine", + "complot", + "deep state", + "establishment", + "elite", + "herstelbetaling", + "excuses", ], - "Progressief\u2013Conservatief": [ + "Progressief–Conservatief": [ # environment "klimaat", "stikstof", @@ -109,16 +170,6 @@ _KEYWORDS: Dict[str, List[str]] = { "religie", "geloof", ], - "Nationaal\u2013Internationaal": [ - "navo", - "nato", - "europees", - "europese", - " eu ", - "verdrag", - " vn ", - "internationaal", - ], } # Pre-compiled regexes for keyword matching. We escape keywords but do NOT add @@ -454,24 +505,6 @@ def classify_axes( if not ideology and not motion_path_available: return axes - -def display_label_for_modal(modal_label: Optional[str], axis: str) -> str: - """Return a user-facing axis label for a modal/internal label. - - Keeps existing behavior: map numeric fallback names 'As 1' / 'Stempatroon As 1' - to the conventional semantic defaults used in the UI. Any other label is - returned unchanged; None is treated as the semantic fallback for the axis. - """ - if modal_label is None: - return "Links\u2013Rechts" if axis == "x" else "Conservatief\u2013Progressief" - if axis == "x" and modal_label in ("As 1", "Stempatroon As 1"): - return "Links\u2013Rechts" - if axis == "y" and modal_label in ("As 2", "Stempatroon As 2"): - return "Conservatief\u2013Progressief" - return modal_label - - # duplicate early-exit guard removed here - x_quality: Dict[str, float] = {} y_quality: Dict[str, float] = {} x_interpretation: Dict[str, str] = {}