From 79003d605690802741dbecc2e3361b7d3ac0db82 Mon Sep 17 00:00:00 2001 From: Dotty Dotter Date: Wed, 29 Apr 2026 23:00:35 +0200 Subject: [PATCH] =?UTF-8?q?feat(#166):=20Ber=C3=BChrungsgruppen-Aufschl?= =?UTF-8?q?=C3=BCsselung=20im=20Stimmverhalten-Tab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stimm-Index pro Beruehrungsgruppe (Matrix-Zeilen A-E) zusaetzlich zur bestehenden Werte-Aufschluesselung (Spalten 1-5). Toggle-Buttons in der 3. Sub-Section schalten zwischen Werte/Gruppen. - `aggregate_stimm_index_pro_gruppe()` analog zu `_pro_wert`, aber gruppiert nach `field[0]` (A-E) statt `field[-1]` (1-5). - `_gruppen_score_for_assessment()` Helper. - `GET /api/auswertungen/stimm-index-pro-gruppe`. - UI-Toggle "Pro GWÖ-Wert" / "Pro Berührungsgruppe" mit `setMatrixAxis()`. - 6 neue Tests, Suite jetzt 995 grün. Beruehrungsgruppen-Labels (aus app/models.py:MATRIX_LABELS gekuerzt): - A: Ausgelagerte Betriebe / Lieferant:innen - B: Finanzpartner:innen / Steuerzahler:innen - C: Politische Führung / Verwaltung / Ehrenamt - D: Bürger:innen und Wirtschaft - E: Staat, Gesellschaft und Natur Co-Authored-By: Claude Opus 4.7 (1M context) --- app/auswertungen.py | 94 ++++++++++++++++++ app/main.py | 17 ++++ app/templates/v2/screens/auswertungen.html | 107 +++++++++++++++++++-- tests/test_auswertungen_stimmverhalten.py | 61 ++++++++++++ 4 files changed, 272 insertions(+), 7 deletions(-) diff --git a/app/auswertungen.py b/app/auswertungen.py index f1093b9..8ca063a 100644 --- a/app/auswertungen.py +++ b/app/auswertungen.py @@ -271,6 +271,16 @@ GWOE_WERTE = { "5": "Transparenz & Demokratie", } +# Zeilen der GWOe-Matrix: {field-prefix → Beruehrungsgruppe-Label}. Aus +# app/models.py:MATRIX_LABELS uebernommen, gekuerzt fuer UI-Spalten. +GWOE_BERUEHRUNGSGRUPPEN = { + "A": "Ausgelagerte Betriebe / Lieferant:innen", + "B": "Finanzpartner:innen / Steuerzahler:innen", + "C": "Politische Führung / Verwaltung / Ehrenamt", + "D": "Bürger:innen und Wirtschaft", + "E": "Staat, Gesellschaft und Natur", +} + def _load_assessments_with_votes( filter_bl: Optional[str] = None, @@ -518,6 +528,22 @@ def _wert_score_for_assessment(matrix: list[dict]) -> dict[str, float]: return {col: sum(vals) / len(vals) for col, vals in by_col.items()} +def _gruppen_score_for_assessment(matrix: list[dict]) -> dict[str, float]: + """Mittelwert pro Beruehrungsgruppe-Zeile (A..E). Field-Prefix ist die + Zeile (`field[0]`), z.B. A1..A5 alle in Gruppe A.""" + by_row: defaultdict[str, list[float]] = defaultdict(list) + for entry in matrix or []: + if not isinstance(entry, dict): + continue + field = entry.get("field") or "" + rating = entry.get("rating") + if not field or rating is None: + continue + if len(field) >= 2 and field[0] in GWOE_BERUEHRUNGSGRUPPEN: + by_row[field[0]].append(float(rating)) + return {row: sum(vals) / len(vals) for row, vals in by_row.items()} + + def aggregate_stimm_index_pro_wert( filter_bl: Optional[str] = None, filter_wp: Optional[str] = None, @@ -588,6 +614,74 @@ def aggregate_stimm_index_pro_wert( } +def aggregate_stimm_index_pro_gruppe( + filter_bl: Optional[str] = None, + filter_wp: Optional[str] = None, + exclude_antragsteller: bool = True, + min_n: int = 5, + db_path: Optional[Path] = None, +) -> dict: + """Pro (Fraktion, Beruehrungsgruppe A-E) ein Stimm-Index analog zu + pro_wert, aber gegen den Gruppen-Score statt den Wert-Score (#166). + + Gruppen-Score eines Antrags = Ø(rating der gwoe_matrix-Felder mit + dem entsprechenden Zeilen-Prefix). Domain: -5..+5 pro Gruppe. + """ + rows = _load_assessments_with_votes(filter_bl, filter_wp, db_path) + + gruppen_namen = list(GWOE_BERUEHRUNGSGRUPPEN.values()) + gruppen_keys = list(GWOE_BERUEHRUNGSGRUPPEN.keys()) + + ja: defaultdict[tuple[str, str], list[float]] = defaultdict(list) + nein: defaultdict[tuple[str, str], list[float]] = defaultdict(list) + parteien_seen: set[str] = set() + + for row in rows: + gruppen_scores = _gruppen_score_for_assessment(row["gwoe_matrix"]) + if not gruppen_scores: + continue + skip = row["antragsteller"] if exclude_antragsteller else set() + for f in row["ja"] - skip: + parteien_seen.add(f) + for row_key, sc in gruppen_scores.items(): + ja[(f, row_key)].append(sc) + for f in row["nein"] - skip: + parteien_seen.add(f) + for row_key, sc in gruppen_scores.items(): + nein[(f, row_key)].append(sc) + + parteien = sorted(parteien_seen) + cells: dict[str, dict[str, dict]] = {} + for p in parteien: + cells[p] = {} + for row_key, gruppen_name in zip(gruppen_keys, gruppen_namen): + n_ja = len(ja[(p, row_key)]) + n_nein = len(nein[(p, row_key)]) + avg_ja = _avg(ja[(p, row_key)]) + avg_nein = _avg(nein[(p, row_key)]) + idx = (round(avg_ja - avg_nein, 2) + if avg_ja is not None and avg_nein is not None else None) + cells[p][gruppen_name] = { + "stimm_index": idx, + "n_ja": n_ja, + "n_nein": n_nein, + "ausreichend": n_ja >= min_n and n_nein >= min_n, + } + + return { + "fraktionen": parteien, + "gruppen": gruppen_namen, + "cells": cells, + "n_assessments_matched": len({r["drucksache"] for r in rows}), + "filter": { + "bundesland": filter_bl, + "wahlperiode": filter_wp, + "exclude_antragsteller": exclude_antragsteller, + "min_n": min_n, + }, + } + + def aggregate_empfehlungs_konsistenz( filter_bl: Optional[str] = None, filter_wp: Optional[str] = None, diff --git a/app/main.py b/app/main.py index b00b0c2..18ca4d6 100644 --- a/app/main.py +++ b/app/main.py @@ -2370,6 +2370,23 @@ async def auswertungen_stimm_index_cross_bl( ) +@app.get("/api/auswertungen/stimm-index-pro-gruppe") +async def auswertungen_stimm_index_pro_gruppe( + bundesland: Optional[str] = None, + wahlperiode: Optional[str] = None, + exclude_antragsteller: bool = True, + min_n: int = 5, +): + """Stimm-Index pro Beruehrungsgruppe (A-E der GWÖ-Matrix-Zeilen) (#166).""" + from .auswertungen import aggregate_stimm_index_pro_gruppe + return aggregate_stimm_index_pro_gruppe( + filter_bl=bundesland, + filter_wp=wahlperiode, + exclude_antragsteller=exclude_antragsteller, + min_n=min_n, + ) + + @app.get("/api/auswertungen/empfehlungs-konsistenz") async def auswertungen_empfehlungs_konsistenz( bundesland: Optional[str] = None, diff --git a/app/templates/v2/screens/auswertungen.html b/app/templates/v2/screens/auswertungen.html index 0da2090..c259546 100644 --- a/app/templates/v2/screens/auswertungen.html +++ b/app/templates/v2/screens/auswertungen.html @@ -262,15 +262,33 @@ table.modal-table th { background: var(--ecg-bg-subtle); font-weight: 700; }
- +

3. Stimm-Index pro GWÖ-Wert

+ margin:1.5rem 0 0.5rem;">3. Stimm-Index pro Matrix-Achse

- Aufschlüsselung nach den fünf GWÖ-Werten (Würde, Solidarität, - Nachhaltigkeit, Gerechtigkeit, Demokratie). Pro Zelle: Stimm-Index analog - Aussage 1, aber gegen den Wert-Score statt den Gesamt-Score. Domain - pro Zelle: −5..+5. + Aufschlüsselung der Stimm-Indizes pro GWÖ-Wert + (Spalten: Würde, Solidarität, Nachhaltigkeit, Gerechtigkeit, + Demokratie) oder pro Berührungsgruppe (Zeilen A–E: + Lieferant:innen, Finanzpartner, Politik, Bürger, Staat). Pro Zelle: + Stimm-Index analog Aussage 1, aber gegen den Achsen-Score statt den + Gesamt-Score. Domain pro Zelle: −5..+5.

+
+ + +
@@ -334,6 +352,36 @@ table.modal-table th { background: var(--ecg-bg-subtle); font-weight: 700; }