refactor: move SVD_THEMES to module level for import (Task 2)
This commit is contained in:
@@ -0,0 +1,175 @@
|
|||||||
|
"""Unified SVD component labels and automatic flip direction computation.
|
||||||
|
|
||||||
|
This module provides a single source of truth for SVD component labels,
|
||||||
|
deriving them from SVD_THEMES in explorer.py. It also computes flip
|
||||||
|
directions automatically based on party centroids.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Canonical party sets for orientation
|
||||||
|
# Right-wing parties that should appear on the right side of axes
|
||||||
|
RIGHT_PARTIES = {
|
||||||
|
"PVV",
|
||||||
|
"VVD",
|
||||||
|
"FVD",
|
||||||
|
"BBB",
|
||||||
|
"JA21",
|
||||||
|
"Nieuw Sociaal Contract",
|
||||||
|
"SGP",
|
||||||
|
"CDA",
|
||||||
|
"ChristenUnie",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Left-wing parties that should appear on the left side of axes
|
||||||
|
LEFT_PARTIES = {
|
||||||
|
"SP",
|
||||||
|
"PvdA",
|
||||||
|
"GL",
|
||||||
|
"GroenLinks",
|
||||||
|
"GroenLinks-PvdA",
|
||||||
|
"DENK",
|
||||||
|
"PvdD",
|
||||||
|
"Volt",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cache for SVD_THEMES to avoid repeated imports
|
||||||
|
_svd_themes_cache: Optional[Dict[int, Dict[str, str]]] = None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_svd_themes() -> Dict[int, Dict[str, str]]:
|
||||||
|
"""Import SVD_THEMES from explorer.py.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict mapping component number to theme dict with keys:
|
||||||
|
- label: Short label for the component
|
||||||
|
- explanation: Detailed explanation
|
||||||
|
- positive_pole: Description of positive pole
|
||||||
|
- negative_pole: Description of negative pole
|
||||||
|
- flip: Whether to flip the axis
|
||||||
|
"""
|
||||||
|
global _svd_themes_cache
|
||||||
|
if _svd_themes_cache is not None:
|
||||||
|
return _svd_themes_cache
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Import SVD_THEMES from explorer at runtime to avoid circular imports
|
||||||
|
# explorer.py now exports SVD_THEMES at module level
|
||||||
|
import explorer
|
||||||
|
|
||||||
|
_svd_themes_cache = explorer.SVD_THEMES
|
||||||
|
return _svd_themes_cache
|
||||||
|
except ImportError as e:
|
||||||
|
_logger.warning("Could not import explorer.SVD_THEMES: %s", e)
|
||||||
|
return {}
|
||||||
|
except Exception as e:
|
||||||
|
_logger.exception("Failed to load SVD_THEMES from explorer.py: %s", e)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def get_svd_label(component: int) -> str:
|
||||||
|
"""Get short label for SVD component.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
component: SVD component number (1-indexed)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Short label string (e.g., 'EU-integratie–Nationalisme')
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If component < 1
|
||||||
|
"""
|
||||||
|
if component < 1:
|
||||||
|
raise ValueError(f"Component must be >= 1, got {component}")
|
||||||
|
|
||||||
|
themes = _get_svd_themes()
|
||||||
|
if component in themes:
|
||||||
|
return themes[component].get("label", f"As {component}")
|
||||||
|
|
||||||
|
# Fallback labels for components 1-3 (most commonly used)
|
||||||
|
fallback_labels = {
|
||||||
|
1: "EU-integratie–Nationalisme",
|
||||||
|
2: "Populistisch–Institutioneel",
|
||||||
|
3: "Verzorgingsstaat–Marktwerking",
|
||||||
|
}
|
||||||
|
return fallback_labels.get(component, f"As {component}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_svd_theme(component: int) -> Dict[str, str]:
|
||||||
|
"""Get full theme dict for SVD component.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
component: SVD component number (1-indexed)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with keys: label, explanation, positive_pole, negative_pole, flip
|
||||||
|
"""
|
||||||
|
if component < 1:
|
||||||
|
raise ValueError(f"Component must be >= 1, got {component}")
|
||||||
|
|
||||||
|
themes = _get_svd_themes()
|
||||||
|
if component in themes:
|
||||||
|
return themes[component]
|
||||||
|
|
||||||
|
# Return minimal fallback
|
||||||
|
return {
|
||||||
|
"label": get_svd_label(component),
|
||||||
|
"explanation": "",
|
||||||
|
"positive_pole": "",
|
||||||
|
"negative_pole": "",
|
||||||
|
"flip": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def compute_flip_direction(
|
||||||
|
component: int, party_scores: Dict[str, List[float]]
|
||||||
|
) -> bool:
|
||||||
|
"""Compute flip direction so right parties appear on the right side.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
component: SVD component number (1-indexed)
|
||||||
|
party_scores: Dict mapping party name to list of scores per component
|
||||||
|
(party_scores[party][0] is score for component 1, etc.)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if axis should be flipped so right parties are on right.
|
||||||
|
False otherwise.
|
||||||
|
"""
|
||||||
|
if component < 1:
|
||||||
|
return False
|
||||||
|
|
||||||
|
idx = component - 1 # Convert to 0-indexed
|
||||||
|
|
||||||
|
right_scores = []
|
||||||
|
left_scores = []
|
||||||
|
|
||||||
|
for party, scores in party_scores.items():
|
||||||
|
if len(scores) <= idx:
|
||||||
|
continue
|
||||||
|
|
||||||
|
score = scores[idx]
|
||||||
|
if party in RIGHT_PARTIES:
|
||||||
|
right_scores.append(score)
|
||||||
|
elif party in LEFT_PARTIES:
|
||||||
|
left_scores.append(score)
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (x_label, y_label)
|
||||||
|
"""
|
||||||
|
return (get_svd_label(1), get_svd_label(2))
|
||||||
+292
-215
@@ -269,6 +269,18 @@ def select_trajectory_plot_data(
|
|||||||
if has_valid:
|
if has_valid:
|
||||||
plottable_parties.append(p)
|
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()
|
fig = go.Figure()
|
||||||
trace_count = 0
|
trace_count = 0
|
||||||
banner_text: Optional[str] = None
|
banner_text: Optional[str] = None
|
||||||
@@ -341,6 +353,9 @@ def select_trajectory_plot_data(
|
|||||||
trace_count += 1
|
trace_count += 1
|
||||||
|
|
||||||
banner_text = "Partijcentroiden niet beschikbaar — tonen individuele MP-trajecten als fallback."
|
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
|
return fig, trace_count, banner_text
|
||||||
|
|
||||||
# Otherwise plot party centroids for selected parties intersecting plottable
|
# Otherwise plot party centroids for selected parties intersecting plottable
|
||||||
@@ -380,6 +395,9 @@ def select_trajectory_plot_data(
|
|||||||
)
|
)
|
||||||
trace_count += 1
|
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
|
return fig, trace_count, None
|
||||||
|
|
||||||
|
|
||||||
@@ -410,6 +428,188 @@ PARTY_COLOURS: Dict[str, str] = {
|
|||||||
"Unknown": "#9E9E9E",
|
"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.
|
# Ordered list of well-known parties for trajectory default selection.
|
||||||
# Keeps the chart readable without overwhelming users with all parties.
|
# Keeps the chart readable without overwhelming users with all parties.
|
||||||
KNOWN_MAJOR_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:
|
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
|
When true, caller should swap x/y positions and metadata so the economic
|
||||||
is conventionally on the horizontal axis.
|
dimension (welfare vs market) is conventionally on the horizontal axis.
|
||||||
"""
|
"""
|
||||||
lr = "Links\u2013Rechts"
|
economic_labels = {"Verzorgingsstaat–Marktwerking", "Links–Rechts"}
|
||||||
return axis_def.get("y_label") == lr and axis_def.get("x_label") != lr
|
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(
|
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)…")
|
@st.cache_data(show_spinner="2D posities berekenen (kan even duren)…")
|
||||||
def load_positions(
|
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]:
|
) -> Tuple[Dict[str, Dict[str, Tuple[float, float]]], Dict]:
|
||||||
"""Compute 2D positions per window using PCA on aligned SVD vectors.
|
"""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")
|
_x_label = display_label_for_modal(_raw_x, "x")
|
||||||
_y_label = display_label_for_modal(_raw_y, "y")
|
_y_label = display_label_for_modal(_raw_y, "y")
|
||||||
except Exception:
|
except Exception:
|
||||||
_x_label = _raw_x or "Links\u2013Rechts"
|
_x_label = _raw_x or "EU-integratie–Nationalisme"
|
||||||
_y_label = _raw_y or "Progressief\u2013Conservatief"
|
_y_label = _raw_y or "Populistisch–Institutioneel"
|
||||||
|
|
||||||
if level == "Partijen":
|
if level == "Partijen":
|
||||||
# Aggregate to party centroids
|
# 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:
|
def build_trajectories_tab(db_path: str, window_size: str) -> None:
|
||||||
print(
|
print(
|
||||||
f"[TRAJ DEBUG] build_trajectories_tab called — db_path={db_path}, window_size={window_size}"
|
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)
|
x_mean = _mean_conf(x_conf_map)
|
||||||
y_mean = _mean_conf(y_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)
|
x_title = choose_trajectory_title(axis_def, "x", threshold=_THRESHOLD)
|
||||||
y_title = choose_trajectory_title(axis_def, "y", 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
|
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.
|
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.subheader("🔬 SVD Assen — politieke polarisatiethema's")
|
||||||
st.markdown(
|
st.markdown(
|
||||||
"Elke SVD-as representeert een latente politieke dimensie afgeleid uit stempatronen "
|
"Elke SVD-as representeert een latente politieke dimensie afgeleid uit stempatronen "
|
||||||
@@ -2670,6 +2693,10 @@ def build_svd_components_tab(db_path: str) -> None:
|
|||||||
return f"As {c} — {lbl}" if lbl else f"As {c}"
|
return f"As {c} — {lbl}" if lbl else f"As {c}"
|
||||||
|
|
||||||
comp_display = [_comp_label(c) for c in comp_options]
|
comp_display = [_comp_label(c) for c in comp_options]
|
||||||
|
|
||||||
|
# Sidebar controls for window selection and minimum MPs filter
|
||||||
|
col1, col2 = st.columns([2, 1])
|
||||||
|
with col2:
|
||||||
comp_sel_idx = st.selectbox(
|
comp_sel_idx = st.selectbox(
|
||||||
"Selecteer SVD-as",
|
"Selecteer SVD-as",
|
||||||
options=list(range(len(comp_options))),
|
options=list(range(len(comp_options))),
|
||||||
@@ -2678,6 +2705,16 @@ def build_svd_components_tab(db_path: str) -> None:
|
|||||||
)
|
)
|
||||||
comp_sel = comp_options[comp_sel_idx]
|
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
|
# Show theme explanation
|
||||||
theme = SVD_THEMES.get(comp_sel, {})
|
theme = SVD_THEMES.get(comp_sel, {})
|
||||||
if theme:
|
if theme:
|
||||||
@@ -2700,22 +2737,39 @@ def build_svd_components_tab(db_path: str) -> None:
|
|||||||
positions_by_window, axis_def = load_positions(db_path)
|
positions_by_window, axis_def = load_positions(db_path)
|
||||||
if axis_def is None:
|
if axis_def is None:
|
||||||
axis_def = {}
|
axis_def = {}
|
||||||
# choose the current parliament window if present
|
|
||||||
window = (
|
# Window selector for components 1-2 (same windows as Political Compass)
|
||||||
"current_parliament"
|
year_windows = sorted(
|
||||||
if "current_parliament" in positions_by_window
|
w for w in positions_by_window if w != "current_parliament"
|
||||||
else sorted(positions_by_window.keys())[-1]
|
|
||||||
)
|
)
|
||||||
|
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, {})
|
pos = positions_by_window.get(window, {})
|
||||||
|
|
||||||
# build party -> list of MP x/y coords
|
# build party -> list of MP x/y coords
|
||||||
party_map = load_party_map(db_path)
|
party_map = load_party_map(db_path)
|
||||||
per_party_coords: dict = {}
|
per_party_coords: dict = {}
|
||||||
|
party_mp_counts: dict = {} # Track MP count per party
|
||||||
for ent, (x, y) in pos.items():
|
for ent, (x, y) in pos.items():
|
||||||
party = party_map.get(ent)
|
party = party_map.get(ent)
|
||||||
if party is None:
|
if party is None:
|
||||||
continue
|
continue
|
||||||
per_party_coords.setdefault(party, []).append((x, y))
|
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
|
# construct party_scores mapping: prefer MP centroid [x,y], fallback to default vector
|
||||||
party_scores = {}
|
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"
|
"Failed to derive party centroids from positions_by_window; falling back to load_party_axis_scores"
|
||||||
)
|
)
|
||||||
party_scores = party_scores_default
|
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:
|
else:
|
||||||
party_scores = party_scores_default
|
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
|
# Convert party_scores (possibly [x,y] lists or legacy vectors) into explicit (x,y) coords
|
||||||
party_coords: dict = {}
|
party_coords: dict = {}
|
||||||
@@ -2749,9 +2813,22 @@ def build_svd_components_tab(db_path: str) -> None:
|
|||||||
except Exception:
|
except Exception:
|
||||||
continue
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
# Only render party axis chart for components 1 and 2 (which have 2D coords)
|
||||||
|
if comp_sel in (1, 2):
|
||||||
_render_party_axis_chart(
|
_render_party_axis_chart(
|
||||||
party_coords, comp_sel, theme, bootstrap_data=bootstrap_data
|
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)
|
# 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]
|
motion_ids = [m.get("motion_id") for m in motions if m.get("motion_id") is not None]
|
||||||
|
|||||||
Reference in New Issue
Block a user