#!/usr/bin/env python3 """ GWÖ-Wahlprüfsteine Aggregator Erstellt Partei-Aggregationen und Berichte. """ import sqlite3 import json from pathlib import Path from dataclasses import dataclass from datetime import datetime @dataclass class ParteiStatistik: partei: str anzahl_kandidaten: int anzahl_antworten: int avg_substanz: float avg_gwoe: float avg_wortanzahl: float ja_quote: float # Anteil Ja-Antworten top_felder: list[str] # Häufigste Matrix-Felder beste_zitate: list[dict] # Top 3 Antworten schwachpunkte: list[str] def get_partei_statistiken(conn: sqlite3.Connection) -> list[ParteiStatistik]: """Holt aggregierte Statistiken pro Partei.""" cursor = conn.cursor() # Basisstatistiken cursor.execute(""" SELECT k.partei_normalisiert AS partei, COUNT(DISTINCT k.id) AS anzahl_kandidaten, COUNT(b.id) AS anzahl_antworten, ROUND(AVG(b.substanz_score), 2) AS avg_substanz, ROUND(AVG(b.gwoe_score), 2) AS avg_gwoe, ROUND(AVG(b.wortanzahl), 0) AS avg_wortanzahl FROM kandidaten k LEFT JOIN antworten_raw ar ON k.id = ar.kandidat_id LEFT JOIN bewertungen b ON ar.id = b.antwort_id WHERE b.id IS NOT NULL GROUP BY k.partei_normalisiert ORDER BY avg_gwoe DESC """) basis = {row['partei']: dict(row) for row in cursor.fetchall()} # Ja-Quoten pro Partei cursor.execute(""" SELECT k.partei_normalisiert AS partei, SUM(CASE WHEN ar.antwort_kurz = 'Ja' THEN 1 ELSE 0 END) * 1.0 / COUNT(*) AS ja_quote FROM kandidaten k JOIN antworten_raw ar ON k.id = ar.kandidat_id GROUP BY k.partei_normalisiert """) ja_quoten = {row['partei']: row['ja_quote'] for row in cursor.fetchall()} # Matrix-Felder pro Partei (häufigste) matrix_felder = {} for partei in basis.keys(): cursor.execute(""" SELECT b.matrix_felder FROM bewertungen b JOIN antworten_raw ar ON b.antwort_id = ar.id JOIN kandidaten k ON ar.kandidat_id = k.id WHERE k.partei_normalisiert = ? AND b.matrix_felder IS NOT NULL """, (partei,)) feld_counts = {} for row in cursor.fetchall(): try: felder = json.loads(row['matrix_felder']) for f in felder: feld_counts[f] = feld_counts.get(f, 0) + 1 except: pass # Top 5 Felder sorted_felder = sorted(feld_counts.items(), key=lambda x: -x[1]) matrix_felder[partei] = [f[0] for f in sorted_felder[:5]] # Beste Zitate pro Partei (Top 3 nach GWÖ-Score) beste_zitate = {} for partei in basis.keys(): cursor.execute(""" SELECT k.vorname || ' ' || k.nachname AS name, k.kommune, f.kurztext AS frage, ar.antwort_erlaeuterung AS zitat, b.gwoe_score, b.gwoe_begruendung FROM bewertungen b JOIN antworten_raw ar ON b.antwort_id = ar.id JOIN kandidaten k ON ar.kandidat_id = k.id JOIN fragen f ON ar.frage_id = f.id WHERE k.partei_normalisiert = ? AND ar.antwort_erlaeuterung IS NOT NULL AND LENGTH(ar.antwort_erlaeuterung) > 50 ORDER BY b.gwoe_score DESC LIMIT 3 """, (partei,)) beste_zitate[partei] = [dict(row) for row in cursor.fetchall()] # Zusammenbauen statistiken = [] for partei, stats in basis.items(): statistiken.append(ParteiStatistik( partei=partei, anzahl_kandidaten=stats['anzahl_kandidaten'], anzahl_antworten=stats['anzahl_antworten'], avg_substanz=stats['avg_substanz'] or 0, avg_gwoe=stats['avg_gwoe'] or 0, avg_wortanzahl=stats['avg_wortanzahl'] or 0, ja_quote=ja_quoten.get(partei, 0), top_felder=matrix_felder.get(partei, []), beste_zitate=beste_zitate.get(partei, []), schwachpunkte=[] # Wird später gefüllt )) return sorted(statistiken, key=lambda x: -x.avg_gwoe) def get_fragen_statistiken(conn: sqlite3.Connection) -> list[dict]: """Statistiken pro Frage.""" cursor = conn.cursor() cursor.execute(""" SELECT f.nummer, f.kurztext, f.volltext, COUNT(b.id) AS anzahl, ROUND(AVG(b.substanz_score), 2) AS avg_substanz, ROUND(AVG(b.gwoe_score), 2) AS avg_gwoe, SUM(CASE WHEN ar.antwort_kurz = 'Ja' THEN 1 ELSE 0 END) AS ja_count, SUM(CASE WHEN ar.antwort_kurz = 'Nein' THEN 1 ELSE 0 END) AS nein_count FROM fragen f LEFT JOIN antworten_raw ar ON f.id = ar.frage_id LEFT JOIN bewertungen b ON ar.id = b.antwort_id GROUP BY f.id ORDER BY f.nummer """) return [dict(row) for row in cursor.fetchall()] def generate_partei_report(statistiken: list[ParteiStatistik], fragen_stats: list[dict], conn: sqlite3.Connection) -> str: """Generiert den Markdown-Report für Parteien.""" # Kandidaten für Bandbreiten-Berechnung holen cursor = conn.cursor() cursor.execute(""" SELECT k.partei_normalisiert AS partei, k.vorname || ' ' || k.nachname AS name, k.kommune, ROUND(AVG(b.gwoe_score), 2) AS avg_gwoe FROM kandidaten k JOIN antworten_raw ar ON k.id = ar.kandidat_id JOIN bewertungen b ON ar.id = b.antwort_id GROUP BY k.id """) kandidaten_scores = {} for row in cursor.fetchall(): if row['partei'] not in kandidaten_scores: kandidaten_scores[row['partei']] = [] kandidaten_scores[row['partei']].append({ 'name': row['name'], 'kommune': row['kommune'], 'avg_gwoe': row['avg_gwoe'] }) lines = [ "# GWÖ-Wahlprüfsteine Bayern 2026 — Partei-Auswertung", "", f"*Stand: {datetime.now().strftime('%d.%m.%Y %H:%M')}*", "", "---", "", "## Zusammenfassung", "", "| Partei | Kandidat:innen | Ø GWÖ | Bandbreite | Ø Substanz | Ja-Quote |", "|--------|---------------|-------|------------|------------|----------|", ] for s in statistiken: # Bandbreite berechnen if s.partei in kandidaten_scores and len(kandidaten_scores[s.partei]) > 1: scores = [k['avg_gwoe'] for k in kandidaten_scores[s.partei]] bandbreite = f"{min(scores):.1f}–{max(scores):.1f}" elif s.partei in kandidaten_scores: bandbreite = f"{kandidaten_scores[s.partei][0]['avg_gwoe']:.1f}" else: bandbreite = "—" lines.append(f"| **{s.partei}** | {s.anzahl_kandidaten} | {s.avg_gwoe:.1f} | {bandbreite} | {s.avg_substanz:.1f}/3 | {s.ja_quote*100:.0f}% |") lines.extend([ "", "---", "", "## Übergreifende Beobachtungen", "", "### Das Substanz-Problem", "", "Die Analyse zeigt ein strukturelles Muster: **Hohe Zustimmungsquoten, aber wenig konkrete Maßnahmen.**", "", "- Die meisten Parteien haben Ja-Quoten von 75–100%", "- Der durchschnittliche Substanz-Score liegt jedoch nur bei 0.3–1.8 von 3", "- Typisch: *\"Ja\"* ohne Erläuterung oder mit Floskeln wie *\"Gespräche führen\"*", "", "### Bandbreite innerhalb der Parteien", "", "Die Unterschiede *innerhalb* einer Partei sind oft größer als *zwischen* Parteien:", "", ]) for s in statistiken: if s.partei in kandidaten_scores and len(kandidaten_scores[s.partei]) > 1: scores = [k['avg_gwoe'] for k in kandidaten_scores[s.partei]] spanne = max(scores) - min(scores) if spanne >= 2: best = max(kandidaten_scores[s.partei], key=lambda x: x['avg_gwoe']) worst = min(kandidaten_scores[s.partei], key=lambda x: x['avg_gwoe']) lines.append(f"- **{s.partei}:** {worst['avg_gwoe']:.1f} ({worst['name']}) bis {best['avg_gwoe']:.1f} ({best['name']}) — Δ {spanne:.1f}") lines.extend([ "", "**→ Fazit:** Das Parteilabel allein sagt wenig über die GWÖ-Affinität der einzelnen Kandidat:innen aus.", "", "---", "", "## Detailauswertung nach Parteien", "" ]) for s in statistiken: lines.extend([ f"### {s.partei}", "", f"**Kandidat:innen:** {s.anzahl_kandidaten} | **Antworten:** {s.anzahl_antworten} | **Ø Wortanzahl:** {s.avg_wortanzahl:.0f}", "", f"**GWÖ-Score:** {s.avg_gwoe:.1f}/10 | **Substanz:** {s.avg_substanz:.1f}/3 | **Ja-Quote:** {s.ja_quote*100:.0f}%", "", ]) if s.top_felder: lines.append(f"**Häufigste Matrix-Felder:** {', '.join(s.top_felder)}") lines.append("") if s.beste_zitate: lines.append("**Beste Antworten:**") lines.append("") for z in s.beste_zitate: zitat = z['zitat'][:300] + "..." if len(z['zitat']) > 300 else z['zitat'] lines.extend([ f"> *\"{zitat}\"*", f"> — {z['name']} ({z['kommune']}), zu Frage \"{z['frage']}\" (GWÖ: {z['gwoe_score']:.1f})", "" ]) lines.append("---") lines.append("") # Fragen-Statistik lines.extend([ "## Auswertung nach Fragen", "", "| Nr | Frage | Ja | Nein | Ø GWÖ | Ø Substanz |", "|----|-------|-----|------|-------|------------|", ]) for f in fragen_stats: lines.append(f"| {f['nummer']} | {f['kurztext']} | {f['ja_count']} | {f['nein_count']} | {f['avg_gwoe']:.1f} | {f['avg_substanz']:.1f} |") return "\n".join(lines) def generate_landscape_report(statistiken: list[ParteiStatistik]) -> str: """Generiert die komprimierte Parteienlandschafts-Analyse.""" # Kategorisieren vorreiter = [s for s in statistiken if s.avg_gwoe >= 6] mittelfeld = [s for s in statistiken if 3 <= s.avg_gwoe < 6] nachzuegler = [s for s in statistiken if s.avg_gwoe < 3] lines = [ "# GWÖ-Parteienlandschaft Bayern 2026", "", "*Kommunalwahlen — komprimierte Analyse*", "", "---", "", ] if vorreiter: lines.extend([ "## 🟢 Vorreiter (GWÖ ≥ 6)", "", ]) for s in vorreiter: lines.append(f"**{s.partei}** (Ø {s.avg_gwoe:.1f}): Hohe GWÖ-Affinität, {s.ja_quote*100:.0f}% Zustimmung zu allen Fragen.") lines.append("") if mittelfeld: lines.extend([ "## 🟡 Mittelfeld (GWÖ 3-6)", "", ]) for s in mittelfeld: lines.append(f"**{s.partei}** (Ø {s.avg_gwoe:.1f}): Partielle Übereinstimmung, erkennbare Offenheit für GWÖ-Themen.") lines.append("") if nachzuegler: lines.extend([ "## 🔴 Zurückhaltend (GWÖ < 3)", "", ]) for s in nachzuegler: lines.append(f"**{s.partei}** (Ø {s.avg_gwoe:.1f}): Geringe GWÖ-Resonanz, {(1-s.ja_quote)*100:.0f}% Ablehnung.") lines.append("") # Kernaussagen lines.extend([ "---", "", "## Kernaussagen", "", ]) if statistiken: best = statistiken[0] worst = statistiken[-1] lines.extend([ f"- **Höchste GWÖ-Affinität:** {best.partei} mit Ø {best.avg_gwoe:.1f}", f"- **Niedrigste GWÖ-Affinität:** {worst.partei} mit Ø {worst.avg_gwoe:.1f}", f"- **Spannweite:** {best.avg_gwoe - worst.avg_gwoe:.1f} Punkte zwischen Spitze und Ende", "", ]) return "\n".join(lines) def get_kandidaten_ranking(conn: sqlite3.Connection) -> list[dict]: """Holt Einzelranking aller Kandidat:innen.""" cursor = conn.cursor() cursor.execute(""" SELECT k.vorname || ' ' || k.nachname AS name, k.kommune, k.partei_normalisiert AS partei, k.partei_raw, ROUND(AVG(b.gwoe_score), 2) AS avg_gwoe, ROUND(AVG(b.substanz_score), 2) AS avg_substanz, MAX(b.gwoe_score) AS max_gwoe, MIN(b.gwoe_score) AS min_gwoe, COUNT(b.id) AS anzahl_antworten FROM kandidaten k JOIN antworten_raw ar ON k.id = ar.kandidat_id JOIN bewertungen b ON ar.id = b.antwort_id GROUP BY k.id ORDER BY avg_gwoe DESC, avg_substanz DESC """) return [dict(row) for row in cursor.fetchall()] def generate_kandidaten_report(conn: sqlite3.Connection, statistiken: list[ParteiStatistik]) -> str: """Generiert den Kandidat:innen-Report.""" kandidaten = get_kandidaten_ranking(conn) lines = [ "# GWÖ-Wahlprüfsteine Bayern 2026 — Kandidat:innen-Ranking", "", f"*Stand: {datetime.now().strftime('%d.%m.%Y %H:%M')}*", "", "---", "", "## Gesamtranking", "", "| Rang | Name | Kommune | Partei | Ø GWÖ | Ø Substanz | Spanne |", "|------|------|---------|--------|-------|------------|--------|", ] for i, k in enumerate(kandidaten, 1): spanne = f"{k['min_gwoe']:.1f}–{k['max_gwoe']:.1f}" lines.append(f"| {i} | **{k['name']}** | {k['kommune']} | {k['partei']} | {k['avg_gwoe']:.1f} | {k['avg_substanz']:.1f}/3 | {spanne} |") # Kategorisierung vorreiter = [k for k in kandidaten if k['avg_gwoe'] >= 5] solide = [k for k in kandidaten if 3 <= k['avg_gwoe'] < 5] schwach = [k for k in kandidaten if k['avg_gwoe'] < 3] lines.extend([ "", "---", "", "## Kategorisierung", "", ]) if vorreiter: lines.extend([ "### 🟢 GWÖ-Vorreiter:innen (Ø ≥ 5.0)", "", ]) for k in vorreiter: lines.append(f"- **{k['name']}** ({k['kommune']}, {k['partei']}): Ø {k['avg_gwoe']:.1f}, Spanne {k['min_gwoe']:.1f}–{k['max_gwoe']:.1f}") lines.append("") if solide: lines.extend([ "### 🟡 Solide Basis (Ø 3.0–5.0)", "", ]) for k in solide: lines.append(f"- **{k['name']}** ({k['kommune']}, {k['partei']}): Ø {k['avg_gwoe']:.1f}") lines.append("") if schwach: lines.extend([ "### 🔴 Wenig GWÖ-Substanz (Ø < 3.0)", "", ]) for k in schwach: lines.append(f"- {k['name']} ({k['kommune']}, {k['partei']}): Ø {k['avg_gwoe']:.1f}") lines.append("") # Bandbreite innerhalb Parteien lines.extend([ "---", "", "## Bandbreite innerhalb der Parteien", "", "Die Durchschnittswerte pro Partei verdecken teils erhebliche Unterschiede zwischen einzelnen Kandidat:innen:", "", ]) for s in statistiken: partei_kandidaten = [k for k in kandidaten if k['partei'] == s.partei] if len(partei_kandidaten) > 1: scores = [k['avg_gwoe'] for k in partei_kandidaten] best = max(partei_kandidaten, key=lambda x: x['avg_gwoe']) worst = min(partei_kandidaten, key=lambda x: x['avg_gwoe']) spanne = max(scores) - min(scores) lines.append(f"### {s.partei}") lines.append(f"- **Partei-Durchschnitt:** {s.avg_gwoe:.1f}") lines.append(f"- **Bandbreite:** {min(scores):.1f} – {max(scores):.1f} (Δ {spanne:.1f})") lines.append(f"- **Beste:r:** {best['name']} ({best['kommune']}) mit Ø {best['avg_gwoe']:.1f}") lines.append(f"- **Schwächste:r:** {worst['name']} ({worst['kommune']}) mit Ø {worst['avg_gwoe']:.1f}") lines.append("") elif len(partei_kandidaten) == 1: k = partei_kandidaten[0] lines.append(f"### {s.partei}") lines.append(f"- Nur 1 Kandidat:in: {k['name']} ({k['kommune']}) mit Ø {k['avg_gwoe']:.1f}") lines.append("") return "\n".join(lines) def generate_recommendation(statistiken: list[ParteiStatistik], conn: sqlite3.Connection) -> str: """Generiert die begründete Wahlempfehlung.""" kandidaten = get_kandidaten_ranking(conn) vorreiter = [k for k in kandidaten if k['avg_gwoe'] >= 5] # Berechne Gesamtstatistik alle_gwoe = [k['avg_gwoe'] for k in kandidaten] alle_substanz = [k['avg_substanz'] for k in kandidaten] lines = [ "# GWÖ-Wahlempfehlung Bayern 2026", "", "*Basierend auf der Analyse der Wahlprüfstein-Antworten*", "", "---", "", "## Methodik", "", "Diese Empfehlung basiert auf:", "- GWÖ-Score (0-10) nach Matrix 2.0 für Gemeinden", "- Substanz der Antworten (konkrete Maßnahmen vs. Floskeln)", "- Zustimmungsquote zu den 6 GWÖ-Kernfragen", "", "---", "", "## Übergreifende Beobachtungen", "", "### Das Substanz-Problem", "", f"Von {len(kandidaten)} Kandidat:innen erreichen nur **{len(vorreiter)}** einen GWÖ-Durchschnitt ≥ 5.0.", "", "Die Analyse zeigt ein strukturelles Problem: **Viele Ja-Antworten ohne konkrete Maßnahmen.**", "", f"- **Ø GWÖ-Score aller Kandidat:innen:** {sum(alle_gwoe)/len(alle_gwoe):.1f}/10", f"- **Ø Substanz-Score:** {sum(alle_substanz)/len(alle_substanz):.1f}/3", f"- **Ja-Quote:** hoch (85-100% bei den meisten Parteien)", f"- **Aber:** Konkrete Umsetzungsideen fehlen häufig", "", "Typisches Muster: *\"Ja\"* ohne Erläuterung oder mit Floskeln wie *\"Gespräche führen\"*, *\"Unterstützung anbieten\"*.", "", "### Parteilabel ≠ Kandidat:innen-Qualität", "", "Die Bandbreite *innerhalb* der Parteien ist oft größer als *zwischen* den Parteien:", "", ] # Beispiele für Bandbreite for s in statistiken: partei_kandidaten = [k for k in kandidaten if k['partei'] == s.partei] if len(partei_kandidaten) > 1: scores = [k['avg_gwoe'] for k in partei_kandidaten] spanne = max(scores) - min(scores) if spanne >= 3: best = max(partei_kandidaten, key=lambda x: x['avg_gwoe']) worst = min(partei_kandidaten, key=lambda x: x['avg_gwoe']) lines.append(f"- **{s.partei}:** {worst['name']} ({worst['avg_gwoe']:.1f}) bis {best['name']} ({best['avg_gwoe']:.1f}) — Δ {spanne:.1f} Punkte!") lines.extend([ "", "---", "", "## Wahlempfehlung", "", "### Keine pauschale Parteiempfehlung möglich", "", "**Für Bayern können wir keine übergreifende Wahlempfehlung auf Parteiebene geben.**", "", "Die Unterschiede zwischen einzelnen Kandidat:innen derselben Partei sind zu groß. ", "Ein Grünen-Kandidat kann GWÖ-Vorreiter sein, während ein anderer kaum Substanz liefert. ", "Das gleiche gilt für ÖDP, Freie Wähler und andere.", "", "**→ Empfehlung: Prüfen Sie die konkreten Kandidat:innen in Ihrer Kommune!**", "", "Siehe dazu: [Kandidat:innen-Ranking](kandidaten-ranking.md)", "", ]) if vorreiter: lines.extend([ "### 🟢 GWÖ-Vorreiter:innen (individuell empfehlenswert)", "", "Diese Kandidat:innen zeigen überdurchschnittliches GWÖ-Engagement:", "", ]) for k in vorreiter: lines.append(f"- **{k['name']}** ({k['kommune']}, {k['partei']}): Ø {k['avg_gwoe']:.1f}") lines.append("") # Eingeschränkt eingeschraenkt = [s for s in statistiken if 3 <= s.avg_gwoe < 5] if eingeschraenkt: lines.extend([ "### ⚠️ Parteien mit partieller Übereinstimmung", "", "Partei-Durchschnitt im Mittelfeld — individuelle Prüfung empfohlen:", "", ]) for s in eingeschraenkt: lines.append(f"- **{s.partei}** (Ø {s.avg_gwoe:.1f})") lines.append("") # Nicht empfohlen nicht_empfohlen = [s for s in statistiken if s.avg_gwoe < 3] if nicht_empfohlen: lines.extend([ "### ❌ Geringe GWÖ-Resonanz", "", "Diese Parteien zeigen im Durchschnitt wenig GWÖ-Affinität:", "", ]) for s in nicht_empfohlen: lines.append(f"- **{s.partei}** (Ø {s.avg_gwoe:.1f})") lines.append("") lines.extend([ "---", "", "## Fazit", "", "Die bayerischen Kommunalwahlen 2026 zeigen: **GWÖ-Unterstützung ist Sache einzelner Personen, nicht ganzer Parteien.**", "", "Wer GWÖ-affine Bürgermeister:innen wählen möchte, sollte:", "1. Das Kandidat:innen-Ranking konsultieren", "2. Die konkreten Antworten der lokalen Kandidat:innen lesen", "3. Bei Interesse nachfragen: *\"Welche konkreten Maßnahmen planen Sie?\"*", "", "---", "", "## Disclaimer", "", "Diese Empfehlung bezieht sich ausschließlich auf die Übereinstimmung mit GWÖ-Werten ", "und ersetzt keine umfassende politische Bewertung. Die Analyse basiert auf den ", "freiwilligen Antworten der Kandidat:innen auf die ECOnGOOD-Wahlprüfsteine.", "", f"*Erstellt: {datetime.now().strftime('%d.%m.%Y')}*" ]) return "\n".join(lines) def main(): import argparse parser = argparse.ArgumentParser(description='GWÖ-Wahlprüfsteine Aggregator') parser.add_argument('--db', type=Path, default=Path(__file__).parent / 'wahlpruefsteine.db', help='Pfad zur SQLite-Datenbank') parser.add_argument('--output', type=Path, default=Path(__file__).parent / 'output', help='Ausgabeverzeichnis') args = parser.parse_args() # Output-Verzeichnis erstellen args.output.mkdir(parents=True, exist_ok=True) # Datenbank öffnen conn = sqlite3.connect(args.db) conn.row_factory = sqlite3.Row # Statistiken holen print("Lade Statistiken...") statistiken = get_partei_statistiken(conn) fragen_stats = get_fragen_statistiken(conn) if not statistiken: print("FEHLER: Keine Bewertungen in der Datenbank. Erst analyzer.py ausführen!") return 1 print(f"Gefunden: {len(statistiken)} Parteien/Gruppen") # Reports generieren print("Generiere Reports...") # 1. Partei-Report partei_report = generate_partei_report(statistiken, fragen_stats, conn) partei_path = args.output / "partei-auswertung.md" partei_path.write_text(partei_report) print(f" → {partei_path}") # 2. Parteienlandschaft landscape_report = generate_landscape_report(statistiken) landscape_path = args.output / "parteienlandschaft.md" landscape_path.write_text(landscape_report) print(f" → {landscape_path}") # 3. Kandidat:innen-Ranking kandidaten_report = generate_kandidaten_report(conn, statistiken) kandidaten_path = args.output / "kandidaten-ranking.md" kandidaten_path.write_text(kandidaten_report) print(f" → {kandidaten_path}") # 4. Wahlempfehlung recommendation = generate_recommendation(statistiken, conn) rec_path = args.output / "wahlempfehlung.md" rec_path.write_text(recommendation) print(f" → {rec_path}") conn.close() print("\nFertig!") return 0 if __name__ == '__main__': exit(main())