aufwandsspreizung (wip)
versuch, aufwand zeitleich besser zu streuen. unklar, ob es funktioniert, daher auskommentiert
This commit is contained in:
parent
de93cb1aa1
commit
59534a98f0
@ -383,13 +383,14 @@ class Elterndienstplaner:
|
||||
fairness_abweichung_lokal: Dict,
|
||||
fairness_abweichung_global: Dict,
|
||||
fairness_abweichung_gesamt_global: Dict,
|
||||
fairness_abweichung_gesamt_lokal: Dict
|
||||
fairness_abweichung_gesamt_lokal: Dict,
|
||||
spreizungs_penalty: pulp.LpAffineExpression = None
|
||||
) -> None:
|
||||
"""Erstellt die Zielfunktion mit Fairness und Praeferenzen"""
|
||||
"""Erstellt die Zielfunktion mit Fairness, Präferenzen und Spreizung"""
|
||||
objective_terms = []
|
||||
|
||||
gewicht_global = 50
|
||||
gewicht_lokal = 50
|
||||
gewicht_global = 60
|
||||
gewicht_lokal = 40
|
||||
gewicht_f1 = gewicht_global
|
||||
gewicht_f2 = gewicht_lokal
|
||||
gewicht_f3_global = 0.25 * gewicht_global
|
||||
@ -415,6 +416,10 @@ class Elterndienstplaner:
|
||||
if (eltern, tag, dienst) in x and praef == -1:
|
||||
objective_terms.append(20 * dienst.aufwand * x[eltern, tag, dienst])
|
||||
|
||||
# Spreizungs-Penalty (neu)
|
||||
if spreizungs_penalty is not None and spreizungs_penalty != 0:
|
||||
objective_terms.append(spreizungs_penalty)
|
||||
|
||||
if objective_terms:
|
||||
prob += pulp.lpSum(objective_terms)
|
||||
else:
|
||||
@ -427,11 +432,7 @@ class Elterndienstplaner:
|
||||
pulp.LpProblem,
|
||||
Entscheidungsvariablen
|
||||
]:
|
||||
"""Erstellt das PuLP Optimierungsmodell
|
||||
|
||||
Returns:
|
||||
Tuple mit (prob, x, ziel_dienste_lokal, ziel_dienste_global)
|
||||
"""
|
||||
"""Erstellt das PuLP Optimierungsmodell mit Aufwandsspreizung"""
|
||||
print("Erstelle Optimierungsmodell...")
|
||||
|
||||
print("\nDebug: Verfuegbarkeit analysieren...")
|
||||
@ -473,8 +474,17 @@ class Elterndienstplaner:
|
||||
prob, x, ziel_dienste_lokal, "lokal"
|
||||
)
|
||||
|
||||
# Spreizungs-Penalty (neu)
|
||||
#print("\nErstelle Aufwandsspreizungs-Penalty...")
|
||||
#spreizungs_penalty = self._erstelle_spreizungs_penalty_mit_dienstfaktor(
|
||||
# x, fenster_groesse=15, sigma=0.8, spreizungs_gewicht=1.0
|
||||
#)
|
||||
#self.visualisiere_fenster_gewichte_mit_dienstfaktoren(fenster_groesse=15, sigma=0.8)
|
||||
spreizungs_penalty = None
|
||||
|
||||
self._erstelle_zielfunktion(prob, x, fairness_abweichung_lokal, fairness_abweichung_global,
|
||||
fairness_abweichung_gesamt_global, fairness_abweichung_gesamt_lokal)
|
||||
fairness_abweichung_gesamt_global, fairness_abweichung_gesamt_lokal,
|
||||
spreizungs_penalty)
|
||||
|
||||
print(f"Modell erstellt mit {len(x)} Variablen und {len(prob.constraints)} Constraints")
|
||||
return prob, x
|
||||
@ -518,6 +528,184 @@ class Elterndienstplaner:
|
||||
|
||||
return loesung
|
||||
|
||||
def _berechne_fenster_gewichte_glocke(self, num_fenster: int, sigma: float = 0.8) -> List[float]:
|
||||
"""Berechnet Fenster-Gewichte basierend auf Gauß-Glockenkurve
|
||||
|
||||
Mittelfenster: Gewicht ≈ 1.0 (höchste Bestrafung von Häufungen)
|
||||
Randfenster: Gewicht ≈ 0.3 (niedrigere Bestrafung)
|
||||
|
||||
Args:
|
||||
num_fenster: Anzahl der Fenster
|
||||
sigma: Steile der Kurve (0.3=spitz, 1.5=flach)
|
||||
|
||||
Returns:
|
||||
Liste mit Gewichten [0.0...1.0] pro Fenster
|
||||
"""
|
||||
import math
|
||||
|
||||
if num_fenster <= 1:
|
||||
return [1.0]
|
||||
|
||||
fenster_gewichte = []
|
||||
mitte = (num_fenster - 1) / 2.0
|
||||
|
||||
for i in range(num_fenster):
|
||||
# Normalisierte Position relativ zur Mitte (-1...+1)
|
||||
position = (i - mitte) / (mitte + 1)
|
||||
# Gauß-Funktion: exp(-(position² / sigma²))
|
||||
gewicht = math.exp(-(position ** 2) / (sigma ** 2))
|
||||
fenster_gewichte.append(gewicht)
|
||||
|
||||
return fenster_gewichte
|
||||
|
||||
def _erstelle_spreizungs_penalty_mit_dienstfaktor(
|
||||
self,
|
||||
x: Entscheidungsvariablen,
|
||||
fenster_groesse: int = 15,
|
||||
sigma: float = 0.8,
|
||||
spreizungs_gewicht: float = 0.5
|
||||
) -> pulp.LpAffineExpression:
|
||||
"""Erstellt Penalty-Term für Aufwandsspreizung mit Dienstfaktor-Normalisierung
|
||||
|
||||
Verwendet 15-Tage-Sliding-Window (1-Tage-Schritte) zur Erkennung von Häufungen.
|
||||
Höhere Aufwände in der Fenster-Mitte werden stärker bestraft.
|
||||
|
||||
Args:
|
||||
x: Entscheidungsvariablen
|
||||
fenster_groesse: Größe des Sliding-Window in Tagen (Standard: 15)
|
||||
sigma: Parameter für Glockenkurven-Steile
|
||||
spreizungs_gewicht: Gewichtung relative zur Fairness (Standard: 0.5)
|
||||
|
||||
Returns:
|
||||
Penalty-Term als LpAffineExpression für Zielfunktion
|
||||
"""
|
||||
import math
|
||||
from datetime import timedelta
|
||||
|
||||
penalty_terms = []
|
||||
|
||||
planungstage = self.daten.planungszeitraum
|
||||
|
||||
if len(planungstage) < fenster_groesse:
|
||||
print(f" Warnung: Planungszeitraum ({len(planungstage)} Tage) < Fenster ({fenster_groesse} Tage)")
|
||||
return pulp.lpSum([])
|
||||
|
||||
# Berechne Anzahl der Fenster für Glockenkurve
|
||||
num_fenster = len(planungstage) - fenster_groesse + 1
|
||||
fenster_gewichte = self._berechne_fenster_gewichte_glocke(num_fenster, sigma)
|
||||
|
||||
print(f" Spreizungs-Constraint: {len(planungstage)} Planungstage, "
|
||||
f"{fenster_groesse}-Tage-Fenster → {num_fenster} Fenster")
|
||||
print(f" Fenster-Gewichte (Glockenkurve, σ={sigma}):")
|
||||
for i, gew in enumerate(fenster_gewichte):
|
||||
print(f" Fenster {i+1}: {gew:.3f}")
|
||||
|
||||
# Erstelle Sliding-Windows (1-Tage-Schritte)
|
||||
for fenster_idx in range(num_fenster):
|
||||
fenster_start_idx = fenster_idx
|
||||
fenster_ende_idx = fenster_idx + fenster_groesse
|
||||
fenster_start_tag = planungstage[fenster_start_idx]
|
||||
fenster_ende_tag = planungstage[fenster_ende_idx - 1]
|
||||
|
||||
fenster_gewicht = fenster_gewichte[fenster_idx]
|
||||
|
||||
# Für jeden Elterns: Aufwand im Fenster berechnen (normalisiert durch Dienstfaktor)
|
||||
for eltern in self.daten.eltern:
|
||||
dienstfaktor_summe = sum(
|
||||
self.daten.dienstfaktoren[eltern][tag]
|
||||
for tag in planungstage[fenster_start_idx:fenster_ende_idx]
|
||||
)
|
||||
|
||||
# Schutz vor Division-by-Zero
|
||||
if dienstfaktor_summe <= 0:
|
||||
continue
|
||||
|
||||
# Summiere Aufwände aller Dienste im Fenster für diesen Elterns
|
||||
aufwand_im_fenster = pulp.lpSum(
|
||||
dienst.aufwand * x[eltern, tag, dienst]
|
||||
for tag in planungstage[fenster_start_idx:fenster_ende_idx]
|
||||
for dienst in self.daten.dienste
|
||||
if (eltern, tag, dienst) in x
|
||||
) if any(
|
||||
(eltern, tag, dienst) in x
|
||||
for tag in planungstage[fenster_start_idx:fenster_ende_idx]
|
||||
for dienst in self.daten.dienste
|
||||
) else 0
|
||||
|
||||
if aufwand_im_fenster == 0:
|
||||
continue
|
||||
|
||||
# Normalisiere durch Dienstfaktor und multipliziere mit Fenster-Gewicht
|
||||
# penalty = (aufwand / dienstfaktor) × fenster_gewicht
|
||||
penalty_terms.append(
|
||||
spreizungs_gewicht * fenster_gewicht * (aufwand_im_fenster / dienstfaktor_summe)
|
||||
)
|
||||
|
||||
if penalty_terms:
|
||||
return pulp.lpSum(penalty_terms)
|
||||
else:
|
||||
return pulp.lpSum([])
|
||||
|
||||
def visualisiere_fenster_gewichte_mit_dienstfaktoren(
|
||||
self,
|
||||
fenster_groesse: int = 15,
|
||||
sigma: float = 0.8
|
||||
) -> None:
|
||||
"""Debug-Hilfsfunktion: Zeigt Fenster-Gewichte und Dienstfaktoren tabellarisch
|
||||
|
||||
Args:
|
||||
fenster_groesse: Größe des Sliding-Window in Tagen
|
||||
sigma: Parameter für Glockenkurven-Steile
|
||||
"""
|
||||
planungstage = self.daten.planungszeitraum
|
||||
|
||||
if len(planungstage) < fenster_groesse:
|
||||
print(f"Planungszeitraum ({len(planungstage)} Tage) < Fenster ({fenster_groesse} Tage)")
|
||||
return
|
||||
|
||||
num_fenster = len(planungstage) - fenster_groesse + 1
|
||||
fenster_gewichte = self._berechne_fenster_gewichte_glocke(num_fenster, sigma)
|
||||
|
||||
print("\n" + "="*100)
|
||||
print(f"DEBUG: Fenster-Gewichte ({fenster_groesse}-Tage-Sliding-Window, σ={sigma})")
|
||||
print("="*100)
|
||||
|
||||
print(f"\nPlanungszeitraum: {planungstage[0]} bis {planungstage[-1]} ({len(planungstage)} Tage)")
|
||||
print(f"Fenster: {num_fenster} × {fenster_groesse} Tage\n")
|
||||
|
||||
# Kopfzeile
|
||||
print(f"{'Fenster':<12} {'Start':12} {'Ende':12} {'Gewicht':>10} {'→ Bedeutung':<30}")
|
||||
print("-" * 100)
|
||||
|
||||
for fenster_idx in range(num_fenster):
|
||||
fenster_start_idx = fenster_idx
|
||||
fenster_ende_idx = fenster_idx + fenster_groesse
|
||||
fenster_start_tag = planungstage[fenster_start_idx]
|
||||
fenster_ende_tag = planungstage[fenster_ende_idx - 1]
|
||||
|
||||
fenster_gewicht = fenster_gewichte[fenster_idx]
|
||||
|
||||
# Interpretation
|
||||
if fenster_gewicht > 0.9:
|
||||
bedeutung = "Hoch (Häufungen stark bestraft)"
|
||||
farbe = "\033[91m" # Rot
|
||||
elif fenster_gewicht > 0.7:
|
||||
bedeutung = "Mittel (Häufungen moderat bestraft)"
|
||||
farbe = "\033[93m" # Gelb
|
||||
else:
|
||||
bedeutung = "Niedrig (Häufungen toleriert)"
|
||||
farbe = "\033[92m" # Grün
|
||||
|
||||
reset = "\033[0m"
|
||||
|
||||
print(f"{fenster_idx+1:<12} {str(fenster_start_tag):12} {str(fenster_ende_tag):12} "
|
||||
f"{farbe}{fenster_gewicht:>10.3f}{reset} {bedeutung}")
|
||||
|
||||
print("\n" + "-" * 100)
|
||||
print("Legende:")
|
||||
print(f" {'\033[91m'}Gewicht > 0.9{'\033[0m'}: Häufungen in der Mitte stark bestraft")
|
||||
print(f" {'\033[93m'}Gewicht 0.7-0.9{'\033[0m'}: Häufungen moderat bestraft")
|
||||
print(f" {'\033[92m'}Gewicht < 0.7{'\033[0m'}: Häufungen am Rand toleriert")
|
||||
|
||||
def main() -> None:
|
||||
# New: stats-only mode: ./elterndienstplaner.py --stats-only <eltern.csv> [<vorherige-ausgaben.csv>]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user