# 0010 — Stimmverhalten × GWÖ-Bewertung als JOIN-Aggregat | | | |---|---| | **Status** | accepted | | **Datum** | 2026-05-06 | | **Refs** | #165, #166, #167, #168, #169, #171, #162, MEMORY/project_konsistenz_marker.md | ## Kontext Vor dieser Entscheidung lagen zwei Erkenntnisquellen unverbunden in der DB: - `assessments` (~96 produktiv, dev) — pro Drucksache eine GWÖ-Bewertung mit `gwoe_score` (0–10), `gwoe_matrix` (5×5 Felder, Rating −5..+5), `wahlprogramm_scores` (pro Fraktion: WP-Treue + PP-Treue + Zitate). - `plenum_vote_results` (~7281 produktiv, dev) — pro Drucksache wer wie abgestimmt hat (`fraktionen_ja`/`_nein`/`_enthaltung` als JSON-Arrays, pro Fraktion normalisiert via `normalize_partei`). Die User-Frage „Welche Parteien stimmen häufiger gemeinwohlorientierten Anträgen zu?" — und die diametrale „Wer stimmt gegen das eigene Wahlprogramm?" — verlangt einen JOIN auf `(bundesland, drucksache)`. Heute schneiden sich nur ~19 von 7281 Votes mit Assessments. Die Aussagekraft ist also methodisch absichtlich konservativ ausgelegt (`min_n=5`, „ausreichend"-Flag, Sample-Size-Banner sichtbar). ## Optionen ### Option A — Materialisiertes Aggregat als eigene Tabelle (`stimm_index`) Bei jedem Assessment-Insert oder Vote-Insert ein Trigger, der ein fertiges Aggregat in eine eigene Tabelle schreibt. - **Pro:** Lese-Latenz minimal. - **Kontra:** Konsistenz-Logik in zwei Codepfaden (Insert-Trigger + Bulk-Reindex), Schema-Migration nötig, schwer testbar. ### Option B — On-the-fly-JOIN mit TTL-Cache Aggregat-Funktionen lesen synchron aus den Quelltabellen, JOIN per SQL, Aggregation in Python, Ergebnis 60s im Process-Memory cachen. - **Pro:** Keine Schema-Migration, ein Codepfad, testbar wie reine Funktionen, 60s-TTL deckt Burst-Traffic ab. - **Kontra:** Erste Anfrage nach Cache-Miss ~3 s, danach ~0,17 s. ### Option C — Computed-View SQLite-View mit dem JOIN. Aggregation im SQL. - **Pro:** Eine Query. - **Kontra:** SQLite-Views sind nicht persistent indexiert, Performance schlechter als B; aggregations-Logik mit GROUP_CONCAT-JSON-Tricks unleserlich. ## Entscheidung **Option B** umgesetzt, ergänzt um zwei Marker im Antrag-Detail: 1. **Vier Aggregat-Funktionen** in `app/auswertungen.py`: - `aggregate_stimm_index(filter_bl, filter_wp, exclude_antragsteller, min_n)` → pro Fraktion `Ø(gwoe_score|JA) − Ø(gwoe_score|NEIN)`, Domain `[−10, +10]`. - `aggregate_heuchelei(...)` → Anteil der Anträge mit `wahlprogramm.score(F)≥7 ∧ F∈NEIN`. - `aggregate_stimm_index_pro_wert(...)` → Heatmap Fraktion×{Würde, Solidarität, Nachhaltigkeit, Gerechtigkeit, Demokratie}. - `aggregate_stimm_index_cross_bl(...)` → gleiche Fraktion in mehreren Ländern nebeneinander. 2. **Helper `_load_assessments_with_votes`** mit dem zentralen JOIN: ```sql SELECT a.…, p.fraktionen_ja, p.fraktionen_nein, p.fraktionen_enthaltung FROM assessments a INNER JOIN plenum_vote_results p ON a.bundesland = p.bundesland AND a.drucksache = p.drucksache WHERE a.gwoe_score IS NOT NULL ``` 3. **Antrag-Detail-Marker** (`templates/v2/screens/antrag_detail.html`): - **Heuchelei-⚠** neben jeder NEIN-Fraktion mit eigenem `wahlprogramm.score≥7`. Tooltip nennt den Score. - **Konsistenz-Block** über dem Vote-Block: rot bei Konflikt („unterstützen" + abgelehnt; oder „ablehnen" + angenommen), grün bei Übereinstimmung. Bei mehreren Votes (Überweisung → Endabstimmung) wird das erste *definitive* Outcome (`angenommen|abgelehnt|bestätigt`) genommen. 4. **Sample-Size-Ehrlichkeit:** `min_n=5` als Default, „ausreichend"- Flag pro Fraktion, Caveat-Banner über jedem Chart, Nutzer kann Antragsteller-Ausschluss togglen. 5. **CSV-Export** `/api/auswertungen/stimmverhalten.csv` als Long-Format für externe Auswertung. ## Konsequenzen ### Positiv - Eine Domain-Funktion pro Aussage, leicht testbar. - Cache-Invalidation trivial: nach Assessment-Insert oder Vote-Ingest `cache_clear()` aufrufen (siehe `news_aggregator.run_aggregator`). - Fraktionsnamen sind in beiden Quellen vornormalisiert (`SPD`, `GRÜNE`, …) — kein zusätzlicher Mapping-Layer im JOIN. - Heuchelei- und Konsistenz-Marker im Detail sind die punktgenauen Belege für die aggregierten Aussagen im `/auswertungen`-Tab — der Nutzer kann von Aggregat zu Antrag durchklicken. ### Negativ - Daten-Lücke zwischen Votes und Bewertungen ist sichtbar: 7281 vs. ~96 Bewertungen, nur 19 in beiden Mengen. Die Aggregate sagen „ausreichend=False" für viele Fraktionen. Das ist methodisch ehrlich, aber bedeutet: das Feature wird erst nach **Phase 3 (Vote-Orphans-Auto-Bewertung)** seine volle Aussagekraft entfalten. - Cache-Lebensdauer 60 s: bei `--reload`-Dev-Server ist der Cache nach jedem Auto-Reload weg. Akzeptabel für dev, in prod ein Worker-Prozess. ### Folgen für andere ADRs - **0009 Protokoll-Parser-Registry**: pro neuem BL-Parser wachsen die Daten in `plenum_vote_results`, was den JOIN-Recall direkt hebt. Phase 5 hardcases verstärken Aussagen aus Phase 1. - **0011 Aktuelle-Themen × PM-Generator** (folgend): der Konsistenz-Block ist Vorlage für Empfehlungs-vs-Vote-Konflikte als PM-Anlass.