diff --git a/explorer.py b/explorer.py index 4864465..96ec740 100644 --- a/explorer.py +++ b/explorer.py @@ -258,7 +258,10 @@ def compute_party_discipline( Rice index per motion per party = fraction of party MPs voting with the party majority. The per-party score is the average Rice index across all motions in the date range. + Only 'voor' and 'tegen' votes are counted; absent and abstaining MPs are excluded from the + Rice index calculation. """ + conn = None try: conn = duckdb.connect(db_path, read_only=True) result = conn.execute( @@ -272,7 +275,7 @@ def compute_party_discipline( WHERE mp_name LIKE '%,%' AND date >= CAST(? AS DATE) AND date <= CAST(? AS DATE) - AND vote IN ('voor', 'tegen', 'afwezig', 'onthouden') + AND vote IN ('voor', 'tegen') ), vote_counts AS ( SELECT @@ -313,11 +316,16 @@ def compute_party_discipline( """, [start_date, end_date], ).fetchdf() - conn.close() return result except Exception as exc: logger.warning("compute_party_discipline failed: %s", exc) return pd.DataFrame(columns=["party", "n_motions", "discipline"]) + finally: + if conn is not None: + try: + conn.close() + except Exception: + pass @st.cache_data(show_spinner="Partijposities op SVD-assen laden…") @@ -955,74 +963,79 @@ def build_compass_tab(db_path: str, window_size: str) -> None: disc_df = compute_party_discipline(db_path, start_date, end_date) st.subheader("Stemgedrag cohesie") - if disc_df.empty or disc_df["n_motions"].max() < _MIN_MOTIONS_FOR_DISCIPLINE: + if disc_df.empty: st.caption( "Te weinig hoofdelijke stemmingen in dit venster voor een cohesieanalyse." ) else: - compass_parties = set(df_pos["party"].unique()) - disc_df = disc_df[disc_df["party"].isin(compass_parties)].copy() - + disc_df = disc_df[disc_df["n_motions"] >= _MIN_MOTIONS_FOR_DISCIPLINE].copy() if disc_df.empty: - st.caption("Geen overlappende partijen tussen kompas en stemmingsdata.") - else: - disc_df["discipline_pct"] = (disc_df["discipline"] * 100).round(1) - disc_df["party_label"] = disc_df.apply( - lambda r: f"{r['party']} ({int(r['n_motions'])} moties)", axis=1 + st.caption( + "Te weinig hoofdelijke stemmingen in dit venster voor een cohesieanalyse." ) + else: + compass_parties = set(df_pos["party"].unique()) + disc_df = disc_df[disc_df["party"].isin(compass_parties)].copy() + if disc_df.empty: + st.caption("Geen overlappende partijen tussen kompas en stemmingsdata.") + else: + disc_df["discipline_pct"] = (disc_df["discipline"] * 100).round(1) + disc_df["party_label"] = disc_df.apply( + lambda r: f"{r['party']} ({int(r['n_motions'])} moties)", axis=1 + ) - bar_fig = px.bar( - disc_df.sort_values("discipline"), - x="discipline_pct", - y="party_label", - orientation="h", - color="discipline_pct", - color_continuous_scale="RdYlGn", - range_color=[80, 100], - labels={"discipline_pct": "Cohesie (%)", "party_label": "Partij"}, - title="Cohesie bij hoofdelijke stemmingen", - ) - bar_fig.update_layout( - height=max(300, len(disc_df) * 35 + 80), - showlegend=False, - coloraxis_showscale=False, - yaxis_title="", - ) - st.plotly_chart(bar_fig, use_container_width=True) - - top3 = disc_df.nlargest(3, "discipline")[ - ["party", "discipline_pct", "n_motions"] - ] - bot3 = disc_df.nsmallest(3, "discipline")[ - ["party", "discipline_pct", "n_motions"] - ] - col_a, col_b = st.columns(2) - with col_a: - st.markdown("**Meest eensgezind**") - st.dataframe( - top3.rename( - columns={ - "party": "Partij", - "discipline_pct": "Cohesie (%)", - "n_motions": "Moties", - } - ), - hide_index=True, - use_container_width=True, + bar_fig = px.bar( + disc_df.sort_values("discipline"), + x="discipline_pct", + y="party_label", + orientation="h", + color="discipline_pct", + color_continuous_scale="RdYlGn", + range_color=[80, 100], + labels={"discipline_pct": "Cohesie (%)", "party_label": "Partij"}, + title="Cohesie bij hoofdelijke stemmingen", ) - with col_b: - st.markdown("**Meest verdeeld**") - st.dataframe( - bot3.rename( - columns={ - "party": "Partij", - "discipline_pct": "Cohesie (%)", - "n_motions": "Moties", - } - ), - hide_index=True, - use_container_width=True, + bar_fig.update_layout( + height=max(300, len(disc_df) * 35 + 80), + showlegend=False, + coloraxis_showscale=False, + yaxis_title="", ) + st.plotly_chart(bar_fig, use_container_width=True) + + top3 = disc_df.nlargest(3, "discipline")[ + ["party", "discipline_pct", "n_motions"] + ] + bot3 = disc_df.nsmallest(3, "discipline")[ + ["party", "discipline_pct", "n_motions"] + ] + col_a, col_b = st.columns(2) + with col_a: + st.markdown("**Meest eensgezind**") + st.dataframe( + top3.rename( + columns={ + "party": "Partij", + "discipline_pct": "Cohesie (%)", + "n_motions": "Moties", + } + ), + hide_index=True, + use_container_width=True, + ) + with col_b: + st.markdown("**Meest verdeeld**") + st.dataframe( + bot3.rename( + columns={ + "party": "Partij", + "discipline_pct": "Cohesie (%)", + "n_motions": "Moties", + } + ), + hide_index=True, + use_container_width=True, + ) # ---------------------------------------------------------------------------