|
|
|
|
@ -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] = {} |
|
|
|
|
|