252 lines
10 KiB
Python
252 lines
10 KiB
Python
#!/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)")
|