gwoe-antragspruefer/docs/adr/0010-stimmverhalten-gwoe-aggregat.md
Dotty Dotter 1ba9d8e5d9 docs(adr): 0010 Stimmverhalten×GWÖ-Aggregat + 0011 Aktuelle-Themen+PM
ADR 0010 dokumentiert die JOIN-Aggregat-Entscheidung (Option B) mit
4 Aggregat-Funktionen, Heuchelei- und Konsistenz-Markern im Detail,
Sample-Size-Ehrlichkeit und Cache-Strategie.

ADR 0011 dokumentiert Aktuelle-Themen mit Persona-Prompt-Strategie,
Versionierung statt Force-Override, AI-Bann-Quellen-Filter und
json_object_mode-Recovery.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:39:29 +02:00

5.2 KiB
Raw Blame History

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 (010), 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:

    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.