- Scraper: HTML-Extraktion von ECOnGOOD-Webseite - Analyzer: LLM-Bewertung (Qwen) nach GWÖ-Matrix 2.0 - Aggregator: Partei-Auswertung + Kandidat:innen-Ranking - CLI: Reproduzierbarer Workflow (scrape → analyze → aggregate) - Output: 7 Dokumente inkl. Pressemitteilung und Methodik - 27 Kandidat:innen, 162 Einzelbewertungen
677 lines
24 KiB
Python
677 lines
24 KiB
Python
#!/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())
|