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
This commit is contained in:
+132
-10
@@ -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
Reference in New Issue
Block a user