|
|
|
|
@ -269,6 +269,18 @@ def select_trajectory_plot_data( |
|
|
|
|
if has_valid: |
|
|
|
|
plottable_parties.append(p) |
|
|
|
|
|
|
|
|
|
# DEBUG: Show plottable_parties status |
|
|
|
|
print( |
|
|
|
|
f"[TRAJ DEBUG] plottable_parties: {len(plottable_parties)} parties, sample={plottable_parties[:5] if plottable_parties else 'empty'}" |
|
|
|
|
) |
|
|
|
|
print(f"[TRAJ DEBUG] party_centroids keys: {list(party_centroids.keys())[:10]}") |
|
|
|
|
if party_centroids: |
|
|
|
|
sample_party = list(party_centroids.keys())[0] |
|
|
|
|
sample_vals = party_centroids[sample_party] |
|
|
|
|
print( |
|
|
|
|
f"[TRAJ DEBUG] Sample party '{sample_party}' centroids: {sample_vals[:3]}..." |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
fig = go.Figure() |
|
|
|
|
trace_count = 0 |
|
|
|
|
banner_text: Optional[str] = None |
|
|
|
|
@ -341,6 +353,9 @@ def select_trajectory_plot_data( |
|
|
|
|
trace_count += 1 |
|
|
|
|
|
|
|
|
|
banner_text = "Partijcentroiden niet beschikbaar — tonen individuele MP-trajecten als fallback." |
|
|
|
|
print( |
|
|
|
|
f"[TRAJ DEBUG] Fallback to MP trajectories: trace_count={trace_count}, top_mps={len(top_mps)}" |
|
|
|
|
) |
|
|
|
|
return fig, trace_count, banner_text |
|
|
|
|
|
|
|
|
|
# Otherwise plot party centroids for selected parties intersecting plottable |
|
|
|
|
@ -380,6 +395,9 @@ def select_trajectory_plot_data( |
|
|
|
|
) |
|
|
|
|
trace_count += 1 |
|
|
|
|
|
|
|
|
|
print( |
|
|
|
|
f"[TRAJ DEBUG] Final trace_count={trace_count}, plottable_parties={len(plottable_parties)}, to_plot={len(to_plot) if 'to_plot' in dir() else 'N/A'}" |
|
|
|
|
) |
|
|
|
|
return fig, trace_count, None |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -410,6 +428,188 @@ PARTY_COLOURS: Dict[str, str] = { |
|
|
|
|
"Unknown": "#9E9E9E", |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
# Political polarisation themes per SVD component (1-indexed, window=2025) |
|
|
|
|
# Produced by per-axis analysis of all 10 unique top motions (zero cross-axis overlap). |
|
|
|
|
# This is the canonical source of truth for SVD component labels. |
|
|
|
|
SVD_THEMES: dict[int, dict[str, str]] = { |
|
|
|
|
1: { |
|
|
|
|
"label": "EU-integratie en internationalisme versus nationalisme", |
|
|
|
|
"explanation": ( |
|
|
|
|
"De dominante dimensie van het parlement: niet economisch links-rechts, maar " |
|
|
|
|
"EU-integratie en internationalisme versus nationalisme en soevereiniteit. " |
|
|
|
|
"Aan de linkerkant (Volt, GroenLinks-PvdA, DENK, PvdD, SP) staan moties over " |
|
|
|
|
"EU-verdragswijzigingen, EU-sancties tegen Israël, internationale samenwerking " |
|
|
|
|
"en het behouden van vluchtelingenstatus. Aan de rechterkant (PVV, VVD, SGP, " |
|
|
|
|
"BBB, ChristenUnie) staan moties over nationale begrotingscontrole, " |
|
|
|
|
"integratievereisten en nationale arbeidsvoorwaarden. " |
|
|
|
|
"Deze as verklaart méér stemverschil dan economische tegenstellingen: " |
|
|
|
|
"het onderscheidt pro-EU progressieven van nationalistisch-conservatieven, " |
|
|
|
|
"ongeacht hun standpunt over de verzorgingsstaat." |
|
|
|
|
), |
|
|
|
|
"positive_pole": "Progressief-pro-EU: Volt, GroenLinks-PvdA, DENK, PvdD, SP", |
|
|
|
|
"negative_pole": "Nationalistisch-conservatief: PVV, VVD, SGP, BBB, ChristenUnie", |
|
|
|
|
"flip": True, |
|
|
|
|
}, |
|
|
|
|
2: { |
|
|
|
|
"label": "Populistisch nationalisme versus institutioneel progressivisme", |
|
|
|
|
"explanation": ( |
|
|
|
|
"Deze as scheidt het populistisch-nationalistische bloc (PVV, FVD, Groep Markuszower, " |
|
|
|
|
"BBB) van het volledige overige parlement. Alleen PVV (+18), FVD (+4) en Groep " |
|
|
|
|
"Markuszower (+2) scoren positief; alle andere partijen scoren negatief, inclusief " |
|
|
|
|
"VVD (−15), CDA (−14), SGP (−25) en ChristenUnie (−59). Positieve moties: artsen " |
|
|
|
|
"vrijpleiten voor hydroxychloroquine/ivermectine, Syriërs terugsturen, geen geld " |
|
|
|
|
"aan Jordanië, tijdelijke bescherming Oekraïne beëindigen. Negatieve moties: " |
|
|
|
|
"digitale toegankelijkheid Caribisch Nederland, ethiekprogramma Defensie, zorg voor " |
|
|
|
|
"slachtoffers bombardement Hawija, zorgkwaliteitsstandaarden. Dit is geen links-rechts " |
|
|
|
|
"verdeling maar een nativistisch-populistisch vs. institutioneel onderscheid." |
|
|
|
|
), |
|
|
|
|
"positive_pole": "Populistisch-nationalistisch: PVV, FVD, Groep Markuszower, BBB", |
|
|
|
|
"negative_pole": "Institutioneel: alle overige partijen — van VVD en SGP tot GroenLinks-PvdA en Volt", |
|
|
|
|
"flip": False, |
|
|
|
|
}, |
|
|
|
|
3: { |
|
|
|
|
"label": "Verzorgingsstaat versus bezuinigingen en marktwerking", |
|
|
|
|
"explanation": ( |
|
|
|
|
"Deze as weerspiegelt de spanning tussen staatsingrijpen en marktliberalisme, " |
|
|
|
|
"aangescherpt door de kabinetscrisis van 2025. Aan de positieve kant staan moties " |
|
|
|
|
"die bezuinigingen op zorg en het gemeentefonds willen terugdraaien, winstuitkeringen " |
|
|
|
|
"in de zorg verbieden en publieke controle over ziekenhuisfusies eisen. SP, PvdD, " |
|
|
|
|
"GroenLinks-PvdA stemmen hier gelijk — ondanks hun tegengestelde PC1-posities. " |
|
|
|
|
"Aan de negatieve kant staan moties " |
|
|
|
|
"over marktwerking in de zorg, fiscale bedrijfsopvolgingsfaciliteiten (VVD), " |
|
|
|
|
"doorgaan met besturen ondanks de kabinetscrisis (VVD/BBB) en defensie-" |
|
|
|
|
"uitgaven van 3,5% bbp." |
|
|
|
|
), |
|
|
|
|
"positive_pole": "Pro-verzorgingsstaat: SP, PvdD, GroenLinks-PvdA (anti-bezuinigingen)", |
|
|
|
|
"negative_pole": "Marktliberaal en fiscaal conservatief: VVD, D66, CDA, SGP, BBB", |
|
|
|
|
"flip": False, |
|
|
|
|
}, |
|
|
|
|
4: { |
|
|
|
|
"label": "Pragmatisch centrisme versus ideologische radicaliteit", |
|
|
|
|
"explanation": ( |
|
|
|
|
"De gevestigde centrumpartijen (D66, CDA, VVD, 50PLUS) staan tegenover zowel " |
|
|
|
|
"rechts-radicale als identiteitspolitieke posities. Aan de positieve kant staan " |
|
|
|
|
"moties over openbare toiletten, vaderbetrokkenheid bij opvoeding, internationale " |
|
|
|
|
"samenwerking met Australië en Canada, en long covid-expertise. Dit zijn pragmatische, " |
|
|
|
|
"institutionele beleidsposities. Aan de negatieve kant staan moties over een " |
|
|
|
|
"migratiesaldo-cap van 60.000, het verlaten van de WHO, kinderen in pleeggezinnen " |
|
|
|
|
"van hetzelfde geslacht (FVD) en de bescherming van religieuze schoolidentiteit " |
|
|
|
|
"via artikel 23. De negatieve pool combineert populistisch-rechts met " |
|
|
|
|
"identiteitsgerichte posities van zowel rechts als links." |
|
|
|
|
), |
|
|
|
|
"positive_pole": "Constructief centrum: D66, CDA, VVD, 50PLUS — pragmatisch en internationaal", |
|
|
|
|
"negative_pole": "Radicaal-ideologisch: FVD, Groep Markuszower (rechts), ChristenUnie, DENK (religieus/identiteit)", |
|
|
|
|
"flip": False, |
|
|
|
|
}, |
|
|
|
|
5: { |
|
|
|
|
"label": "Christelijk-sociaal communitarisme", |
|
|
|
|
"explanation": ( |
|
|
|
|
"Deze as scheidt partijen die gemeenschapszorg, burgerplicht en informele " |
|
|
|
|
"ondersteuningsstructuren benadrukken van partijen die individuele vrijheden en " |
|
|
|
|
"progressieve maatschappelijke hervorming voorstaan. Aan de positieve kant staan " |
|
|
|
|
"moties over schuldhulpverlening via vrijwilligersorganisaties, de maatschappelijke " |
|
|
|
|
"diensttijd voor jongeren met een afstand tot de arbeidsmarkt, en de gastouderopvang. " |
|
|
|
|
"ChristenUnie, SGP en CDA voeren hier de toon; ook D66 scoort positief door steun " |
|
|
|
|
"aan sociaal beleid. Aan de negatieve kant staan moties over wettelijke erkenning " |
|
|
|
|
"van meerouderschap, abortusrecht in het EU-Handvest, armoedebeleid en " |
|
|
|
|
"buitenlandse beïnvloeding. PvdD, GroenLinks-PvdA en VVD scoren hier negatief." |
|
|
|
|
), |
|
|
|
|
"positive_pole": "Gemeenschapsgericht: ChristenUnie, SGP, CDA, D66 — vrijwilligers, diensttijd, zorgsystemen", |
|
|
|
|
"negative_pole": "Individualistisch-progressief: PvdD, GroenLinks-PvdA, VVD, PVV", |
|
|
|
|
"flip": False, |
|
|
|
|
}, |
|
|
|
|
6: { |
|
|
|
|
"label": "Klimaat, energie en culturele integratie", |
|
|
|
|
"explanation": ( |
|
|
|
|
"Aan de positieve kant staan moties die LNG-capaciteit prefereren als alternatief " |
|
|
|
|
"voor strenge vulgraadverplichtingen, kernenergie als volwaardig CO₂-arm onderdeel " |
|
|
|
|
"van de energiemix willen erkennen op COP30, en discriminatie- en inclusiemeldpunten " |
|
|
|
|
"willen inventariseren. SGP, JA21, FVD en PVV scoren sterk positief. Aan de " |
|
|
|
|
"negatieve kant staan moties die fossiele-industrie-vertegenwoordigers willen weren " |
|
|
|
|
"van klimaatconferenties, structureel overleg met moslimgemeenschappen willen bij " |
|
|
|
|
"integratiebeleid, en aanvallen van Israël op Libanon veroordelen. " |
|
|
|
|
"PvdD, GroenLinks-PvdA, Volt en D66 scoren negatief. " |
|
|
|
|
"Deze as combineert energieideologie met culturele polarisatie rondom klimaat, " |
|
|
|
|
"integratie en buitenlandspolitiek." |
|
|
|
|
), |
|
|
|
|
"positive_pole": "Pro-fossiel, nationaal energiebeleid: SGP, JA21, FVD, PVV", |
|
|
|
|
"negative_pole": "Klimaatgericht en inclusief: PvdD, GroenLinks-PvdA, Volt, D66", |
|
|
|
|
"flip": False, |
|
|
|
|
}, |
|
|
|
|
7: { |
|
|
|
|
"label": "Bestuurlijk pragmatisme en implementatie (indicatief)", |
|
|
|
|
"explanation": ( |
|
|
|
|
"Een residuele as die overwegend beleidsdossiers uit 2024 (vorige parlementaire " |
|
|
|
|
"periode) omvat. De scores zijn smal (max ~11 punten) en de partijcombinaties " |
|
|
|
|
"ideologisch divers — dit label is indicatief. Aan de positieve kant staan " |
|
|
|
|
"pragmatische bestuursmoties: een compleet kostenoverzicht van producten van eigen " |
|
|
|
|
"bodem, papieren schoolboeken voor basisvaardigheden, een invoeringstoets voor het " |
|
|
|
|
"minimumloon en de A2-snelwegplanning. ChristenUnie, Volt, DENK en SP scoren " |
|
|
|
|
"positief. Aan de negatieve kant staan meer ideologisch geladen moties: een " |
|
|
|
|
"landelijk stookverbod (PvdD), het strafbaar stellen van verbranding van religieuze " |
|
|
|
|
"geschriften (DENK), chroom-6 schadevergoedingen en tegenhouden van nieuwe " |
|
|
|
|
"gaswinning. GroenLinks-PvdA, VVD, FVD en JA21 scoren negatief." |
|
|
|
|
), |
|
|
|
|
"positive_pole": "Praktisch-bestuurlijk: ChristenUnie, Volt, SGP, DENK, SP", |
|
|
|
|
"negative_pole": "Ideologisch-principieel: GroenLinks-PvdA, VVD, FVD, JA21", |
|
|
|
|
"flip": True, |
|
|
|
|
}, |
|
|
|
|
8: { |
|
|
|
|
"label": "Europese defensie-integratie (indicatief)", |
|
|
|
|
"explanation": ( |
|
|
|
|
"Aan de positieve kant staan moties die pleiten voor militaire mobiliteit als " |
|
|
|
|
"topprioriteit in EU/NAVO-verband en toewerken naar een militair Schengengebied, " |
|
|
|
|
"35% van defensiematerieel Europees inkopen en een Europees defensie-R&D-instituut " |
|
|
|
|
"oprichten. Ook het Nationaal Groeifonds en gewasbeschermingsonderzoek vallen " |
|
|
|
|
"positief. Volt en D66 scoren sterk positief. Aan de negatieve kant staan moties " |
|
|
|
|
"over ketenverantwoordelijkheid bij toeslagen (DENK), het coronaoversterfte-onderzoek " |
|
|
|
|
"(PVV/BBB), energiecontracten en huisvestingsregulering. SP (−39), DENK (−35) en " |
|
|
|
|
"PvdD (−26) scoren sterk negatief — dit betekent dat zij actief tégen deze " |
|
|
|
|
"EU-defensiemoties stemmen, niet simpelweg het thema negeren. Volt (N=1) domineert " |
|
|
|
|
"de positieve pool maar is als centroïde van één Kamerlid statistisch onbetrouwbaar." |
|
|
|
|
), |
|
|
|
|
"positive_pole": "Pro-EU defensie en innovatie: Volt, D66", |
|
|
|
|
"negative_pole": "Nationaal/pacifistisch of binnenlandsgericht: SP, DENK, PvdD, 50PLUS", |
|
|
|
|
"flip": False, |
|
|
|
|
}, |
|
|
|
|
9: { |
|
|
|
|
"label": "Decentraal bestuur en gemeenschapswaarden (indicatief)", |
|
|
|
|
"explanation": ( |
|
|
|
|
"Aan de positieve kant staan moties over naleving van de Financiële-verhoudingswet " |
|
|
|
|
"voor gemeenten, beperking van arbeidsmigratie binnen de EU, een nieuwe " |
|
|
|
|
"tandartsopleiding in Rotterdam, een actieplan tegen misbruik van hallucinerende " |
|
|
|
|
"geneesmiddelen en een oplossing voor milieuproblemen op Bonaire. SGP en " |
|
|
|
|
"ChristenUnie scoren sterk positief; ook DENK en SP. Aan de negatieve kant staan " |
|
|
|
|
"moties over een moratorium op geitenstallen, een verbod op gokadvertenties, " |
|
|
|
|
"verduidelijking van gronden voor voorlopige hechtenis, een leegstandbelasting voor " |
|
|
|
|
"woningen en end-to-end-encryptie. D66, JA21 en PVV scoren negatief. Deze as " |
|
|
|
|
"scheidt een nadruk op decentrale dienstverlening en gemeenschapsregulering van " |
|
|
|
|
"progressieve systeem- en rechtshervorming." |
|
|
|
|
), |
|
|
|
|
"positive_pole": "Lokaal en gemeenschapsgericht: SGP, ChristenUnie, DENK, SP", |
|
|
|
|
"negative_pole": "Progressieve systemen en rechten: D66, JA21, PVV", |
|
|
|
|
"flip": True, |
|
|
|
|
}, |
|
|
|
|
10: { |
|
|
|
|
"label": "Institutioneel toezicht en handhaving (indicatief)", |
|
|
|
|
"explanation": ( |
|
|
|
|
"De tiende as vangt resterende variantie op en scheidt partijen die sceptisch zijn " |
|
|
|
|
"over staatstoezicht van partijen die strikte regulering en handhaving steunen. " |
|
|
|
|
"Aan de positieve kant staan moties over minder tijdsintensieve schoolinspecties, " |
|
|
|
|
"het recht van toeslagenouders op hun persoonlijk dossier, behoud van de " |
|
|
|
|
"tegemoetkoming voor arbeidsongeschikten en een verlaging van de leeftijdsdrempel " |
|
|
|
|
"voor kindgesprekken. DENK, SP en PvdD scoren positief. Aan de negatieve kant " |
|
|
|
|
"staan moties over een aangifteplicht voor scholen bij veiligheidsincidenten, een " |
|
|
|
|
"rookverbod in auto's met kinderen, braakliggende landagrarisch landbouwgrond en verhoogd " |
|
|
|
|
"beloningsgeld voor tipgevers. GroenLinks-PvdA scoort opvallend sterk negatief, " |
|
|
|
|
"waarmee het zich onderscheidt van SP en DENK op handhavingsthema's." |
|
|
|
|
), |
|
|
|
|
"positive_pole": "Kritisch op overheidstoezicht: DENK, SP, PvdD, Volt — minder inspectielast", |
|
|
|
|
"negative_pole": "Pro-handhaving en regulering: GroenLinks-PvdA, CDA, SGP — veiligheid en naleving", |
|
|
|
|
"flip": True, |
|
|
|
|
}, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
# Ordered list of well-known parties for trajectory default selection. |
|
|
|
|
# Keeps the chart readable without overwhelming users with all parties. |
|
|
|
|
KNOWN_MAJOR_PARTIES = [ |
|
|
|
|
@ -527,13 +727,15 @@ def get_uniform_dim_windows(db_path: str) -> List[str]: |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _should_swap_axes(axis_def: dict) -> bool: |
|
|
|
|
"""Return True if the Y axis is 'Links–Rechts' and the X axis is not. |
|
|
|
|
"""Return True if the Y axis is economic left-right and the X axis is not. |
|
|
|
|
|
|
|
|
|
When true, caller should swap x/y positions and metadata so left-right |
|
|
|
|
is conventionally on the horizontal axis. |
|
|
|
|
When true, caller should swap x/y positions and metadata so the economic |
|
|
|
|
dimension (welfare vs market) is conventionally on the horizontal axis. |
|
|
|
|
""" |
|
|
|
|
lr = "Links\u2013Rechts" |
|
|
|
|
return axis_def.get("y_label") == lr and axis_def.get("x_label") != lr |
|
|
|
|
economic_labels = {"Verzorgingsstaat–Marktwerking", "Links–Rechts"} |
|
|
|
|
y_label = axis_def.get("y_label") |
|
|
|
|
x_label = axis_def.get("x_label") |
|
|
|
|
return y_label in economic_labels and x_label not in economic_labels |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _swap_axes( |
|
|
|
|
@ -583,7 +785,7 @@ def _render_axis_motions(label: str, conf_pct: str, top: dict) -> None: |
|
|
|
|
|
|
|
|
|
@st.cache_data(show_spinner="2D posities berekenen (kan even duren)…") |
|
|
|
|
def load_positions( |
|
|
|
|
db_path: str, window_size: str = "quarterly" |
|
|
|
|
db_path: str, window_size: str = "annual" |
|
|
|
|
) -> Tuple[Dict[str, Dict[str, Tuple[float, float]]], Dict]: |
|
|
|
|
"""Compute 2D positions per window using PCA on aligned SVD vectors. |
|
|
|
|
|
|
|
|
|
@ -1417,8 +1619,8 @@ def build_compass_tab(db_path: str, window_size: str) -> None: |
|
|
|
|
_x_label = display_label_for_modal(_raw_x, "x") |
|
|
|
|
_y_label = display_label_for_modal(_raw_y, "y") |
|
|
|
|
except Exception: |
|
|
|
|
_x_label = _raw_x or "Links\u2013Rechts" |
|
|
|
|
_y_label = _raw_y or "Progressief\u2013Conservatief" |
|
|
|
|
_x_label = _raw_x or "EU-integratie–Nationalisme" |
|
|
|
|
_y_label = _raw_y or "Populistisch–Institutioneel" |
|
|
|
|
|
|
|
|
|
if level == "Partijen": |
|
|
|
|
# Aggregate to party centroids |
|
|
|
|
@ -1599,6 +1801,29 @@ def build_compass_tab(db_path: str, window_size: str) -> None: |
|
|
|
|
# --------------------------------------------------------------------------- |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def choose_trajectory_title(axis_def: dict, axis: str, threshold: float = 0.65) -> str: |
|
|
|
|
"""Choose a short trajectory axis title based on aggregated confidence. |
|
|
|
|
|
|
|
|
|
axis: 'x' or 'y'. Returns axis_def label when its mean confidence >= threshold, |
|
|
|
|
otherwise returns the compact fallback 'As 1' / 'As 2'. Matches previous logic. |
|
|
|
|
""" |
|
|
|
|
_TH = threshold |
|
|
|
|
conf_map = axis_def.get(f"{axis}_label_confidence", {}) or {} |
|
|
|
|
vals = [v for v in conf_map.values() if v is not None] |
|
|
|
|
mean = float(sum(vals) / len(vals)) if vals else None |
|
|
|
|
label = axis_def.get(f"{axis}_label") |
|
|
|
|
if mean is not None and mean >= _TH and label: |
|
|
|
|
return label |
|
|
|
|
# Prefer the user-facing semantic fallback via the classifier helper |
|
|
|
|
try: |
|
|
|
|
from analysis.axis_classifier import display_label_for_modal |
|
|
|
|
|
|
|
|
|
fallback_modal = "As 1" if axis == "x" else "As 2" |
|
|
|
|
return display_label_for_modal(fallback_modal, axis) |
|
|
|
|
except Exception: |
|
|
|
|
return "As 1" if axis == "x" else "As 2" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def build_trajectories_tab(db_path: str, window_size: str) -> None: |
|
|
|
|
print( |
|
|
|
|
f"[TRAJ DEBUG] build_trajectories_tab called — db_path={db_path}, window_size={window_size}" |
|
|
|
|
@ -2108,29 +2333,6 @@ def build_trajectories_tab(db_path: str, window_size: str) -> None: |
|
|
|
|
x_mean = _mean_conf(x_conf_map) |
|
|
|
|
y_mean = _mean_conf(y_conf_map) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def choose_trajectory_title(axis_def: dict, axis: str, threshold: float = 0.65) -> str: |
|
|
|
|
"""Choose a short trajectory axis title based on aggregated confidence. |
|
|
|
|
|
|
|
|
|
axis: 'x' or 'y'. Returns axis_def label when its mean confidence >= threshold, |
|
|
|
|
otherwise returns the compact fallback 'As 1' / 'As 2'. Matches previous logic. |
|
|
|
|
""" |
|
|
|
|
_TH = threshold |
|
|
|
|
conf_map = axis_def.get(f"{axis}_label_confidence", {}) or {} |
|
|
|
|
vals = [v for v in conf_map.values() if v is not None] |
|
|
|
|
mean = float(sum(vals) / len(vals)) if vals else None |
|
|
|
|
label = axis_def.get(f"{axis}_label") |
|
|
|
|
if mean is not None and mean >= _TH and label: |
|
|
|
|
return label |
|
|
|
|
# Prefer the user-facing semantic fallback via the classifier helper |
|
|
|
|
try: |
|
|
|
|
from analysis.axis_classifier import display_label_for_modal |
|
|
|
|
|
|
|
|
|
fallback_modal = "As 1" if axis == "x" else "As 2" |
|
|
|
|
return display_label_for_modal(fallback_modal, axis) |
|
|
|
|
except Exception: |
|
|
|
|
return "As 1" if axis == "x" else "As 2" |
|
|
|
|
|
|
|
|
|
x_title = choose_trajectory_title(axis_def, "x", threshold=_THRESHOLD) |
|
|
|
|
y_title = choose_trajectory_title(axis_def, "y", threshold=_THRESHOLD) |
|
|
|
|
|
|
|
|
|
@ -2434,185 +2636,6 @@ def build_svd_components_tab(db_path: str) -> None: |
|
|
|
|
Reads thoughts/explorer/top_svd_top_motions.json and displays a selector |
|
|
|
|
for components 1..10 with theme labels/explanations and a detail pane per motion. |
|
|
|
|
""" |
|
|
|
|
# Political polarisation themes per SVD component (1-indexed, window=2025) |
|
|
|
|
# Produced by per-axis analysis of all 10 unique top motions (zero cross-axis overlap). |
|
|
|
|
SVD_THEMES: dict[int, dict[str, str]] = { |
|
|
|
|
1: { |
|
|
|
|
"label": "Links-rechts hoofdas", |
|
|
|
|
"explanation": ( |
|
|
|
|
"De dominante dimensie van het parlement: de klassieke links-rechts tegenstelling " |
|
|
|
|
"die het meeste verschil in stemgedrag verklaart. Aan de rechterkant (PVV, SGP, VVD, " |
|
|
|
|
"ChristenUnie) staan moties over defensie-uitbreiding, NAVO-verplichtingen, " |
|
|
|
|
"juridische ruimte voor drones en gaswinning. Aan de linkerkant (PvdD, SP, DENK, " |
|
|
|
|
"GroenLinks-PvdA) staan moties over huurverlaging, het veroordelen van " |
|
|
|
|
"antipersoneelslandmijnen, het opzeggen van het militaire verdrag met Israël en het " |
|
|
|
|
"oprichten van zorgbuurthuizen. De scheidslijn loopt dwars door thema's als " |
|
|
|
|
"veiligheid, economie, internationaal recht en sociale bescherming." |
|
|
|
|
), |
|
|
|
|
"positive_pole": "Nationalistisch-conservatief: PVV, SGP, VVD, ChristenUnie", |
|
|
|
|
"negative_pole": "Progressief-links: PvdD, SP, DENK, GroenLinks-PvdA", |
|
|
|
|
"flip": False, |
|
|
|
|
}, |
|
|
|
|
2: { |
|
|
|
|
"label": "Populistisch nationalisme versus institutioneel progressivisme", |
|
|
|
|
"explanation": ( |
|
|
|
|
"Deze as scheidt het populistisch-nationalistische bloc (PVV, FVD, Groep Markuszower, " |
|
|
|
|
"BBB) van het volledige overige parlement. Alleen PVV (+18), FVD (+4) en Groep " |
|
|
|
|
"Markuszower (+2) scoren positief; alle andere partijen scoren negatief, inclusief " |
|
|
|
|
"VVD (−15), CDA (−14), SGP (−25) en ChristenUnie (−59). Positieve moties: artsen " |
|
|
|
|
"vrijpleiten voor hydroxychloroquine/ivermectine, Syriërs terugsturen, geen geld " |
|
|
|
|
"aan Jordanië, tijdelijke bescherming Oekraïne beëindigen. Negatieve moties: " |
|
|
|
|
"digitale toegankelijkheid Caribisch Nederland, ethiekprogramma Defensie, zorg voor " |
|
|
|
|
"slachtoffers bombardement Hawija, zorgkwaliteitsstandaarden. Dit is geen links-rechts " |
|
|
|
|
"verdeling maar een nativistisch-populistisch vs. institutioneel onderscheid." |
|
|
|
|
), |
|
|
|
|
"positive_pole": "Populistisch-nationalistisch: PVV, FVD, Groep Markuszower, BBB", |
|
|
|
|
"negative_pole": "Institutioneel: alle overige partijen — van VVD en SGP tot GroenLinks-PvdA en Volt", |
|
|
|
|
"flip": False, |
|
|
|
|
}, |
|
|
|
|
3: { |
|
|
|
|
"label": "Verzorgingsstaat versus bezuinigingen en marktwerking", |
|
|
|
|
"explanation": ( |
|
|
|
|
"Deze as weerspiegelt de spanning tussen staatsingrijpen en marktliberalisme, " |
|
|
|
|
"aangescherpt door de kabinetscrisis van 2025. Aan de positieve kant staan moties " |
|
|
|
|
"die bezuinigingen op zorg en het gemeentefonds willen terugdraaien, winstuitkeringen " |
|
|
|
|
"in de zorg verbieden en publieke controle over ziekenhuisfusies eisen. SP, PvdD, " |
|
|
|
|
"GroenLinks-PvdA en PVV stemmen hier gelijk — ondanks hun tegengestelde PC1-posities. " |
|
|
|
|
"Aan de negatieve kant staan moties " |
|
|
|
|
"over marktwerking in de zorg, fiscale bedrijfsopvolgingsfaciliteiten (VVD), " |
|
|
|
|
"doorgaan met besturen ondanks de kabinetscrisis (VVD/Yeşilgöz) en defensie-" |
|
|
|
|
"uitgaven van 3,5% bbp." |
|
|
|
|
), |
|
|
|
|
"positive_pole": "Pro-verzorgingsstaat: SP, PvdD, GroenLinks-PvdA, PVV (anti-bezuinigingen)", |
|
|
|
|
"negative_pole": "Marktliberaal en fiscaal conservatief: VVD, D66, CDA, SGP", |
|
|
|
|
"flip": True, |
|
|
|
|
}, |
|
|
|
|
4: { |
|
|
|
|
"label": "Pragmatisch centrisme versus ideologische radicaliteit", |
|
|
|
|
"explanation": ( |
|
|
|
|
"De gevestigde centrumpartijen (D66, CDA, VVD, 50PLUS) staan tegenover zowel " |
|
|
|
|
"rechts-radicale als identiteitspolitieke posities. Aan de positieve kant staan " |
|
|
|
|
"moties over openbare toiletten, vaderbetrokkenheid bij opvoeding, internationale " |
|
|
|
|
"samenwerking met Australië en Canada, en long covid-expertise. Dit zijn pragmatische, " |
|
|
|
|
"institutionele beleidsposities. Aan de negatieve kant staan moties over een " |
|
|
|
|
"migratiesaldo-cap van 60.000, het verlaten van de WHO, kinderen in pleeggezinnen " |
|
|
|
|
"van hetzelfde geslacht (FVD) en de bescherming van religieuze schoolidentiteit " |
|
|
|
|
"via artikel 23. De negatieve pool combineert populistisch-rechts met " |
|
|
|
|
"identiteitsgerichte posities van zowel rechts als links." |
|
|
|
|
), |
|
|
|
|
"positive_pole": "Constructief centrum: D66, CDA, VVD, 50PLUS — pragmatisch en internationaal", |
|
|
|
|
"negative_pole": "Radicaal-ideologisch: FVD, Groep Markuszower (rechts), ChristenUnie, DENK (religieus/identiteit)", |
|
|
|
|
"flip": True, |
|
|
|
|
}, |
|
|
|
|
5: { |
|
|
|
|
"label": "Christelijk-sociaal communitarisme", |
|
|
|
|
"explanation": ( |
|
|
|
|
"Deze as scheidt partijen die gemeenschapszorg, burgerplicht en informele " |
|
|
|
|
"ondersteuningsstructuren benadrukken van partijen die individuele vrijheden en " |
|
|
|
|
"progressieve maatschappelijke hervorming voorstaan. Aan de positieve kant staan " |
|
|
|
|
"moties over schuldhulpverlening via vrijwilligersorganisaties, de maatschappelijke " |
|
|
|
|
"diensttijd voor jongeren met een afstand tot de arbeidsmarkt, en de gastouderopvang. " |
|
|
|
|
"ChristenUnie, SGP en CDA voeren hier de toon; ook D66 scoort positief door steun " |
|
|
|
|
"aan sociaal beleid. Aan de negatieve kant staan moties over wettelijke erkenning " |
|
|
|
|
"van meerouderschap, abortusrecht in het EU-Handvest, armoedebeleid en " |
|
|
|
|
"buitenlandse beïnvloeding. PvdD, GroenLinks-PvdA en VVD scoren hier negatief." |
|
|
|
|
), |
|
|
|
|
"positive_pole": "Gemeenschapsgericht: ChristenUnie, SGP, CDA, D66 — vrijwilligers, diensttijd, zorgsystemen", |
|
|
|
|
"negative_pole": "Individualistisch-progressief: PvdD, GroenLinks-PvdA, VVD, PVV", |
|
|
|
|
"flip": False, |
|
|
|
|
}, |
|
|
|
|
6: { |
|
|
|
|
"label": "Klimaat, energie en culturele integratie", |
|
|
|
|
"explanation": ( |
|
|
|
|
"Aan de positieve kant staan moties die LNG-capaciteit prefereren als alternatief " |
|
|
|
|
"voor strenge vulgraadverplichtingen, kernenergie als volwaardig CO₂-arm onderdeel " |
|
|
|
|
"van de energiemix willen erkennen op COP30, en discriminatie- en inclusiemeldpunten " |
|
|
|
|
"willen inventariseren. SGP, JA21, FVD en PVV scoren sterk positief. Aan de " |
|
|
|
|
"negatieve kant staan moties die fossiele-industrie-vertegenwoordigers willen weren " |
|
|
|
|
"van klimaatconferenties, structureel overleg met moslimgemeenschappen willen bij " |
|
|
|
|
"integratiebeleid, en aanvallen van Israël op Libanon veroordelen. " |
|
|
|
|
"PvdD, GroenLinks-PvdA, Volt en D66 scoren negatief. " |
|
|
|
|
"Deze as combineert energieideologie met culturele polarisatie rondom klimaat, " |
|
|
|
|
"integratie en buitenlandspolitiek." |
|
|
|
|
), |
|
|
|
|
"positive_pole": "Pro-fossiel, nationaal energiebeleid: SGP, JA21, FVD, PVV", |
|
|
|
|
"negative_pole": "Klimaatgericht en inclusief: PvdD, GroenLinks-PvdA, Volt, D66", |
|
|
|
|
"flip": False, |
|
|
|
|
}, |
|
|
|
|
7: { |
|
|
|
|
"label": "Bestuurlijk pragmatisme en implementatie (indicatief)", |
|
|
|
|
"explanation": ( |
|
|
|
|
"Een residuele as die overwegend beleidsdossiers uit 2024 (vorige parlementaire " |
|
|
|
|
"periode) omvat. De scores zijn smal (max ~11 punten) en de partijcombinaties " |
|
|
|
|
"ideologisch divers — dit label is indicatief. Aan de positieve kant staan " |
|
|
|
|
"pragmatische bestuursmoties: een compleet kostenoverzicht van producten van eigen " |
|
|
|
|
"bodem, papieren schoolboeken voor basisvaardigheden, een invoeringstoets voor het " |
|
|
|
|
"minimumloon en de A2-snelwegplanning. ChristenUnie, Volt, DENK en SP scoren " |
|
|
|
|
"positief. Aan de negatieve kant staan meer ideologisch geladen moties: een " |
|
|
|
|
"landelijk stookverbod (PvdD), het strafbaar stellen van verbranding van religieuze " |
|
|
|
|
"geschriften (DENK), chroom-6 schadevergoedingen en tegenhouden van nieuwe " |
|
|
|
|
"gaswinning. GroenLinks-PvdA, VVD, FVD en JA21 scoren negatief." |
|
|
|
|
), |
|
|
|
|
"positive_pole": "Praktisch-bestuurlijk: ChristenUnie, Volt, SGP, DENK, SP", |
|
|
|
|
"negative_pole": "Ideologisch-principieel: GroenLinks-PvdA, VVD, FVD, JA21", |
|
|
|
|
"flip": True, |
|
|
|
|
}, |
|
|
|
|
8: { |
|
|
|
|
"label": "Europese defensie-integratie (indicatief)", |
|
|
|
|
"explanation": ( |
|
|
|
|
"Aan de positieve kant staan moties die pleiten voor militaire mobiliteit als " |
|
|
|
|
"topprioriteit in EU/NAVO-verband en toewerken naar een militair Schengengebied, " |
|
|
|
|
"35% van defensiematerieel Europees inkopen en een Europees defensie-R&D-instituut " |
|
|
|
|
"oprichten. Ook het Nationaal Groeifonds en gewasbeschermingsonderzoek vallen " |
|
|
|
|
"positief. Volt en D66 scoren sterk positief. Aan de negatieve kant staan moties " |
|
|
|
|
"over ketenverantwoordelijkheid bij toeslagen (DENK), het coronaoversterfte-onderzoek " |
|
|
|
|
"(PVV/BBB), energiecontracten en huisvestingsregulering. SP (−39), DENK (−35) en " |
|
|
|
|
"PvdD (−26) scoren sterk negatief — dit betekent dat zij actief tégen deze " |
|
|
|
|
"EU-defensiemoties stemmen, niet simpelweg het thema negeren. Volt (N=1) domineert " |
|
|
|
|
"de positieve pool maar is als centroïde van één Kamerlid statistisch onbetrouwbaar." |
|
|
|
|
), |
|
|
|
|
"positive_pole": "Pro-EU defensie en innovatie: Volt, D66", |
|
|
|
|
"negative_pole": "Nationaal/pacifistisch of binnenlandsgericht: SP, DENK, PvdD, 50PLUS", |
|
|
|
|
"flip": False, |
|
|
|
|
}, |
|
|
|
|
9: { |
|
|
|
|
"label": "Decentraal bestuur en gemeenschapswaarden (indicatief)", |
|
|
|
|
"explanation": ( |
|
|
|
|
"Aan de positieve kant staan moties over naleving van de Financiële-verhoudingswet " |
|
|
|
|
"voor gemeenten, beperking van arbeidsmigratie binnen de EU, een nieuwe " |
|
|
|
|
"tandartsopleiding in Rotterdam, een actieplan tegen misbruik van hallucinerende " |
|
|
|
|
"geneesmiddelen en een oplossing voor milieuproblemen op Bonaire. SGP en " |
|
|
|
|
"ChristenUnie scoren sterk positief; ook DENK en SP. Aan de negatieve kant staan " |
|
|
|
|
"moties over een moratorium op geitenstallen, een verbod op gokadvertenties, " |
|
|
|
|
"verduidelijking van gronden voor voorlopige hechtenis, een leegstandbelasting voor " |
|
|
|
|
"woningen en end-to-end-encryptie. D66, JA21 en PVV scoren negatief. Deze as " |
|
|
|
|
"scheidt een nadruk op decentrale dienstverlening en gemeenschapsregulering van " |
|
|
|
|
"progressieve systeem- en rechtshervorming." |
|
|
|
|
), |
|
|
|
|
"positive_pole": "Lokaal en gemeenschapsgericht: SGP, ChristenUnie, DENK, SP", |
|
|
|
|
"negative_pole": "Progressieve systemen en rechten: D66, JA21, PVV", |
|
|
|
|
"flip": True, |
|
|
|
|
}, |
|
|
|
|
10: { |
|
|
|
|
"label": "Institutioneel toezicht en handhaving (indicatief)", |
|
|
|
|
"explanation": ( |
|
|
|
|
"De tiende as vangt resterende variantie op en scheidt partijen die sceptisch zijn " |
|
|
|
|
"over staatstoezicht van partijen die strikte regulering en handhaving steunen. " |
|
|
|
|
"Aan de positieve kant staan moties over minder tijdsintensieve schoolinspecties, " |
|
|
|
|
"het recht van toeslagenouders op hun persoonlijk dossier, behoud van de " |
|
|
|
|
"tegemoetkoming voor arbeidsongeschikten en een verlaging van de leeftijdsdrempel " |
|
|
|
|
"voor kindgesprekken. DENK, SP en PvdD scoren positief. Aan de negatieve kant " |
|
|
|
|
"staan moties over een aangifteplicht voor scholen bij veiligheidsincidenten, een " |
|
|
|
|
"rookverbod in auto's met kinderen, braakliggende landbouwgrond en verhoogd " |
|
|
|
|
"beloningsgeld voor tipgevers. GroenLinks-PvdA scoort opvallend sterk negatief, " |
|
|
|
|
"waarmee het zich onderscheidt van SP en DENK op handhavingsthema's." |
|
|
|
|
), |
|
|
|
|
"positive_pole": "Kritisch op overheidstoezicht: DENK, SP, PvdD, Volt — minder inspectielast", |
|
|
|
|
"negative_pole": "Pro-handhaving en regulering: GroenLinks-PvdA, CDA, SGP — veiligheid en naleving", |
|
|
|
|
"flip": True, |
|
|
|
|
}, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
st.subheader("🔬 SVD Assen — politieke polarisatiethema's") |
|
|
|
|
st.markdown( |
|
|
|
|
"Elke SVD-as representeert een latente politieke dimensie afgeleid uit stempatronen " |
|
|
|
|
@ -2670,13 +2693,27 @@ def build_svd_components_tab(db_path: str) -> None: |
|
|
|
|
return f"As {c} — {lbl}" if lbl else f"As {c}" |
|
|
|
|
|
|
|
|
|
comp_display = [_comp_label(c) for c in comp_options] |
|
|
|
|
comp_sel_idx = st.selectbox( |
|
|
|
|
"Selecteer SVD-as", |
|
|
|
|
options=list(range(len(comp_options))), |
|
|
|
|
format_func=lambda i: comp_display[i], |
|
|
|
|
index=0, |
|
|
|
|
) |
|
|
|
|
comp_sel = comp_options[comp_sel_idx] |
|
|
|
|
|
|
|
|
|
# Sidebar controls for window selection and minimum MPs filter |
|
|
|
|
col1, col2 = st.columns([2, 1]) |
|
|
|
|
with col2: |
|
|
|
|
comp_sel_idx = st.selectbox( |
|
|
|
|
"Selecteer SVD-as", |
|
|
|
|
options=list(range(len(comp_options))), |
|
|
|
|
format_func=lambda i: comp_display[i], |
|
|
|
|
index=0, |
|
|
|
|
) |
|
|
|
|
comp_sel = comp_options[comp_sel_idx] |
|
|
|
|
|
|
|
|
|
# Minimum MPs filter (only relevant for components 1-2 which use party centroids) |
|
|
|
|
min_mps = st.number_input( |
|
|
|
|
"Min. Kamerleden per partij", |
|
|
|
|
min_value=1, |
|
|
|
|
max_value=20, |
|
|
|
|
value=1, |
|
|
|
|
step=1, |
|
|
|
|
help="Partijen met minder dan dit aantal Kamerleden worden niet weergegeven.", |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
# Show theme explanation |
|
|
|
|
theme = SVD_THEMES.get(comp_sel, {}) |
|
|
|
|
@ -2700,22 +2737,39 @@ def build_svd_components_tab(db_path: str) -> None: |
|
|
|
|
positions_by_window, axis_def = load_positions(db_path) |
|
|
|
|
if axis_def is None: |
|
|
|
|
axis_def = {} |
|
|
|
|
# choose the current parliament window if present |
|
|
|
|
window = ( |
|
|
|
|
"current_parliament" |
|
|
|
|
if "current_parliament" in positions_by_window |
|
|
|
|
else sorted(positions_by_window.keys())[-1] |
|
|
|
|
|
|
|
|
|
# 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, {}) |
|
|
|
|
|
|
|
|
|
# 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 = {} |
|
|
|
|
@ -2737,8 +2791,18 @@ def build_svd_components_tab(db_path: str) -> None: |
|
|
|
|
"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 {} |
|
|
|
|
) |
|
|
|
|
else: |
|
|
|
|
party_scores = party_scores_default |
|
|
|
|
# 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 {} |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
# Convert party_scores (possibly [x,y] lists or legacy vectors) into explicit (x,y) coords |
|
|
|
|
party_coords: dict = {} |
|
|
|
|
@ -2749,9 +2813,22 @@ def build_svd_components_tab(db_path: str) -> None: |
|
|
|
|
except Exception: |
|
|
|
|
continue |
|
|
|
|
|
|
|
|
|
_render_party_axis_chart( |
|
|
|
|
party_coords, comp_sel, theme, bootstrap_data=bootstrap_data |
|
|
|
|
) |
|
|
|
|
# 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 |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
# Only render party axis chart for components 1 and 2 (which have 2D coords) |
|
|
|
|
if comp_sel in (1, 2): |
|
|
|
|
_render_party_axis_chart( |
|
|
|
|
party_coords, comp_sel, theme, bootstrap_data=bootstrap_data |
|
|
|
|
) |
|
|
|
|
else: |
|
|
|
|
st.caption( |
|
|
|
|
"_Partijposities zijn alleen beschikbaar voor de eerste twee SVD-assen._" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
# 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] |
|
|
|
|
|