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.
|
"""New tab: show top motions contributing to top SVD components.
|
||||||
|
|
||||||
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 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")
|
json_path = os.path.join("thoughts", "explorer", "top_svd_top_motions.json")
|
||||||
if not os.path.exists(json_path):
|
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")
|
st.info("Geen top-moties in dataset")
|
||||||
return
|
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)
|
# Build mapping component -> list of motions (deduplicate by motion_id per component)
|
||||||
comp_map: dict[int, list] = {}
|
comp_map: dict[int, list] = {}
|
||||||
@@ -663,16 +763,38 @@ def build_svd_components_tab(db_path: str) -> None:
|
|||||||
bucket.append(r)
|
bucket.append(r)
|
||||||
|
|
||||||
comp_options = sorted(comp_map.keys())
|
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])
|
col1, col2 = st.columns([1, 2])
|
||||||
with col1:
|
with col1:
|
||||||
st.markdown("**Top motions (title)**")
|
st.markdown("**Top-moties (titels)**")
|
||||||
motions = comp_map.get(comp_sel, [])
|
|
||||||
for m in motions:
|
for m in motions:
|
||||||
mid = m.get("motion_id")
|
mid = m.get("motion_id")
|
||||||
|
score = m.get("score", 0.0)
|
||||||
title = m.get("title") or f"Motie #{mid}"
|
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
|
st.session_state["svd_selected_mid"] = mid
|
||||||
|
|
||||||
with col2:
|
with col2:
|
||||||
@@ -701,10 +823,10 @@ def build_svd_components_tab(db_path: str) -> None:
|
|||||||
if row[4] and str(row[4]).startswith("http"):
|
if row[4] and str(row[4]).startswith("http"):
|
||||||
st.markdown(f"[🔗 Bekijk op Tweede Kamer]({row[4]})")
|
st.markdown(f"[🔗 Bekijk op Tweede Kamer]({row[4]})")
|
||||||
if row[5]:
|
if row[5]:
|
||||||
with st.expander("Show body text"):
|
with st.expander("Toon volledige tekst"):
|
||||||
st.write(row[5])
|
st.write(row[5])
|
||||||
else:
|
else:
|
||||||
st.info(f"Metadata not found in DB for motion {sel_mid}")
|
st.info(f"Metadata not found in DB for motion {sel_mid}")
|
||||||
|
|
||||||
|
|
||||||
def build_mp_quiz_tab(db_path: str) -> None:
|
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