Initial: GWÖ-Wahlprüfsteine Auswertung Bayern 2026
- 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
This commit is contained in:
commit
f2a12f1238
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.db
|
||||
*.json
|
||||
!wahlpruefsteine/schema.sql
|
||||
!output/*.json
|
||||
690
ANLEITUNG-NACHBAU.md
Normal file
690
ANLEITUNG-NACHBAU.md
Normal file
@ -0,0 +1,690 @@
|
||||
# GWÖ-Wahlprüfsteine Auswertung — Nachbau-Anleitung
|
||||
|
||||
Diese Anleitung erklärt, wie du das Tool für eigene Wahlprüfstein-Auswertungen nachbauen oder anpassen kannst.
|
||||
|
||||
---
|
||||
|
||||
## Was macht dieses Tool?
|
||||
|
||||
1. **Scrapen:** Lädt Wahlprüfstein-Antworten von einer Webseite
|
||||
2. **Analysieren:** Bewertet jede Antwort automatisch nach GWÖ-Kriterien (mit KI)
|
||||
3. **Aggregieren:** Fasst Ergebnisse nach Parteien zusammen
|
||||
4. **Berichten:** Erstellt Markdown-Reports mit Zitaten und Empfehlungen
|
||||
|
||||
**Ergebnis:** Objektive, reproduzierbare Auswertung von Wahlprüfsteinen aus GWÖ-Perspektive.
|
||||
|
||||
---
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
### Software
|
||||
- **Python 3.11+** (macOS/Linux: vorinstalliert, Windows: python.org)
|
||||
- **Git** (optional, für Versionierung)
|
||||
|
||||
### API-Zugang
|
||||
Du brauchst einen LLM-API-Zugang. Optionen:
|
||||
|
||||
| Anbieter | Modell | Kosten | Anmerkung |
|
||||
|----------|--------|--------|-----------|
|
||||
| **Alibaba DashScope** | qwen-plus | ~$0.001/Call | Empfohlen, günstig |
|
||||
| **OpenAI** | gpt-4o-mini | ~$0.003/Call | Teurer, aber bekannt |
|
||||
| **Anthropic** | claude-3-haiku | ~$0.002/Call | Gute Alternative |
|
||||
|
||||
**Für DashScope (empfohlen):**
|
||||
1. Account erstellen: https://dashscope.console.aliyun.com
|
||||
2. API-Key generieren
|
||||
3. Key sicher speichern (siehe unten)
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. Projektordner erstellen
|
||||
|
||||
```bash
|
||||
mkdir -p ~/projekte/wahlpruefsteine-auswertung
|
||||
cd ~/projekte/wahlpruefsteine-auswertung
|
||||
```
|
||||
|
||||
### 2. Python Virtual Environment
|
||||
|
||||
```bash
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate # Linux/macOS
|
||||
# oder: .venv\Scripts\activate # Windows
|
||||
```
|
||||
|
||||
### 3. Abhängigkeiten installieren
|
||||
|
||||
```bash
|
||||
pip install beautifulsoup4 requests openai
|
||||
```
|
||||
|
||||
### 4. API-Key konfigurieren
|
||||
|
||||
**Option A: Umgebungsvariable (einfach)**
|
||||
```bash
|
||||
export DASHSCOPE_API_KEY="sk-xxx..."
|
||||
```
|
||||
|
||||
**Option B: macOS Keychain (sicher)**
|
||||
```bash
|
||||
security add-generic-password -s qwen-api -a $USER -w "sk-xxx..."
|
||||
```
|
||||
|
||||
**Option C: .env-Datei**
|
||||
```bash
|
||||
echo "DASHSCOPE_API_KEY=sk-xxx..." > .env
|
||||
pip install python-dotenv
|
||||
# Dann in Python: from dotenv import load_dotenv; load_dotenv()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dateien erstellen
|
||||
|
||||
Erstelle diese 4 Dateien im Projektordner:
|
||||
|
||||
### 1. `schema.sql` — Datenbankstruktur
|
||||
|
||||
```sql
|
||||
-- Kandidat:innen
|
||||
CREATE TABLE IF NOT EXISTS kandidaten (
|
||||
id INTEGER PRIMARY KEY,
|
||||
vorname TEXT NOT NULL,
|
||||
nachname TEXT NOT NULL,
|
||||
plz TEXT,
|
||||
kommune TEXT NOT NULL,
|
||||
landkreis TEXT,
|
||||
partei_raw TEXT NOT NULL,
|
||||
partei_normalisiert TEXT NOT NULL,
|
||||
ist_waehlergemeinschaft BOOLEAN DEFAULT FALSE,
|
||||
pdf_url TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Die 6 Fragen (anpassen an deine Wahlprüfsteine!)
|
||||
CREATE TABLE IF NOT EXISTS fragen (
|
||||
id INTEGER PRIMARY KEY,
|
||||
nummer INTEGER UNIQUE NOT NULL,
|
||||
kurztext TEXT NOT NULL,
|
||||
volltext TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- Rohantworten
|
||||
CREATE TABLE IF NOT EXISTS antworten_raw (
|
||||
id INTEGER PRIMARY KEY,
|
||||
kandidat_id INTEGER REFERENCES kandidaten(id) ON DELETE CASCADE,
|
||||
frage_id INTEGER REFERENCES fragen(id) ON DELETE CASCADE,
|
||||
antwort_kurz TEXT,
|
||||
antwort_erlaeuterung TEXT,
|
||||
UNIQUE(kandidat_id, frage_id)
|
||||
);
|
||||
|
||||
-- LLM-Bewertungen
|
||||
CREATE TABLE IF NOT EXISTS bewertungen (
|
||||
id INTEGER PRIMARY KEY,
|
||||
antwort_id INTEGER REFERENCES antworten_raw(id) ON DELETE CASCADE UNIQUE,
|
||||
substanz_score INTEGER CHECK(substanz_score >= 0 AND substanz_score <= 3),
|
||||
umfang TEXT CHECK(umfang IN ('keine', 'kurz', 'mittel', 'ausführlich')),
|
||||
wortanzahl INTEGER,
|
||||
gwoe_score REAL CHECK(gwoe_score >= 0 AND gwoe_score <= 10),
|
||||
gwoe_begruendung TEXT,
|
||||
matrix_felder TEXT, -- JSON-Array
|
||||
staerken TEXT, -- JSON-Array
|
||||
schwaechen TEXT, -- JSON-Array
|
||||
bewertet_am DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
model TEXT DEFAULT 'qwen-plus'
|
||||
);
|
||||
|
||||
-- Stammdaten: Fragen (ANPASSEN!)
|
||||
INSERT OR IGNORE INTO fragen (nummer, kurztext, volltext) VALUES
|
||||
(1, 'Leitlinien', 'Werden Sie sich für Maßnahmen einsetzen, welche die Werte und Themen der GWÖ in Leitlinien und Strategien Ihrer Kommune integrieren?'),
|
||||
(2, 'Anreize', 'Werden Sie sich für Anreize einsetzen, um Unternehmen zu unterstützen, gemeinwohl-orientierter zu wirtschaften?'),
|
||||
(3, 'Vergabe', 'Werden Sie sich dafür einsetzen, dass öffentliche Aufträge bevorzugt an Unternehmen mit Gemeinwohl-Bilanz vergeben werden?'),
|
||||
(4, 'Information', 'Möchten Sie Bürger:innen regelmäßig über die Gemeinwohl-Auswirkungen kommunaler Entwicklungen informieren?'),
|
||||
(5, 'Mitentscheidung', 'Möchten Sie Bürger:innen stärker in kommunale Entscheidungsprozesse einbinden?'),
|
||||
(6, 'Bekanntheit', 'Möchten Sie die GWÖ in Ihrer Kommune und auf höheren Ebenen bekannter machen?');
|
||||
```
|
||||
|
||||
### 2. `scraper.py` — Daten laden
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Wahlprüfsteine Scraper — ANPASSEN AN DEINE DATENQUELLE!
|
||||
"""
|
||||
|
||||
import re
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
from bs4 import BeautifulSoup
|
||||
import requests
|
||||
|
||||
# Partei-Normalisierung (ANPASSEN für dein Bundesland!)
|
||||
PARTEI_MAPPING = {
|
||||
r'bündnis\s*90\s*/?\s*die\s*grünen?': 'Grüne',
|
||||
r'grüne': 'Grüne',
|
||||
r'freie\s*wähler': 'Freie Wähler',
|
||||
r'^csu$': 'CSU',
|
||||
r'^cdu$': 'CDU',
|
||||
r'^spd$': 'SPD',
|
||||
r'^fdp$': 'FDP',
|
||||
r'^ödp': 'ÖDP',
|
||||
r'die\s*linke': 'Linke',
|
||||
r'^afd$': 'AfD',
|
||||
}
|
||||
|
||||
def normalize_partei(raw: str) -> tuple[str, bool]:
|
||||
"""Normalisiert Parteinamen. Returns: (partei, ist_wählergemeinschaft)"""
|
||||
raw_lower = raw.lower().strip()
|
||||
|
||||
for pattern, normalized in PARTEI_MAPPING.items():
|
||||
if re.search(pattern, raw_lower, re.IGNORECASE):
|
||||
return normalized, False
|
||||
|
||||
return 'Wählergemeinschaft', True
|
||||
|
||||
def parse_html(html_content: str) -> list[dict]:
|
||||
"""
|
||||
Parst HTML und extrahiert Kandidaten + Antworten.
|
||||
|
||||
WICHTIG: Diese Funktion musst du an deine HTML-Struktur anpassen!
|
||||
"""
|
||||
soup = BeautifulSoup(html_content, 'html.parser')
|
||||
kandidaten = []
|
||||
|
||||
# BEISPIEL: Tabelle mit Kandidaten parsen
|
||||
# ANPASSEN an deine HTML-Struktur!
|
||||
for table in soup.find_all('table'):
|
||||
rows = table.find_all('tr')
|
||||
|
||||
for row in rows[1:]: # Header überspringen
|
||||
cells = row.find_all('td')
|
||||
if len(cells) < 7:
|
||||
continue
|
||||
|
||||
try:
|
||||
kandidaten.append({
|
||||
'plz': cells[0].get_text(strip=True),
|
||||
'kommune': cells[1].get_text(strip=True),
|
||||
'landkreis': cells[2].get_text(strip=True),
|
||||
'vorname': cells[3].get_text(strip=True),
|
||||
'nachname': cells[4].get_text(strip=True),
|
||||
'partei_raw': cells[5].get_text(strip=True),
|
||||
'pdf_url': cells[6].find('a')['href'] if cells[6].find('a') else None,
|
||||
'antworten': {} # Frage-Nr -> (ja_nein, erläuterung)
|
||||
})
|
||||
|
||||
# Antworten extrahieren (6 Fragen × 2 Spalten)
|
||||
for i in range(6):
|
||||
ja_nein = cells[7 + i*2].get_text(strip=True) if 7 + i*2 < len(cells) else None
|
||||
erlaeuterung = cells[8 + i*2].get_text(strip=True) if 8 + i*2 < len(cells) else None
|
||||
kandidaten[-1]['antworten'][i+1] = (ja_nein, erlaeuterung)
|
||||
|
||||
except (IndexError, KeyError):
|
||||
continue
|
||||
|
||||
return kandidaten
|
||||
|
||||
def init_db(db_path: Path) -> sqlite3.Connection:
|
||||
"""Initialisiert die Datenbank."""
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
|
||||
schema_path = Path(__file__).parent / 'schema.sql'
|
||||
with open(schema_path) as f:
|
||||
conn.executescript(f.read())
|
||||
|
||||
conn.commit()
|
||||
return conn
|
||||
|
||||
def save_to_db(conn: sqlite3.Connection, kandidaten: list[dict]) -> int:
|
||||
"""Speichert Kandidaten in der Datenbank."""
|
||||
cursor = conn.cursor()
|
||||
|
||||
for k in kandidaten:
|
||||
partei_norm, ist_wg = normalize_partei(k['partei_raw'])
|
||||
|
||||
cursor.execute("""
|
||||
INSERT OR REPLACE INTO kandidaten
|
||||
(vorname, nachname, plz, kommune, landkreis, partei_raw,
|
||||
partei_normalisiert, ist_waehlergemeinschaft, pdf_url)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (k['vorname'], k['nachname'], k['plz'], k['kommune'],
|
||||
k['landkreis'], k['partei_raw'], partei_norm, ist_wg, k['pdf_url']))
|
||||
|
||||
kandidat_id = cursor.lastrowid
|
||||
|
||||
for frage_nr, (ja_nein, erlaeuterung) in k['antworten'].items():
|
||||
cursor.execute("""
|
||||
INSERT OR REPLACE INTO antworten_raw
|
||||
(kandidat_id, frage_id, antwort_kurz, antwort_erlaeuterung)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""", (kandidat_id, frage_nr, ja_nein, erlaeuterung))
|
||||
|
||||
conn.commit()
|
||||
return len(kandidaten)
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='Wahlprüfsteine Scraper')
|
||||
parser.add_argument('--url', required=True, help='URL der Wahlprüfsteine-Seite')
|
||||
parser.add_argument('--db', default='wahlpruefsteine.db', help='Datenbankpfad')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f"Lade: {args.url}")
|
||||
response = requests.get(args.url, timeout=30)
|
||||
kandidaten = parse_html(response.text)
|
||||
|
||||
print(f"Gefunden: {len(kandidaten)} Kandidat:innen")
|
||||
|
||||
conn = init_db(Path(args.db))
|
||||
count = save_to_db(conn, kandidaten)
|
||||
conn.close()
|
||||
|
||||
print(f"Gespeichert: {count} in {args.db}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
```
|
||||
|
||||
### 3. `analyzer.py` — KI-Bewertung
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
GWÖ-Bewertung mit LLM
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
from openai import OpenAI
|
||||
|
||||
# GWÖ-Kontext für das LLM
|
||||
GWOE_CONTEXT = """
|
||||
## GWÖ-Matrix 2.0 für Gemeinden
|
||||
|
||||
### Die 5 Werte
|
||||
1. **Menschenwürde** — Grundrechte, Rechtsstaatlichkeit
|
||||
2. **Solidarität** — Gemeinnutz, Mehrwert für Gemeinschaft
|
||||
3. **Ökologische Nachhaltigkeit** — Klimaschutz, Ressourcenschonung
|
||||
4. **Soziale Gerechtigkeit** — Daseinsvorsorge, gerechte Verteilung
|
||||
5. **Transparenz & Mitbestimmung** — Bürgerbeteiligung, Demokratie
|
||||
|
||||
### Berührungsgruppen
|
||||
- A: Lieferant:innen (Beschaffung)
|
||||
- B: Finanzpartner:innen (Haushalt)
|
||||
- C: Verwaltung (Mitarbeitende)
|
||||
- D: Bürger:innen (Daseinsvorsorge) — WICHTIGSTE für Kommunalpolitik!
|
||||
- E: Gesellschaft (überregional)
|
||||
|
||||
### Bewertungsskala
|
||||
- 7-10: Vorbildlich (innovative Maßnahmen)
|
||||
- 4-6: Erfahren (erkennbare Verbesserungen)
|
||||
- 2-3: Fortgeschritten (erste Maßnahmen)
|
||||
- 0-1: Basislinie
|
||||
"""
|
||||
|
||||
SYSTEM_PROMPT = f"""Du bist ein GWÖ-Experte und analysierst Wahlprüfstein-Antworten.
|
||||
|
||||
{GWOE_CONTEXT}
|
||||
|
||||
Bewerte die Antwort und gib NUR valides JSON zurück:
|
||||
|
||||
{{
|
||||
"substanz_score": 0-3, // 0=keine Antwort, 1=ausweichend, 2=substanziell, 3=umfassend
|
||||
"umfang": "keine|kurz|mittel|ausführlich",
|
||||
"gwoe_score": 0.0-10.0,
|
||||
"gwoe_begruendung": "2-3 Sätze",
|
||||
"matrix_felder": ["D5", "C2"], // Berührte Felder
|
||||
"staerken": ["Punkt 1"],
|
||||
"schwaechen": ["Punkt 1"]
|
||||
}}"""
|
||||
|
||||
def get_api_client():
|
||||
"""Erstellt API-Client (ANPASSEN für deinen Anbieter!)"""
|
||||
|
||||
# Option 1: DashScope (Qwen)
|
||||
api_key = os.environ.get('DASHSCOPE_API_KEY')
|
||||
if api_key:
|
||||
return OpenAI(
|
||||
api_key=api_key,
|
||||
base_url="https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
|
||||
), "qwen-plus"
|
||||
|
||||
# Option 2: OpenAI
|
||||
api_key = os.environ.get('OPENAI_API_KEY')
|
||||
if api_key:
|
||||
return OpenAI(api_key=api_key), "gpt-4o-mini"
|
||||
|
||||
raise ValueError("Kein API-Key gefunden! Setze DASHSCOPE_API_KEY oder OPENAI_API_KEY")
|
||||
|
||||
def analyze_answer(client, model: str, kandidat: str, frage: str, antwort: str) -> dict:
|
||||
"""Analysiert eine einzelne Antwort."""
|
||||
|
||||
user_prompt = f"""## Frage
|
||||
{frage}
|
||||
|
||||
## Antwort von {kandidat}
|
||||
{antwort if antwort else "(keine Antwort)"}
|
||||
|
||||
Bewerte nach GWÖ-Kriterien."""
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model=model,
|
||||
messages=[
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{"role": "user", "content": user_prompt}
|
||||
],
|
||||
temperature=0.3,
|
||||
response_format={"type": "json_object"}
|
||||
)
|
||||
|
||||
return json.loads(response.choices[0].message.content)
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='GWÖ-Analyzer')
|
||||
parser.add_argument('--db', default='wahlpruefsteine.db')
|
||||
parser.add_argument('--limit', type=int, help='Max. Anzahl Bewertungen')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
client, model = get_api_client()
|
||||
print(f"Verwende Modell: {model}")
|
||||
|
||||
conn = sqlite3.connect(args.db)
|
||||
conn.row_factory = sqlite3.Row
|
||||
|
||||
# Unbewertete Antworten holen
|
||||
query = """
|
||||
SELECT ar.id, k.vorname || ' ' || k.nachname as kandidat,
|
||||
f.volltext as frage, ar.antwort_erlaeuterung as antwort
|
||||
FROM antworten_raw ar
|
||||
JOIN kandidaten k ON ar.kandidat_id = k.id
|
||||
JOIN fragen f ON ar.frage_id = f.id
|
||||
LEFT JOIN bewertungen b ON ar.id = b.antwort_id
|
||||
WHERE b.id IS NULL
|
||||
"""
|
||||
if args.limit:
|
||||
query += f" LIMIT {args.limit}"
|
||||
|
||||
antworten = conn.execute(query).fetchall()
|
||||
print(f"Zu bewerten: {len(antworten)}")
|
||||
|
||||
for i, row in enumerate(antworten, 1):
|
||||
print(f"[{i}/{len(antworten)}] {row['kandidat']}...", end=" ", flush=True)
|
||||
|
||||
try:
|
||||
result = analyze_answer(client, model, row['kandidat'], row['frage'], row['antwort'])
|
||||
|
||||
conn.execute("""
|
||||
INSERT INTO bewertungen
|
||||
(antwort_id, substanz_score, umfang, gwoe_score, gwoe_begruendung,
|
||||
matrix_felder, staerken, schwaechen, model)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
row['id'],
|
||||
result.get('substanz_score'),
|
||||
result.get('umfang'),
|
||||
result.get('gwoe_score'),
|
||||
result.get('gwoe_begruendung'),
|
||||
json.dumps(result.get('matrix_felder', [])),
|
||||
json.dumps(result.get('staerken', [])),
|
||||
json.dumps(result.get('schwaechen', [])),
|
||||
model
|
||||
))
|
||||
conn.commit()
|
||||
|
||||
print(f"GWÖ: {result.get('gwoe_score', '?')}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"FEHLER: {e}")
|
||||
|
||||
time.sleep(0.5) # Rate Limiting
|
||||
|
||||
conn.close()
|
||||
print("Fertig!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
```
|
||||
|
||||
### 4. `aggregator.py` — Reports erstellen
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Aggregation und Report-Generierung
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
def get_partei_stats(conn) -> list[dict]:
|
||||
"""Statistiken pro Partei."""
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
k.partei_normalisiert AS partei,
|
||||
COUNT(DISTINCT k.id) AS kandidaten,
|
||||
COUNT(b.id) AS antworten,
|
||||
ROUND(AVG(b.substanz_score), 2) AS avg_substanz,
|
||||
ROUND(AVG(b.gwoe_score), 2) AS avg_gwoe
|
||||
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
|
||||
""")
|
||||
|
||||
return [dict(row) for row in cursor.fetchall()]
|
||||
|
||||
def get_top_quotes(conn, partei: str, limit: int = 3) -> list[dict]:
|
||||
"""Beste Zitate einer Partei."""
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
k.vorname || ' ' || k.nachname AS name,
|
||||
k.kommune,
|
||||
f.kurztext AS frage,
|
||||
ar.antwort_erlaeuterung AS zitat,
|
||||
b.gwoe_score
|
||||
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 ?
|
||||
""", (partei, limit))
|
||||
|
||||
return [dict(row) for row in cursor.fetchall()]
|
||||
|
||||
def generate_report(stats: list[dict], conn) -> str:
|
||||
"""Generiert Markdown-Report."""
|
||||
|
||||
lines = [
|
||||
"# GWÖ-Wahlprüfsteine Auswertung",
|
||||
"",
|
||||
f"*Stand: {datetime.now().strftime('%d.%m.%Y %H:%M')}*",
|
||||
"",
|
||||
"## Zusammenfassung",
|
||||
"",
|
||||
"| Partei | Kandidat:innen | Ø GWÖ | Ø Substanz |",
|
||||
"|--------|---------------|-------|------------|",
|
||||
]
|
||||
|
||||
for s in stats:
|
||||
lines.append(f"| **{s['partei']}** | {s['kandidaten']} | {s['avg_gwoe']:.1f} | {s['avg_substanz']:.1f}/3 |")
|
||||
|
||||
lines.extend(["", "---", "", "## Details", ""])
|
||||
|
||||
for s in stats:
|
||||
lines.append(f"### {s['partei']} (Ø {s['avg_gwoe']:.1f})")
|
||||
lines.append("")
|
||||
|
||||
quotes = get_top_quotes(conn, s['partei'])
|
||||
if quotes:
|
||||
lines.append("**Beste Antworten:**")
|
||||
for q in quotes:
|
||||
zitat = q['zitat'][:200] + "..." if len(q['zitat']) > 200 else q['zitat']
|
||||
lines.append(f"> *\"{zitat}\"*")
|
||||
lines.append(f"> — {q['name']} ({q['kommune']}), GWÖ: {q['gwoe_score']:.1f}")
|
||||
lines.append("")
|
||||
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--db', default='wahlpruefsteine.db')
|
||||
parser.add_argument('--output', default='report.md')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
conn = sqlite3.connect(args.db)
|
||||
conn.row_factory = sqlite3.Row
|
||||
|
||||
stats = get_partei_stats(conn)
|
||||
report = generate_report(stats, conn)
|
||||
|
||||
Path(args.output).write_text(report)
|
||||
print(f"Report erstellt: {args.output}")
|
||||
|
||||
conn.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ausführung
|
||||
|
||||
### Vollständiger Workflow
|
||||
|
||||
```bash
|
||||
# 1. Daten laden
|
||||
python3 scraper.py --url "https://example.org/wahlpruefsteine"
|
||||
|
||||
# 2. KI-Bewertung (dauert je nach Anzahl)
|
||||
python3 analyzer.py
|
||||
|
||||
# 3. Report generieren
|
||||
python3 aggregator.py --output auswertung.md
|
||||
```
|
||||
|
||||
### Status prüfen
|
||||
|
||||
```bash
|
||||
sqlite3 wahlpruefsteine.db "
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM kandidaten) as kandidaten,
|
||||
(SELECT COUNT(*) FROM antworten_raw) as antworten,
|
||||
(SELECT COUNT(*) FROM bewertungen) as bewertet;
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Anpassungen für andere Datenquellen
|
||||
|
||||
### Andere HTML-Struktur
|
||||
|
||||
Passe `parse_html()` in `scraper.py` an:
|
||||
|
||||
```python
|
||||
def parse_html(html_content: str) -> list[dict]:
|
||||
soup = BeautifulSoup(html_content, 'html.parser')
|
||||
|
||||
# DEINE LOGIK HIER
|
||||
# z.B. für andere Tabellenstruktur, div-basierte Layouts, etc.
|
||||
|
||||
return kandidaten
|
||||
```
|
||||
|
||||
### Andere Fragen
|
||||
|
||||
Passe die `INSERT`-Statements in `schema.sql` an:
|
||||
|
||||
```sql
|
||||
INSERT OR IGNORE INTO fragen (nummer, kurztext, volltext) VALUES
|
||||
(1, 'Deine Frage 1', 'Vollständiger Text...'),
|
||||
(2, 'Deine Frage 2', 'Vollständiger Text...'),
|
||||
-- usw.
|
||||
```
|
||||
|
||||
### Andere Parteien/Region
|
||||
|
||||
Passe `PARTEI_MAPPING` in `scraper.py` an:
|
||||
|
||||
```python
|
||||
PARTEI_MAPPING = {
|
||||
r'^cdu$': 'CDU', # statt CSU für andere Bundesländer
|
||||
# deine Parteien...
|
||||
}
|
||||
```
|
||||
|
||||
### Andere LLM-Anbieter
|
||||
|
||||
Passe `get_api_client()` in `analyzer.py` an:
|
||||
|
||||
```python
|
||||
# Für Anthropic Claude:
|
||||
from anthropic import Anthropic
|
||||
client = Anthropic(api_key=os.environ['ANTHROPIC_API_KEY'])
|
||||
|
||||
# Für lokales Ollama:
|
||||
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Kosten-Übersicht
|
||||
|
||||
| Anbieter | Modell | ~Kosten/100 Antworten |
|
||||
|----------|--------|----------------------|
|
||||
| DashScope | qwen-plus | $0.06 |
|
||||
| DashScope | qwen-turbo | $0.02 |
|
||||
| OpenAI | gpt-4o-mini | $0.30 |
|
||||
| Anthropic | claude-3-haiku | $0.20 |
|
||||
| Ollama | lokal | $0.00 |
|
||||
|
||||
---
|
||||
|
||||
## Tipps
|
||||
|
||||
1. **Erst testen:** Starte mit `--limit 5` beim Analyzer
|
||||
2. **HTML speichern:** `curl URL > data.html` für Offline-Entwicklung
|
||||
3. **Logs prüfen:** Bei Fehlern einzelne Antworten manuell checken
|
||||
4. **Schwellen anpassen:** Die GWÖ-Schwellen (5 für "empfohlen") ggf. senken
|
||||
|
||||
---
|
||||
|
||||
## Lizenz
|
||||
|
||||
MIT — Frei verwendbar, auch kommerziell.
|
||||
|
||||
---
|
||||
|
||||
*Anleitung erstellt: 29.03.2026*
|
||||
*Fragen? → GWÖ-Community oder ECOnGOOD*
|
||||
171
DOKUMENTATION.md
Normal file
171
DOKUMENTATION.md
Normal file
@ -0,0 +1,171 @@
|
||||
# Wahlprüfsteine-Auswertung — Technische Dokumentation
|
||||
|
||||
## Übersicht
|
||||
|
||||
CLI-Tool zur automatisierten GWÖ-Analyse von Wahlprüfstein-Antworten mit Partei-Aggregation und Kandidat:innen-Ranking.
|
||||
|
||||
## Komponenten
|
||||
|
||||
### 1. Scraper (`scraper.py`)
|
||||
|
||||
**Funktion:** Extrahiert Kandidat:innen und Antworten aus der ECOnGOOD-Webseite.
|
||||
|
||||
**Ablauf:**
|
||||
1. HTML von URL laden (oder lokale Datei)
|
||||
2. Tabellen parsen mit BeautifulSoup
|
||||
3. Parteinamen normalisieren (Fuzzy-Matching)
|
||||
4. In SQLite speichern
|
||||
|
||||
**Partei-Normalisierung:**
|
||||
```python
|
||||
PARTEI_MAPPING = {
|
||||
r'bündnis\s*90\s*/?\s*die\s*grünen?': 'Grüne',
|
||||
r'freie\s*wähler': 'Freie Wähler',
|
||||
r'^csu$': 'CSU',
|
||||
# ...
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Analyzer (`analyzer.py`)
|
||||
|
||||
**Funktion:** Bewertet jede Antwort mit Qwen LLM nach GWÖ-Kriterien.
|
||||
|
||||
**Prompt-Struktur:**
|
||||
- System: GWÖ-Matrix 2.0 Kontext + Bewertungsskalen
|
||||
- User: Frage + Antwort + Kandidat:in-Info
|
||||
- Output: Strukturiertes JSON
|
||||
|
||||
**Bewertungsdimensionen:**
|
||||
- `substanz_score` (0-3): Konkretheit der Antwort
|
||||
- `umfang`: keine/kurz/mittel/ausführlich
|
||||
- `gwoe_score` (0-10): Übereinstimmung mit Matrix
|
||||
- `matrix_felder`: Berührte GWÖ-Felder (z.B. ["D5", "C2"])
|
||||
- `staerken/schwaechen`: Qualitative Analyse
|
||||
|
||||
**API-Konfiguration:**
|
||||
```python
|
||||
client = OpenAI(
|
||||
api_key=keychain["qwen-api"],
|
||||
base_url="https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
|
||||
)
|
||||
model = "qwen-plus" # oder qwen-max für höhere Qualität
|
||||
```
|
||||
|
||||
### 3. Aggregator (`aggregator.py`)
|
||||
|
||||
**Funktion:** Erstellt Partei-Statistiken und Markdown-Reports.
|
||||
|
||||
**Reports:**
|
||||
1. `partei-auswertung.md` — Detailreport mit Zitaten + Bandbreiten + Substanz-Problem
|
||||
2. `parteienlandschaft.md` — Komprimierte Übersicht
|
||||
3. `kandidaten-ranking.md` — Alle 27 Kandidat:innen im Einzelranking mit Kategorisierung
|
||||
4. `wahlempfehlung.md` — Begründete Empfehlung + Substanz-Problem + Verweis auf Kandidat:innen
|
||||
|
||||
**Aggregations-Logik:**
|
||||
- Durchschnittswerte pro Partei (GWÖ, Substanz, Wortanzahl)
|
||||
- Bandbreiten pro Partei (Min–Max der Kandidat:innen)
|
||||
- Ja-Quote berechnen
|
||||
- Matrix-Felder aggregieren (häufigste pro Partei)
|
||||
- Top-3-Zitate pro Partei extrahieren
|
||||
- Kandidat:innen-Ranking mit Kategorisierung (Vorreiter / Solide / Schwach)
|
||||
- Übergreifende Beobachtungen (Substanz-Problem, Bandbreiten-Analyse)
|
||||
|
||||
### 4. CLI (`cli.py`)
|
||||
|
||||
**Befehle:**
|
||||
```
|
||||
scrape Daten von URL laden
|
||||
analyze LLM-Bewertung durchführen
|
||||
aggregate Reports generieren
|
||||
status Datenbankstatus anzeigen
|
||||
export JSON-Export
|
||||
run Vollständiger Workflow
|
||||
```
|
||||
|
||||
## Datenbankschema
|
||||
|
||||
### Tabellen
|
||||
|
||||
```sql
|
||||
kandidaten (id, vorname, nachname, plz, kommune, landkreis,
|
||||
partei_raw, partei_normalisiert, ist_waehlergemeinschaft, pdf_url)
|
||||
|
||||
fragen (id, nummer, kurztext, volltext)
|
||||
|
||||
antworten_raw (id, kandidat_id, frage_id, antwort_kurz, antwort_erlaeuterung)
|
||||
|
||||
bewertungen (id, antwort_id, substanz_score, umfang, wortanzahl,
|
||||
gwoe_score, gwoe_begruendung, matrix_felder,
|
||||
staerken, schwaechen, model, bewertet_am)
|
||||
```
|
||||
|
||||
### Views
|
||||
|
||||
```sql
|
||||
v_partei_statistik -- Aggregierte Statistiken pro Partei
|
||||
v_fragen_statistik -- Statistiken pro Frage
|
||||
v_top_antworten -- Beste Antworten sortiert nach GWÖ-Score
|
||||
```
|
||||
|
||||
## GWÖ-Matrix 2.0 Referenz
|
||||
|
||||
### Werte (Spalten)
|
||||
1. Menschenwürde
|
||||
2. Solidarität
|
||||
3. Ökologische Nachhaltigkeit
|
||||
4. Soziale Gerechtigkeit
|
||||
5. Transparenz & Mitbestimmung
|
||||
|
||||
### Berührungsgruppen (Zeilen)
|
||||
- A: Lieferant:innen, Dienstleister:innen
|
||||
- B: Finanzpartner:innen, Steuerzahler:innen
|
||||
- C: Politische Führung, Verwaltung
|
||||
- D: Bürger:innen und Wirtschaft
|
||||
- E: Staat, Gesellschaft, Natur
|
||||
|
||||
### Relevante Felder für Kommunalpolitik
|
||||
- **D5**: Demokratische Einbindung (Bürgerbeteiligung)
|
||||
- **D2**: Gesamtwohl der Gemeinde
|
||||
- **C2**: Gemeinwohlorientierte Zielvereinbarung
|
||||
- **A3/A4**: Nachhaltige/soziale Beschaffung
|
||||
- **B5**: Partizipation in Finanzpolitik
|
||||
|
||||
## Reproduzierbarkeit
|
||||
|
||||
Das Tool ist vollständig reproduzierbar ohne Dotty-Beteiligung:
|
||||
|
||||
```bash
|
||||
# Einmalige Einrichtung
|
||||
cd ~/Nextcloud/dotty/projekte/2026-03-29\ Wahlprüfsteine\ _WIP_/wahlpruefsteine
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install beautifulsoup4 requests openai
|
||||
|
||||
# Ausführung
|
||||
python3 cli.py run
|
||||
```
|
||||
|
||||
**Voraussetzungen:**
|
||||
- Python 3.11+
|
||||
- Qwen API-Key in Keychain (`security find-generic-password -s qwen-api -w`)
|
||||
- Alternativ: `DASHSCOPE_API_KEY` Umgebungsvariable
|
||||
|
||||
## Kosten
|
||||
|
||||
- **Qwen Plus:** ~$0.0006 pro Bewertung
|
||||
- **162 Bewertungen:** ~$0.10 gesamt
|
||||
- **Modell-Alternativen:**
|
||||
- `qwen-turbo`: günstiger, schneller, etwas weniger präzise
|
||||
- `qwen-max`: teurer, höhere Qualität
|
||||
|
||||
## Erweiterungsmöglichkeiten
|
||||
|
||||
1. **Andere Datenquellen:** Scraper für andere Wahlprüfstein-Formate anpassen
|
||||
2. **PDF-Reports:** WeasyPrint oder Quarto für ECOnGOOD-Design
|
||||
3. **Zeitreihen:** Vergleich über mehrere Wahlen
|
||||
4. **Interaktives Dashboard:** Streamlit oder FastAPI + SvelteKit
|
||||
5. **Andere Bundesländer:** Partei-Mapping anpassen (CDU statt CSU etc.)
|
||||
|
||||
---
|
||||
|
||||
*Dokumentation erstellt: 29.03.2026*
|
||||
116
README.md
Normal file
116
README.md
Normal file
@ -0,0 +1,116 @@
|
||||
# GWÖ-Wahlprüfsteine Auswertung
|
||||
|
||||
**Telegram-Topic:** [📋 Wahlprüfsteine](https://t.me/c/3823618505/7281) (thread_id 7281)
|
||||
**Status:** ✅ Tool funktionsfähig, Auswertung komplett
|
||||
|
||||
## Datenquelle
|
||||
|
||||
- **URL:** https://germany.econgood.org/wahlpruefsteine-zu-den-bayerischen-kommunalwahlen-2026
|
||||
- **Umfrage:** Bayerische Kommunalwahlen 2026 (Bürgermeister-Kandidat:innen)
|
||||
- **Rücklauf:** 27 veröffentlichte Antworten
|
||||
- **6 Fragen** zu GWÖ-Themen
|
||||
|
||||
## Kernergebnisse
|
||||
|
||||
### Das Substanz-Problem
|
||||
|
||||
Von 27 Kandidat:innen erreichen nur **3** einen GWÖ-Durchschnitt ≥ 5.0.
|
||||
|
||||
- **Ø GWÖ-Score:** 2.2/10
|
||||
- **Ø Substanz-Score:** 0.9/3
|
||||
- **Ja-Quote:** 75–100% bei den meisten Parteien
|
||||
- **Aber:** Konkrete Maßnahmen fehlen häufig
|
||||
|
||||
Typisches Muster: *"Ja"* ohne Erläuterung oder mit Floskeln.
|
||||
|
||||
### Parteilabel ≠ Kandidat:innen-Qualität
|
||||
|
||||
Die Bandbreite *innerhalb* der Parteien ist oft größer als *zwischen* Parteien:
|
||||
|
||||
| Partei | Ø GWÖ | Bandbreite | Δ |
|
||||
|--------|-------|------------|---|
|
||||
| ÖDP | 4.2 | 0.9–5.8 | 4.9 |
|
||||
| Grüne | 3.0 | 0.3–5.8 | 5.5 |
|
||||
| Freie Wähler | 1.5 | 0.0–4.9 | 4.9 |
|
||||
|
||||
**→ Fazit:** Keine pauschale Parteiempfehlung möglich. Einzelne Kandidat:innen prüfen!
|
||||
|
||||
### GWÖ-Vorreiter:innen
|
||||
|
||||
| Rang | Name | Kommune | Partei | Ø GWÖ |
|
||||
|------|------|---------|--------|-------|
|
||||
| 1 | Heiko Helmbrecht | Landshut | ÖDP | 5.8 |
|
||||
| 2 | Dominik Krause | München | Grüne | 5.8 |
|
||||
| 3 | Nicole Kreußel | Bad Rodach | ÖDP | 5.8 |
|
||||
|
||||
## Output-Dateien
|
||||
|
||||
```
|
||||
wahlpruefsteine/output/
|
||||
├── partei-auswertung.md # Detailreport mit Zitaten + Bandbreiten
|
||||
├── parteienlandschaft.md # Komprimierte Übersicht
|
||||
├── kandidaten-ranking.md # Alle 27 Kandidat:innen im Ranking
|
||||
└── wahlempfehlung.md # Begründete Empfehlung + Substanz-Problem
|
||||
```
|
||||
|
||||
## CLI-Nutzung
|
||||
|
||||
```bash
|
||||
cd wahlpruefsteine
|
||||
source .venv/bin/activate
|
||||
|
||||
# Vollständiger Workflow
|
||||
python3 cli.py run
|
||||
|
||||
# Einzelne Schritte
|
||||
python3 cli.py scrape # Daten laden
|
||||
python3 cli.py analyze # LLM-Bewertung (Qwen)
|
||||
python3 cli.py aggregate # Reports generieren
|
||||
python3 cli.py status # Übersicht
|
||||
python3 cli.py export # JSON-Export
|
||||
```
|
||||
|
||||
## Architektur
|
||||
|
||||
```
|
||||
wahlpruefsteine/
|
||||
├── cli.py # Haupteinstiegspunkt
|
||||
├── scraper.py # HTML → SQLite
|
||||
├── analyzer.py # LLM-Bewertung (Qwen via DashScope)
|
||||
├── aggregator.py # Partei-Aggregation + Reports
|
||||
├── schema.sql # Datenbankschema
|
||||
├── wahlpruefsteine.db # SQLite (162 Bewertungen)
|
||||
└── .venv/ # Python Virtual Environment
|
||||
```
|
||||
|
||||
## Bewertungskriterien
|
||||
|
||||
### Substanz-Score (0-3)
|
||||
- 0: Keine Antwort / nur Ja-Nein
|
||||
- 1: Ausweichend, Floskeln
|
||||
- 2: Substanziell, erkennbare Haltung
|
||||
- 3: Umfassend mit konkreten Maßnahmen
|
||||
|
||||
### GWÖ-Score (0-10)
|
||||
Nach Matrix 2.0 für Gemeinden:
|
||||
- 7-10: Vorbildlich
|
||||
- 4-6: Erfahren
|
||||
- 2-3: Fortgeschritten
|
||||
- 0-1: Basislinie
|
||||
|
||||
## Technologie
|
||||
|
||||
- **Python 3.11+** mit venv
|
||||
- **SQLite** (lokale DB)
|
||||
- **Qwen Plus** via DashScope API
|
||||
- **BeautifulSoup** (HTML-Parsing)
|
||||
- **Kosten:** ~$0.10 für 162 Bewertungen
|
||||
|
||||
## Weiterführende Dokumentation
|
||||
|
||||
- `DOKUMENTATION.md` — Technische Details
|
||||
- `ANLEITUNG-NACHBAU.md` — Schritt-für-Schritt für eigene Projekte
|
||||
|
||||
---
|
||||
|
||||
*Erstellt: 29.03.2026*
|
||||
596
chats/chat-history.md
Normal file
596
chats/chat-history.md
Normal file
@ -0,0 +1,596 @@
|
||||
|
||||
### ASSISTANT (2026-03-29T09:59:11)
|
||||
|
||||
Thread + Projektordner angelegt! 📁
|
||||
|
||||
Worum geht's — Kommunal, Landtag oder Bund? Und seid ihr die Fragenden oder die Befragten?
|
||||
|
||||
---
|
||||
|
||||
### USER (2026-03-29T09:59:35)
|
||||
|
||||
Es geht um diese Umfrage
|
||||
|
||||
https://germany.econgood.org/wahlpruefsteine-zu-den-bayerischen-kommunalwahlen-2026
|
||||
|
||||
[Bootstrap truncation warning]
|
||||
Some workspace bootstrap files were truncated before injection.
|
||||
Treat Project Context as partial and read the relevant files directly if details seem missing.
|
||||
- TOOLS.md: 23567 raw -> 18108 injected (~23% removed; max/file).
|
||||
- If unintentional, raise agents.defaults.bootstrapMaxChars and/or agents.defaults.bootstrapTotalMaxChars.
|
||||
|
||||
---
|
||||
|
||||
### ASSISTANT (2026-03-29T09:59:38)
|
||||
|
||||
Ah, sehr gut! Das ist die **GWÖ-Wahlprüfsteine-Umfrage** zu den bayerischen Kommunalwahlen 2026 — ein ECOnGOOD-Projekt.
|
||||
|
||||
**Zusammenfassung:**
|
||||
- **Zielgruppe:** Bürgermeister-Kandidierende in Bayern
|
||||
- **Zeitraum:** Nov 2025 – Feb 2026
|
||||
- **Rücklauf:** 34 Teilnehmende, davon 27 mit Veröffentlichungs-Einwilligung
|
||||
- **6 Fragen** zu GWÖ-Themen (Leitlinien, Anreize, Vergabe, Info, Beteiligung, Bekanntheit)
|
||||
|
||||
**Was willst du damit?**
|
||||
1. Als **Vorlage** für eigene Wahlprüfsteine (z.B. NRW, Hagen)?
|
||||
2. **Analyse** der Antworten?
|
||||
3. **Format** für andere Ebenen adaptieren (Landtag, Bund)?
|
||||
4. Etwas anderes?
|
||||
|
||||
<ctx>42k/200k · 5h🟢8%(4h24m) · wk🟢29%(4d15h) · fc🟡14% @opus-4-5
|
||||
|
||||
---
|
||||
|
||||
### USER (2026-03-29T10:06:43)
|
||||
|
||||
Ich würde das gerne aggregiert auswerten. Einzelklassifikationen in "Auf die Frage tatsächlich geantwortet, nicht ausweichend", Umfang der Antwort, Bewertung der Antwort nach GWÖ-Kriterien (schau hierzu nach den Bewertungsmaßstäben vom GWÖ-Antragsprüfer). Die Einzelklassifikationen möchte ich dann nach Parteien/Wählergemeinschaften (Unschärfe-Match!) zusammenstellen und eine belegte (Zitate!) Auswertung nach Parteien/Wählergemeinschaften zusammenstellen. Als letztes möchte ich eine sehr komprimierte Auswertung der Parteienlandschaft aus Sicht der GWÖ haben. Separat davon eine Begründete Wahlempfehlung.
|
||||
|
||||
Technik: Datenbank für alle auswertungsmetriken und Texte. model: qwen-plus oder -max.
|
||||
|
||||
Jetzt:Softwareentwicklung, damit die Auswertung an sich reproduzierbar ohne beteiligung von dotty läuft.
|
||||
|
||||
Fragen?
|
||||
|
||||
---
|
||||
|
||||
### ASSISTANT (2026-03-29T10:06:43)
|
||||
|
||||
Verstanden. Ein paar Klärungsfragen:
|
||||
|
||||
**1. Datenerfassung:**
|
||||
- Die Seite hat strukturierte Tabellen — soll ich die 27 Antworten direkt scrapen, oder gibt's die Einzel-PDFs als bessere Quelle?
|
||||
|
||||
**2. Partei-Matching:**
|
||||
- "Unschärfe-Match" = z.B. "Bündnis 90/Die Grünen OV Altdorf" → "Grüne", "FREIE WÄHLER Eschau" → "Freie Wähler"?
|
||||
- Wie granular? Nur Hauptparteien (CDU/CSU, SPD, Grüne, FDP, Linke, ÖDP, FW, AfD) + "Sonstige Wählergemeinschaften"?
|
||||
|
||||
**3. GWÖ-Bewertungsmaßstäbe:**
|
||||
- Vom GWÖ-Antragsprüfer (gwoe.toppyr.de) die Matrix-Kriterien nehmen? Oder das Scoring-System?
|
||||
|
||||
**4. Output-Format:**
|
||||
- Markdown-Report? PDF? Beides?
|
||||
- Wo soll die Datenbank hin — VServer (wie Antragstracker) oder lokal?
|
||||
|
||||
**5. Deployment:**
|
||||
- Eigenständiger Service auf toppyr.de (z.B. wahlpruefsteine.toppyr.de)?
|
||||
- Oder CLI-Tool das on-demand läuft?
|
||||
|
||||
<ctx>48k/200k · 5h🟢8%(4h22m) · wk🟢29%(4d15h) · fc🟡14% @opus-4-5
|
||||
|
||||
---
|
||||
|
||||
### USER (2026-03-29T10:11:42)
|
||||
|
||||
1. Kannst du alle 27 der antworten ohne ajax-scraping sehen? Dann html-extract.
|
||||
2. Hauptparteien. Wählergemeinschaften separat.
|
||||
3. sowhl als auch als grundlage.
|
||||
4. Datenbank darf erst mal lokal bleiben. Ausgabe- und Auswertungsschema vom Antragsprüfer übernehmen.
|
||||
5. reicht erst mal als cli tool denke ich.
|
||||
6.
|
||||
|
||||
---
|
||||
|
||||
### ASSISTANT (2026-03-29T10:11:42)
|
||||
|
||||
Gut, ich prüfe erstmal die Datenlage und lese dann die GWÖ-Prüfer-Logik.
|
||||
|
||||
---
|
||||
|
||||
### ASSISTANT (2026-03-29T10:12:00)
|
||||
|
||||
Sehr gut, ich habe jetzt alles was ich brauche. Ich sehe, dass ich alle 27 Antworten direkt aus dem HTML extrahieren kann (die Tabelle enthält alle Daten inline).
|
||||
|
||||
Ich baue jetzt das Tool:
|
||||
|
||||
---
|
||||
|
||||
### ASSISTANT (2026-03-29T10:15:37)
|
||||
|
||||
Jetzt teste ich den Scraper:
|
||||
|
||||
---
|
||||
|
||||
### ASSISTANT (2026-03-29T10:15:51)
|
||||
|
||||
Scraper funktioniert. Jetzt prüfe ich die DB:
|
||||
|
||||
---
|
||||
|
||||
### ASSISTANT (2026-03-29T10:15:54)
|
||||
|
||||
27 Kandidat:innen, 162 Antworten (27 × 6). Jetzt teste ich den Analyzer mit ein paar Antworten:
|
||||
|
||||
---
|
||||
|
||||
### ASSISTANT (2026-03-29T10:16:32)
|
||||
|
||||
Funktioniert. Jetzt alle 162 Antworten bewerten:
|
||||
|
||||
---
|
||||
|
||||
### ASSISTANT (2026-03-29T10:26:39)
|
||||
|
||||
92 bewertet, 70 offen. Ich führe den Rest durch:
|
||||
|
||||
---
|
||||
|
||||
### ASSISTANT (2026-03-29T10:34:16)
|
||||
|
||||
Alle 162 Antworten bewertet. Jetzt die Reports generieren:
|
||||
|
||||
---
|
||||
|
||||
### ASSISTANT (2026-03-29T10:34:31)
|
||||
|
||||
Fertig! Das Tool läuft reproduzierbar. Ein paar Beobachtungen:
|
||||
|
||||
**Ergebnisse:**
|
||||
- **Linke + ÖDP** führen (Ø 4.2–4.6) — beide haben GWÖ explizit im Programm
|
||||
- **Grüne** überraschend nur 3.0 — viele Kandidat:innen mit kurzen/oberflächlichen Antworten
|
||||
- **FDP** 0.0 — alle 6 Fragen mit "Nein" ohne Erläuterung
|
||||
- **Substanz-Problem:** Viele Ja-Antworten ohne konkrete Maßnahmen (besonders CSU, SPD, FW)
|
||||
|
||||
**Was noch fehlt:**
|
||||
1. Einzelne Spitzen-Antworten rausziehen (der Grüne Dominik Krause aus München hat 8.5!)
|
||||
2. Bewertungs-Schwellen anpassen (Score ≥5 für Empfehlung ist zu hoch)
|
||||
|
||||
Soll ich:
|
||||
1. Die Schwellen für die Wahlempfehlung anpassen?
|
||||
2. Einen Kandidat:innen-Report (nicht nur Parteien) hinzufügen?
|
||||
3. Alles so lassen und du schaust es dir erst mal an?
|
||||
|
||||
<ctx>81k/200k · 5h🟢7%(4h10m) · wk🟢29%(4d15h) · fc🟡14% @opus-4-5
|
||||
|
||||
---
|
||||
|
||||
### USER (2026-03-29T10:35:03)
|
||||
|
||||
dokumentiere!
|
||||
|
||||
---
|
||||
|
||||
### ASSISTANT (2026-03-29T10:36:06)
|
||||
|
||||
Dokumentiert! 📁
|
||||
|
||||
**Projektordner:** `~/Nextcloud/dotty/projekte/2026-03-29 Wahlprüfsteine _WIP_/`
|
||||
|
||||
**Dateien:**
|
||||
- `README.md` — Übersicht + Ergebnisse + CLI-Nutzung
|
||||
- `DOKUMENTATION.md` — Technische Details (Schema, API, Reproduzierbarkeit)
|
||||
- `wahlpruefsteine/` — Das CLI-Tool mit allen Komponenten
|
||||
- `wahlpruefsteine/output/` — Generierte Reports
|
||||
|
||||
**Nächste Schritte?**
|
||||
1. Schwellen anpassen
|
||||
2. Kandidat:innen-Report
|
||||
3. Erstmal so lassen
|
||||
|
||||
<ctx>84k/200k · 5h🟢6%(4h08m) · wk🟢29%(4d15h) · fc🟡14% @opus-4-5
|
||||
|
||||
---
|
||||
|
||||
### USER (2026-03-29T10:36:16)
|
||||
|
||||
Gib mir hinterher eine Dokumentation, mit der andere dieses Projekt nachbauen können.
|
||||
|
||||
---
|
||||
|
||||
### ASSISTANT (2026-03-29T10:36:16)
|
||||
|
||||
Ich erstelle eine ausführliche Nachbau-Anleitung:
|
||||
|
||||
---
|
||||
|
||||
### ASSISTANT (2026-03-29T10:37:50)
|
||||
|
||||
Fertig! 📄
|
||||
|
||||
**Nachbau-Anleitung:** `ANLEITUNG-NACHBAU.md`
|
||||
|
||||
Enthält:
|
||||
- Vollständige Installation (Python, venv, Deps)
|
||||
- Alle 4 Code-Dateien zum Copy-Paste
|
||||
- API-Key-Konfiguration (3 Optionen)
|
||||
- Anpassungspunkte für andere Datenquellen
|
||||
- Kosten-Übersicht nach Anbieter
|
||||
- Tipps für Erstnutzung
|
||||
|
||||
Andere können damit in ~15 Min ihr eigenes Tool aufsetzen.
|
||||
|
||||
<ctx>98k/200k · 5h🟢6%(4h05m) · wk🟢29%(4d15h) · fc🟡14% @opus-4-5
|
||||
|
||||
---
|
||||
|
||||
### USER (2026-03-29T10:40:45)
|
||||
|
||||
Stelle im jeweiligen Pool die Bandbreite dar. Formuliere auch übergreifende Beobachtungen ("Substanz-Problem...")
|
||||
|
||||
Mach den Kandidat*innen report fertig. Wahlempfehlung so lassen und in verbindung mit dem SubstanzProblem "Für Bayern können wir keine übergriefende Wahlempfehlung geben, [Verbindung Kadidatinnen report]"...
|
||||
|
||||
---
|
||||
|
||||
### ASSISTANT (2026-03-29T10:41:49)
|
||||
|
||||
Jetzt noch die Partei-Auswertung um Bandbreiten ergänzen:
|
||||
|
||||
---
|
||||
|
||||
### ASSISTANT (2026-03-29T10:42:31)
|
||||
|
||||
Alle Reports sind fertig und enthalten jetzt:
|
||||
|
||||
**Partei-Auswertung:**
|
||||
- Bandbreiten-Spalte in Übersichtstabelle
|
||||
- "Das Substanz-Problem" als übergreifende Beobachtung
|
||||
- Konkrete Beispiele für Bandbreiten (Δ-Werte)
|
||||
- Fazit: "Parteilabel sagt wenig aus"
|
||||
|
||||
**Kandidat:innen-Ranking:**
|
||||
- Gesamtranking (27 Personen)
|
||||
- Kategorisierung (Vorreiter / Solide / Schwach)
|
||||
- Bandbreite pro Partei mit Beste:r und Schwächste:r
|
||||
|
||||
**Wahlempfehlung:**
|
||||
- Substanz-Problem prominent
|
||||
- "Keine pauschale Parteiempfehlung möglich"
|
||||
- Verweis auf Kandidat:innen-Ranking
|
||||
- Nur 3 konkret empfohlene Vorreiter:innen
|
||||
|
||||
**Output-Dateien:**
|
||||
```
|
||||
output/
|
||||
├── partei-auswertung.md
|
||||
├── parteienlandschaft.md
|
||||
├── kandidaten-ranking.md
|
||||
└── wahlempfehlung.md
|
||||
```
|
||||
|
||||
<ctx>107k/200k · 5h🟢5%(4h01m) · wk🟢29%(4d15h) · fc🟡14% @opus-4-5
|
||||
|
||||
---
|
||||
|
||||
### USER (2026-03-29T10:43:29)
|
||||
|
||||
doku anpassen
|
||||
|
||||
---
|
||||
|
||||
### ASSISTANT (2026-03-29T10:44:19)
|
||||
|
||||
Doku angepasst!
|
||||
|
||||
**Änderungen:**
|
||||
- README.md: Kernergebnisse mit Substanz-Problem und Bandbreiten-Tabelle
|
||||
- DOKUMENTATION.md: Kandidat:innen-Ranking als 4. Report, erweiterte Aggregations-Logik
|
||||
|
||||
<ctx>110k/200k · 5h🟢5%(3h59m) · wk🟢29%(4d15h) · fc🟡14% @opus-4-5
|
||||
|
||||
---
|
||||
|
||||
### USER (2026-03-29T10:56:04)
|
||||
|
||||
Schreibe eine Pressemitteilung der GWÖ Bayern über das Ranking.
|
||||
|
||||
---
|
||||
|
||||
### ASSISTANT (2026-03-29T10:56:32)
|
||||
|
||||
Pressemitteilung erstellt: `output/pressemitteilung.md`
|
||||
|
||||
**Struktur:**
|
||||
- Headline mit Kernaussage
|
||||
- Lead: Substanz-Problem + Zahlen
|
||||
- Parteilabel-Erkenntnis mit konkreten Bandbreiten
|
||||
- Top 3 Vorreiter:innen namentlich
|
||||
- Keine pauschale Empfehlung → Handlungsaufforderung
|
||||
- Hintergrund zu den Wahlprüfsteinen
|
||||
- Kontaktdaten (Platzhalter)
|
||||
|
||||
Soll ich noch etwas anpassen (Tonalität, Länge, Zitate)?
|
||||
|
||||
<ctx>114k/200k · 5h🟢4%(3h56m) · wk🟢29%(4d15h) · fc🟡14% @opus-4-5
|
||||
|
||||
---
|
||||
|
||||
### USER (2026-03-29T11:01:00)
|
||||
|
||||
Erzeuge auch die erwähnten angehängten Dokumente. Konsolidiere alles im Output Ordner im projektordner. bennenne die dateien mit XX_ in einer der Logik folgenden Reihenfolge. Mit einer allgemeinen Erklärung und Index startend.
|
||||
|
||||
---
|
||||
|
||||
### ASSISTANT (2026-03-29T11:02:22)
|
||||
|
||||
Fertig! Output-Ordner konsolidiert:
|
||||
|
||||
```
|
||||
output/
|
||||
├── 00_index.md # Übersicht + Kernergebnisse
|
||||
├── 01_pressemitteilung.md # Für Medien
|
||||
├── 02_wahlempfehlung.md # Begründete Empfehlung
|
||||
├── 03_kandidaten-ranking.md # Alle 27 Kandidat:innen
|
||||
├── 04_partei-auswertung.md # Parteien + Zitate
|
||||
├── 05_parteienlandschaft.md # Komprimiert
|
||||
├── 06_methodik.md # Bewertungskriterien
|
||||
└── 07_rohdaten.json # Vollständige Daten (202 KB)
|
||||
```
|
||||
|
||||
Index enthält verlinkte Dateiliste und Kernergebnisse. Pressemitteilung verweist auf Anhänge mit korrekten Dateinamen.
|
||||
|
||||
<ctx>119k/200k · 5h🟢4%(3h52m) · wk🟢29%(4d15h) · fc🟡14% @opus-4-5
|
||||
|
||||
---
|
||||
|
||||
### USER (2026-03-29T11:16:58)
|
||||
|
||||
Ich habe dir gerade eine Mail weitergeleitet. Die die Grundlage für diese Augabe und diesen Thread war.
|
||||
|
||||
Antworte auf die Mail als Dotti meiner Assistentin und erzähle in kurz, was du hier mit diesem Thread gemacht hast. Erzähle wenig technisch und verständlich für nicht technikaffine Personen. Gib auch einen Link mit auf den öffentlich geteilten Ordner Output geschützt mit Passwort. Nur lesender Zugriff
|
||||
|
||||
FRagen?
|
||||
|
||||
---
|
||||
|
||||
### ASSISTANT (2026-03-29T11:16:58)
|
||||
|
||||
Zwei kurze Fragen:
|
||||
|
||||
1. **Nextcloud-Share:** Soll ich den Output-Ordner jetzt auf cloud.toppyr.de teilen und ein Passwort setzen? (Brauche dann kurz Zugriff auf Nextcloud-CLI oder du machst es manuell)
|
||||
|
||||
2. **Passwort:** Soll ich eines generieren oder hast du ein bestimmtes im Kopf?
|
||||
|
||||
<ctx>120k/200k · 5h🟢4%(3h50m) · wk🟢29%(4d15h) · fc🟡14% @opus-4-5
|
||||
|
||||
---
|
||||
|
||||
### USER (2026-03-29T11:18:47)
|
||||
|
||||
du kannst das über den dotty account und deinen app-zuriff. Das hat zumindest schon mal geklappt. generiere ein einfach einzugebendes aber sicheres
|
||||
|
||||
---
|
||||
|
||||
### ASSISTANT (2026-03-29T11:18:59)
|
||||
|
||||
Share erstellt:
|
||||
|
||||
- **Link:** https://cloud.toppyr.de/index.php/s/M336edg4qze4Hem
|
||||
- **Passwort:** `GemeinWohl-Bayern-2026`
|
||||
- **Zugriff:** Nur lesen
|
||||
|
||||
Jetzt die Mail — wo finde ich sie? (Inbox checken?)
|
||||
|
||||
<ctx>121k/200k · 5h🟢3%(3h49m) · wk🟢29%(4d15h) · fc🟡14% @opus-4-5
|
||||
|
||||
---
|
||||
|
||||
### USER (2026-03-29T11:19:57)
|
||||
|
||||
inbox checken
|
||||
|
||||
---
|
||||
|
||||
### ASSISTANT (2026-03-29T11:20:24)
|
||||
|
||||
Mail-Entwurf erstellt: `mail-entwurf.md`
|
||||
|
||||
**Zusammenfassung:**
|
||||
- Stellt mich als deine Assistentin vor
|
||||
- Erklärt verständlich was ich gemacht habe (keine Technik)
|
||||
- Hebt die 3 Vorreiter + Piazolo hervor (bestätigt Georgs Beobachtung)
|
||||
- Erklärt das Substanz-Problem
|
||||
- Enthält Link + Passwort
|
||||
- Listet Ordnerinhalt auf
|
||||
|
||||
Soll ich die Mail so versenden, oder möchtest du noch etwas ändern?
|
||||
|
||||
<ctx>125k/200k · 5h🟢3%(3h47m) · wk🟢29%(4d15h) · fc🟡14% @opus-4-5
|
||||
|
||||
---
|
||||
|
||||
### USER (2026-03-29T11:20:35)
|
||||
|
||||
Antworte an alle aus der Mail von Georg.
|
||||
|
||||
---
|
||||
|
||||
### ASSISTANT (2026-03-29T11:20:35)
|
||||
|
||||
Ich bereite die Mail vor — aber bevor ich sende: **Darf ich?**
|
||||
|
||||
Empfänger wären:
|
||||
- **An:** Georg Hengster
|
||||
- **CC:** Toni Hassenmeier, Anne Rehme-Schlüter, Tobias Rödel (mail@tobiasroedel.de), Annabel Konermann, Christel Freyer, Carola Chowdry, Neno Rieger
|
||||
|
||||
Inhalt wie im Entwurf. Soll ich senden?
|
||||
|
||||
<ctx>126k/200k · 5h🟢3%(3h46m) · wk🟢29%(4d15h) · fc🟡14% @opus-4-5
|
||||
|
||||
---
|
||||
|
||||
### USER (2026-03-29T11:21:49)
|
||||
|
||||
schreibe hier den mailtext rein
|
||||
|
||||
---
|
||||
|
||||
### ASSISTANT (2026-03-29T11:21:49)
|
||||
|
||||
**Mail-Text:**
|
||||
|
||||
---
|
||||
|
||||
**An:** georg.hengster@econgood.org
|
||||
**CC:** toni.hassenmeier@ecogood.org, rehmeschlueter@gmx.de, mail@tobiasroedel.de, annabel.konermann@econgood.org, christel.freyer@econgood.org, carola.chowdry@econgood.org, neno.rieger@econgood.org
|
||||
**Betreff:** Re: Auswertung Wahlprüfsteine Kommunalwahl Bayern
|
||||
|
||||
---
|
||||
|
||||
Hallo Herr Hengster, hallo zusammen,
|
||||
|
||||
mein Name ist Dotty, ich bin die digitale Assistentin von Tobias Rödel. Er hat mir Ihre Mail weitergeleitet und mich gebeten, eine systematische Auswertung der Wahlprüfstein-Antworten zu erstellen.
|
||||
|
||||
**Das habe ich gemacht:**
|
||||
|
||||
Ich habe alle 27 veröffentlichten Antworten ausgewertet und jede einzelne Antwort nach zwei Kriterien bewertet:
|
||||
|
||||
1. **Substanz:** Hat die Person konkrete Maßnahmen genannt — oder nur „Ja" gesagt?
|
||||
2. **GWÖ-Übereinstimmung:** Wie gut passen die Antworten zur GWÖ-Matrix für Gemeinden?
|
||||
|
||||
**Die wichtigsten Ergebnisse:**
|
||||
|
||||
Nur **3 von 27** Kandidat:innen erreichen einen guten GWÖ-Wert (über 5 von 10 Punkten):
|
||||
|
||||
- **Heiko Helmbrecht** (ÖDP, Landshut)
|
||||
- **Dominik Krause** (Grüne, München)
|
||||
- **Nicole Kreußel** (ÖDP, Bad Rodach)
|
||||
|
||||
Michael Piazolo (Freie Wähler, München) liegt knapp dahinter mit 4,9 Punkten — Ihre Einschätzung zu Krause und Piazolo kann ich also bestätigen!
|
||||
|
||||
**Ein überraschendes Muster:**
|
||||
|
||||
Die Unterschiede *innerhalb* der Parteien sind oft größer als *zwischen* den Parteien. Bei den Grünen zum Beispiel reicht die Spanne von 0,3 bis 5,8 Punkte — je nach Kandidat:in. Das Parteilabel allein sagt also wenig aus.
|
||||
|
||||
**Viele sagen „Ja", aber ohne konkrete Ideen:**
|
||||
|
||||
Die meisten Kandidat:innen haben mit „Ja" geantwortet (75–100%), aber bei den Erläuterungen fehlen oft konkrete Maßnahmen. Das führt zu niedrigen Werten, obwohl die Grundhaltung positiv scheint.
|
||||
|
||||
---
|
||||
|
||||
**Die vollständige Auswertung habe ich hier bereitgestellt:**
|
||||
|
||||
🔗 https://cloud.toppyr.de/index.php/s/M336edg4qze4Hem
|
||||
🔐 Passwort: GemeinWohl-Bayern-2026
|
||||
|
||||
Im Ordner finden Sie:
|
||||
- Eine Pressemitteilung (falls gewünscht)
|
||||
- Das komplette Kandidat:innen-Ranking
|
||||
- Eine Partei-Auswertung mit Zitaten
|
||||
- Die Methodik der Bewertung
|
||||
|
||||
---
|
||||
|
||||
Falls Sie Fragen haben oder etwas angepasst werden soll, können Sie mir gerne schreiben oder sich direkt an Tobias wenden.
|
||||
|
||||
Herzliche Grüße aus dem Mac mini,
|
||||
|
||||
Dotty
|
||||
Digitale Assistentin von Tobias Rödel
|
||||
|
||||
---
|
||||
|
||||
Freigabe zum Senden?
|
||||
|
||||
<ctx>127k/200k · 5h🟢2%(3h44m) · wk🟢29%(4d15h) · fc🟡14% @opus-4-5
|
||||
|
||||
---
|
||||
|
||||
### USER (2026-03-29T11:23:15)
|
||||
|
||||
Wir sind per du, ihr dürft das auch sein. Nicht formelle Kommunikation.
|
||||
|
||||
---
|
||||
|
||||
### ASSISTANT (2026-03-29T11:23:15)
|
||||
|
||||
**Mail-Text (überarbeitet):**
|
||||
|
||||
---
|
||||
|
||||
**An:** georg.hengster@econgood.org
|
||||
**CC:** toni.hassenmeier@ecogood.org, rehmeschlueter@gmx.de, mail@tobiasroedel.de, annabel.konermann@econgood.org, christel.freyer@econgood.org, carola.chowdry@econgood.org, neno.rieger@econgood.org
|
||||
**Betreff:** Re: Auswertung Wahlprüfsteine Kommunalwahl Bayern
|
||||
|
||||
---
|
||||
|
||||
Hallo Georg, hallo zusammen,
|
||||
|
||||
ich bin Dotty, die digitale Assistentin von Tobias. Er hat mir eure Mail weitergeleitet und mich gebeten, eine systematische Auswertung der Wahlprüfstein-Antworten zu erstellen.
|
||||
|
||||
**Das habe ich gemacht:**
|
||||
|
||||
Ich habe alle 27 veröffentlichten Antworten ausgewertet und jede einzelne nach zwei Kriterien bewertet:
|
||||
|
||||
1. **Substanz:** Hat die Person konkrete Maßnahmen genannt — oder nur „Ja" gesagt?
|
||||
2. **GWÖ-Übereinstimmung:** Wie gut passen die Antworten zur GWÖ-Matrix für Gemeinden?
|
||||
|
||||
**Die wichtigsten Ergebnisse:**
|
||||
|
||||
Nur **3 von 27** Kandidat:innen erreichen einen guten GWÖ-Wert (über 5 von 10 Punkten):
|
||||
|
||||
- **Heiko Helmbrecht** (ÖDP, Landshut)
|
||||
- **Dominik Krause** (Grüne, München)
|
||||
- **Nicole Kreußel** (ÖDP, Bad Rodach)
|
||||
|
||||
Michael Piazolo (Freie Wähler, München) liegt knapp dahinter mit 4,9 Punkten — Georgs Einschätzung zu Krause und Piazolo kann ich also bestätigen!
|
||||
|
||||
**Ein überraschendes Muster:**
|
||||
|
||||
Die Unterschiede *innerhalb* der Parteien sind oft größer als *zwischen* den Parteien. Bei den Grünen zum Beispiel reicht die Spanne von 0,3 bis 5,8 Punkte — je nach Kandidat:in. Das Parteilabel allein sagt also wenig aus.
|
||||
|
||||
**Viele sagen „Ja", aber ohne konkrete Ideen:**
|
||||
|
||||
Die meisten Kandidat:innen haben mit „Ja" geantwortet (75–100%), aber bei den Erläuterungen fehlen oft konkrete Maßnahmen. Das führt zu niedrigen Werten, obwohl die Grundhaltung positiv scheint.
|
||||
|
||||
---
|
||||
|
||||
**Die vollständige Auswertung findet ihr hier:**
|
||||
|
||||
🔗 https://cloud.toppyr.de/index.php/s/M336edg4qze4Hem
|
||||
🔐 Passwort: GemeinWohl-Bayern-2026
|
||||
|
||||
Im Ordner gibt's:
|
||||
- Eine Pressemitteilung (falls gewünscht)
|
||||
- Das komplette Kandidat:innen-Ranking
|
||||
- Eine Partei-Auswertung mit Zitaten
|
||||
- Die Methodik der Bewertung
|
||||
|
||||
---
|
||||
|
||||
Falls ihr Fragen habt oder etwas angepasst werden soll — schreibt mir gerne oder wendet euch direkt an Tobias.
|
||||
|
||||
Viele Grüße aus dem Mac mini,
|
||||
|
||||
Dotty
|
||||
Digitale Assistentin von Tobias
|
||||
|
||||
---
|
||||
|
||||
Freigabe?
|
||||
|
||||
<ctx>128k/200k · 5h🟢2%(3h43m) · wk🟢29%(4d15h) · fc🟡14% @opus-4-5
|
||||
|
||||
---
|
||||
|
||||
### USER (2026-03-29T11:23:57)
|
||||
|
||||
senden
|
||||
|
||||
---
|
||||
|
||||
### ASSISTANT (2026-03-29T11:24:10)
|
||||
|
||||
✅ Mail gesendet!
|
||||
|
||||
<ctx>130k/200k · 5h🟢2%(3h42m) · wk🟢29%(4d15h) · fc🟡14% @opus-4-5
|
||||
|
||||
---
|
||||
59
mail-entwurf.md
Normal file
59
mail-entwurf.md
Normal file
@ -0,0 +1,59 @@
|
||||
# Mail-Entwurf
|
||||
|
||||
**An:** Georg Hengster <georg.hengster@econgood.org>
|
||||
**CC:** Toni Hassenmeier, Anne Rehme-Schlüter, Tobias Rödel, Annabel Konermann, Christel Freyer, Carola Chowdry, Neno Rieger
|
||||
**Betreff:** Re: Auswertung Wahlprüfsteine Kommunalwahl Bayern
|
||||
|
||||
---
|
||||
|
||||
Hallo Herr Hengster, hallo zusammen,
|
||||
|
||||
mein Name ist Dotty, ich bin die digitale Assistentin von Tobias Rödel. Er hat mir Ihre Mail weitergeleitet und mich gebeten, eine systematische Auswertung der Wahlprüfstein-Antworten zu erstellen.
|
||||
|
||||
**Das habe ich gemacht:**
|
||||
|
||||
Ich habe alle 27 veröffentlichten Antworten ausgewertet und jede einzelne Antwort nach zwei Kriterien bewertet:
|
||||
|
||||
1. **Substanz:** Hat die Person konkrete Maßnahmen genannt — oder nur „Ja" gesagt?
|
||||
2. **GWÖ-Übereinstimmung:** Wie gut passen die Antworten zur GWÖ-Matrix für Gemeinden?
|
||||
|
||||
**Die wichtigsten Ergebnisse:**
|
||||
|
||||
- Nur **3 von 27** Kandidat:innen erreichen einen guten GWÖ-Wert (über 5 von 10 Punkten)
|
||||
- Das sind: **Heiko Helmbrecht** (ÖDP, Landshut), **Dominik Krause** (Grüne, München) und **Nicole Kreußel** (ÖDP, Bad Rodach)
|
||||
- Michael Piazolo (Freie Wähler, München) liegt knapp dahinter mit 4,9 Punkten
|
||||
|
||||
**Ein überraschendes Muster:**
|
||||
|
||||
Die Unterschiede *innerhalb* der Parteien sind oft größer als *zwischen* den Parteien. Bei den Grünen zum Beispiel reicht die Spanne von 0,3 bis 5,8 Punkte — je nach Kandidat:in. Das Parteilabel allein sagt also wenig aus.
|
||||
|
||||
**Viele sagen „Ja", aber ohne konkrete Ideen:**
|
||||
|
||||
Die meisten Kandidat:innen haben mit „Ja" geantwortet (75–100%), aber bei den Erläuterungen fehlen oft konkrete Maßnahmen. Das führt zu niedrigen Werten, obwohl die Grundhaltung positiv scheint.
|
||||
|
||||
---
|
||||
|
||||
**Die vollständige Auswertung habe ich hier bereitgestellt:**
|
||||
|
||||
🔗 https://cloud.toppyr.de/index.php/s/M336edg4qze4Hem
|
||||
🔐 Passwort: `GemeinWohl-Bayern-2026`
|
||||
|
||||
Im Ordner finden Sie:
|
||||
- Eine **Pressemitteilung** (falls gewünscht)
|
||||
- Das komplette **Kandidat:innen-Ranking**
|
||||
- Eine **Partei-Auswertung** mit Zitaten
|
||||
- Die **Methodik** der Bewertung
|
||||
- Alle **Rohdaten** als JSON
|
||||
|
||||
---
|
||||
|
||||
Falls Sie Fragen haben oder etwas angepasst werden soll, können Sie mir gerne schreiben oder sich direkt an Tobias wenden.
|
||||
|
||||
Herzliche Grüße aus dem Mac mini,
|
||||
|
||||
Dotty
|
||||
Digitale Assistentin von Tobias Rödel
|
||||
|
||||
---
|
||||
|
||||
*Diese Mail wurde von Dotty verfasst, einer KI-Assistentin. Tobias hat die Auswertung beauftragt und den Inhalt freigegeben.*
|
||||
99
output/00_index.md
Normal file
99
output/00_index.md
Normal file
@ -0,0 +1,99 @@
|
||||
# GWÖ-Wahlprüfsteine Bayern 2026 — Dokumentation
|
||||
|
||||
**Gemeinwohl-Ökonomie Bayern e.V.**
|
||||
|
||||
*Auswertung der Wahlprüfsteine zu den Bayerischen Kommunalwahlen 2026*
|
||||
|
||||
---
|
||||
|
||||
## Übersicht
|
||||
|
||||
Diese Dokumentensammlung enthält die systematische Auswertung der GWÖ-Wahlprüfsteine, die im Vorfeld der bayerischen Kommunalwahlen am 8. März 2026 an Bürgermeister-Kandidat:innen versandt wurden.
|
||||
|
||||
**Datenbasis:**
|
||||
- 27 Kandidat:innen mit veröffentlichten Antworten
|
||||
- 6 Fragen zu GWÖ-Kernthemen
|
||||
- 162 Einzelantworten, systematisch bewertet
|
||||
|
||||
**Bewertungsmethodik:**
|
||||
- GWÖ-Matrix 2.0 für Gemeinden
|
||||
- Substanz-Score (0-3): Konkretheit der Antworten
|
||||
- GWÖ-Score (0-10): Übereinstimmung mit Gemeinwohl-Werten
|
||||
|
||||
---
|
||||
|
||||
## Dokumentenverzeichnis
|
||||
|
||||
| Nr. | Datei | Inhalt |
|
||||
|-----|-------|--------|
|
||||
| 00 | [00_index.md](00_index.md) | Übersicht und Erläuterungen (dieses Dokument) |
|
||||
| 01 | [01_pressemitteilung.md](01_pressemitteilung.md) | Zusammenfassung für Medien |
|
||||
| 02 | [02_wahlempfehlung.md](02_wahlempfehlung.md) | Begründete Empfehlung mit Kernaussagen |
|
||||
| 03 | [03_kandidaten-ranking.md](03_kandidaten-ranking.md) | Alle 27 Kandidat:innen im Einzelvergleich |
|
||||
| 04 | [04_partei-auswertung.md](04_partei-auswertung.md) | Detailanalyse nach Parteien mit Zitaten |
|
||||
| 05 | [05_parteienlandschaft.md](05_parteienlandschaft.md) | Komprimierte Übersicht |
|
||||
| 06 | [06_methodik.md](06_methodik.md) | Bewertungskriterien und Vorgehen |
|
||||
| 07 | [07_rohdaten.json](07_rohdaten.json) | Vollständige Bewertungen (JSON, 202 KB) |
|
||||
|
||||
---
|
||||
|
||||
## Kernergebnisse
|
||||
|
||||
### Das Substanz-Problem
|
||||
|
||||
Von 27 Kandidat:innen erreichen nur **3** einen GWÖ-Durchschnitt ≥ 5.0.
|
||||
|
||||
- Ø GWÖ-Score: **2.2/10**
|
||||
- Ø Substanz-Score: **0.9/3**
|
||||
- Ja-Quote: **75–100%** bei den meisten Parteien
|
||||
|
||||
**Interpretation:** Hohe grundsätzliche Zustimmung zu GWÖ-Themen, aber wenig konkrete Umsetzungsideen.
|
||||
|
||||
### Bandbreite innerhalb der Parteien
|
||||
|
||||
| Partei | Ø GWÖ | Bandbreite | Differenz |
|
||||
|--------|-------|------------|-----------|
|
||||
| ÖDP | 4.2 | 0.9–5.8 | 4.9 |
|
||||
| Grüne | 3.0 | 0.3–5.8 | 5.5 |
|
||||
| Freie Wähler | 1.5 | 0.0–4.9 | 4.9 |
|
||||
|
||||
**Interpretation:** Das Parteilabel allein sagt wenig über die GWÖ-Affinität einzelner Kandidat:innen aus.
|
||||
|
||||
### GWÖ-Vorreiter:innen
|
||||
|
||||
| Name | Kommune | Partei | Ø GWÖ |
|
||||
|------|---------|--------|-------|
|
||||
| Heiko Helmbrecht | Landshut | ÖDP | 5.8 |
|
||||
| Dominik Krause | München | Grüne | 5.8 |
|
||||
| Nicole Kreußel | Bad Rodach | ÖDP | 5.8 |
|
||||
|
||||
---
|
||||
|
||||
## Die 6 Wahlprüfstein-Fragen
|
||||
|
||||
1. **Leitlinien:** Integration von GWÖ-Werten in kommunale Strategien
|
||||
2. **Anreize:** Unterstützung für gemeinwohlorientiertes Wirtschaften
|
||||
3. **Vergabe:** Berücksichtigung der Gemeinwohl-Bilanz bei öffentlichen Aufträgen
|
||||
4. **Information:** Transparente Kommunikation über Gemeinwohl-Auswirkungen
|
||||
5. **Mitentscheidung:** Stärkere Bürgerbeteiligung
|
||||
6. **Bekanntheit:** Förderung der GWÖ auf kommunaler und höherer Ebene
|
||||
|
||||
---
|
||||
|
||||
## Hinweise zur Nutzung
|
||||
|
||||
- Die Bewertungen dienen der Orientierung und ersetzen keine umfassende politische Beurteilung
|
||||
- Alle Antworten wurden freiwillig gegeben; nicht alle Kandidat:innen haben teilgenommen
|
||||
- Die Analyse basiert auf der GWÖ-Matrix 2.0 für Gemeinden
|
||||
|
||||
---
|
||||
|
||||
## Kontakt
|
||||
|
||||
Gemeinwohl-Ökonomie Bayern e.V.
|
||||
E-Mail: bayern@econgood.org
|
||||
Web: https://germany.econgood.org
|
||||
|
||||
---
|
||||
|
||||
*Stand: 29. März 2026*
|
||||
85
output/01_pressemitteilung.md
Normal file
85
output/01_pressemitteilung.md
Normal file
@ -0,0 +1,85 @@
|
||||
# Pressemitteilung
|
||||
|
||||
**Gemeinwohl-Ökonomie Bayern e.V.**
|
||||
|
||||
München, 29. März 2026
|
||||
|
||||
---
|
||||
|
||||
## GWÖ-Wahlprüfsteine: Große Unterschiede zwischen Bürgermeister-Kandidat:innen — Parteilabel wenig aussagekräftig
|
||||
|
||||
**Auswertung der Kommunalwahl-Befragung zeigt: Nur 3 von 27 Kandidat:innen mit überzeugenden GWÖ-Konzepten — Hohe Zustimmung, aber wenig konkrete Maßnahmen**
|
||||
|
||||
---
|
||||
|
||||
Die Gemeinwohl-Ökonomie Bayern hat die Antworten von 27 Bürgermeister-Kandidat:innen auf ihre Wahlprüfsteine zur Kommunalwahl 2026 systematisch ausgewertet. Das Ergebnis ist ernüchternd — und überraschend zugleich.
|
||||
|
||||
### Viel Zustimmung, wenig Substanz
|
||||
|
||||
„Die gute Nachricht: 85 bis 100 Prozent der Kandidat:innen bekennen sich grundsätzlich zu GWÖ-Themen wie Bürgerbeteiligung, nachhaltiger Beschaffung oder Gemeinwohl-Bilanzierung", erklärt [Name], Sprecher:in der GWÖ Bayern. „Die schlechte Nachricht: Auf Nachfrage fehlen meist konkrete Umsetzungsideen."
|
||||
|
||||
Im Durchschnitt erreichten die Kandidat:innen nur 2,2 von 10 möglichen Punkten auf der GWÖ-Bewertungsskala. Der Substanz-Score — der misst, ob konkrete Maßnahmen genannt werden — lag bei nur 0,9 von 3 Punkten.
|
||||
|
||||
### Parteilabel sagt wenig aus
|
||||
|
||||
Die vielleicht wichtigste Erkenntnis: Die Unterschiede *innerhalb* der Parteien sind oft größer als *zwischen* den Parteien.
|
||||
|
||||
- Bei den **Grünen** reicht die Spanne von 0,3 bis 5,8 Punkte
|
||||
- Bei der **ÖDP** von 0,9 bis 5,8 Punkte
|
||||
- Bei den **Freien Wählern** von 0,0 bis 4,9 Punkte
|
||||
|
||||
„Wer gemeinwohlorientierte Kommunalpolitik wählen will, muss die einzelnen Kandidat:innen prüfen — das Parteilogo allein reicht nicht", so [Name].
|
||||
|
||||
### Die GWÖ-Vorreiter:innen
|
||||
|
||||
Drei Kandidat:innen stechen mit einem Durchschnitt von 5,8 Punkten heraus:
|
||||
|
||||
1. **Heiko Helmbrecht** (ÖDP, Landshut)
|
||||
2. **Dominik Krause** (Grüne, München)
|
||||
3. **Nicole Kreußel** (ÖDP, Bad Rodach)
|
||||
|
||||
Sie überzeugten mit konkreten Maßnahmen wie Pilotprojekten zur Gemeinwohl-Bilanzierung, nachhaltigen Vergaberichtlinien und verbindlichen Beteiligungsformaten.
|
||||
|
||||
### Keine pauschale Wahlempfehlung
|
||||
|
||||
„Wir können für Bayern keine übergreifende Parteiempfehlung geben", betont [Name]. „Aber wir können sagen: Fragen Sie Ihre lokalen Kandidat:innen nach konkreten Plänen. Ein ‚Ja' ohne Umsetzungsidee ist wenig wert."
|
||||
|
||||
Die vollständige Auswertung mit allen 27 Kandidat:innen im Ranking ist auf der Website der GWÖ Bayern veröffentlicht.
|
||||
|
||||
---
|
||||
|
||||
### Hintergrund: Die Wahlprüfsteine
|
||||
|
||||
Die GWÖ Bayern hat im Vorfeld der Kommunalwahlen 2026 rund 200 Bürgermeister-Kandidat:innen angeschrieben. 34 nahmen teil, 27 stimmten einer Veröffentlichung zu. Die sechs Fragen bezogen sich auf:
|
||||
|
||||
1. Integration von GWÖ-Werten in kommunale Leitlinien
|
||||
2. Anreize für gemeinwohlorientiertes Wirtschaften
|
||||
3. Berücksichtigung der Gemeinwohl-Bilanz bei öffentlichen Aufträgen
|
||||
4. Transparente Kommunikation über Gemeinwohl-Auswirkungen
|
||||
5. Stärkere Bürgerbeteiligung
|
||||
6. Bekanntmachung der GWÖ
|
||||
|
||||
Die Antworten wurden anhand der GWÖ-Matrix 2.0 für Gemeinden bewertet.
|
||||
|
||||
---
|
||||
|
||||
### Kontakt
|
||||
|
||||
Gemeinwohl-Ökonomie Bayern e.V.
|
||||
[Adresse]
|
||||
[E-Mail]
|
||||
[Telefon]
|
||||
|
||||
Pressekontakt: [Name], [E-Mail]
|
||||
|
||||
---
|
||||
|
||||
### Anhang
|
||||
|
||||
- 03_kandidaten-ranking.md — Vollständiges Kandidat:innen-Ranking
|
||||
- 04_partei-auswertung.md — Partei-Auswertung mit Zitaten
|
||||
- 06_methodik.md — Bewertungskriterien und Vorgehen
|
||||
|
||||
---
|
||||
|
||||
*Die Gemeinwohl-Ökonomie ist ein Wirtschaftsmodell, das auf gemeinwohlfördernden Werten aufbaut. Über 1.000 Unternehmen, Gemeinden und Organisationen weltweit erstellen bereits Gemeinwohl-Bilanzen. Mehr Informationen: germany.econgood.org*
|
||||
101
output/02_wahlempfehlung.md
Normal file
101
output/02_wahlempfehlung.md
Normal file
@ -0,0 +1,101 @@
|
||||
# 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
|
||||
|
||||
Von 27 Kandidat:innen erreichen nur **3** einen GWÖ-Durchschnitt ≥ 5.0.
|
||||
|
||||
Die Analyse zeigt ein strukturelles Problem: **Viele Ja-Antworten ohne konkrete Maßnahmen.**
|
||||
|
||||
- **Ø GWÖ-Score aller Kandidat:innen:** 2.2/10
|
||||
- **Ø Substanz-Score:** 0.9/3
|
||||
- **Ja-Quote:** hoch (85-100% bei den meisten Parteien)
|
||||
- **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:
|
||||
|
||||
- **ÖDP:** Erich Wohnig (0.9) bis Heiko Helmbrecht (5.8) — Δ 4.9 Punkte!
|
||||
- **Grüne:** Christian Heilmann-Tröster (0.3) bis Dominik Krause (5.8) — Δ 5.5 Punkte!
|
||||
- **Freie Wähler:** Bastian Wittmann (0.0) bis Michael Piazolo (4.9) — Δ 4.9 Punkte!
|
||||
|
||||
---
|
||||
|
||||
## 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)
|
||||
|
||||
### 🟢 GWÖ-Vorreiter:innen (individuell empfehlenswert)
|
||||
|
||||
Diese Kandidat:innen zeigen überdurchschnittliches GWÖ-Engagement:
|
||||
|
||||
- **Heiko Helmbrecht** (Landshut, ÖDP): Ø 5.8
|
||||
- **Dominik Krause** (München, Grüne): Ø 5.8
|
||||
- **Nicole Kreußel** (Bad Rodach, ÖDP): Ø 5.8
|
||||
|
||||
### ⚠️ Parteien mit partieller Übereinstimmung
|
||||
|
||||
Partei-Durchschnitt im Mittelfeld — individuelle Prüfung empfohlen:
|
||||
|
||||
- **Linke** (Ø 4.6)
|
||||
- **ÖDP** (Ø 4.2)
|
||||
|
||||
### ❌ Geringe GWÖ-Resonanz
|
||||
|
||||
Diese Parteien zeigen im Durchschnitt wenig GWÖ-Affinität:
|
||||
|
||||
- **Grüne** (Ø 3.0)
|
||||
- **Bayernpartei** (Ø 1.9)
|
||||
- **Freie Wähler** (Ø 1.5)
|
||||
- **Wählergemeinschaft** (Ø 1.2)
|
||||
- **SPD** (Ø 0.8)
|
||||
- **CSU** (Ø 0.7)
|
||||
- **FDP** (Ø 0.0)
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
*Erstellt: 29.03.2026*
|
||||
125
output/03_kandidaten-ranking.md
Normal file
125
output/03_kandidaten-ranking.md
Normal file
@ -0,0 +1,125 @@
|
||||
# GWÖ-Wahlprüfsteine Bayern 2026 — Kandidat:innen-Ranking
|
||||
|
||||
*Stand: 29.03.2026 12:42*
|
||||
|
||||
---
|
||||
|
||||
## Gesamtranking
|
||||
|
||||
| Rang | Name | Kommune | Partei | Ø GWÖ | Ø Substanz | Spanne |
|
||||
|------|------|---------|--------|-------|------------|--------|
|
||||
| 1 | **Heiko Helmbrecht** | Landshut | ÖDP | 5.8 | 2.2/3 | 5.0–7.5 |
|
||||
| 2 | **Dominik Krause** | München | Grüne | 5.8 | 2.0/3 | 3.5–8.5 |
|
||||
| 3 | **Nicole Kreußel** | Bad Rodach | ÖDP | 5.8 | 2.0/3 | 5.0–6.5 |
|
||||
| 4 | **Michael Piazolo** | München | Freie Wähler | 4.9 | 1.7/3 | 2.5–6.5 |
|
||||
| 5 | **Stefan Jagel** | München | Linke | 4.6 | 1.8/3 | 2.5–5.5 |
|
||||
| 6 | **Katrin Dengler** | Straubing | ÖDP | 4.5 | 1.8/3 | 2.5–5.5 |
|
||||
| 7 | **Nico Heitz** | Freising | Grüne | 4.0 | 1.7/3 | 2.0–5.5 |
|
||||
| 8 | **Frieder Dr. Kleefeld** | Wilhermsdorf | Grüne | 3.9 | 1.7/3 | 2.0–5.5 |
|
||||
| 9 | **Hans-Dieter Pletz** | Altdorf b. Nürnberg | Grüne | 3.0 | 1.3/3 | 2.0–4.5 |
|
||||
| 10 | **Herbert Gutmann** | Gunzenhausen | Grüne | 2.6 | 1.2/3 | 2.0–5.0 |
|
||||
| 11 | **Stefan Müller-Kroehling, Dr.** | Landshut | Wählergemeinschaft | 2.2 | 1.0/3 | 2.0–2.5 |
|
||||
| 12 | **Heidi Lau** | Fürth | Freie Wähler | 2.0 | 1.0/3 | 2.0–2.0 |
|
||||
| 13 | **Walter Schmidtke** | Grafing | Bayernpartei | 1.9 | 0.8/3 | 0.0–2.5 |
|
||||
| 14 | **Josef Baumann** | Großkarolinenfeld | Freie Wähler | 1.8 | 0.8/3 | 0.0–2.5 |
|
||||
| 15 | **Jochen Weikert** | Markt Eschau | Freie Wähler | 1.8 | 0.8/3 | 0.0–2.5 |
|
||||
| 16 | **Sebastian Stanglmaier** | Markt Altdorf | Wählergemeinschaft | 1.4 | 0.7/3 | 0.0–2.5 |
|
||||
| 17 | **Josef Peis** | Ebersberg | CSU | 1.3 | 0.7/3 | 0.0–2.0 |
|
||||
| 18 | **Erika Vogel** | Lauf a. d. Peg. | Grüne | 1.2 | 0.5/3 | 0.0–5.0 |
|
||||
| 19 | **Erich Wohnig** | Coburg | ÖDP | 0.9 | 0.3/3 | 0.0–5.5 |
|
||||
| 20 | **Julia Worch** | Olching | SPD | 0.8 | 0.3/3 | 0.0–2.5 |
|
||||
| 21 | **Christian Heilmann-Tröster** | Deggendorf | Grüne | 0.3 | 0.2/3 | 0.0–2.0 |
|
||||
| 22 | **Morten Faust** | Weilheim | FDP | 0.0 | 0.0/3 | 0.0–0.0 |
|
||||
| 23 | **Roland Biswurm** | Landshut | Wählergemeinschaft | 0.0 | 0.0/3 | 0.0–0.0 |
|
||||
| 24 | **Jürgen Raab** | Münster | CSU | 0.0 | 0.0/3 | 0.0–0.0 |
|
||||
| 25 | **Bastian Wittmann** | Sengenthal | Freie Wähler | 0.0 | 0.0/3 | 0.0–0.0 |
|
||||
| 26 | **Patrick Popelyszyn** | Markt Schöllnach | Freie Wähler | 0.0 | 0.0/3 | 0.0–0.0 |
|
||||
| 27 | **Maria Somann** | Aussernzell | Freie Wähler | 0.0 | 0.0/3 | 0.0–0.0 |
|
||||
|
||||
---
|
||||
|
||||
## Kategorisierung
|
||||
|
||||
### 🟢 GWÖ-Vorreiter:innen (Ø ≥ 5.0)
|
||||
|
||||
- **Heiko Helmbrecht** (Landshut, ÖDP): Ø 5.8, Spanne 5.0–7.5
|
||||
- **Dominik Krause** (München, Grüne): Ø 5.8, Spanne 3.5–8.5
|
||||
- **Nicole Kreußel** (Bad Rodach, ÖDP): Ø 5.8, Spanne 5.0–6.5
|
||||
|
||||
### 🟡 Solide Basis (Ø 3.0–5.0)
|
||||
|
||||
- **Michael Piazolo** (München, Freie Wähler): Ø 4.9
|
||||
- **Stefan Jagel** (München, Linke): Ø 4.6
|
||||
- **Katrin Dengler** (Straubing, ÖDP): Ø 4.5
|
||||
- **Nico Heitz** (Freising, Grüne): Ø 4.0
|
||||
- **Frieder Dr. Kleefeld** (Wilhermsdorf, Grüne): Ø 3.9
|
||||
- **Hans-Dieter Pletz** (Altdorf b. Nürnberg, Grüne): Ø 3.0
|
||||
|
||||
### 🔴 Wenig GWÖ-Substanz (Ø < 3.0)
|
||||
|
||||
- Herbert Gutmann (Gunzenhausen, Grüne): Ø 2.6
|
||||
- Stefan Müller-Kroehling, Dr. (Landshut, Wählergemeinschaft): Ø 2.2
|
||||
- Heidi Lau (Fürth, Freie Wähler): Ø 2.0
|
||||
- Walter Schmidtke (Grafing, Bayernpartei): Ø 1.9
|
||||
- Josef Baumann (Großkarolinenfeld, Freie Wähler): Ø 1.8
|
||||
- Jochen Weikert (Markt Eschau, Freie Wähler): Ø 1.8
|
||||
- Sebastian Stanglmaier (Markt Altdorf, Wählergemeinschaft): Ø 1.4
|
||||
- Josef Peis (Ebersberg, CSU): Ø 1.3
|
||||
- Erika Vogel (Lauf a. d. Peg., Grüne): Ø 1.2
|
||||
- Erich Wohnig (Coburg, ÖDP): Ø 0.9
|
||||
- Julia Worch (Olching, SPD): Ø 0.8
|
||||
- Christian Heilmann-Tröster (Deggendorf, Grüne): Ø 0.3
|
||||
- Morten Faust (Weilheim, FDP): Ø 0.0
|
||||
- Roland Biswurm (Landshut, Wählergemeinschaft): Ø 0.0
|
||||
- Jürgen Raab (Münster, CSU): Ø 0.0
|
||||
- Bastian Wittmann (Sengenthal, Freie Wähler): Ø 0.0
|
||||
- Patrick Popelyszyn (Markt Schöllnach, Freie Wähler): Ø 0.0
|
||||
- Maria Somann (Aussernzell, Freie Wähler): Ø 0.0
|
||||
|
||||
---
|
||||
|
||||
## Bandbreite innerhalb der Parteien
|
||||
|
||||
Die Durchschnittswerte pro Partei verdecken teils erhebliche Unterschiede zwischen einzelnen Kandidat:innen:
|
||||
|
||||
### Linke
|
||||
- Nur 1 Kandidat:in: Stefan Jagel (München) mit Ø 4.6
|
||||
|
||||
### ÖDP
|
||||
- **Partei-Durchschnitt:** 4.2
|
||||
- **Bandbreite:** 0.9 – 5.8 (Δ 4.9)
|
||||
- **Beste:r:** Heiko Helmbrecht (Landshut) mit Ø 5.8
|
||||
- **Schwächste:r:** Erich Wohnig (Coburg) mit Ø 0.9
|
||||
|
||||
### Grüne
|
||||
- **Partei-Durchschnitt:** 3.0
|
||||
- **Bandbreite:** 0.3 – 5.8 (Δ 5.5)
|
||||
- **Beste:r:** Dominik Krause (München) mit Ø 5.8
|
||||
- **Schwächste:r:** Christian Heilmann-Tröster (Deggendorf) mit Ø 0.3
|
||||
|
||||
### Bayernpartei
|
||||
- Nur 1 Kandidat:in: Walter Schmidtke (Grafing) mit Ø 1.9
|
||||
|
||||
### Freie Wähler
|
||||
- **Partei-Durchschnitt:** 1.5
|
||||
- **Bandbreite:** 0.0 – 4.9 (Δ 4.9)
|
||||
- **Beste:r:** Michael Piazolo (München) mit Ø 4.9
|
||||
- **Schwächste:r:** Bastian Wittmann (Sengenthal) mit Ø 0.0
|
||||
|
||||
### Wählergemeinschaft
|
||||
- **Partei-Durchschnitt:** 1.2
|
||||
- **Bandbreite:** 0.0 – 2.2 (Δ 2.2)
|
||||
- **Beste:r:** Stefan Müller-Kroehling, Dr. (Landshut) mit Ø 2.2
|
||||
- **Schwächste:r:** Roland Biswurm (Landshut) mit Ø 0.0
|
||||
|
||||
### SPD
|
||||
- Nur 1 Kandidat:in: Julia Worch (Olching) mit Ø 0.8
|
||||
|
||||
### CSU
|
||||
- **Partei-Durchschnitt:** 0.7
|
||||
- **Bandbreite:** 0.0 – 1.3 (Δ 1.3)
|
||||
- **Beste:r:** Josef Peis (Ebersberg) mit Ø 1.3
|
||||
- **Schwächste:r:** Jürgen Raab (Münster) mit Ø 0.0
|
||||
|
||||
### FDP
|
||||
- Nur 1 Kandidat:in: Morten Faust (Weilheim) mit Ø 0.0
|
||||
227
output/04_partei-auswertung.md
Normal file
227
output/04_partei-auswertung.md
Normal file
@ -0,0 +1,227 @@
|
||||
# GWÖ-Wahlprüfsteine Bayern 2026 — Partei-Auswertung
|
||||
|
||||
*Stand: 29.03.2026 12:42*
|
||||
|
||||
---
|
||||
|
||||
## Zusammenfassung
|
||||
|
||||
| Partei | Kandidat:innen | Ø GWÖ | Bandbreite | Ø Substanz | Ja-Quote |
|
||||
|--------|---------------|-------|------------|------------|----------|
|
||||
| **Linke** | 1 | 4.6 | 4.6 | 1.8/3 | 100% |
|
||||
| **ÖDP** | 4 | 4.2 | 0.9–5.8 | 1.6/3 | 100% |
|
||||
| **Grüne** | 7 | 3.0 | 0.3–5.8 | 1.2/3 | 98% |
|
||||
| **Bayernpartei** | 1 | 1.9 | 1.9 | 0.8/3 | 67% |
|
||||
| **Freie Wähler** | 7 | 1.5 | 0.0–4.9 | 0.6/3 | 90% |
|
||||
| **Wählergemeinschaft** | 3 | 1.2 | 0.0–2.2 | 0.6/3 | 94% |
|
||||
| **SPD** | 1 | 0.8 | 0.8 | 0.3/3 | 83% |
|
||||
| **CSU** | 2 | 0.7 | 0.0–1.3 | 0.3/3 | 75% |
|
||||
| **FDP** | 1 | 0.0 | 0.0 | 0.0/3 | 17% |
|
||||
|
||||
---
|
||||
|
||||
## Ü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:
|
||||
|
||||
- **ÖDP:** 0.9 (Erich Wohnig) bis 5.8 (Heiko Helmbrecht) — Δ 4.9
|
||||
- **Grüne:** 0.3 (Christian Heilmann-Tröster) bis 5.8 (Dominik Krause) — Δ 5.5
|
||||
- **Freie Wähler:** 0.0 (Bastian Wittmann) bis 4.9 (Michael Piazolo) — Δ 4.9
|
||||
- **Wählergemeinschaft:** 0.0 (Roland Biswurm) bis 2.2 (Stefan Müller-Kroehling, Dr.) — Δ 2.2
|
||||
|
||||
**→ Fazit:** Das Parteilabel allein sagt wenig über die GWÖ-Affinität der einzelnen Kandidat:innen aus.
|
||||
|
||||
---
|
||||
|
||||
## Detailauswertung nach Parteien
|
||||
|
||||
### Linke
|
||||
|
||||
**Kandidat:innen:** 1 | **Antworten:** 6 | **Ø Wortanzahl:** 46
|
||||
|
||||
**GWÖ-Score:** 4.6/10 | **Substanz:** 1.8/3 | **Ja-Quote:** 100%
|
||||
|
||||
**Häufigste Matrix-Felder:** D5, C2, A3, A4, D2
|
||||
|
||||
**Beste Antworten:**
|
||||
|
||||
> *"Die Gemeinwohl-Ökonomie ist für uns ein erster und wichtiger Schritt die kapitalistische Wirtschaftslogik zu durchbrechen. Besonders Eigenbetriebe können sich eignen, die ersten Maßnahmen umzusetzen – wie stärkere Mitbestimmung der Belegschaft (Stichwort niedrigstes Gehalt im Vergleich zum höchsten)..."*
|
||||
> — Stefan Jagel (München), zu Frage "Leitlinien" (GWÖ: 5.5)
|
||||
|
||||
> *"Ja, wir sehen Bürger*innebeteiligung als ein wichtiges Instrument direkter Demokratie – hier ist das Stichwort echte Mitbestimmung. Diese Prozesse benötigen viel Zeit und erfahrene Moderation in mehrstufigen Verfahren. Wichtig ist hierbei, dass die Entscheidungen dann auch wirklich umgesetzt werden...."*
|
||||
> — Stefan Jagel (München), zu Frage "Mitentscheidung" (GWÖ: 5.5)
|
||||
|
||||
> *"Die Vergabekriterien sollten aus unserer Sicht daraufhin verändert werden. Der rechtliche Rahmen erlaubt das Hinzufügen von ökologischen und sozialen Kriterien."*
|
||||
> — Stefan Jagel (München), zu Frage "Anreize" (GWÖ: 5.0)
|
||||
|
||||
---
|
||||
|
||||
### ÖDP
|
||||
|
||||
**Kandidat:innen:** 4 | **Antworten:** 24 | **Ø Wortanzahl:** 37
|
||||
|
||||
**GWÖ-Score:** 4.2/10 | **Substanz:** 1.6/3 | **Ja-Quote:** 100%
|
||||
|
||||
**Häufigste Matrix-Felder:** C2, D5, A3, A4, D2
|
||||
|
||||
**Beste Antworten:**
|
||||
|
||||
> *"Dabei denke ich unter anderem daran, zentrale kommunale Strategien an Gemeinwohl-, Nachhaltigkeits- und Beteiligungskriterien auszurichten. In Vergabe, Beschaffung und Haushaltsentscheidungen sollen neben Kosten auch soziale, ökologische und regionale Wirkungen stärker berücksichtigt werden. Knappe ..."*
|
||||
> — Heiko Helmbrecht (Landshut), zu Frage "Leitlinien" (GWÖ: 7.5)
|
||||
|
||||
> *"Perspektivisch kann eine Gemeinwohl-Bilanz bei öffentlichen Aufträgen als positives Zusatzkriterium berücksichtigt werden. Voraussetzung ist, dass dies rechtssicher, freiwillig und ohne Benachteiligung kleiner und mittlerer Unternehmen möglich ist. Auf alle Fälle setze ich mich dafür ein, dass gemei..."*
|
||||
> — Heiko Helmbrecht (Landshut), zu Frage "Vergabe" (GWÖ: 6.5)
|
||||
|
||||
> *"Ja. Mögliche Anreize sind Beratungsangebote, Vernetzung, bevorzugte Berücksichtigung bei Förderprogrammen, Unterstützung bei der Erstellung einer Gemeinwohl-Bilanz sowie die Prüfung finanzieller oder organisatorischer Erleichterungen für Unternehmen, die nachweislich gemeinwohl-orientiert wirtschaft..."*
|
||||
> — Nicole Kreußel (Bad Rodach), zu Frage "Anreize" (GWÖ: 6.5)
|
||||
|
||||
---
|
||||
|
||||
### Grüne
|
||||
|
||||
**Kandidat:innen:** 7 | **Antworten:** 42 | **Ø Wortanzahl:** 23
|
||||
|
||||
**GWÖ-Score:** 3.0/10 | **Substanz:** 1.2/3 | **Ja-Quote:** 98%
|
||||
|
||||
**Häufigste Matrix-Felder:** D5, A3, A4, C2, D2
|
||||
|
||||
**Beste Antworten:**
|
||||
|
||||
> *"Im Münchner Stadtrat haben wir beantragt, dass Teile der Stadtverwaltung und städtischer Gesellschaften in einem Pilotprojekt Gemeinwohlbilanzen erstellen, um hieraus Erfahrungen für die gesamte Landeshauptstadt München zu ziehen und den Weg zu ebnen für anstehende Vorgaben auf Bundes- und EU-Ebene ..."*
|
||||
> — Dominik Krause (München), zu Frage "Leitlinien" (GWÖ: 8.5)
|
||||
|
||||
> *"Mit dem Ortswissen von Anwohnenden, den vielfältigen Erfahrungen und Perspektiven engagierter Münchner*innen werden kommunale Projekte erfolgreicher, zielgruppengenauer und besser angenommen. Niedrigschwellige Beteiligungsformen auch mit Blick auf besondere Gruppen wie beispielsweise Kinder und Juge..."*
|
||||
> — Dominik Krause (München), zu Frage "Mitentscheidung" (GWÖ: 6.5)
|
||||
|
||||
> *"Wir haben in München mit einer breiten Förderkulisse für Klima- und Umweltschutzprojekte und der Nachhaltigen Finanzstrategie der Stadt bereits viele private sowie öffentliche Investitionen in gemeinwohlorientierte Wertschöpfung gelenkt und werden das auch zukünftig ausbauen. Gleichzeitig fördern wi..."*
|
||||
> — Dominik Krause (München), zu Frage "Anreize" (GWÖ: 5.5)
|
||||
|
||||
---
|
||||
|
||||
### Bayernpartei
|
||||
|
||||
**Kandidat:innen:** 1 | **Antworten:** 6 | **Ø Wortanzahl:** 14
|
||||
|
||||
**GWÖ-Score:** 1.9/10 | **Substanz:** 0.8/3 | **Ja-Quote:** 67%
|
||||
|
||||
**Häufigste Matrix-Felder:** D2, A3, D5, C2, A4
|
||||
|
||||
**Beste Antworten:**
|
||||
|
||||
> *"Bei der Wirtschaftsstruktur einer Kleinstadt wie Grafing, sind die Möglichkeiten eher beschränkt. Aber in Bereichen wie des Wasserschutzes oder der regionalen Vermarktung regionaler Produkte gibt es Möglichkeiten."*
|
||||
> — Walter Schmidtke (Grafing), zu Frage "Anreize" (GWÖ: 2.5)
|
||||
|
||||
> *"Internetauftritt, soziale Medien und vor allem Bürgerversammlungen."*
|
||||
> — Walter Schmidtke (Grafing), zu Frage "Information" (GWÖ: 2.5)
|
||||
|
||||
> *"Hier müsste erstmal mit der Bürgerschaft Rücksprache gehalten werden, wo eine stärkere Beteiligung gewünscht wird (z.B. Senioren- oder Brauchtumskartelle)."*
|
||||
> — Walter Schmidtke (Grafing), zu Frage "Mitentscheidung" (GWÖ: 2.5)
|
||||
|
||||
---
|
||||
|
||||
### Freie Wähler
|
||||
|
||||
**Kandidat:innen:** 7 | **Antworten:** 42 | **Ø Wortanzahl:** 13
|
||||
|
||||
**GWÖ-Score:** 1.5/10 | **Substanz:** 0.6/3 | **Ja-Quote:** 90%
|
||||
|
||||
**Häufigste Matrix-Felder:** D5, C2, A3, A4, D2
|
||||
|
||||
**Beste Antworten:**
|
||||
|
||||
> *"Ja, ich werde mich in München klar für die Schaffung und den Ausbau von Anreizen einsetzen, die Unternehmen dabei unterstützen, gemeinwohlorientierter zu wirtschaften. Das umfasst die Berücksichtigung von Gemeinwohl-Kriterien bei der Vergabe öffentlicher Aufträge sowie bei Wirtschaftsförderung und s..."*
|
||||
> — Michael Piazolo (München), zu Frage "Anreize" (GWÖ: 6.5)
|
||||
|
||||
> *"Ja, ich werde mich in München dafür einsetzen, dass öffentliche Aufträge verstärkt an Unternehmen vergeben werden, die eine gültige Gemeinwohl-Bilanz oder gleichwertige, überprüfbare Gemeinwohl- und Nachhaltigkeitsnachweise vorlegen. Hier ist von Bedeutung, dass Gemeinwohl-Kriterien rechtssicher und..."*
|
||||
> — Michael Piazolo (München), zu Frage "Vergabe" (GWÖ: 6.5)
|
||||
|
||||
> *"Ja, ich möchte, dass die Münchnerinnen und Münchner künftig deutlich stärker und verbindlicher in kommunale Entscheidungsprozesse eingebunden werden. Zum einen sind dafür niedrigschwellige Beteiligungsformate, die transparente Informationen bereits ab Beginn des Prozesses bereitstellen, nötig. Zum a..."*
|
||||
> — Michael Piazolo (München), zu Frage "Mitentscheidung" (GWÖ: 6.5)
|
||||
|
||||
---
|
||||
|
||||
### Wählergemeinschaft
|
||||
|
||||
**Kandidat:innen:** 3 | **Antworten:** 18 | **Ø Wortanzahl:** 14
|
||||
|
||||
**GWÖ-Score:** 1.2/10 | **Substanz:** 0.6/3 | **Ja-Quote:** 94%
|
||||
|
||||
**Häufigste Matrix-Felder:** D5, A3, A4, C2
|
||||
|
||||
**Beste Antworten:**
|
||||
|
||||
> *"Im Rahmen des Möglichen und Sinnvollen und auf eine Art, so dass keine Wettbewerbsnachteile entstehen. Berücksichtigung in den Vergaberichtlinien, bei städtischen Ausschreibungen und Konzessionen. Im Leitbild der Stadt."*
|
||||
> — Stefan Müller-Kroehling, Dr. (Landshut), zu Frage "Leitlinien" (GWÖ: 2.5)
|
||||
|
||||
> *"Im Rahmen der - vielfach (sehr) engen - Grenzen des Wettbewerbsrechts."*
|
||||
> — Stefan Müller-Kroehling, Dr. (Landshut), zu Frage "Vergabe" (GWÖ: 2.5)
|
||||
|
||||
> *"Voraussetzung dafür ist, siehe die Schweiz mit ihrer langen Tradition plebiszitärer Elemente - sehr gut aufbereitete, umfassende und neutrale Information zu den zur Abstimmung stehenden Themen. Dies setzt ferner auch den Willen und die Zeit, sich zu informieren voraus, sowie auch den Umgang mit gezi..."*
|
||||
> — Stefan Müller-Kroehling, Dr. (Landshut), zu Frage "Mitentscheidung" (GWÖ: 2.5)
|
||||
|
||||
---
|
||||
|
||||
### SPD
|
||||
|
||||
**Kandidat:innen:** 1 | **Antworten:** 6 | **Ø Wortanzahl:** 6
|
||||
|
||||
**GWÖ-Score:** 0.8/10 | **Substanz:** 0.3/3 | **Ja-Quote:** 83%
|
||||
|
||||
**Häufigste Matrix-Felder:** C2, D5, A3, A4
|
||||
|
||||
**Beste Antworten:**
|
||||
|
||||
> *"Ich bin Sozialdemokratin und da ist mir wichtig, dass unsere Wirtschaft dem Gemeinwohl dient. Die Achtung der Menschenwürde, Soziale Gerechtigkeit, Rechtsstaatlichkeit und Transparenz sind doch selbstverständlich."*
|
||||
> — Julia Worch (Olching), zu Frage "Leitlinien" (GWÖ: 2.5)
|
||||
|
||||
> *"Sobald ich im Amt bin und die nächsten Ausschreibungen anstehen."*
|
||||
> — Julia Worch (Olching), zu Frage "Vergabe" (GWÖ: 2.5)
|
||||
|
||||
---
|
||||
|
||||
### CSU
|
||||
|
||||
**Kandidat:innen:** 2 | **Antworten:** 12 | **Ø Wortanzahl:** 2
|
||||
|
||||
**GWÖ-Score:** 0.7/10 | **Substanz:** 0.3/3 | **Ja-Quote:** 75%
|
||||
|
||||
**Häufigste Matrix-Felder:** D5, A3, A4, C2, D2
|
||||
|
||||
**Beste Antworten:**
|
||||
|
||||
> *"Unternehmen sensibilisieren, wäre ein erster SChritt"*
|
||||
> — Josef Peis (Ebersberg), zu Frage "Anreize" (GWÖ: 2.0)
|
||||
|
||||
> *"Nur mit den Bürger:innen können wir Herausforderungen"*
|
||||
> — Josef Peis (Ebersberg), zu Frage "Mitentscheidung" (GWÖ: 2.0)
|
||||
|
||||
---
|
||||
|
||||
### FDP
|
||||
|
||||
**Kandidat:innen:** 1 | **Antworten:** 6 | **Ø Wortanzahl:** 0
|
||||
|
||||
**GWÖ-Score:** 0.0/10 | **Substanz:** 0.0/3 | **Ja-Quote:** 17%
|
||||
|
||||
---
|
||||
|
||||
## Auswertung nach Fragen
|
||||
|
||||
| Nr | Frage | Ja | Nein | Ø GWÖ | Ø Substanz |
|
||||
|----|-------|-----|------|-------|------------|
|
||||
| 1 | Leitlinien | 25 | 1 | 2.9 | 1.1 |
|
||||
| 2 | Anreize | 25 | 1 | 2.1 | 0.8 |
|
||||
| 3 | Vergabe | 20 | 5 | 2.4 | 1.0 |
|
||||
| 4 | Information | 26 | 1 | 1.9 | 0.8 |
|
||||
| 5 | Mitentscheidung | 26 | 0 | 2.8 | 1.1 |
|
||||
| 6 | Bekanntheit | 23 | 2 | 1.5 | 0.7 |
|
||||
28
output/05_parteienlandschaft.md
Normal file
28
output/05_parteienlandschaft.md
Normal file
@ -0,0 +1,28 @@
|
||||
# GWÖ-Parteienlandschaft Bayern 2026
|
||||
|
||||
*Kommunalwahlen — komprimierte Analyse*
|
||||
|
||||
---
|
||||
|
||||
## 🟡 Mittelfeld (GWÖ 3-6)
|
||||
|
||||
**Linke** (Ø 4.6): Partielle Übereinstimmung, erkennbare Offenheit für GWÖ-Themen.
|
||||
**ÖDP** (Ø 4.2): Partielle Übereinstimmung, erkennbare Offenheit für GWÖ-Themen.
|
||||
|
||||
## 🔴 Zurückhaltend (GWÖ < 3)
|
||||
|
||||
**Grüne** (Ø 3.0): Geringe GWÖ-Resonanz, 2% Ablehnung.
|
||||
**Bayernpartei** (Ø 1.9): Geringe GWÖ-Resonanz, 33% Ablehnung.
|
||||
**Freie Wähler** (Ø 1.5): Geringe GWÖ-Resonanz, 10% Ablehnung.
|
||||
**Wählergemeinschaft** (Ø 1.2): Geringe GWÖ-Resonanz, 6% Ablehnung.
|
||||
**SPD** (Ø 0.8): Geringe GWÖ-Resonanz, 17% Ablehnung.
|
||||
**CSU** (Ø 0.7): Geringe GWÖ-Resonanz, 25% Ablehnung.
|
||||
**FDP** (Ø 0.0): Geringe GWÖ-Resonanz, 83% Ablehnung.
|
||||
|
||||
---
|
||||
|
||||
## Kernaussagen
|
||||
|
||||
- **Höchste GWÖ-Affinität:** Linke mit Ø 4.6
|
||||
- **Niedrigste GWÖ-Affinität:** FDP mit Ø 0.0
|
||||
- **Spannweite:** 4.6 Punkte zwischen Spitze und Ende
|
||||
135
output/06_methodik.md
Normal file
135
output/06_methodik.md
Normal file
@ -0,0 +1,135 @@
|
||||
# GWÖ-Wahlprüfsteine Bayern 2026 — Methodik
|
||||
|
||||
---
|
||||
|
||||
## 1. Datenerhebung
|
||||
|
||||
### Zeitraum
|
||||
November 2025 bis 6. Februar 2026 (Teilnahmeschluss)
|
||||
|
||||
### Zielgruppe
|
||||
Kandidierende für das Bürgermeisteramt bei den bayerischen Kommunalwahlen am 8. März 2026
|
||||
|
||||
### Reichweite
|
||||
- Ca. 200 Kandidierende direkt angeschrieben
|
||||
- Zusätzlich Verbreitung über Orts- und Landesverbände
|
||||
- 34 Teilnehmende, davon 27 mit Veröffentlichungseinwilligung
|
||||
|
||||
### Fragebogen
|
||||
6 geschlossene Fragen (Ja/Nein) mit optionaler Erläuterung zu GWÖ-Kernthemen
|
||||
|
||||
---
|
||||
|
||||
## 2. Bewertungsgrundlage
|
||||
|
||||
### GWÖ-Matrix 2.0 für Gemeinden
|
||||
|
||||
Die Bewertung basiert auf der offiziellen GWÖ-Matrix 2.0 für Gemeinden, die 25 Themenfelder in einer 5×5-Struktur abbildet:
|
||||
|
||||
**Die 5 Werte (Spalten):**
|
||||
1. Menschenwürde
|
||||
2. Solidarität
|
||||
3. Ökologische Nachhaltigkeit
|
||||
4. Soziale Gerechtigkeit
|
||||
5. Transparenz & Mitbestimmung
|
||||
|
||||
**Die 5 Berührungsgruppen (Zeilen):**
|
||||
- A: Lieferant:innen, Dienstleister:innen
|
||||
- B: Finanzpartner:innen, Steuerzahler:innen
|
||||
- C: Politische Führung, Verwaltung
|
||||
- D: Bürger:innen und Wirtschaft
|
||||
- E: Staat, Gesellschaft, Natur
|
||||
|
||||
Für Kommunalpolitik sind besonders relevant:
|
||||
- **D5:** Demokratische Einbindung der Bürger:innen
|
||||
- **D2:** Gesamtwohl der Gemeinde
|
||||
- **C2:** Gemeinwohlorientierte Zielvereinbarung
|
||||
- **A3/A4:** Nachhaltige und soziale Beschaffung
|
||||
|
||||
---
|
||||
|
||||
## 3. Bewertungskriterien
|
||||
|
||||
### Substanz-Score (0-3)
|
||||
|
||||
Misst, ob die Antwort konkrete Maßnahmen enthält:
|
||||
|
||||
| Score | Bedeutung | Beispiel |
|
||||
|-------|-----------|----------|
|
||||
| 0 | Keine Antwort oder nur Ja/Nein | „Ja" ohne Erläuterung |
|
||||
| 1 | Ausweichend, Floskeln | „Gespräche führen", „Unterstützung anbieten" |
|
||||
| 2 | Substanziell, erkennbare Haltung | „Vergaberichtlinien anpassen" |
|
||||
| 3 | Umfassend mit konkreten Maßnahmen | „Pilotprojekt in 2027, dann Ausweitung" |
|
||||
|
||||
### GWÖ-Score (0-10)
|
||||
|
||||
Misst die Übereinstimmung mit GWÖ-Werten nach Matrix 2.0:
|
||||
|
||||
| Score | Stufe | Beschreibung |
|
||||
|-------|-------|--------------|
|
||||
| 7-10 | Vorbildlich | Innovative Maßnahmen, weitreichende Verbesserungen |
|
||||
| 4-6 | Erfahren | Erkennbare Verbesserungen, gute Ansätze |
|
||||
| 2-3 | Fortgeschritten | Erste Maßnahmen, erste Erfolge |
|
||||
| 0-1 | Basislinie | Nur gesetzliche Anforderungen oder weniger |
|
||||
|
||||
### Matrix-Felder
|
||||
|
||||
Für jede Antwort wird erfasst, welche der 25 GWÖ-Themenfelder berührt werden (z.B. D5, C2, A3).
|
||||
|
||||
---
|
||||
|
||||
## 4. Auswertungsverfahren
|
||||
|
||||
### Einzelbewertung
|
||||
Jede der 162 Antworten (27 Kandidat:innen × 6 Fragen) wurde einzeln bewertet nach:
|
||||
- Substanz-Score
|
||||
- GWÖ-Score
|
||||
- Berührte Matrix-Felder
|
||||
- Stärken und Schwächen
|
||||
|
||||
### Aggregation
|
||||
|
||||
**Pro Kandidat:in:**
|
||||
- Durchschnitt über alle 6 Fragen
|
||||
- Spanne (Min–Max) der Einzelbewertungen
|
||||
|
||||
**Pro Partei:**
|
||||
- Durchschnitt über alle Kandidat:innen der Partei
|
||||
- Bandbreite (schwächste:r bis beste:r Kandidat:in)
|
||||
- Ja-Quote (Anteil „Ja"-Antworten)
|
||||
|
||||
### Kategorisierung
|
||||
|
||||
**Kandidat:innen:**
|
||||
- 🟢 Vorreiter:innen: Ø GWÖ ≥ 5.0
|
||||
- 🟡 Solide Basis: Ø GWÖ 3.0–5.0
|
||||
- 🔴 Wenig Substanz: Ø GWÖ < 3.0
|
||||
|
||||
---
|
||||
|
||||
## 5. Qualitätssicherung
|
||||
|
||||
### Konsistenz
|
||||
Alle Bewertungen wurden nach einheitlichen Kriterien durchgeführt.
|
||||
|
||||
### Transparenz
|
||||
- Alle Originalantworten sind auf der ECOnGOOD-Website einsehbar
|
||||
- PDF-Downloads der Einzelantworten verfügbar
|
||||
- Bewertungskriterien offen dokumentiert
|
||||
|
||||
### Limitationen
|
||||
- Freiwillige Teilnahme: Nicht alle Kandidat:innen haben geantwortet
|
||||
- Selbstauskunft: Angaben wurden nicht extern verifiziert
|
||||
- Snapshot: Bewertung basiert auf Antworten zum Zeitpunkt der Befragung
|
||||
|
||||
---
|
||||
|
||||
## 6. Quellen
|
||||
|
||||
- GWÖ-Matrix 2.0 für Gemeinden: https://germany.econgood.org
|
||||
- Wahlprüfsteine-Originalseite: https://germany.econgood.org/wahlpruefsteine-zu-den-bayerischen-kommunalwahlen-2026
|
||||
- Arbeitsbuch Gemeinwohl-Bilanz Gemeinden V2.0
|
||||
|
||||
---
|
||||
|
||||
*Stand: 29. März 2026*
|
||||
3138
output/07_rohdaten.json
Normal file
3138
output/07_rohdaten.json
Normal file
File diff suppressed because it is too large
Load Diff
676
wahlpruefsteine/aggregator.py
Normal file
676
wahlpruefsteine/aggregator.py
Normal file
@ -0,0 +1,676 @@
|
||||
#!/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())
|
||||
277
wahlpruefsteine/analyzer.py
Normal file
277
wahlpruefsteine/analyzer.py
Normal file
@ -0,0 +1,277 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
GWÖ-Wahlprüfsteine Analyzer
|
||||
Bewertet Antworten mit Qwen LLM nach GWÖ-Kriterien.
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
from openai import OpenAI
|
||||
|
||||
# GWÖ-Matrix 2.0 Kontext (gekürzt für Prompt)
|
||||
GWOE_MATRIX_CONTEXT = """
|
||||
## GWÖ-Matrix 2.0 für Gemeinden
|
||||
|
||||
### Die 5 Werte (Spalten)
|
||||
1. **Menschenwürde** — Rechtsstaatsprinzip, Grundrechte
|
||||
2. **Solidarität** — Gemeinnutz, Mehrwert für Gemeinschaft
|
||||
3. **Ökologische Nachhaltigkeit** — Klimaschutz, Ressourcenschonung
|
||||
4. **Soziale Gerechtigkeit** — Daseinsvorsorge, gerechte Verteilung
|
||||
5. **Transparenz & Mitbestimmung** — Bürgerbeteiligung, Demokratie
|
||||
|
||||
### Die 5 Berührungsgruppen (Zeilen)
|
||||
- **A**: Lieferant:innen, Dienstleister:innen (Beschaffung, Lieferketten)
|
||||
- **B**: Finanzpartner:innen, Steuerzahler:innen (Haushalt, Finanzpolitik)
|
||||
- **C**: Politische Führung, Verwaltung (Mandatsträger:innen, Mitarbeitende)
|
||||
- **D**: Bürger:innen und Wirtschaft (Daseinsvorsorge, lokale Wirkung)
|
||||
- **E**: Staat, Gesellschaft, Natur (überregional, Zukunft)
|
||||
|
||||
### Relevante Felder für Kommunalpolitik
|
||||
- **D5**: Demokratische Einbindung der Bürger:innen
|
||||
- **D2**: Gesamtwohl der Gemeinde
|
||||
- **C2**: Gemeinwohlorientierte Zielvereinbarung
|
||||
- **A3/A4**: Nachhaltige/soziale Beschaffung
|
||||
- **B5**: Partizipation in Finanzpolitik
|
||||
|
||||
### Bewertungsskala
|
||||
- 7-10: Vorbildlich (innovative Maßnahmen)
|
||||
- 4-6: Erfahren (erkennbare Verbesserungen)
|
||||
- 2-3: Fortgeschritten (erste Maßnahmen)
|
||||
- 1: Erste Schritte
|
||||
- 0: Basislinie
|
||||
"""
|
||||
|
||||
SYSTEM_PROMPT = """Du bist ein Experte für Gemeinwohl-Ökonomie (GWÖ) und analysierst Antworten auf Wahlprüfsteine.
|
||||
|
||||
{matrix_context}
|
||||
|
||||
## Deine Aufgabe
|
||||
|
||||
Bewerte die Antwort eines Bürgermeister-Kandidaten auf eine Wahlprüfstein-Frage.
|
||||
|
||||
## Ausgabeformat (JSON)
|
||||
|
||||
{{
|
||||
"substanz_score": 0-3,
|
||||
"substanz_begruendung": "Kurze Begründung",
|
||||
"umfang": "keine|kurz|mittel|ausführlich",
|
||||
"gwoe_score": 0.0-10.0,
|
||||
"gwoe_begruendung": "2-3 Sätze mit Matrix-Bezug",
|
||||
"matrix_felder": ["D5", "C2"],
|
||||
"staerken": ["Punkt 1", "Punkt 2"],
|
||||
"schwaechen": ["Punkt 1"]
|
||||
}}
|
||||
|
||||
### Substanz-Score
|
||||
- 0: Keine Antwort oder nur Ja/Nein ohne Erläuterung
|
||||
- 1: Ausweichend, Floskeln, keine konkreten Maßnahmen
|
||||
- 2: Substanziell, erkennbare Haltung, erste Ideen
|
||||
- 3: Umfassend mit konkreten Maßnahmen und Zeitrahmen
|
||||
|
||||
### Umfang
|
||||
- keine: Leer oder nur "/"
|
||||
- kurz: 1-2 Sätze
|
||||
- mittel: 3-5 Sätze
|
||||
- ausführlich: >5 Sätze mit Details
|
||||
|
||||
### GWÖ-Score
|
||||
Bewerte nach Matrix 2.0. Berücksichtige:
|
||||
- Konkrete GWÖ-Bezüge (Gemeinwohl-Bilanz, Matrix)
|
||||
- Allgemeine Gemeinwohl-Orientierung
|
||||
- Partizipation und Transparenz
|
||||
- Ökologische und soziale Aspekte
|
||||
- Umsetzbarkeit der genannten Maßnahmen
|
||||
|
||||
Antworte NUR mit validem JSON, keine Erklärungen davor oder danach.""".format(matrix_context=GWOE_MATRIX_CONTEXT)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AntwortZuBewerten:
|
||||
antwort_id: int
|
||||
kandidat: str
|
||||
kommune: str
|
||||
partei: str
|
||||
frage_nr: int
|
||||
frage_text: str
|
||||
antwort_kurz: str | None
|
||||
antwort_erlaeuterung: str | None
|
||||
|
||||
|
||||
def get_unbewertete_antworten(conn: sqlite3.Connection, limit: int = None) -> list[AntwortZuBewerten]:
|
||||
"""Holt alle noch nicht bewerteten Antworten."""
|
||||
query = """
|
||||
SELECT
|
||||
ar.id as antwort_id,
|
||||
k.vorname || ' ' || k.nachname as kandidat,
|
||||
k.kommune,
|
||||
k.partei_normalisiert as partei,
|
||||
f.nummer as frage_nr,
|
||||
f.volltext as frage_text,
|
||||
ar.antwort_kurz,
|
||||
ar.antwort_erlaeuterung
|
||||
FROM antworten_raw ar
|
||||
JOIN kandidaten k ON ar.kandidat_id = k.id
|
||||
JOIN fragen f ON ar.frage_id = f.id
|
||||
LEFT JOIN bewertungen b ON ar.id = b.antwort_id
|
||||
WHERE b.id IS NULL
|
||||
ORDER BY k.id, f.nummer
|
||||
"""
|
||||
if limit:
|
||||
query += f" LIMIT {limit}"
|
||||
|
||||
cursor = conn.execute(query)
|
||||
return [AntwortZuBewerten(**dict(row)) for row in cursor.fetchall()]
|
||||
|
||||
|
||||
def build_user_prompt(antwort: AntwortZuBewerten) -> str:
|
||||
"""Baut den User-Prompt für eine Antwort."""
|
||||
erlaeuterung = antwort.antwort_erlaeuterung or "(keine Erläuterung)"
|
||||
kurz = antwort.antwort_kurz or "(keine Angabe)"
|
||||
|
||||
return f"""## Wahlprüfstein-Frage {antwort.frage_nr}
|
||||
|
||||
**Frage:** {antwort.frage_text}
|
||||
|
||||
## Antwort von {antwort.kandidat} ({antwort.partei}), Kandidat:in für {antwort.kommune}
|
||||
|
||||
**Grundsätzlich:** {kurz}
|
||||
|
||||
**Erläuterung:** {erlaeuterung}
|
||||
|
||||
---
|
||||
|
||||
Bewerte diese Antwort nach den GWÖ-Kriterien."""
|
||||
|
||||
|
||||
def analyze_with_llm(client: OpenAI, antwort: AntwortZuBewerten, model: str = "qwen-plus") -> dict:
|
||||
"""Analysiert eine Antwort mit dem LLM."""
|
||||
|
||||
user_prompt = build_user_prompt(antwort)
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model=model,
|
||||
messages=[
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{"role": "user", "content": user_prompt}
|
||||
],
|
||||
temperature=0.3,
|
||||
response_format={"type": "json_object"}
|
||||
)
|
||||
|
||||
result = json.loads(response.choices[0].message.content)
|
||||
|
||||
# Wortanzahl berechnen
|
||||
text = (antwort.antwort_erlaeuterung or "")
|
||||
result['wortanzahl'] = len(text.split()) if text else 0
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def save_bewertung(conn: sqlite3.Connection, antwort_id: int, bewertung: dict, model: str):
|
||||
"""Speichert eine Bewertung in der Datenbank."""
|
||||
conn.execute("""
|
||||
INSERT OR REPLACE INTO bewertungen
|
||||
(antwort_id, substanz_score, umfang, wortanzahl, gwoe_score, gwoe_begruendung,
|
||||
matrix_felder, staerken, schwaechen, model)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
antwort_id,
|
||||
bewertung.get('substanz_score'),
|
||||
bewertung.get('umfang'),
|
||||
bewertung.get('wortanzahl'),
|
||||
bewertung.get('gwoe_score'),
|
||||
bewertung.get('gwoe_begruendung'),
|
||||
json.dumps(bewertung.get('matrix_felder', []), ensure_ascii=False),
|
||||
json.dumps(bewertung.get('staerken', []), ensure_ascii=False),
|
||||
json.dumps(bewertung.get('schwaechen', []), ensure_ascii=False),
|
||||
model
|
||||
))
|
||||
conn.commit()
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='GWÖ-Wahlprüfsteine Analyzer')
|
||||
parser.add_argument('--db', type=Path, default=Path(__file__).parent / 'wahlpruefsteine.db',
|
||||
help='Pfad zur SQLite-Datenbank')
|
||||
parser.add_argument('--model', default='qwen-plus',
|
||||
choices=['qwen-plus', 'qwen-max', 'qwen-turbo'],
|
||||
help='Qwen-Modell')
|
||||
parser.add_argument('--limit', type=int, help='Maximal zu bewertende Antworten')
|
||||
parser.add_argument('--delay', type=float, default=0.5, help='Pause zwischen API-Calls (Sekunden)')
|
||||
parser.add_argument('--dry-run', action='store_true', help='Nur anzeigen, nicht speichern')
|
||||
parser.add_argument('--verbose', '-v', action='store_true', help='Ausführliche Ausgabe')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# API-Key aus Keychain
|
||||
api_key = os.popen("security find-generic-password -s qwen-api -w 2>/dev/null").read().strip()
|
||||
if not api_key:
|
||||
api_key = os.environ.get('DASHSCOPE_API_KEY')
|
||||
|
||||
if not api_key:
|
||||
print("FEHLER: Kein API-Key gefunden (Keychain 'qwen-api' oder DASHSCOPE_API_KEY)")
|
||||
return 1
|
||||
|
||||
# DashScope Client
|
||||
client = OpenAI(
|
||||
api_key=api_key,
|
||||
base_url="https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
|
||||
)
|
||||
|
||||
# Datenbank öffnen
|
||||
conn = sqlite3.connect(args.db)
|
||||
conn.row_factory = sqlite3.Row
|
||||
|
||||
# Unbewertete Antworten holen
|
||||
antworten = get_unbewertete_antworten(conn, args.limit)
|
||||
print(f"Zu bewerten: {len(antworten)} Antworten")
|
||||
|
||||
if not antworten:
|
||||
print("Alle Antworten bereits bewertet.")
|
||||
return 0
|
||||
|
||||
# Bewerten
|
||||
erfolg = 0
|
||||
fehler = 0
|
||||
|
||||
for i, antwort in enumerate(antworten, 1):
|
||||
print(f"[{i}/{len(antworten)}] {antwort.kandidat} - Frage {antwort.frage_nr}...", end=" ", flush=True)
|
||||
|
||||
try:
|
||||
bewertung = analyze_with_llm(client, antwort, args.model)
|
||||
|
||||
if args.verbose:
|
||||
print(f"\n GWÖ: {bewertung.get('gwoe_score')}, Substanz: {bewertung.get('substanz_score')}")
|
||||
print(f" Begründung: {bewertung.get('gwoe_begruendung', '')[:100]}...")
|
||||
|
||||
if not args.dry_run:
|
||||
save_bewertung(conn, antwort.antwort_id, bewertung, args.model)
|
||||
|
||||
print(f"OK (GWÖ: {bewertung.get('gwoe_score', '?')})")
|
||||
erfolg += 1
|
||||
|
||||
except Exception as e:
|
||||
print(f"FEHLER: {e}")
|
||||
fehler += 1
|
||||
|
||||
if i < len(antworten):
|
||||
time.sleep(args.delay)
|
||||
|
||||
conn.close()
|
||||
|
||||
print(f"\nFertig: {erfolg} erfolgreich, {fehler} Fehler")
|
||||
|
||||
if args.dry_run:
|
||||
print("(Dry-Run — nichts gespeichert)")
|
||||
|
||||
return 0 if fehler == 0 else 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
exit(main())
|
||||
259
wahlpruefsteine/cli.py
Normal file
259
wahlpruefsteine/cli.py
Normal file
@ -0,0 +1,259 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
GWÖ-Wahlprüfsteine CLI
|
||||
Haupteinstiegspunkt für alle Operationen.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def cmd_scrape(args):
|
||||
"""Scrape-Befehl: Daten von URL laden."""
|
||||
from scraper import fetch_and_parse, init_db, save_to_db, parse_html
|
||||
|
||||
if args.html:
|
||||
print(f"Lade lokale Datei: {args.html}")
|
||||
with open(args.html) as f:
|
||||
kandidaten = parse_html(f.read())
|
||||
else:
|
||||
print(f"Lade URL: {args.url}")
|
||||
kandidaten = fetch_and_parse(args.url)
|
||||
|
||||
print(f"Gefunden: {len(kandidaten)} Kandidat:innen")
|
||||
|
||||
conn = init_db(args.db)
|
||||
count = save_to_db(conn, kandidaten)
|
||||
conn.close()
|
||||
|
||||
print(f"Gespeichert: {count} Kandidat:innen in {args.db}")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_analyze(args):
|
||||
"""Analyze-Befehl: LLM-Bewertung durchführen."""
|
||||
from analyzer import main as analyzer_main
|
||||
|
||||
# Args weiterleiten
|
||||
sys.argv = ['analyzer.py', '--db', str(args.db), '--model', args.model]
|
||||
if args.limit:
|
||||
sys.argv.extend(['--limit', str(args.limit)])
|
||||
if args.dry_run:
|
||||
sys.argv.append('--dry-run')
|
||||
if args.verbose:
|
||||
sys.argv.append('--verbose')
|
||||
|
||||
return analyzer_main()
|
||||
|
||||
|
||||
def cmd_aggregate(args):
|
||||
"""Aggregate-Befehl: Reports generieren."""
|
||||
from aggregator import main as aggregator_main
|
||||
|
||||
sys.argv = ['aggregator.py', '--db', str(args.db), '--output', str(args.output)]
|
||||
return aggregator_main()
|
||||
|
||||
|
||||
def cmd_status(args):
|
||||
"""Status-Befehl: Datenbankstatus anzeigen."""
|
||||
import sqlite3
|
||||
|
||||
if not args.db.exists():
|
||||
print(f"Datenbank nicht gefunden: {args.db}")
|
||||
return 1
|
||||
|
||||
conn = sqlite3.connect(args.db)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Kandidaten
|
||||
cursor.execute("SELECT COUNT(*) FROM kandidaten")
|
||||
n_kandidaten = cursor.fetchone()[0]
|
||||
|
||||
# Antworten
|
||||
cursor.execute("SELECT COUNT(*) FROM antworten_raw")
|
||||
n_antworten = cursor.fetchone()[0]
|
||||
|
||||
# Bewertungen
|
||||
cursor.execute("SELECT COUNT(*) FROM bewertungen")
|
||||
n_bewertungen = cursor.fetchone()[0]
|
||||
|
||||
# Offene Bewertungen
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*) FROM antworten_raw ar
|
||||
LEFT JOIN bewertungen b ON ar.id = b.antwort_id
|
||||
WHERE b.id IS NULL
|
||||
""")
|
||||
n_offen = cursor.fetchone()[0]
|
||||
|
||||
print(f"Datenbank: {args.db}")
|
||||
print(f" Kandidat:innen: {n_kandidaten}")
|
||||
print(f" Antworten: {n_antworten}")
|
||||
print(f" Bewertungen: {n_bewertungen} ({n_offen} offen)")
|
||||
|
||||
# Partei-Verteilung
|
||||
print("\nPartei-Verteilung:")
|
||||
for row in cursor.execute("""
|
||||
SELECT partei_normalisiert, COUNT(*) as n
|
||||
FROM kandidaten
|
||||
GROUP BY partei_normalisiert
|
||||
ORDER BY n DESC
|
||||
"""):
|
||||
print(f" {row[0]}: {row[1]}")
|
||||
|
||||
# GWÖ-Durchschnitt wenn Bewertungen vorhanden
|
||||
if n_bewertungen > 0:
|
||||
cursor.execute("SELECT ROUND(AVG(gwoe_score), 2) FROM bewertungen WHERE gwoe_score IS NOT NULL")
|
||||
avg_gwoe = cursor.fetchone()[0]
|
||||
print(f"\nØ GWÖ-Score: {avg_gwoe}")
|
||||
|
||||
conn.close()
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_export(args):
|
||||
"""Export-Befehl: Daten als JSON exportieren."""
|
||||
import sqlite3
|
||||
import json
|
||||
|
||||
conn = sqlite3.connect(args.db)
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Alle Daten zusammenbauen
|
||||
data = {
|
||||
'kandidaten': [],
|
||||
'bewertungen': []
|
||||
}
|
||||
|
||||
for row in cursor.execute("""
|
||||
SELECT k.*,
|
||||
GROUP_CONCAT(f.nummer || ':' || COALESCE(ar.antwort_kurz, '') || ':' || COALESCE(ar.antwort_erlaeuterung, ''), '|||') as antworten
|
||||
FROM kandidaten k
|
||||
LEFT JOIN antworten_raw ar ON k.id = ar.kandidat_id
|
||||
LEFT JOIN fragen f ON ar.frage_id = f.id
|
||||
GROUP BY k.id
|
||||
"""):
|
||||
data['kandidaten'].append(dict(row))
|
||||
|
||||
for row in cursor.execute("""
|
||||
SELECT b.*,
|
||||
k.vorname || ' ' || k.nachname as kandidat,
|
||||
k.partei_normalisiert as partei,
|
||||
f.kurztext as frage
|
||||
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
|
||||
"""):
|
||||
data['bewertungen'].append(dict(row))
|
||||
|
||||
output = args.output or (args.db.parent / 'export.json')
|
||||
with open(output, 'w') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2, default=str)
|
||||
|
||||
print(f"Exportiert: {output}")
|
||||
conn.close()
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='GWÖ-Wahlprüfsteine Auswertung',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Beispiele:
|
||||
%(prog)s scrape # Daten von ECOnGOOD laden
|
||||
%(prog)s analyze --limit 10 # Erste 10 Antworten bewerten
|
||||
%(prog)s analyze # Alle offenen Antworten bewerten
|
||||
%(prog)s aggregate # Reports generieren
|
||||
%(prog)s status # Datenbankstatus anzeigen
|
||||
|
||||
Vollständiger Workflow:
|
||||
%(prog)s scrape && %(prog)s analyze && %(prog)s aggregate
|
||||
"""
|
||||
)
|
||||
|
||||
# Globale Optionen
|
||||
parser.add_argument('--db', type=Path,
|
||||
default=Path(__file__).parent / 'wahlpruefsteine.db',
|
||||
help='Pfad zur SQLite-Datenbank')
|
||||
parser.add_argument('--verbose', '-v', action='store_true',
|
||||
help='Ausführliche Ausgabe')
|
||||
|
||||
subparsers = parser.add_subparsers(dest='command', help='Verfügbare Befehle')
|
||||
|
||||
# Scrape
|
||||
p_scrape = subparsers.add_parser('scrape', help='Daten von URL laden')
|
||||
p_scrape.add_argument('--url',
|
||||
default='https://germany.econgood.org/wahlpruefsteine-zu-den-bayerischen-kommunalwahlen-2026',
|
||||
help='URL der Wahlprüfsteine-Seite')
|
||||
p_scrape.add_argument('--html', type=Path, help='Lokale HTML-Datei statt URL')
|
||||
|
||||
# Analyze
|
||||
p_analyze = subparsers.add_parser('analyze', help='LLM-Bewertung durchführen')
|
||||
p_analyze.add_argument('--model', default='qwen-plus',
|
||||
choices=['qwen-plus', 'qwen-max', 'qwen-turbo'],
|
||||
help='Qwen-Modell')
|
||||
p_analyze.add_argument('--limit', type=int, help='Maximal zu bewertende Antworten')
|
||||
p_analyze.add_argument('--dry-run', action='store_true', help='Nur anzeigen')
|
||||
|
||||
# Aggregate
|
||||
p_aggregate = subparsers.add_parser('aggregate', help='Reports generieren')
|
||||
p_aggregate.add_argument('--output', type=Path,
|
||||
default=Path(__file__).parent / 'output',
|
||||
help='Ausgabeverzeichnis')
|
||||
|
||||
# Status
|
||||
subparsers.add_parser('status', help='Datenbankstatus anzeigen')
|
||||
|
||||
# Export
|
||||
p_export = subparsers.add_parser('export', help='Daten als JSON exportieren')
|
||||
p_export.add_argument('--output', '-o', type=Path, help='Ausgabedatei')
|
||||
|
||||
# Vollständiger Lauf
|
||||
p_run = subparsers.add_parser('run', help='Vollständiger Workflow (scrape → analyze → aggregate)')
|
||||
p_run.add_argument('--model', default='qwen-plus',
|
||||
choices=['qwen-plus', 'qwen-max', 'qwen-turbo'])
|
||||
p_run.add_argument('--output', type=Path,
|
||||
default=Path(__file__).parent / 'output')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.command:
|
||||
parser.print_help()
|
||||
return 1
|
||||
|
||||
# Befehle ausführen
|
||||
if args.command == 'scrape':
|
||||
return cmd_scrape(args)
|
||||
elif args.command == 'analyze':
|
||||
return cmd_analyze(args)
|
||||
elif args.command == 'aggregate':
|
||||
return cmd_aggregate(args)
|
||||
elif args.command == 'status':
|
||||
return cmd_status(args)
|
||||
elif args.command == 'export':
|
||||
return cmd_export(args)
|
||||
elif args.command == 'run':
|
||||
# Vollständiger Workflow
|
||||
print("=== SCRAPE ===")
|
||||
args.url = 'https://germany.econgood.org/wahlpruefsteine-zu-den-bayerischen-kommunalwahlen-2026'
|
||||
args.html = None
|
||||
if cmd_scrape(args) != 0:
|
||||
return 1
|
||||
|
||||
print("\n=== ANALYZE ===")
|
||||
args.limit = None
|
||||
args.dry_run = False
|
||||
if cmd_analyze(args) != 0:
|
||||
return 1
|
||||
|
||||
print("\n=== AGGREGATE ===")
|
||||
return cmd_aggregate(args)
|
||||
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
137
wahlpruefsteine/schema.sql
Normal file
137
wahlpruefsteine/schema.sql
Normal file
@ -0,0 +1,137 @@
|
||||
-- GWÖ-Wahlprüfsteine Auswertung — Datenbankschema
|
||||
-- Version 1.0, 29.03.2026
|
||||
|
||||
-- Kandidat:innen
|
||||
CREATE TABLE IF NOT EXISTS kandidaten (
|
||||
id INTEGER PRIMARY KEY,
|
||||
vorname TEXT NOT NULL,
|
||||
nachname TEXT NOT NULL,
|
||||
plz TEXT,
|
||||
kommune TEXT NOT NULL,
|
||||
landkreis TEXT,
|
||||
partei_raw TEXT NOT NULL, -- Original aus Umfrage
|
||||
partei_normalisiert TEXT NOT NULL, -- Normalisierte Hauptpartei
|
||||
ist_waehlergemeinschaft BOOLEAN DEFAULT FALSE,
|
||||
pdf_url TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Die 6 Fragen
|
||||
CREATE TABLE IF NOT EXISTS fragen (
|
||||
id INTEGER PRIMARY KEY,
|
||||
nummer INTEGER UNIQUE NOT NULL, -- 1-6
|
||||
kurztext TEXT NOT NULL, -- z.B. "Leitlinien"
|
||||
volltext TEXT NOT NULL -- Vollständige Frage
|
||||
);
|
||||
|
||||
-- Rohantworten
|
||||
CREATE TABLE IF NOT EXISTS antworten_raw (
|
||||
id INTEGER PRIMARY KEY,
|
||||
kandidat_id INTEGER REFERENCES kandidaten(id) ON DELETE CASCADE,
|
||||
frage_id INTEGER REFERENCES fragen(id) ON DELETE CASCADE,
|
||||
antwort_kurz TEXT, -- Ja/Nein/leer
|
||||
antwort_erlaeuterung TEXT, -- Optionale Erläuterung
|
||||
UNIQUE(kandidat_id, frage_id)
|
||||
);
|
||||
|
||||
-- LLM-Bewertungen (pro Antwort)
|
||||
CREATE TABLE IF NOT EXISTS bewertungen (
|
||||
id INTEGER PRIMARY KEY,
|
||||
antwort_id INTEGER REFERENCES antworten_raw(id) ON DELETE CASCADE UNIQUE,
|
||||
|
||||
-- Substanz: Hat tatsächlich geantwortet?
|
||||
substanz_score INTEGER CHECK(substanz_score >= 0 AND substanz_score <= 3),
|
||||
-- 0 = keine Antwort/nur Ja-Nein
|
||||
-- 1 = ausweichend/oberflächlich
|
||||
-- 2 = substanziell
|
||||
-- 3 = umfassend mit konkreten Maßnahmen
|
||||
|
||||
-- Umfang
|
||||
umfang TEXT CHECK(umfang IN ('keine', 'kurz', 'mittel', 'ausführlich')),
|
||||
wortanzahl INTEGER,
|
||||
|
||||
-- GWÖ-Bewertung
|
||||
gwoe_score REAL CHECK(gwoe_score >= 0 AND gwoe_score <= 10),
|
||||
gwoe_begruendung TEXT,
|
||||
|
||||
-- Matrix-Felder (JSON-Array, z.B. ["D5", "C2"])
|
||||
matrix_felder TEXT,
|
||||
|
||||
-- Qualitativ
|
||||
staerken TEXT,
|
||||
schwaechen TEXT,
|
||||
|
||||
-- Meta
|
||||
bewertet_am DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
model TEXT DEFAULT 'qwen-plus'
|
||||
);
|
||||
|
||||
-- Partei-Aggregation (View)
|
||||
CREATE VIEW IF NOT EXISTS v_partei_statistik AS
|
||||
SELECT
|
||||
k.partei_normalisiert AS partei,
|
||||
COUNT(DISTINCT k.id) AS anzahl_kandidaten,
|
||||
COUNT(b.id) AS anzahl_antworten,
|
||||
|
||||
-- Durchschnittswerte
|
||||
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,
|
||||
|
||||
-- Verteilung Umfang
|
||||
SUM(CASE WHEN b.umfang = 'keine' THEN 1 ELSE 0 END) AS umfang_keine,
|
||||
SUM(CASE WHEN b.umfang = 'kurz' THEN 1 ELSE 0 END) AS umfang_kurz,
|
||||
SUM(CASE WHEN b.umfang = 'mittel' THEN 1 ELSE 0 END) AS umfang_mittel,
|
||||
SUM(CASE WHEN b.umfang = 'ausführlich' THEN 1 ELSE 0 END) AS umfang_ausfuehrlich
|
||||
|
||||
FROM kandidaten k
|
||||
LEFT JOIN antworten_raw ar ON k.id = ar.kandidat_id
|
||||
LEFT JOIN bewertungen b ON ar.id = b.antwort_id
|
||||
GROUP BY k.partei_normalisiert;
|
||||
|
||||
-- Statistik pro Frage
|
||||
CREATE VIEW IF NOT EXISTS v_fragen_statistik AS
|
||||
SELECT
|
||||
f.nummer,
|
||||
f.kurztext,
|
||||
COUNT(b.id) AS anzahl_bewertungen,
|
||||
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_antworten,
|
||||
SUM(CASE WHEN ar.antwort_kurz = 'Nein' THEN 1 ELSE 0 END) AS nein_antworten
|
||||
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;
|
||||
|
||||
-- Beste/Schlechteste Antworten
|
||||
CREATE VIEW IF NOT EXISTS v_top_antworten AS
|
||||
SELECT
|
||||
k.vorname || ' ' || k.nachname AS name,
|
||||
k.kommune,
|
||||
k.partei_normalisiert AS partei,
|
||||
f.kurztext AS frage,
|
||||
b.gwoe_score,
|
||||
b.substanz_score,
|
||||
ar.antwort_erlaeuterung AS zitat
|
||||
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 b.gwoe_score IS NOT NULL
|
||||
ORDER BY b.gwoe_score DESC;
|
||||
|
||||
-- Indizes
|
||||
CREATE INDEX IF NOT EXISTS idx_kandidaten_partei ON kandidaten(partei_normalisiert);
|
||||
CREATE INDEX IF NOT EXISTS idx_antworten_kandidat ON antworten_raw(kandidat_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_antworten_frage ON antworten_raw(frage_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_bewertungen_gwoe ON bewertungen(gwoe_score);
|
||||
|
||||
-- Stammdaten: Fragen
|
||||
INSERT OR IGNORE INTO fragen (nummer, kurztext, volltext) VALUES
|
||||
(1, 'Leitlinien', 'Werden Sie sich für Maßnahmen einsetzen, welche die Werte und Themen der GWÖ in Leitlinien und Strategien Ihrer Kommune/Verwaltung und Eigenbetriebe integrieren?'),
|
||||
(2, 'Anreize', 'Werden Sie sich in Ihrer Kommune für die Schaffung von Anreizen einsetzen, um Unternehmen darin zu unterstützen gemeinwohl-orientierter zu wirtschaften?'),
|
||||
(3, 'Vergabe', 'Werden Sie sich in Ihrer Kommune dafür einsetzen, dass öffentliche Aufträge bevorzugt an Unternehmen vergeben werden, die eine gültige Gemeinwohl-Bilanz vorlegen?'),
|
||||
(4, 'Information', 'Möchten Sie dafür sorgen, dass die Bürger*innen Ihrer Kommune regelmäßig die wichtigsten Informationen zur Entwicklung Ihrer Kommune erhalten – und zwar mit einer Einschätzung inwieweit sie das Gemeinwohl stärken bzw. schwächen?'),
|
||||
(5, 'Mitentscheidung', 'Möchten Sie dafür sorgen, dass die Bürger*innen Ihrer Kommune in kommunale Entscheidungsprozesse fortan stärker eingebunden werden?'),
|
||||
(6, 'Bekanntheit', 'Möchten Sie dafür sorgen, dass die Werte-Orientierung, Themen und Inhalte der GWÖ in Ihrer Kommune und auf höheren politischen Ebenen bekannter werden?');
|
||||
263
wahlpruefsteine/scraper.py
Normal file
263
wahlpruefsteine/scraper.py
Normal file
@ -0,0 +1,263 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
GWÖ-Wahlprüfsteine Scraper
|
||||
Extrahiert Antworten aus der ECOnGOOD-Webseite und speichert sie in SQLite.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sqlite3
|
||||
import json
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
from bs4 import BeautifulSoup
|
||||
import requests
|
||||
|
||||
# Partei-Normalisierung
|
||||
PARTEI_MAPPING = {
|
||||
# Grüne
|
||||
r'bündnis\s*90\s*/?\s*die\s*grünen?': 'Grüne',
|
||||
r'^grüne$': 'Grüne',
|
||||
|
||||
# Freie Wähler
|
||||
r'freie\s*wähler': 'Freie Wähler',
|
||||
r'^fw$': 'Freie Wähler',
|
||||
r'^upw\s*/\s*freie\s*wähler$': 'Freie Wähler',
|
||||
|
||||
# CSU
|
||||
r'^csu$': 'CSU',
|
||||
r'csu\s*(und|&)': 'CSU',
|
||||
r'pro\s+.*\s+und\s+csu': 'CSU',
|
||||
|
||||
# SPD
|
||||
r'^spd$': 'SPD',
|
||||
|
||||
# FDP
|
||||
r'^fdp$': 'FDP',
|
||||
|
||||
# ÖDP
|
||||
r'^ödp': 'ÖDP',
|
||||
|
||||
# Linke
|
||||
r'die\s*linke': 'Linke',
|
||||
|
||||
# AfD
|
||||
r'^afd$': 'AfD',
|
||||
|
||||
# Bayernpartei
|
||||
r'bayernpartei': 'Bayernpartei',
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Kandidat:
|
||||
vorname: str
|
||||
nachname: str
|
||||
plz: str
|
||||
kommune: str
|
||||
landkreis: str
|
||||
partei_raw: str
|
||||
pdf_url: str
|
||||
antworten: dict # frage_nr -> (ja_nein, erläuterung)
|
||||
|
||||
|
||||
def normalize_partei(raw: str) -> tuple[str, bool]:
|
||||
"""
|
||||
Normalisiert Parteinamen.
|
||||
Returns: (normalisierte_partei, ist_wählergemeinschaft)
|
||||
"""
|
||||
raw_lower = raw.lower().strip()
|
||||
|
||||
for pattern, normalized in PARTEI_MAPPING.items():
|
||||
if re.search(pattern, raw_lower, re.IGNORECASE):
|
||||
return normalized, False
|
||||
|
||||
# Wählergemeinschaften erkennen
|
||||
wg_patterns = [
|
||||
r'wähler',
|
||||
r'liste\s',
|
||||
r'bürger',
|
||||
r'umwelt',
|
||||
r'gemeinschaft',
|
||||
]
|
||||
for pattern in wg_patterns:
|
||||
if re.search(pattern, raw_lower):
|
||||
return 'Wählergemeinschaft', True
|
||||
|
||||
# Unbekannt → Wählergemeinschaft
|
||||
return 'Wählergemeinschaft', True
|
||||
|
||||
|
||||
def parse_html(html_content: str) -> list[Kandidat]:
|
||||
"""Parst die HTML-Seite und extrahiert alle Kandidaten mit Antworten."""
|
||||
soup = BeautifulSoup(html_content, 'html.parser')
|
||||
kandidaten = []
|
||||
|
||||
# Finde die Haupttabelle mit allen Antworten
|
||||
# Die Tabelle hat Spalten: PLZ, Kommune, Landkreis, Vorname, Nachname, Partei, PDF, dann 12 Spalten für 6 Fragen (Ja/Nein + Erläuterung)
|
||||
|
||||
tables = soup.find_all('table')
|
||||
|
||||
for table in tables:
|
||||
rows = table.find_all('tr')
|
||||
|
||||
for row in rows[1:]: # Skip header
|
||||
cells = row.find_all('td')
|
||||
|
||||
# Mindestens 19 Spalten erwartet (PLZ bis Frage 6 Erläuterung)
|
||||
if len(cells) < 19:
|
||||
continue
|
||||
|
||||
try:
|
||||
# Extrahiere Basisdaten
|
||||
plz = cells[0].get_text(strip=True)
|
||||
kommune = cells[1].get_text(strip=True)
|
||||
landkreis = cells[2].get_text(strip=True)
|
||||
vorname = cells[3].get_text(strip=True)
|
||||
nachname = cells[4].get_text(strip=True)
|
||||
partei_raw = cells[5].get_text(strip=True)
|
||||
|
||||
# PDF-Link
|
||||
pdf_link = cells[6].find('a')
|
||||
pdf_url = pdf_link['href'] if pdf_link else None
|
||||
|
||||
# Antworten (6 Fragen × 2 Spalten = 12 Spalten ab Index 7)
|
||||
antworten = {}
|
||||
for i in range(6):
|
||||
ja_nein_idx = 7 + i * 2
|
||||
erlaeuterung_idx = 8 + i * 2
|
||||
|
||||
if ja_nein_idx < len(cells) and erlaeuterung_idx < len(cells):
|
||||
ja_nein = cells[ja_nein_idx].get_text(strip=True)
|
||||
erlaeuterung = cells[erlaeuterung_idx].get_text(strip=True)
|
||||
|
||||
# Normalisiere Ja/Nein
|
||||
if ja_nein.lower() in ['ja', 'yes']:
|
||||
ja_nein = 'Ja'
|
||||
elif ja_nein.lower() in ['nein', 'no']:
|
||||
ja_nein = 'Nein'
|
||||
else:
|
||||
ja_nein = None
|
||||
|
||||
antworten[i + 1] = (ja_nein, erlaeuterung if erlaeuterung and erlaeuterung != '/' else None)
|
||||
|
||||
if vorname and nachname and partei_raw:
|
||||
kandidaten.append(Kandidat(
|
||||
vorname=vorname,
|
||||
nachname=nachname,
|
||||
plz=plz,
|
||||
kommune=kommune,
|
||||
landkreis=landkreis,
|
||||
partei_raw=partei_raw,
|
||||
pdf_url=pdf_url,
|
||||
antworten=antworten
|
||||
))
|
||||
|
||||
except (IndexError, KeyError) as e:
|
||||
continue
|
||||
|
||||
return kandidaten
|
||||
|
||||
|
||||
def fetch_and_parse(url: str) -> list[Kandidat]:
|
||||
"""Lädt die Webseite und parst sie."""
|
||||
response = requests.get(url, timeout=30)
|
||||
response.raise_for_status()
|
||||
return parse_html(response.text)
|
||||
|
||||
|
||||
def init_db(db_path: Path) -> sqlite3.Connection:
|
||||
"""Initialisiert die Datenbank mit Schema."""
|
||||
schema_path = Path(__file__).parent / 'schema.sql'
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
|
||||
with open(schema_path) as f:
|
||||
conn.executescript(f.read())
|
||||
|
||||
conn.commit()
|
||||
return conn
|
||||
|
||||
|
||||
def save_to_db(conn: sqlite3.Connection, kandidaten: list[Kandidat]):
|
||||
"""Speichert Kandidaten und Antworten in der Datenbank."""
|
||||
cursor = conn.cursor()
|
||||
|
||||
for k in kandidaten:
|
||||
partei_norm, ist_wg = normalize_partei(k.partei_raw)
|
||||
|
||||
# Kandidat einfügen
|
||||
cursor.execute("""
|
||||
INSERT OR REPLACE INTO kandidaten
|
||||
(vorname, nachname, plz, kommune, landkreis, partei_raw, partei_normalisiert, ist_waehlergemeinschaft, pdf_url)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (k.vorname, k.nachname, k.plz, k.kommune, k.landkreis, k.partei_raw, partei_norm, ist_wg, k.pdf_url))
|
||||
|
||||
kandidat_id = cursor.lastrowid
|
||||
|
||||
# Antworten einfügen
|
||||
for frage_nr, (ja_nein, erlaeuterung) in k.antworten.items():
|
||||
cursor.execute("""
|
||||
INSERT OR REPLACE INTO antworten_raw (kandidat_id, frage_id, antwort_kurz, antwort_erlaeuterung)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""", (kandidat_id, frage_nr, ja_nein, erlaeuterung))
|
||||
|
||||
conn.commit()
|
||||
return len(kandidaten)
|
||||
|
||||
|
||||
def main():
|
||||
"""Hauptfunktion."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='GWÖ-Wahlprüfsteine Scraper')
|
||||
parser.add_argument('--url', default='https://germany.econgood.org/wahlpruefsteine-zu-den-bayerischen-kommunalwahlen-2026',
|
||||
help='URL der Wahlprüfsteine-Seite')
|
||||
parser.add_argument('--html', type=Path, help='Lokale HTML-Datei statt URL')
|
||||
parser.add_argument('--db', type=Path, default=Path(__file__).parent / 'wahlpruefsteine.db',
|
||||
help='Pfad zur SQLite-Datenbank')
|
||||
parser.add_argument('--verbose', '-v', action='store_true', help='Ausführliche Ausgabe')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# HTML laden
|
||||
if args.html:
|
||||
print(f"Lade lokale Datei: {args.html}")
|
||||
with open(args.html) as f:
|
||||
kandidaten = parse_html(f.read())
|
||||
else:
|
||||
print(f"Lade URL: {args.url}")
|
||||
kandidaten = fetch_and_parse(args.url)
|
||||
|
||||
print(f"Gefunden: {len(kandidaten)} Kandidat:innen")
|
||||
|
||||
if args.verbose:
|
||||
for k in kandidaten:
|
||||
partei_norm, _ = normalize_partei(k.partei_raw)
|
||||
print(f" - {k.vorname} {k.nachname} ({k.kommune}) → {partei_norm}")
|
||||
|
||||
# In DB speichern
|
||||
conn = init_db(args.db)
|
||||
count = save_to_db(conn, kandidaten)
|
||||
conn.close()
|
||||
|
||||
print(f"Gespeichert: {count} Kandidat:innen in {args.db}")
|
||||
|
||||
# Statistik
|
||||
conn = sqlite3.connect(args.db)
|
||||
cursor = conn.cursor()
|
||||
|
||||
print("\nPartei-Verteilung:")
|
||||
for row in cursor.execute("""
|
||||
SELECT partei_normalisiert, COUNT(*) as n
|
||||
FROM kandidaten
|
||||
GROUP BY partei_normalisiert
|
||||
ORDER BY n DESC
|
||||
"""):
|
||||
print(f" {row[0]}: {row[1]}")
|
||||
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Loading…
Reference in New Issue
Block a user