"""Bericht aus kippfaelle.json: welche fehlenden Stimmen koennen Sitze oder Personen verschieben?""" import csv, json, sys from pathlib import Path stand = sys.argv[1] quelle = Path(sys.argv[2]) if len(sys.argv) > 2 else Path(f"nds/out/stand/{stand}/kippfaelle.json") ziel = Path(sys.argv[3]) if len(sys.argv) > 3 else quelle.with_name("KIPPFAELLE.md") E = json.load(open(quelle, encoding="utf-8")) gem = {r["ags"]: r["gemeinde"] for r in csv.DictReader(open("nds/out/gemeinden.csv", encoding="utf-8"))} KREIS = {} try: for r in csv.DictReader(open(f"nds/out/stand/{stand}/wirkung_wahlen.csv", encoding="utf-8")): KREIS[r["kreis"]] = r["landkreis"] except FileNotFoundError: pass RANG = {"schon bei Referenz": 0, "realistisch": 1, "denkbar": 2, "unrealistisch": 3, "unmoeglich": 4} STUFE = { "A1": "Wechsel sehr wahrscheinlich: alle Schätzungen verschieben einen Sitz (Parallelwahl, Listenlücke oder, " "wo diese nicht greift, gleichmäßig), das amtliche " "Ergebnis hielte nur bei unrealistischer Verteilung", "A2": "Wechsel wahrscheinlich: die Hauptschätzung verschiebt einen Sitz, aber eine andere Schätzung oder " "eine realistische Abweichung ließe das Ergebnis stehen", "B": "je nach Annahme: nur eine Nebenschätzung (gleichmäßig oder Listenlücke) verschiebt einen Sitz", "C": "kippt bei realistischer Abweichung", "D": "kippt bei deutlicher, aber denkbarer Abweichung", "E": "kippt nur bei unrealistischer Abweichung", "F": "nach dieser Rechnung kein Wechsel (vorbehaltlich Fehlmenge und nicht sichtbarer Fehler)", } ORDNUNG = list(STUFE) TYPEN = ["Gemeinde-/Stadtrat", "Ortsrat", "Samtgemeinderat"] def pct(x, nk=1): return "–" if x is None else f"{100 * x:.{nk}f}".replace(".", ",") + " %" def zahl(n): return f"{n:,}".replace(",", ".") def hat_parallel(e): return any(not b["parallel"].startswith("keine") for b in e["bezirke"]) def personen_robust(e): ps = e.get("personen_szenarien", {}) for sz in ("B", "A") if hat_parallel(e) else ("A",): if ps.get(f"{sz}_nur_liste") and ps.get(f"{sz}_nur_liste") == ps.get(f"{sz}_nur_personen"): return ps[f"{sz}_nur_liste"] return "" def personen_moeglich(e): ps = e.get("personen_szenarien", {}) return sorted({v for v in ps.values() if v}) def schaetzungen(e): """Verfuegbare Schaetzungen und ihr Ergebnis: A gleichmaessig, B Parallelwahl, L Listenluecke. Zeigen die Daten eine Listenluecke (L anwendbar), sind die Listen gerade nicht gleichmaessig betroffen; L tritt dann an die Stelle von A.""" s = {} if e.get("luecke_anwendbar"): s["L"] = e.get("szenario_luecke", "") else: s["A"] = e["szenario_prop"] if hat_parallel(e): s["B"] = e["szenario_par"] return s def haupt(e): return "B" if hat_parallel(e) else ("L" if e.get("luecke_anwendbar") else "A") def evidenz(e): """Schaetzungen, die fuer Stufe A1 alle kippen muessen: B (falls Parallelwahl), dazu L oder, wo L nicht anwendbar ist, A.""" s = schaetzungen(e) return list(s) def halt_urteil(e): """Wie realistisch ist es, dass das amtliche Ergebnis haelt, obwohl die Hauptschaetzung kippt? -> (bewertung, art, schwelle) mit art 'schaetzung' (eine andere Schaetzung haelt), 'schwelle' oder 'keine'.""" s = schaetzungen(e) haelt = [k for k, v in s.items() if not v] if haelt: return "realistisch", "schaetzung", haelt k = naechste(e, "haelt nur", nur_partei=True) if k: return k[3]["bewertung"], "schwelle", k return ("unrealistisch" if e.get("haltbar", True) else "unmoeglich"), "keine", None def stufe_partei(e): s = schaetzungen(e) if s[haupt(e)]: alle = all(s[k] for k in evidenz(e)) return "A2" if not alle or RANG[halt_urteil(e)[0]] <= 2 else "A1" if any(s.values()): return "B" werte = [RANG[x["bewertung"]] for x in e["schwellen"] if x["art"] == "kippt"] m = min(werte, default=9) return "C" if m <= 1 else "D" if m == 2 else "E" if werte else "F" def ueberwiegend_liste(e): """Fehlmenge nach dem Listenanteil der uebrigen Bezirke zu mindestens 80 % Listenstimmen.""" ls = [b["liste"] for b in e["bezirke"] if b.get("liste")] return bool(ls) and all(l["fehlmenge_liste"] >= 0.8 * b["fehlend"] for l, b in zip(ls, [b for b in e["bezirke"] if b.get("liste")])) def stufe_person(e): if personen_robust(e): return "A2" if personen_moeglich(e): s = "C" else: werte = [RANG[p["bewertung"]] for p in e["personen"]] m = min(werte, default=9) s = "C" if m <= 1 else "D" if m == 2 else "E" if werte else "F" # Fehlen ueberwiegend Listenstimmen, bleibt die Reihenfolge der Personen in der Regel unberuehrt. if s == "C" and ueberwiegend_liste(e): s = "D" return s def stufe(e): return min(stufe_partei(e), stufe_person(e), key=ORDNUNG.index) def naechste(e, art="kippt", nur_partei=False): """Schwelle mit der realistischsten Abweichung (Partei oder Person).""" kand = [] for s in e["schwellen"]: if s["art"] == art and s["ziel"] is not None: kand.append((RANG[s["bewertung"]], abs(s["ziel"] - s["erw"]), "p", s)) if art == "kippt" and not nur_partei: for p in e["personen"]: if p["ziel"] is not None: kand.append((RANG[p["bewertung"]], abs(p["ziel"] - p["r"]), "k", p)) kand.sort(key=lambda x: (x[0], x[1])) return kand[0] if kand else None def schwelle_text(k, art="kippt"): if not k: return ("hält bei keiner Verteilung der Fehlmenge" if art == "haelt nur" else "keine Schwelle innerhalb der Fehlmenge") _, _, typ, s = k if typ == "p": return (f"{s['partei']} mit {pct(s['ziel'])} statt erwartet {pct(s['erw'])} im Bezirk " f"({s['stimmen_noetig']} statt {s['stimmen_ref']} der fehlenden Stimmen, " f"gezählt {pct(s.get('anteil_gezaehlt'))}; {s['bewertung']})" + (f" → {s['was']}" if art == "kippt" else "")) return (f"{s['kandidat']} ({s['partei']}) mit {pct(s['ziel'])} statt {pct(s['r'])} Personenstimmen-Anteil " f"im Bezirk ({s['bewertung']}) → zieht an {s['letzter']} vorbei") NAME = {"A": "gleichmäßig", "B": "nach Parallelwahl", "L": "nach Listenlücke"} def halt_md(e): """Haelt das amtliche Ergebnis, obwohl die Hauptschaetzung kippt?""" urteil, art, x = halt_urteil(e) if art == "schaetzung": return "hält bei Schätzung " + " und ".join(f"{k} ({NAME[k]})" for k in x) if art == "schwelle": return "amtliches Ergebnis hielte nur, wenn " + schwelle_text(x, "haelt nur") return ("hält bei keiner Verteilung der Fehlmenge" if urteil == "unmoeglich" else "hält nur bei einer ganz anderen Verteilung der Fehlmenge") def titel(e): return f"{gem.get(e['ags'], e['ags'])} – {e['wahl']}" for e in E: e["stufe"] = stufe(e) e["anteil"] = e["M"] / e["stimmen"] e["kreis"] = KREIS.get(e["ags"][:5], "") out = [] w = out.append w("# Kippfälle: Können die fehlenden Stimmen Sitze oder Mandate verschieben?") w("") w(f"Kommunalwahl Niedersachsen 13.09.2026 · Stand `{stand}` · Grundlage: `nds/out/stand/{stand}/zettelcheck.csv` " "(Bezirke mit Urteil „Stimmen fehlen“ oder „Tendenz: Stimmen fehlen“) und die Rohdaten desselben Stands. " "Die Sitzverteilung ist unabhängig von `kw_ebenen.py` nachgerechnet (`nds/out/kippfaelle.py`, " "Bericht `nds/out/kippfaelle_bericht.py`).") w("") w("## Worum es geht") w("") w("Ein Befund „Stimmen fehlen“ wird erst dann erheblich, wenn die fehlenden Stimmen die Sitzverteilung " "oder die gewählten Personen ändern könnten. Für jede betroffene Wahl steht hier deshalb:") w("") w("1. **Knappheit:** Wie viele Stimmen fehlten der nächsten Liste zu einem weiteren Sitz?") w("2. **Schätzung A, gleichmäßig:** Die fehlenden Stimmen werden so verteilt, wie der Bezirk sonst gewählt hat " "(alle Listen gleich stark betroffen, Ostercappeln-Muster).") w("3. **Schätzung B, nach Parallelwahl:** Erwartet wird die Parteienverteilung der Parallelwahl im selben Bezirk " "(gleiche Wählerzahl ±6 %). Herausgerechnet ist der Unterschied, den beide Wahlen in den *übrigen* Bezirken " "üblicherweise haben. Die Fehlmenge geht an die Listen, die im Bezirk unter dieser Erwartung liegen. " "So zeigt sich, wenn eine einzelne Liste ausgefallen ist, etwa weil ihre Listenstimmen nicht erfasst wurden.") w("4. **Schätzung L, Listenlücke:** Fehlen einer Liste die Listenstimmen, liegt ihr Verhältnis von Listen- " "zu Personenstimmen im Bezirk unter dem der übrigen Bezirke derselben Wahl. Die Lücke je Liste ist " "Personenstimmen im Bezirk mal dieses Verhältnis minus gezählte Listenstimmen. Angewandt nur mit mindestens " "drei Vergleichsbezirken und wenn die Lücken mindestens die Hälfte der Fehlmenge erklären; den Rest bekommen " "die Listen wie in Schätzung A. Diese Schätzung stützt sich allein auf die Zahlen der Wahl selbst.") w("5. **Schwelle:** Welchen Anteil der fehlenden Stimmen müsste eine Liste (oder ein Kandidat) bekommen, " "damit sich etwas ändert, und wie weit liegt das vom zu erwartenden Anteil entfernt?") w("") w("**Bewertung.** Zwei Maßstäbe, es zählt der strengere. (1) Der Anteil der Liste im *vollständigen* Bezirk " "(gezählte plus fehlende Stimmen) gegen den erwarteten Anteil (bereinigte Parallelwahl, sonst heutiger Anteil " "im Bezirk). (2) Der Anteil der Liste an den *fehlenden* Stimmen gegen den erwarteten – gemessen an der " "näheren der Schätzungen A und B. Maßstab 2 ist nötig, weil die gezählten Stimmen feststehen: 4 Punkte im " "vollständigen Bezirk können 20 Punkte unter den fehlenden Stimmen sein. Für beide gilt: **realistisch** heißt " "höchstens das 1,75-Fache (bzw. 1/1,75) *und* höchstens 10 Prozentpunkte Abstand – Beispiel: 12 % erwartet, " "20 % nötig; Abstände bis 1 Punkt (bei den fehlenden Stimmen: bis 3 Stimmen) gelten immer als realistisch. " "**Denkbar** heißt bis zum 2,5-Fachen und 15 Punkte, alles darüber **unrealistisch**. Für einzelne Kandidaten " "gelten 0,5 bzw. 1 Punkt als Untergrenze. Kippt das Ergebnis schon bei der Hauptschätzung, steht umgekehrt " "da, wie weit man abweichen müsste, damit das amtliche Ergebnis *hält*.") w("") w("**Diese Grenzen sind gesetzt, nicht hergeleitet.** Wie ungewöhnlich eine Verteilung fehlender Stimmen sein " "darf, lässt sich aus offenen Daten nicht bestimmen. 1,75-fach und 10 Punkte sind eine Annahme, ebenso die " "Aufgreifschwelle des Zettelchecks (0,25 Stimmen je Zettel unter dem Median) und die Schätzung der Fehlmenge. " "Die Stufen ordnen deshalb nach Plausibilität; sie sagen nicht, dass ein Ergebnis falsch ist, und eine " "niedrige Stufe sagt nicht, dass es stimmt.") w("") w("Innerhalb einer Liste wird die Fehlmenge auf Listen- und Personenstimmen aufgeteilt. Maßstab ist der " "Anteil der Listenstimmen in den übrigen Bezirken derselben Wahl. In den bisher geprüften Fällen fehlten " "meist Listenstimmen.") w("") w("Sitzverteilung nach § 36 NKWG: Hare/Niemeyer, Mehrheitsklausel (Abs. 3), Aufteilung Liste/Personen " "nach Hare/Niemeyer (Abs. 4), Personensitze nach Stimmenzahl, Listensitze nach Listenplatz. " f"Die Rechnung reproduziert die votemanager-Sitzverteilung in " f"{sum(1 for e in E if e['ok_sitze'])} von {len(E)} Wahlen exakt, die gewählten Personen in " f"{sum(1 for e in E if e['ok_pers'])} von {sum(1 for e in E if e['ok_pers'] is not None and not e['mehrere_wb'])} " "prüfbaren Wahlen. Die Abweichungen erklärt der Abschnitt „Grenzen“.") w("") w("| Stufe | Bedeutung |") w("|---|---|") for k, v in STUFE.items(): w(f"| {k} | {v} |") w("") # ---------------------------------------------------------------- Uebersicht w("## Auf einen Blick") w("") for t in TYPEN: T = [e for e in E if e["typ"] == t] zaehl = {s: sum(1 for e in T if e["stufe"] == s) for s in STUFE} w(f"- **{t}** ({len(T)} Wahlen mit fehlenden Stimmen): " + ", ".join(f"{zaehl[s]}× {s}" for s in STUFE if zaehl[s])) w("") w("Die folgenden Tabellen zeigen je Ebene die zehn Wahlen mit dem größten Anteil fehlender Stimmen. " "Weitere Wahlen der Stufen A bis C stehen im Anschluss.") w("") def zeile(e): sp = stufe_partei(e) was = schaetzungen(e)[haupt(e)] pr = personen_robust(e) kn = e["knapp"][0] if e["knapp"] else None knapp = f"{kn['partei']} +{kn['stimmen']} (zu Lasten {kn['auf_kosten']})" if kn else "–" teile = [] if sp == "B": teile.append("; ".join(f"nur Schätzung {k} ({NAME[k]}): {v}" for k, v in schaetzungen(e).items() if v) + f"; nach Schätzung {haupt(e)} hält das Ergebnis") elif was: teile.append(halt_md(e)) elif sp in ("C", "D"): teile.append(schwelle_text(naechste(e, nur_partei=True))) if not pr and stufe_person(e) in ("C", "D") and ORDNUNG.index(stufe_person(e)) <= ORDNUNG.index(stufe_partei(e)): pm = personen_moeglich(e) if pm: teile.append("Personenwechsel nur bei bestimmter Annahme zu Listen-/Personenstimmen: " + pm[0]) else: k = [p for p in e["personen"] if RANG[p["bewertung"]] <= 2] if k: p = min(k, key=lambda p: RANG[p["bewertung"]]) teile.append(f"{p['kandidat']} ({p['partei']}) bräuchte {pct(p['ziel'])} statt {pct(p['r'])} " f"Personenstimmen im Bezirk ({p['bewertung']}) → vor {p['letzter']}") if not teile: teile.append(schwelle_text(naechste(e, nur_partei=True))) if e["los"]: teile.append("Losentscheid in der amtlichen Verteilung") if any(p.get("gleichauf") for p in e["personen"]): teile.append("Stimmengleichheit an der Personengrenze") aend = "; ".join(x for x in [was, ("Personen: " + pr) if pr else ""] if x) or "–" return (f"| {e['stufe']} | {titel(e)} | {e['sitze']} | {zahl(e['M'])} ({pct(e['anteil'])}) | {knapp} | " f"{aend} | {' · '.join(teile)} |") kopf = ("| Stufe | Wahl | Sitze | fehlend | knappster nächster Sitz | Änderung bei Schätzung | " "Abweichung, die nötig ist |\n|---|---|---|---|---|---|---|") gezeigt = set() for t in TYPEN: T = sorted([e for e in E if e["typ"] == t], key=lambda e: -e["anteil"])[:10] w(f"### {t}: die zehn größten Fehlanteile") w("") w(kopf) for e in T: w(zeile(e)) gezeigt.add((e["ags"], e["wahl"])) w("") rest = sorted([e for e in E if (e["ags"], e["wahl"]) not in gezeigt and e["stufe"] in ("A1", "A2", "B", "C")], key=lambda e: (ORDNUNG.index(e["stufe"]), -e["anteil"])) if rest: w("### Weitere Wahlen der Stufen A1 bis C") w("") w(kopf) for e in rest: w(zeile(e)) w("") # ---------------------------------------------------------------- Einzelfaelle w("## Einzelfälle") w("") w("Alle Wahlen der Stufen A1 bis D, sortiert nach Stufe und Fehlanteil.") w("") for e in sorted([e for e in E if e["stufe"] in ("A1", "A2", "B", "C", "D")], key=lambda e: (ORDNUNG.index(e["stufe"]), -e["anteil"])): w(f"### {e['stufe']} · {titel(e)}") w("") status = "vorläufig" if "vorläufig" in e["status"] else "festgestellt" w(f"{e['kreis']} · {e['typ']} · {e['sitze']} Sitze · {zahl(e['stimmen'])} gültige Stimmen · " f"fehlend {zahl(e['M'])} ({pct(e['anteil'])}) · Sitzverteilung laut votemanager {status}" + ("" if e["api_vollst"] else " (votemanager zeigt nur den Zwischenstand der Wahlnacht)") + (" · mehrere Wahlbereiche, Personen nicht bewertet" if e["mehrere_wb"] else "") + f" · [Ergebnis]({e['link']})") w("") for b in e["bezirke"]: w(f"- Bezirk **{b['nr']} {b['name']}**: {b['C2']} gültige Stimmzettel, {b['D']} Stimmen, " f"fehlend ≈ {b['fehlend']} · Urteil: {b['urteil']} · Vergleich: {b['parallel']}") w("") w("| Liste | Sitze (Quote) | im Bezirk | davon Listenstimmen | erwartet (Parallelwahl) |") w("|---|---|---|---|---|") b0 = e["bezirke"][0] for p in e["parteien"]: bp = b0["parteien"].get(p["partei"], {}) w(f"| {p['partei']} | {p['sitze']} ({p['rest']:.2f}) | {bp.get('ist', '–')} " f"({pct(bp['ist'] / b0['D']) if bp else '–'}) | {bp.get('liste', '–')} | {pct(bp.get('ref'))} |") w("") if len(e["bezirke"]) > 1: w("*(Tabelle zeigt den ersten Bezirk; die Rechnung nutzt alle.)*") w("") if e["knapp"]: w("Knappheit: " + "; ".join(f"{k['partei']} fehlten {k['stimmen']} Stimmen zu einem weiteren Sitz " f"(zu Lasten {k['auf_kosten']})" for k in e["knapp"][:3]) + ".") w("") w(f"- Schätzung A (gleichmäßig): {e['szenario_prop'] or 'Sitze je Liste unverändert'}" + (" – tritt zurück, weil die Daten eine Listenlücke zeigen" if e.get("luecke_anwendbar") else "")) w(f"- Schätzung B (Parallelwahl): " + ((e['szenario_par'] or 'Sitze je Liste unverändert') if hat_parallel(e) else "keine passende Parallelwahl")) lu = e.get("luecke", []) if e.get("luecke_anwendbar"): w(f"- Schätzung L (Listenlücke): {e['szenario_luecke'] or 'Sitze je Liste unverändert'} · Lücken: " + "; ".join(f"Bezirk {x['nr']}: " + ", ".join(f"{k} {v}" for k, v in x.get("je_liste", {}).items()) for x in lu)) else: w("- Schätzung L (Listenlücke): nicht anwendbar (" + "; ".join(f"Bezirk {x['nr']}: Lücke {x['luecke']}, {x['vergleichsbezirke']} Vergleichsbezirke" for x in lu) + ")") if e["szenario_par" if hat_parallel(e) else "szenario_prop"]: w(f"- Hält das amtliche Ergebnis trotzdem? {halt_md(e)}" + ("" if e.get("haltbar", True) else " – rechnerisch bei keiner Verteilung der Fehlmenge")) ps = e.get("personen_szenarien", {}) if any(ps.values()): w("- Gewählte Personen (bei gleicher Sitzzahl je Liste): " + "; ".join(f"{k.replace('_', ', ').replace('A,', 'A:').replace('B,', 'B:').replace('geschaetzt', 'Listenanteil geschätzt').replace('nur, liste', 'nur Listenstimmen').replace('nur, personen', 'nur Personenstimmen')} → {v}" for k, v in ps.items() if v) + (" – **robust**" if personen_robust(e) else " – hängt davon ab, ob Listen- oder Personenstimmen fehlen")) w("") sch = sorted([x for x in e["schwellen"] if x["ziel"] is not None], key=lambda x: (RANG[x["bewertung"]], abs(x["ziel"] - x["erw"]))) if sch: w("| Liste | Anteil im Bezirk: erwartet → nötig (jetzt) | Anteil an den fehlenden Stimmen | " "in anderen Bezirken | Bewertung (Bezirk / fehlende) | Folge |") w("|---|---|---|---|---|---|") for x in sch: andere = (f"{pct(x['andere_min'], 0)}–{pct(x['andere_max'], 0)}, " f"{x['andere_erreicht']} von {x['andere_n']} Bezirken so weit") if x["andere_n"] else "–" folge = x["was"] if x["art"] == "kippt" else f"amtliches Ergebnis hält (sonst: {x['was']})" w(f"| {x['partei']} | {pct(x['erw'])} → **{pct(x['ziel'])}** ({pct(x['jetzt'])}) | " f"{pct(x['s'])} statt {pct(x['s_ref'])} ({x['stimmen_noetig']} statt {x['stimmen_ref']}) | " f"{andere} | **{x['bewertung']}** ({x.get('bewertung_voll', '–')} / " f"{x.get('bewertung_fehlend', '–')}) | {folge} |") w("") nie = [x for x in e["schwellen"] if x["art"] == "haelt nie"] if nie and halt_urteil(e)[1] != "schaetzung": w("Allein über den Anteil von " + ", ".join(x["partei"] for x in nie) + " (übrige Listen im Verhältnis der Schätzung) lässt sich das amtliche Ergebnis nicht halten.") w("") pe = [p for p in e["personen"] if p["bewertung"] in ("schon bei Referenz", "realistisch", "denkbar") or p.get("gleichauf")] if pe: w("Personen an der Grenze:") w("") for p in pe: gl = " – **Stimmengleichheit**, entschieden vermutlich per Los" if p.get("gleichauf") else "" w(f"- {p['partei']}: **{p['kandidat']}** ({p['stimmen']} Stimmen, nicht gewählt) gegen " f"**{p['letzter']}** ({p['letzter_stimmen']}, gewählt){gl}. Im Fehlbezirk: {p['im_bezirk']} " f"Personenstimmen ({pct(p['r'])} der Stimmen). Nötig: {pct(p['ziel'])} im vollständigen Bezirk, " f"also {p['prop'] + p['bedarf'] if p['bedarf'] is not None else '–'} statt {p['prop']} der fehlenden Stimmen → {p['bewertung']}.") w("") # ---------------------------------------------------------------- Grenzen w("## Grenzen der Aussage") w("") w("- Die Fehlmenge ist eine Schätzung: Median der Stimmen je Stimmzettel der übrigen Bezirke mal gültige " "Stimmzettel minus gezählte Stimmen. Liegt der Fehler in der Zettelzahl, fehlen keine Stimmen. Hier stehen " "deshalb nur Bezirke, die `zettelcheck` als „Stimmen fehlen“ einordnet.") w("- Schätzung B setzt voraus, dass die Wähler im Bezirk beide Wahlen ähnlich unterschiedlich wählen wie in " "den übrigen Bezirken. Listen ohne Gegenstück in der Parallelwahl (lokale Wählergruppen, Einzelbewerber) " "bekommen ihren heutigen Bezirksanteil der Fehlmenge, nur der Rest wird nach der Parallelwahl verteilt.") w("- Personenwechsel hängen davon ab, ob Listen- oder Personenstimmen fehlen. Als robust gilt ein Wechsel nur, " "wenn er in beiden Extremen auftritt. Fehlen nach dem Listenanteil überwiegend Listenstimmen, sind " "Personenschwellen höchstens Stufe D.") w("- Kandidaten mit gleicher Stimmenzahl lassen sich den Listenplätzen nicht sicher zuordnen; sie stehen " "mit beiden Namen und dem Vermerk „gleichauf“. Bei Stimmengleichheit an der Grenze hat vermutlich das Los " "entschieden (§ 36 NKWG); dann kann jede einzelne fehlende Stimme den Ausschlag geben.") w("- Die Schwellen verschieben nur eine Liste gegen alle anderen; die übrigen Listen behalten ihr " "Verhältnis aus der Schätzung. Innerhalb der Liste gehen die Zusatzstimmen wie im Bezirk an Liste und Personen. " "Ob es *irgendeine* Verteilung gibt, bei der das amtliche Ergebnis hält, ist gesondert geprüft (alle Listen " "gleichzeitig); nur dann steht „bei keiner Verteilung“.") w("- Hat eine Wahl nur einen oder zwei Vergleichsbezirke (oft nur den Briefwahlbezirk), stehen Fehlmenge und " "Schätzung B auf schmaler Grundlage. Briefwahlbezirke haben landesweit etwas mehr Stimmen je Zettel als " "Urnenbezirke (Median 2,98 gegen 2,94); die Fehlmenge ist dort eher etwas zu hoch geschätzt.") w("- Die Liste ist nicht abschließend. Bezirke unter der Aufgreifschwelle, Wahlen ohne offene Daten und Fehler, " "die die Stimmen je Zettel nicht verändern (etwa vertauschte Listen), erfasst diese Rechnung nicht.") w("- Bei Wahlen mit mehreren Wahlbereichen (§ 37 NKWG) sind nur die Sitze je Liste bewertet, nicht die " "Personen. Die Sitze je Liste stimmen dort mit votemanager überein.") nicht_ok = [e for e in E if not e["ok_sitze"]] if nicht_ok: w("- Sitzverteilung weicht von votemanager ab: " + "; ".join(f"{titel(e)} (votemanager: {e['api_stand']})" for e in nicht_ok) + ". Dort zeigt votemanager noch den Zwischenstand der Wahlnacht; gerechnet ist mit den vollständigen Daten.") w("- Nicht bewertet: Kreistage (eigene Rechnung, `kippfaelle_kreistag.py`), IVU-Quellen ohne " "votemanager-Sitzangabe (Celle, Wietze, Suhlendorf), Wolfsburg (Ortsräte in einer Datei), " "Emden-Stadtrat und die Regionsversammlung Hannover ohne Sitzangabe.") w("- Das ist eine Rechnung auf offenen Daten, kein Nachweis. Klären lässt sich das nur mit der Niederschrift " "und, wo sie es nicht aufklärt, mit einer Nachzählung. " "Die Einspruchsfrist (§ 46 NKWG) läuft zwei Wochen ab Bekanntmachung.") w("") ziel.write_text("\n".join(out), encoding="utf-8") with open(ziel.with_name("kippfaelle.csv"), "w", encoding="utf-8", newline="") as f: cw = csv.writer(f) cw.writerow(["stufe", "stufe_partei", "stufe_person", "landkreis", "ags", "gemeinde", "wahl", "typ", "sitze", "stimmen", "fehlend", "anteil_pct", "knappster_sitz", "schaetzung_a", "schaetzung_b", "schaetzung_l", "personen_robust", "naechste_schwelle", "haelt_nur_wenn", "losentscheid", "bezirke", "link"]) for e in sorted(E, key=lambda e: (ORDNUNG.index(e["stufe"]), -e["anteil"])): kn = e["knapp"][0] if e["knapp"] else None cw.writerow([e["stufe"], stufe_partei(e), stufe_person(e), e["kreis"], e["ags"], gem.get(e["ags"], ""), e["wahl"], e["typ"], e["sitze"], e["stimmen"], e["M"], f"{100 * e['anteil']:.2f}", f"{kn['partei']} +{kn['stimmen']} ({kn['auf_kosten']})" if kn else "", e["szenario_prop"], e["szenario_par"] if hat_parallel(e) else "", (e["szenario_luecke"] or "unverändert") if e.get("luecke_anwendbar") else "nicht anwendbar", personen_robust(e), schwelle_text(naechste(e, nur_partei=True)), halt_md(e) if schaetzungen(e)[haupt(e)] else "", "ja" if e["los"] else "", " ".join(b["nr"] for b in e["bezirke"]), e["link"]]) print("->", ziel) for t in TYPEN: print(t, {s: sum(1 for e in E if e["typ"] == t and e["stufe"] == s) for s in STUFE})