feat: add voting discipline section below political compass

This commit is contained in:
2026-03-28 23:45:38 +01:00
parent ab99b7de18
commit bcf9407957
2 changed files with 298 additions and 0 deletions
+128
View File
@@ -237,3 +237,131 @@ def test_pca_axis_orientation(monkeypatch):
assert prog_y > cons_y, (
f"Expected progressive parties (y={prog_y:.3f}) > conservative parties (y={cons_y:.3f}) on Y-axis"
)
# ---------------------------------------------------------------------------
# Tests for compute_party_discipline
# ---------------------------------------------------------------------------
def _make_mp_votes_db():
"""Create an in-memory DuckDB with mp_votes fixture data.
6 motions, 2 parties (SP, VVD), each with 4 MPs.
SP is perfectly disciplined (all 4 vote the same each time).
VVD has 1 dissident on 2 of 6 motions → Rice index = (4+4+4+4+3+3)/6/4 ≈ 0.917.
Dates span 2023-01-01 to 2023-12-31.
"""
import duckdb
conn = duckdb.connect(":memory:")
conn.execute("""
CREATE TABLE mp_votes (
id INTEGER,
motion_id VARCHAR,
mp_name VARCHAR,
party VARCHAR,
vote VARCHAR,
date DATE,
created_at TIMESTAMP
)
""")
rows = []
dates = [
"2023-01-10",
"2023-03-15",
"2023-05-20",
"2023-07-25",
"2023-09-30",
"2023-11-05",
]
sp_mps = ["Janssen, A.", "Pietersen, B.", "Willemsen, C.", "Hendriksen, D."]
vvd_mps = ["Adams, E.", "Bakker, F.", "Claassen, G.", "Dekker, H."]
for i, date in enumerate(dates, start=1):
m_id = f"M{i:03d}"
for mp in sp_mps:
rows.append((i * 10 + 1, m_id, mp, "SP", "voor", date, "2023-01-01"))
if i <= 4:
for mp in vvd_mps:
rows.append((i * 10 + 2, m_id, mp, "VVD", "voor", date, "2023-01-01"))
else:
for mp in vvd_mps[:3]:
rows.append((i * 10 + 2, m_id, mp, "VVD", "voor", date, "2023-01-01"))
rows.append(
(i * 10 + 3, m_id, vvd_mps[3], "VVD", "tegen", date, "2023-01-01")
)
conn.executemany("INSERT INTO mp_votes VALUES (?, ?, ?, ?, ?, ?, ?)", rows)
return conn
def test_compute_party_discipline_basic(monkeypatch):
"""compute_party_discipline returns correct Rice index for fixture data."""
import duckdb as _duckdb
fixture_conn = _make_mp_votes_db()
monkeypatch.setattr(_duckdb, "connect", lambda path, **kw: fixture_conn)
import importlib
import sys
if "streamlit" not in sys.modules:
import types
st_stub = types.ModuleType("streamlit")
st_stub.cache_data = lambda **kw: lambda f: f
sys.modules["streamlit"] = st_stub
import explorer as _explorer
importlib.reload(_explorer)
df = _explorer.compute_party_discipline(
db_path="dummy",
start_date="2023-01-01",
end_date="2023-12-31",
)
assert not df.empty
assert set(df.columns) >= {"party", "n_motions", "discipline"}
sp_row = df[df["party"] == "SP"].iloc[0]
vvd_row = df[df["party"] == "VVD"].iloc[0]
assert sp_row["n_motions"] == 6
assert sp_row["discipline"] == pytest.approx(1.0, abs=1e-6)
assert vvd_row["n_motions"] == 6
expected_vvd = (4 * 1.0 + 2 * 0.75) / 6
assert vvd_row["discipline"] == pytest.approx(expected_vvd, abs=1e-4)
assert (df["discipline"] >= 0).all() and (df["discipline"] <= 1).all()
def test_compute_party_discipline_empty_range(monkeypatch):
"""Returns empty DataFrame when no motions fall in the date range."""
import duckdb as _duckdb
fixture_conn = _make_mp_votes_db()
monkeypatch.setattr(_duckdb, "connect", lambda path, **kw: fixture_conn)
import importlib, sys
if "streamlit" not in sys.modules:
import types
st_stub = types.ModuleType("streamlit")
st_stub.cache_data = lambda **kw: lambda f: f
sys.modules["streamlit"] = st_stub
import explorer as _explorer
importlib.reload(_explorer)
df = _explorer.compute_party_discipline(
db_path="dummy",
start_date="2000-01-01",
end_date="2000-12-31",
)
assert df.empty