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

128 lines
5.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:
```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.