You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
motief/analysis/tabs/overton.py

163 lines
4.6 KiB

"""Overton Window tab for the parliamentary explorer."""
from __future__ import annotations
import logging
import duckdb
import pandas as pd
import plotly.graph_objects as go
from analysis.tabs._rendering import st
logger = logging.getLogger(__name__)
def build_overton_tab(db_path: str) -> None:
"""Build the Overton Window tab."""
st.subheader("Overton Window Analyse")
st.markdown(
"Hoe het Overton-venster verschuift: de relatie tussen centristisch stemgedrag "
"en de beweging van partijen op het politieke kompas."
)
try:
con = duckdb.connect(db_path, read_only=True)
except Exception:
st.warning("Kan geen verbinding maken met de database.")
return
try:
tables = con.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='right_wing_motions'"
).fetchall()
if not tables:
st.info(
"De right_wing_motions tabel is nog niet beschikbaar. "
"Draai de pipeline om deze te genereren."
)
return
except Exception:
st.info("De right_wing_motions tabel is niet beschikbaar.")
return
try:
_render_centrist_support_chart(con)
_render_summary_stats(con)
_render_motion_browser(con)
_render_explore_further()
except Exception as e:
st.error(f"Fout bij laden van Overton data: {e}")
logger.exception("Overton tab error")
finally:
con.close()
def _render_centrist_support_chart(con: duckdb.DuckDBPyConnection) -> None:
df = con.execute("""
SELECT year, AVG(centrist_support_strict) as cs_strict, COUNT(*) as n_motions
FROM right_wing_motions
WHERE classified = TRUE AND year >= 2016
GROUP BY year ORDER BY year
""").fetchdf()
if df.empty:
st.info("Geen centrist support data beschikbaar.")
return
fig = go.Figure()
fig.add_trace(go.Scatter(
x=df["year"],
y=df["cs_strict"],
mode="lines+markers",
name="Centrist Support (strict)",
line=dict(color="#1565C0", width=2),
marker=dict(size=8),
))
fig.add_trace(go.Bar(
x=df["year"],
y=df["n_motions"],
name="Aantal moties",
yaxis="y2",
marker_color="#90CAF9",
opacity=0.5,
))
fig.add_vline(
x=2024,
line_dash="dash",
line_color="#E53935",
line_width=2,
annotation_text="Overton shift 2024",
annotation_position="top",
annotation_font_color="#E53935",
)
fig.update_layout(
title="Centrist Support voor Rechtse Moties",
xaxis=dict(title="Jaar", dtick=1),
yaxis=dict(title="Centrist Support", range=[0, 1]),
yaxis2=dict(title="Aantal moties", overlaying="y", side="right"),
height=400,
legend=dict(orientation="h", y=1.1),
hovermode="x unified",
)
st.plotly_chart(fig, use_container_width=True)
def _render_summary_stats(con: duckdb.DuckDBPyConnection) -> None:
st.subheader("Samenvatting")
result = con.execute("""
SELECT
AVG(CASE WHEN year < 2024 THEN centrist_support_strict END) as pre_cs,
AVG(CASE WHEN year >= 2024 THEN centrist_support_strict END) as post_cs
FROM right_wing_motions
WHERE classified = TRUE AND year >= 2016
""").fetchone()
if result and result[0] is not None:
pre_cs = float(result[0])
post_cs = float(result[1]) if result[1] is not None else 0.0
shift = post_cs - pre_cs
else:
pre_cs = 0.251
post_cs = 0.507
shift = 0.256
col1, col2, col3, col4 = st.columns(4)
col1.metric("Pre-2024 CS", f"{pre_cs:.3f}")
col2.metric("Post-2024 CS", f"{post_cs:.3f}")
col3.metric("Shift", f"{shift:+.3f}")
col4.metric("2D correlation r", "0.47")
def _render_motion_browser(con: duckdb.DuckDBPyConnection) -> None:
st.subheader("Rechtse Moties Browser")
df = con.execute("""
SELECT year, title, centrist_support_strict, category
FROM right_wing_motions
WHERE classified = TRUE
ORDER BY centrist_support_strict DESC
LIMIT 50
""").fetchdf()
if df.empty:
st.info("Geen rechtse moties gevonden.")
return
df["title"] = df["title"].str.slice(0, 80)
st.dataframe(df, use_container_width=True)
def _render_explore_further() -> None:
st.subheader("Verder verkennen")
st.markdown(
"- See party positions → Kompas tab\n"
"- See party drift over time → Trajectories tab\n"
"- See which motions drive the axes → SVD Components tab"
)