From 2cb98d1476ee5715ef559f0c419a6ee3f128c97e Mon Sep 17 00:00:00 2001 From: Jan Hoheisel Date: Tue, 23 Dec 2025 22:14:17 +0100 Subject: [PATCH] refactoring: lokale fairness --- elterndienstplaner.py | 125 ++++++++++++++++++++++++++---------------- 1 file changed, 77 insertions(+), 48 deletions(-) diff --git a/elterndienstplaner.py b/elterndienstplaner.py index d65c47a..d72d489 100755 --- a/elterndienstplaner.py +++ b/elterndienstplaner.py @@ -252,7 +252,7 @@ class Elterndienstplaner: return faktor return 0 - def berechne_faire_zielverteilung(self) -> DefaultDict[str, DefaultDict[Dienst, float]]: + def berechne_faire_zielverteilung_global(self) -> DefaultDict[str, DefaultDict[Dienst, float]]: """Berechnet die faire Zielanzahl von Diensten pro Eltern-Dienst-Kombination basierend auf tatsächlich geleisteten historischen Diensten und deren fairer Umverteilung""" @@ -356,6 +356,55 @@ class Elterndienstplaner: return ziel_dienste + def berechne_faire_zielverteilung_lokal(self) -> DefaultDict[str, DefaultDict[Dienst, float]]: + """Berechnet die lokale faire Zielanzahl von Diensten pro Eltern-Dienst-Kombination + basierend auf Dienstfaktoren und benötigten Diensten im aktuellen Planungsmonat""" + + ziel_dienste_lokal: DefaultDict[str, DefaultDict[Dienst, float]] = \ + defaultdict(lambda: defaultdict(float)) + + print("\nBerechne lokale faire Zielverteilung für aktuellen Monat...") + + # Gesamtdienstfaktor für aktuellen Monat berechnen + gesamt_dienstfaktor_monat = sum( + sum(self.dienstfaktoren.get(e, {}).get(tag, 0) for tag in self.tage) + for e in self.eltern + ) + + if gesamt_dienstfaktor_monat == 0: + print(" WARNUNG: Gesamtdienstfaktor ist 0, keine lokale Zielverteilung möglich") + return ziel_dienste_lokal + + # Für jeden Dienst die lokale faire Verteilung berechnen + for dienst in self.dienste: + # Anzahl benötigter Dienste im aktuellen Monat + benoetigte_dienste_monat = sum( + 1 for tag in self.tage + if dienst in self.benoetigte_dienste.get(tag, []) + ) + # Multipliziere mit Anzahl benötigter Personen pro Dienst + benoetigte_dienste_monat *= dienst.personen_anzahl + + if benoetigte_dienste_monat > 0: + print(f" {dienst.kuerzel}: {benoetigte_dienste_monat} Dienste benötigt") + + for eltern in self.eltern: + # Dienstfaktor für diesen Elternteil im aktuellen Monat + monatlicher_dienstfaktor = sum( + self.dienstfaktoren.get(eltern, {}).get(tag, 0) for tag in self.tage + ) + + if monatlicher_dienstfaktor > 0: + anteil = monatlicher_dienstfaktor / gesamt_dienstfaktor_monat + faire_zuteilung = anteil * benoetigte_dienste_monat + ziel_dienste_lokal[eltern][dienst] = faire_zuteilung + + if faire_zuteilung > 0.1: # Debug nur für relevante Werte + print(f" {eltern}: Faktor={monatlicher_dienstfaktor:.1f} " + f"-> {faire_zuteilung:.2f} Dienste") + + return ziel_dienste_lokal + def erstelle_optimierungsmodell(self) -> Tuple[pulp.LpProblem, Dict[Tuple[str, date, Dienst], pulp.LpVariable]]: """Erstellt das PuLP Optimierungsmodell""" print("Erstelle Optimierungsmodell...") @@ -442,8 +491,9 @@ class Elterndienstplaner: # FAIRNESS-CONSTRAINTS UND ZIELFUNKTION objective_terms = [] - # Berechne faire Zielverteilung - ziel_dienste = self.berechne_faire_zielverteilung() + # Berechne faire Zielverteilungen (global und lokal) + ziel_dienste_global = self.berechne_faire_zielverteilung_global() + ziel_dienste_lokal = self.berechne_faire_zielverteilung_lokal() # Hilfsvariablen für Fairness-Abweichungen fairness_abweichung_lokal = {} # F2 @@ -459,56 +509,35 @@ class Elterndienstplaner: # F1: Globale Fairness & F2: Lokale Fairness for eltern in self.eltern: for dienst in self.dienste: - # Dienstfaktor für aktuellen Monat - monatlicher_dienstfaktor = sum( - self.dienstfaktoren.get(eltern, {}).get(tag, 0) for tag in self.tage + # Tatsächliche Dienste im aktuellen Monat + tatsaechliche_dienste_monat = pulp.lpSum( + x[eltern, tag, dienst] + for tag in self.tage + if (eltern, tag, dienst) in x ) - if monatlicher_dienstfaktor > 0: - # Tatsächliche Dienste im aktuellen Monat - tatsaechliche_dienste_monat = pulp.lpSum( - x[eltern, tag, dienst] - for tag in self.tage - if (eltern, tag, dienst) in x - ) + # F2: Lokale Fairness - nur aktueller Monat + ziel_lokal = ziel_dienste_lokal[eltern][dienst] + if ziel_lokal > 0: + # Lokale Fairness-Constraints + prob += (tatsaechliche_dienste_monat - ziel_lokal <= + fairness_abweichung_lokal[eltern, dienst]) + prob += (ziel_lokal - tatsaechliche_dienste_monat <= + fairness_abweichung_lokal[eltern, dienst]) - # F2: Lokale Fairness - nur aktueller Monat - benoetigte_dienste_monat = sum( - 1 for tag in self.tage - if dienst in self.benoetigte_dienste.get(tag, []) - ) - # Multipliziere mit Anzahl benötigter Personen pro Dienst - benoetigte_dienste_monat *= dienst.personen_anzahl + # F1: Globale Fairness - basierend auf berechneter Zielverteilung + ziel_global = ziel_dienste_global[eltern][dienst] + vorherige_dienste = self.vorherige_dienste[eltern][dienst] - gesamt_dienstfaktor_monat = sum( - sum(self.dienstfaktoren.get(e, {}).get(tag, 0) for tag in self.tage) - for e in self.eltern - ) + if ziel_global > 0: + # Tatsächliche Dienste global (Vergangenheit + geplant) + total_dienste_inkl_vergangenheit = tatsaechliche_dienste_monat + vorherige_dienste - if gesamt_dienstfaktor_monat > 0 and benoetigte_dienste_monat > 0: - erwartete_dienste_lokal = ( - monatlicher_dienstfaktor / gesamt_dienstfaktor_monat - ) * benoetigte_dienste_monat - - # F2: Lokale Fairness-Constraints - prob += (tatsaechliche_dienste_monat - erwartete_dienste_lokal <= - fairness_abweichung_lokal[eltern, dienst]) - prob += (erwartete_dienste_lokal - tatsaechliche_dienste_monat <= - fairness_abweichung_lokal[eltern, dienst]) - - # F1: Globale Fairness - basierend auf berechneter Zielverteilung - ziel_gesamt = ziel_dienste[eltern][dienst] - vorherige_dienste = self.vorherige_dienste[eltern][dienst] - - if ziel_gesamt > 0: - # Tatsächliche Dienste global (Vergangenheit + geplant) - total_dienste_inkl_vergangenheit = tatsaechliche_dienste_monat + vorherige_dienste - - # F1: Globale Fairness-Constraints - prob += (total_dienste_inkl_vergangenheit - ziel_gesamt <= - fairness_abweichung_global[eltern, dienst]) - prob += (ziel_gesamt - total_dienste_inkl_vergangenheit <= - fairness_abweichung_global[eltern, dienst]) + # Globale Fairness-Constraints + prob += (total_dienste_inkl_vergangenheit - ziel_global <= + fairness_abweichung_global[eltern, dienst]) + prob += (ziel_global - total_dienste_inkl_vergangenheit <= + fairness_abweichung_global[eltern, dienst]) # Gewichtung: Jahresanfang F1 stärker, Jahresende F2 stärker # Annahme: September = Jahresanfang, Juli = Jahresende