feat(explorer): add SVD axis themes and unique-motion deduplication

- Regenerated top_svd_top_motions.json for window=2025 with strict
  cross-axis deduplication: 100 unique motions across 10 axes (10 per
  axis, zero overlap), sorted by absolute SVD score
- Added SVD_THEMES dict to build_svd_components_tab with Dutch-language
  theme label and political-polarisation explanation for each of the 10
  axes (e.g. 'Confessioneel-conservatief vs. seculier-progressief')
- Selectbox now shows 'As N — <theme>' instead of bare component number
- Each selected axis shows an info banner with the full explanation
- Motion list buttons show ▲/▼ to indicate positive/negative SVD loading
- Translated UI strings to Dutch for consistency
main
Sven Geboers 1 month ago
parent e29d8a8055
commit 8b73ab8bce
  1. 142
      explorer.py
  2. 840
      thoughts/explorer/top_svd_top_motions.json

@ -627,9 +627,109 @@ def build_svd_components_tab(db_path: str) -> None:
"""New tab: show top motions contributing to top SVD components.
Reads thoughts/explorer/top_svd_top_motions.json and displays a selector
for components 1..10 and a detail pane for selected motion.
for components 1..10 with theme labels/explanations and a detail pane per motion.
"""
st.subheader("🔬 SVD Components — top contributing motions")
# Political polarisation themes per SVD component (1-indexed, window=2025)
SVD_THEMES: dict[int, dict[str, str]] = {
1: {
"label": "Regulering vs. status-quo",
"explanation": (
"Deze as onderscheidt moties die inzetten op nieuwe regelgeving en "
"handhaving (kansspelen, EU-harmonisatie, parlementaire procedures) van moties "
"die eerder kiezen voor continuïteit of deregulering. "
"Positieve scores wijzen op meer reguleringsbereidheid."
),
},
2: {
"label": "Progressief activisme vs. pragmatisch bestuur",
"explanation": (
"De tweede as capteert het spanningsveld tussen linkse-progressieve eisen "
"(Israël-sancties, stikstofminister wantrouwen, klimaatrechtszaken) en "
"pragmatisch bestuur. Negatieve scores corresponderen met progressief-activistische "
"moties; positieve scores met de bestuurspragmatische kant."
),
},
3: {
"label": "Eigendomsvrijheid en soevereiniteit vs. staatssturing",
"explanation": (
"De sterkste as in het stempatroon (hoogste absolute scores). Hij stelt "
"het recht op vrije keuze van de eigenaar en nationale/individuele soevereiniteit "
"tegenover overheidsinterventie (woningmarkt, detentiebeleid, NGO-regulering). "
"Negatieve scores vertegenwoordigen een voorkeur voor eigendomsvrijheid; "
"positieve scores voor staatssturing."
),
},
4: {
"label": "Betaalbaarheid publieke diensten vs. vrijhandel",
"explanation": (
"Deze as weerspiegelt het debat over betaalbare zorg, openbaar vervoer en "
"defensie-infrastructuur (positief) tegenover internationale handelsakkoorden "
"zoals EU-Mercosur (negatief). Het scheidt partijen die nationale "
"dienstverlening centraal stellen van partijen die vrijhandel prioriteren."
),
},
5: {
"label": "Confessioneel-conservatief vs. seculier-progressief",
"explanation": (
"Een klassieke waarden-as: ChristenUnie- en SGP-moties over euthanasie, "
"'Moeder en Kind'-fonds, kerkruimte en gezinsbelasting scoren positief. "
"Deze as reflecteert het diepste sociale-waardenverschil in de Kamer."
),
},
6: {
"label": "Bescherming kwetsbare groepen vs. marktwerking",
"explanation": (
"Positieve scores: bescherming van pgb-houders, christelijke minderheden in "
"asielbeleid, en kinderopvangtoeslag voor middeninkomens. Negatieve scores: "
"meer marktwerking bij arbeidsmigratie en internationale hulp. Deze as "
"toont het spanningsveld tussen sociale bescherming en liberalisering."
),
},
7: {
"label": "Democratische controle en sociale rechten vs. marktwerking",
"explanation": (
"Moties die parlementaire controle op buitenlandbeleid (Ukraine-coalitie), "
"sociale rechten (BOSA-sportsubsidies, toeslagencompensatie) en publieke zorg "
"bepleiten scoren positief. Private equity in de zorg en marktgerichte "
"warmteoplossingen scoren negatief."
),
},
8: {
"label": "Orde, veiligheid en soevereiniteit vs. sociale hervorming",
"explanation": (
"Veiligheidsmoties (illegaal vuurwapen, Dutch Dome, controversiële-onderwerpenlijst) "
"en soevereiniteitsmoties domineren de negatieve pool. "
"Positief: sociaal-hervormende moties zoals afschaffing van de kostendelersnorm, "
"hernieuwbare energie en stikstof-maatwerk. Een klassieke links-rechts "
"veiligheid-versus-sociale-hervorming kloof."
),
},
9: {
"label": "Caribisch koninkrijksbeleid en institutioneel toezicht",
"explanation": (
"Deze specifiekere as bundelt moties over Caribische defensie en Syrische "
"VN-coalitie (positief) met zorgtoezicht-WNT-normen (negatief). Het "
"weerspiegelt een combinatie van koninkrijks-betrokkenheid en institutioneel "
"toezicht als onderscheidende dimensie."
),
},
10: {
"label": "Arbeidsmigratie, dierenwelzijn en zorgtoezicht",
"explanation": (
"Positieve pool: zorgfraudetoezicht (Wtza), EU-blauwe kaart voor "
"kennismigranten en dierenwelzijn in de kalverhouderij. Negatieve pool: "
"vitale productie onshoren en EU-Israël-betrekkingen. Een gemengde as die "
"pragmatisch migratiemanagement combineert met dierenwelzijn en zorgregulering."
),
},
}
st.subheader("🔬 SVD Assen — politieke polarisatiethema's")
st.markdown(
"Elke SVD-as representeert een latente politieke dimensie afgeleid uit stempatronen "
"van alle Kamerleden. De top-10 moties per as zijn uniek (geen overlap) en illustreren "
"het spanningsveld dat de as beschrijft."
)
json_path = os.path.join("thoughts", "explorer", "top_svd_top_motions.json")
if not os.path.exists(json_path):
@ -651,7 +751,7 @@ def build_svd_components_tab(db_path: str) -> None:
st.info("Geen top-moties in dataset")
return
st.caption(f"Top SVD contributors computed for window: {window}")
st.caption(f"Top SVD-bijdragers berekend voor venster: **{window}**")
# Build mapping component -> list of motions (deduplicate by motion_id per component)
comp_map: dict[int, list] = {}
@ -663,16 +763,38 @@ def build_svd_components_tab(db_path: str) -> None:
bucket.append(r)
comp_options = sorted(comp_map.keys())
comp_sel = st.selectbox("Component", options=comp_options, index=0)
# Build display labels for selectbox: "As 1 — Regulering vs. status-quo"
def _comp_label(c: int) -> str:
theme = SVD_THEMES.get(c, {})
lbl = theme.get("label", "")
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]
# Show theme explanation
theme = SVD_THEMES.get(comp_sel, {})
if theme:
st.info(f"**{theme['label']}** — {theme['explanation']}")
motions = comp_map.get(comp_sel, [])
col1, col2 = st.columns([1, 2])
with col1:
st.markdown("**Top motions (title)**")
motions = comp_map.get(comp_sel, [])
st.markdown("**Top-moties (titels)**")
for m in motions:
mid = m.get("motion_id")
score = m.get("score", 0.0)
title = m.get("title") or f"Motie #{mid}"
if st.button(f"{mid}: {title[:80]}", key=f"btn_{comp_sel}_{mid}"):
sign = "" if score >= 0 else ""
if st.button(f"{sign} {mid}: {title[:72]}", key=f"btn_{comp_sel}_{mid}"):
st.session_state["svd_selected_mid"] = mid
with col2:
@ -701,10 +823,10 @@ def build_svd_components_tab(db_path: str) -> None:
if row[4] and str(row[4]).startswith("http"):
st.markdown(f"[🔗 Bekijk op Tweede Kamer]({row[4]})")
if row[5]:
with st.expander("Show body text"):
with st.expander("Toon volledige tekst"):
st.write(row[5])
else:
st.info(f"Metadata not found in DB for motion {sel_mid}")
else:
st.info(f"Metadata not found in DB for motion {sel_mid}")
def build_mp_quiz_tab(db_path: str) -> None:

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save