#!/usr/bin/env python3 """ Ausgabe-Modul für Elterndienstplaner Visualisierung und Export der Ergebnisse """ from datetime import date from collections import defaultdict from typing import Dict, List, DefaultDict from datenmodell import ElterndienstplanerDaten, Dienst, Eltern, Zielverteilung from csv_io import AusgabeWriter class ElterndienstAusgabe: """Ausgabe und Visualisierung der Optimierungsergebnisse""" def __init__(self, daten: ElterndienstplanerDaten) -> None: self.daten = daten # Zwischenergebnisse aus der Optimierung (über Observer-Pattern gesetzt) self.ziel_lokal: Zielverteilung = None self.ziel_global: Zielverteilung = None def setze_zielverteilungen( self, ziel_lokal: Zielverteilung, ziel_global: Zielverteilung ) -> None: """Setzt die Zielverteilungen (Observer-Callback)""" self.ziel_lokal = ziel_lokal self.ziel_global = ziel_global def schreibe_ausgabe_csv(self, datei: str, lösung: Dict[date, Dict[Dienst, List[Eltern]]]) -> None: """Schreibt die Lösung in die ausgabe.csv""" AusgabeWriter.schreibe_ausgabe_csv(datei, lösung, self.daten.planungszeitraum, self.daten.dienste) def drucke_statistiken(self, lösung: Dict[date, Dict[Dienst, List[Eltern]]]) -> None: """Druckt Statistiken zur Lösung""" print("\n" + "="*50) print("STATISTIKEN") print("="*50) # Dienste pro Eltern zählen dienste_pro_eltern = defaultdict(lambda: defaultdict(int)) for tag, tag_dienste in lösung.items(): for dienst, eltern_liste in tag_dienste.items(): for eltern in eltern_liste: dienste_pro_eltern[eltern][dienst] += 1 # Gesamtübersicht print("\nDienste pro Eltern:") for eltern in sorted(self.daten.eltern): gesamt = sum(dienste_pro_eltern[eltern].values()) dienste_detail = ', '.join(f"{dienst.kuerzel}:{dienste_pro_eltern[eltern][dienst]}" for dienst in self.daten.dienste if dienste_pro_eltern[eltern][dienst] > 0) print(f" {eltern:15} {gesamt:3d} ({dienste_detail})") # Dienstfaktor-Analyse print(f"\nDienstfaktoren im Planungszeitraum:") for eltern in sorted(self.daten.eltern): faktor_summe = sum(self.daten.dienstfaktoren[eltern][tag] for tag in self.daten.planungszeitraum) print(f" {eltern:15} {faktor_summe:.1f}") def visualisiere_praeferenz_verletzungen( self, lösung: Dict[date, Dict[Dienst, List[Eltern]]] ) -> None: """Visualisiert verletzte Präferenzen als Tabelle Args: lösung: Die tatsächliche Lösung nach Optimierung """ print("\n" + "="*110) print("PRÄFERENZ-VERLETZUNGEN") print("="*110) # Sammle alle zugeteilten Dienste pro Eltern zugeteilte_dienste = defaultdict(lambda: defaultdict(list)) # eltern -> dienst -> [dates] for tag, tag_dienste in lösung.items(): for dienst, eltern_liste in tag_dienste.items(): for eltern in eltern_liste: zugeteilte_dienste[eltern][dienst].append(tag) # Sammle Präferenzen strukturiert # praeferenzen_pro_eltern_dienst[eltern][dienst] = {datum: präf_wert} praeferenzen_pro_eltern_dienst = defaultdict(lambda: defaultdict(dict)) for (eltern, tag, dienst), präf in self.daten.präferenzen.items(): praeferenzen_pro_eltern_dienst[eltern][dienst][tag] = präf # Berechne Verletzungen verletzungen = defaultdict(lambda: defaultdict(lambda: {'negativ': 0, 'positiv_nicht_erfuellt': 0})) for eltern in sorted(self.daten.eltern): for dienst in self.daten.dienste: zugeteilte_tage = zugeteilte_dienste[eltern][dienst] praeferenzen_dienst = praeferenzen_pro_eltern_dienst[eltern][dienst] if not zugeteilte_tage: continue # Keine Dienste zugeteilt # a) Negative Präferenzen die verletzt wurden for tag in zugeteilte_tage: if tag in praeferenzen_dienst and praeferenzen_dienst[tag] == -1: verletzungen[eltern][dienst]['negativ'] += 1 # b) Positive Präferenzen nicht erfüllt (Dienst an nicht-präferiertem Tag) # Sammle alle Tage mit positiver Präferenz für diesen Dienst positive_praef_tage = {tag for tag, präf in praeferenzen_dienst.items() if präf == 1} if positive_praef_tage: # Es gibt positive Präferenzen # Prüfe ob ALLE zugeteilten Dienste an nicht-präferierten Tagen sind for tag in zugeteilte_tage: if tag not in positive_praef_tage: # Dienst wurde an nicht-präferiertem Tag zugeteilt verletzungen[eltern][dienst]['positiv_nicht_erfuellt'] += 1 # Tabelle ausgeben print(f"\n{'Eltern':<20} ", end='') for dienst in self.daten.dienste: print(f"{dienst.kuerzel:>12}", end='') print() print(f"{'':20} ", end='') for dienst in self.daten.dienste: print(f"{'neg, pos':>12}", end='') print() print("-" * (20 + 12 * len(self.daten.dienste))) gesamt_negativ = defaultdict(int) gesamt_positiv = defaultdict(int) for eltern in sorted(self.daten.eltern): print(f"{eltern:<20} ", end='') for dienst in self.daten.dienste: neg = verletzungen[eltern][dienst]['negativ'] pos = verletzungen[eltern][dienst]['positiv_nicht_erfuellt'] gesamt_negativ[dienst] += neg gesamt_positiv[dienst] += pos # Farbcodierung farbe = "" reset = "" if neg > 0 or pos > 0: farbe = "\033[91m" if neg > 0 else "\033[93m" # Rot für negativ, Gelb für positiv reset = "\033[0m" print(f"{farbe}{neg:>3}, {pos:>3}{reset:>6}", end='') print() # Summenzeile print("-" * (20 + 12 * len(self.daten.dienste))) print(f"{'SUMME':<20} ", end='') for dienst in self.daten.dienste: neg = gesamt_negativ[dienst] pos = gesamt_positiv[dienst] farbe = "" reset = "" if neg > 0 or pos > 0: farbe = "\033[91m" if neg > 0 else "\033[93m" reset = "\033[0m" print(f"{farbe}{neg:>3}, {pos:>3}{reset:>6}", end='') print() print("\nLegende:") print(" neg = Anzahl negativer Präferenzen (abgelehnte Tage), die verletzt wurden") print(" pos = Anzahl Dienste an nicht-präferierten Tagen (obwohl präferierte Tage angegeben waren)") print(" \033[91mRot\033[0m = Negative Präferenz verletzt") print(" \033[93mGelb\033[0m = Positive Präferenz nicht erfüllt") def visualisiere_verteilungen( self, lösung: Dict[date, Dict[Dienst, List[Eltern]]] ) -> None: """Visualisiert die Verteilungen als Tabelle zum Vergleich Args: lösung: Die tatsächliche Lösung nach Optimierung """ if self.ziel_lokal is None or self.ziel_global is None: print("FEHLER: Zielverteilungen wurden nicht gesetzt!") return # Tatsächliche Dienste zählen tatsaechlich = defaultdict(lambda: defaultdict(int)) for tag_dienste in lösung.values(): for dienst, eltern_liste in tag_dienste.items(): for eltern in eltern_liste: tatsaechlich[eltern][dienst] += 1 print("\n" + "="*110) print("VERTEILUNGSVERGLEICH: SOLL vs. IST") print("="*110) for dienst in self.daten.dienste: print(f"\n{dienst.name} ({dienst.kuerzel}):") print(f"{'Eltern':<20} {'Ziel Global':>12} {'Ziel Lokal':>12} {'Tatsächlich':>12} " f"{'Δ Global':>12} {'Δ Lokal':>12}") print("-" * 110) for eltern in sorted(self.daten.eltern): z_global = self.ziel_global[eltern][dienst] z_lokal = self.ziel_lokal[eltern][dienst] ist = tatsaechlich[eltern][dienst] delta_global = ist - z_global delta_lokal = ist - z_lokal # Farbcodierung für Abweichungen (ANSI-Codes) farbe_global = "" farbe_lokal = "" reset = "" if abs(delta_global) > 0.5: farbe_global = "\033[93m" if abs(delta_global) <= 1.0 else "\033[91m" # Gelb oder Rot reset = "\033[0m" if abs(delta_lokal) > 0.5: farbe_lokal = "\033[93m" if abs(delta_lokal) <= 1.0 else "\033[91m" reset = "\033[0m" print(f"{eltern:<20} {z_global:>12.2f} {z_lokal:>12.2f} {ist:>12} " f"{farbe_global}{delta_global:>+12.2f}{reset} {farbe_lokal}{delta_lokal:>+12.2f}{reset}") # Summen summe_z_global = sum(self.ziel_global[e][dienst] for e in self.daten.eltern) summe_z_lokal = sum(self.ziel_lokal[e][dienst] for e in self.daten.eltern) summe_ist = sum(tatsaechlich[e][dienst] for e in self.daten.eltern) print("-" * 110) print(f"{'SUMME':<20} {summe_z_global:>12.2f} {summe_z_lokal:>12.2f} {summe_ist:>12} " f"{summe_ist - summe_z_global:>+12.2f} {summe_ist - summe_z_lokal:>+12.2f}") # Gesamtstatistik print("\n" + "="*110) print("ZUSAMMENFASSUNG") print("="*110) # Maximale Abweichungen finden max_abw_global = 0 max_abw_lokal = 0 for eltern in self.daten.eltern: for dienst in self.daten.dienste: ist = tatsaechlich[eltern][dienst] max_abw_global = max(max_abw_global, abs(ist - self.ziel_global[eltern][dienst])) max_abw_lokal = max(max_abw_lokal, abs(ist - self.ziel_lokal[eltern][dienst])) print(f"Maximale Abweichung von Global-Ziel: {max_abw_global:.2f} Dienste") print(f"Maximale Abweichung von Lokal-Ziel: {max_abw_lokal:.2f} Dienste") print("\nLegende: Δ = Tatsächlich - Ziel (positiv = mehr als Ziel, negativ = weniger als Ziel)")