Compare commits

..

No commits in common. "5acef1e2ae825952d9497ff5869528a03a8b4341" and "f4791cf7bb6df15ddd40f7f0e2206e277d834759" have entirely different histories.

4 changed files with 206 additions and 241 deletions

219
README.md
View File

@ -2,82 +2,52 @@
Automatische Zuteilung von Elterndiensten im Kinderladen unter Berücksichtigung von Fairness und Präferenzen. Automatische Zuteilung von Elterndiensten im Kinderladen unter Berücksichtigung von Fairness und Präferenzen.
## Wie funktioniert die Dienstplanung? ## Verwendung
Der Elterndienstplaner verteilt die anfallenden Dienstzuteilungen für einen Monat automatisch auf die Eltern. Dabei werden folgende Ziele berücksichtigt: ```bash
./elterndienstplaner.py <eingabe.csv> <eltern.csv> <ausgabe.csv> [<vorherige-ausgaben.csv>]
```
### 1. Regeln (Harte Constraints) **Parameter:**
- `eingabe.csv`: Benötigte Dienste und Eltern-Präferenzen für den Planungsmonat
- `eltern.csv`: Dienstfaktoren der Eltern (Anzahl betreuter Kinder)
- `ausgabe.csv`: Hier wird die Zuteilung geschrieben
- `vorherige-ausgaben.csv` (optional): Historische Daten für Fairness über das Jahr
Diese Regeln **müssen** immer eingehalten werden: ## Dienste
- **C1: Maximal einmal pro Woche pro Diensttyp** - Niemand muss z.B. zweimal in einer Woche Frühstücksdienst machen
- **C2: Maximal eine Zuteilung pro Tag** - Niemand bekommt mehrere Dienstzuteilungen am selben Tag
- **C3: Nur bei Verfügbarkeit** - Zuteilungen erfolgen nur bei Verfügbarkeit (keine Abwesenheiten)
- **C4: Alle Dienste werden besetzt** - Jeder benötigte Diensttyp wird an jedem Tag besetzt
### 2. Fairness und Präferenzen (Weiche Constraints)
Diese Ziele werden optimiert, können aber nicht immer perfekt erfüllt werden:
**Fairness:**
- **F1: Faire Jahresverteilung** - Über das ganze Kitajahr hinweg werden Zuteilungen fair verteilt (pro Diensttyp)
- **F2: Faire Monatsverteilung** - Innerhalb eines Monats werden Zuteilungen fair verteilt (pro Diensttyp). **Besonderheit:** Abwesenheitstage werden aus der Dienstpflicht herausgerechnet, um eine gleichmäßigere Verteilung zu erreichen. Die "verpassten" Dienste werden über F1 im Jahresverlauf ausgeglichen.
- **F3: Ausgewogene Diensttypen (Jahr)** - Verhindert über das ganze Jahr, dass einzelne Familien über alle Diensttypen hinweg zu viele Zuteilungen bekommen
- **F4: Ausgewogene Diensttypen (Monat)** - Verhindert im aktuellen Monat, dass einzelne Familien über alle Diensttypen hinweg zu viele Zuteilungen bekommen
**Präferenzen:**
- **P1: Bevorzugte Tage für Diensttypen** - An bestimmten Tagen bevorzugte Diensttypen werden nach Möglichkeit zugeteilt
- **P2: Vermeiden an bestimmten Tagen** - An bestimmten Tagen abgelehnte Diensttypen werden nach Möglichkeit vermieden
Die Fairness bestimmt **wie viele** Zuteilungen jede Familie erhält, die Präferenzen beeinflussen **an welchen Tagen welcher Diensttyp** zugeteilt wird. Selbst bei Ablehnung kann ein Diensttyp an einem bestimmten Tag zugeteilt werden, wenn das für die faire Verteilung nötig ist.
## Diensttypen
- **F** - Frühstücksdienst (täglich, 1 Person) - **F** - Frühstücksdienst (täglich, 1 Person)
- **P** - Putznotdienst (täglich, 1 Person) - **P** - Putznotdienst (täglich, 1 Person)
- **E** - Essensausgabenotdienst (täglich, 1 Person) - **E** - Essensausgabenotdienst (täglich, 1 Person)
- **K** - Kochen (ca. alle 2 Wochen, 1 Person) - **K** - Kochen (ca. alle 2 Wochen, 1 Person)
- **A** - Elternabend (ca. einmal im Monat, 2 Personen) - **A** - Elternabend (nach Bedarf, 2 Personen)
Die Planung erfolgt für einen Kalendermonat. Die Planung erfolgt für einen Kalendermonat.
## Abwesenheiten und Präferenzen angeben (eingabe.csv) ## Eingabedateien
Diese Datei enthält für jeden Tag des Planungsmonats, welche Dienste anfallen und welche Eltern verfügbar sind bzw. Präferenzen haben. ### eingabe.csv
Informationen zu benötigten Diensten und Verfügbarkeit/Präferenzen der Eltern.
**Format:** **Format:**
``` ```
Datum, Wochentag, Dienste, Sarah & Tim, Leon, Maya, ... Datum,Wochentag,Dienste,Eltern1,Eltern2,...
2026-01-06, Montag, FPE, F+, x, , ... 2025-01-06,Montag,FPE,F+,x,...
2026-01-07, Dienstag, FPE, P-, F+P+, , ... 2025-01-07,Dienstag,FPE,P-,F+P+,...
``` ```
**Spalten:** **Spalten:**
1. **Datum** (ISO-Format: YYYY-MM-DD) 1. Datum (ISO-Format: YYYY-MM-DD)
2. **Wochentag** (zur Information) 2. Wochentag (zur Information)
3. **Diensttypen** - Welche Diensttypen an diesem Tag benötigt werden (z.B. "FPE" für Frühstück, Putzen, Essen) 3. Benötigte Dienste (z.B. "FPE" für Frühstück, Putzen, Essen)
4-n. **Eine Spalte pro Familie** - Abwesenheiten und Präferenzen: 4-n. Für jeden Elternteil:
- `x` = nicht verfügbar (Urlaub, Krankheit, etc.) - `x` = nicht verfügbar
- `Kürzel+` = diesen Dienst bevorzugt (z.B. `F+` für Frühstücksdienst bevorzugt) - `F+` = Frühstücksdienst bevorzugt
- `Kürzel-` = diesen Dienst abgelehnt (z.B. `P-` für Putznotdienst abgelehnt) - `P-` = Putznotdienst nur notfalls
- Mehrere kombinierbar: `F+P-E+` (Frühstück bevorzugt, Putzen abgelehnt, Essen bevorzugt) - Mehrere Präferenzen kombinierbar: `F+P-E+`
- Leer = verfügbar, keine Präferenz - Leer = verfügbar, keine Präferenz
**Beispiel:**
```
Datum ,Wochentag ,Dienste,Sarah & Tim,Leon ,Maya
2026-01-06 ,Montag ,FPE ,x , ,F+
2026-01-07 ,Dienstag ,FPE , ,F+P- ,
2026-01-10 ,Freitag ,FPEK ,F+K+ , ,P-
```
- Sarah & Tim sind am 6.1. nicht verfügbar
- Leon bevorzugt am 7.1. Frühstück, lehnt Putzen ab
- Maya bevorzugt am 6.1. Frühstück
- Am 10.1. bevorzugen Sarah & Tim Frühstück oder Kochen
## Weitere Eingabedateien
### eltern.csv ### eltern.csv
Dienstfaktoren (= Anzahl betreuter Kinder) pro Elternteil und Zeitraum. Dienstfaktoren (= Anzahl betreuter Kinder) pro Elternteil und Zeitraum.
@ -85,25 +55,20 @@ Dienstfaktoren (= Anzahl betreuter Kinder) pro Elternteil und Zeitraum.
**Format:** **Format:**
``` ```
Eltern,Beginn,Ende,Faktor,Beginn,Ende,Faktor,... Eltern,Beginn,Ende,Faktor,Beginn,Ende,Faktor,...
Sarah & Tim, 2024-09-01, 2025-07-31, 2 Müller,2024-09-01,2025-07-31,2
Leon, 2024-09-01, 2024-12-31, 1, 2025-01-01, 2025-07-31, 0 Schmidt,2024-09-01,2024-12-31,1,2025-01-01,2025-07-31,0
Maya, 2024-09-01, 2025-07-31, 1
``` ```
**Spalten:** **Spalten:**
1. **Kind-Name(n)** - Bei mehreren Kindern durch & verbunden (z.B. "Sarah & Tim") 1. Elternname (Kind-Name zur Identifikation)
2-4. **Zeitraum 1:** Beginn (Datum), Ende (Datum), Dienstfaktor 2-4. Zeitraum 1: Beginn, Ende, Dienstfaktor
5-7. **Zeitraum 2:** Beginn, Ende, Dienstfaktor (optional, für Änderungen während des Jahres) 5-7. Zeitraum 2: Beginn, Ende, Dienstfaktor (optional)
... ...
**Hinweise:** **Hinweise:**
- Der Dienstfaktor entspricht der Anzahl der Kinder in der Familie (z.B. 2 für "Sarah & Tim", 1 für "Leon")
- Familien mit mehreren Kindern werden als ein Eintrag mit entsprechendem Dienstfaktor geführt
- Bei überlappenden Zeiträumen gilt der letzte Eintrag - Bei überlappenden Zeiträumen gilt der letzte Eintrag
- Außerhalb definierter Zeiträume: Faktor = 0 (keine Dienstpflicht) - Außerhalb definierter Zeiträume: Faktor = 0 (keine Dienstpflicht)
- Faktor = 0 bedeutet: Befreiung von Diensten (z.B. durch Vorstandsamt) - Faktor = 0 bedeutet: Befreiung (z.B. durch Vorstandsamt)
**Beispiel:** Sarah & Tim (Dienstfaktor 2), Leon (Dienstfaktor 1, aber ab Januar 2025 keine Dienstpflicht), Maya (Dienstfaktor 1).
### vorherige-ausgaben.csv (optional) ### vorherige-ausgaben.csv (optional)
@ -113,114 +78,86 @@ Frühere Ausgaben des Programms zur Berechnung der Jahres-Fairness.
**Verwendung:** **Verwendung:**
- Zu Beginn des Kita-Jahres (September): Keine Datei nötig - Zu Beginn des Kita-Jahres (September): Keine Datei nötig
- Ab Oktober: Vorherige Ausgaben anhängen für kumulative Fairness über das Jahr - Ab Oktober: Vorherige Ausgaben anhängen für kumulative Fairness
- Im Jahresverlauf sammeln sich die Ausgaben an - Im Jahresverlauf sammeln sich die Ausgaben an
## Ausgabedatei ## Ausgabe
### ausgabe.csv ### ausgabe.csv
Dienstzuteilungen pro Tag. Diese Datei wird vom Programm erstellt. Zugeteilte Dienste pro Tag.
**Format:** **Format:**
``` ```
Datum,Wochentag,Frühstücksdienst,Putznotdienst,Essensausgabenotdienst,Kochen,Elternabend Datum,Wochentag,Frühstücksdienst,Putznotdienst,Essensausgabenotdienst,Kochen,Elternabend
2026-01-06, Montag, Sarah & Tim, Leon, Maya, , 2025-01-06,Montag,Müller,Schmidt,Weber,,
2026-01-07, Dienstag, Maya, Sarah & Tim, Leon, , 2025-01-07,Dienstag,Weber,Müller,Schmidt,,
``` ```
Jede Zeile entspricht einem Tag, die Spalten enthalten die Kindernamen, denen die jeweiligen Diensttypen zugeteilt wurden. ## Constraints
## Verwendung ### Harte Constraints (müssen erfüllt sein)
```bash - **C1**: Pro Eltern und Dienst maximal **einmal pro Woche** (Mo-So)
./elterndienstplaner.py <eingabe.csv> <eltern.csv> <ausgabe.csv> [<vorherige-ausgaben.csv>] - **C2**: Pro Eltern maximal **ein Dienst pro Tag**
``` - **C3**: Nur **verfügbare** Eltern einteilen
- **C4**: Alle **benötigten Dienste** müssen besetzt werden
**Parameter:** ### Weiche Constraints (werden optimiert)
- `eingabe.csv`: Benötigte Diensttypen und Eltern-Präferenzen für den Planungsmonat
- `eltern.csv`: Dienstfaktoren der Eltern (Anzahl betreuter Kinder)
- `ausgabe.csv`: Hier werden die Zuteilungen geschrieben
- `vorherige-ausgaben.csv` (optional): Historische Zuteilungen für Fairness über das Jahr
## Wie werden die Constraints umgesetzt? **Fairness** (nach Priorität):
- **F1 (Global)**: Dienste proportional zum Dienstfaktor über das **ganze Jahr**
- Berücksichtigt historische Dienste aus `vorherige-ausgaben.csv`
- Gewichtung: 40% (zu Jahresbeginn) → 60% (zu Jahresende)
Dieser Abschnitt erklärt die technische Umsetzung für technisch interessierte Eltern. - **F2 (Lokal)**: Dienste proportional zum Dienstfaktor im **aktuellen Monat**
- Nur aktueller Planungszeitraum
- Gewichtung: 60% (zu Jahresbeginn) → 40% (zu Jahresende)
### Mathematisches Optimierungsproblem - **F3 (Dienstübergreifend)**: Gleiche Gesamtanzahl über alle Diensttypen
- Verhindert Häufung bei einzelnen Eltern
Der Elterndienstplaner formuliert die Dienstverteilung als **lineares Optimierungsproblem**. Das bedeutet: Es gibt viele mögliche Dienstverteilungen, die alle die harten Constraints (C1-C4) erfüllen. Das Programm sucht diejenige, die die Fairness-Ziele am besten erfüllt und Präferenzen berücksichtigt. **Präferenzen** (niedrigere Priorität):
- **P1**: Bevorzugte Dienste (`+`) werden bevorzugt zugeteilt
- **P2**: Abgelehnte Dienste (`-`) werden vermieden
**Entscheidungsvariablen:** Für jeden Tag, jeden Eintrag und jeden Diensttyp gibt es eine Variable: "Wird Sarah & Tim am 6. Januar der Frühstücksdienst zugeteilt?" (Ja/Nein) ### Fairness-Logik
**Constraints (Nebenbedingungen):** Diese schränken die möglichen Lösungen ein: **Beispiel:** Familie Müller hat 2 Kinder, Familie Schmidt 1 Kind.
- **C1 (Wöchentliches Limit):** Für jeden Eintrag und jeden Diensttyp: Die Summe der Zuweisungen pro Woche ≤ 1 **Lokale Fairness (F2):**
- **C2 (Tageslimit):** Für jeden Eintrag und jeden Tag: Die Summe aller Zuweisungen ≤ 1 - Im Januar sollen beide verfügbar sein
- **C3 (Verfügbarkeit):** Wenn im Feld "x" steht, wird die entsprechende Variable auf 0 gesetzt - Müller sollte 2× so viele Dienste bekommen wie Schmidt
- **C4 (Bedarfsdeckung):** Für jeden Tag und Diensttyp: Summe der Zuweisungen = benötigte Personenzahl - Verhindert: Müller bekommt alle Dienste auf einmal
### Zielfunktion: Fairness und Präferenzen **Globale Fairness (F1):**
- Müller war im Dezember im Urlaub → 0 Dienste
- Im Januar sollte Müller aufholen
- Über das Jahr: 2:1 Verhältnis wird ausgeglichen
Die **Zielfunktion** bewertet, wie gut eine Dienstverteilung ist. Das Programm minimiert Abweichungen von fairer Verteilung und berücksichtigt Präferenzen. **Gewichtung im Jahresverlauf:**
- **September-November**: F2 (lokal) stärker → sanftes Einführen
- **Dezember-Mai**: Ausgewogen
- **Juni-Juli**: F1 (global) stärker → Jahresausgleich
**F1 (Globale Fairness):** ## Ausgabe-Statistiken
- Berechnung: Für jeden Eintrag und jeden Diensttyp wird gezählt, wie viele Zuteilungen bisher über das Jahr verteilt wurden (aus `vorherige-ausgaben.csv`)
- Ziel: Die Gesamtanzahl soll proportional zum Dienstfaktor sein
- Beispiel: Sarah & Tim (Dienstfaktor 2) hatten bisher 10 Zuteilungen, Leon (Dienstfaktor 1) hatte 8 Zuteilungen. Das ist unfair (sollte 2:1 sein, also z.B. 12:6). Im aktuellen Monat sollte Leon bevorzugt werden, um das auszugleichen.
**F2 (Lokale Fairness):**
- Berechnung: Nur für den aktuellen Planungsmonat
- Ziel: Die Anzahl der Zuteilungen im aktuellen Monat soll proportional zum Dienstfaktor sein
- **Besonderheit Abwesenheiten:** Abwesenheitstage werden aus der Dienstpflicht herausgerechnet (Dienstfaktor = 0). Das bedeutet: Bei einer 2-wöchigen Abwesenheit werden in den verbleibenden 2 Wochen keine zusätzlichen Dienste zugeteilt, um die Abwesenheit auszugleichen.
- **Warum?** Dies führt zu einer gleichmäßigeren Verteilung im aktuellen Monat und verhindert, dass Familien in den wenigen verfügbaren Tagen überproportional viele Dienste bekommen müssen.
- **Ausgleich:** Die durch Abwesenheit "verpassten" Dienste werden über F1 (globale Fairness) im Jahresverlauf ausgeglichen.
- Beispiel: Im Januar sollten Sarah & Tim ca. 2× so viele Zuteilungen erhalten wie Leon (sofern beide den ganzen Monat verfügbar sind)
**F3 (Dienstübergreifende Fairness - Global):**
- Berechnung: Gesamtanzahl aller Zuteilungen (über alle Diensttypen) pro Eintrag über das ganze Jahr
- Ziel: Verhindert, dass einzelne Familien über verschiedene Diensttypen hinweg zu viele Zuteilungen bekommen
- Beispiel: Sarah & Tim hatten bisher 10 Zuteilungen über alle Diensttypen, Leon nur 3. Das Verhältnis sollte 2:1 sein (12:6). F3 würde Leon im aktuellen Monat bevorzugen.
**F4 (Dienstübergreifende Fairness - Lokal):**
- Berechnung: Gesamtanzahl aller Zuteilungen (über alle Diensttypen) pro Eintrag im aktuellen Monat
- Ziel: Verhindert, dass einzelne Familien im aktuellen Monat über verschiedene Diensttypen hinweg zu viele Zuteilungen bekommen
- Beispiel: Im Januar werden Sarah & Tim 3× Frühstück, 2× Putzen, 2× Essen = 7 Zuteilungen zugeteilt. Leon bekommt nur 1× Frühstück, 1× Putzen = 2 Zuteilungen. F4 würde Leon weitere Zuteilungen zuweisen, um die Gesamtzahl im Monat anzugleichen.
**P1 und P2 (Präferenzen):**
- An bestimmten Tagen bevorzugte Diensttypen (`+`) bekommen einen Bonus in der Zielfunktion
- An bestimmten Tagen abgelehnte Diensttypen (`-`) bekommen eine Strafe in der Zielfunktion
- Diese Effekte sind **schwächer** als die Fairness-Terme, d.h. Fairness hat Vorrang
- **Wichtig:** Präferenzen beeinflussen nur, an welchen Tagen welcher Diensttyp zugeteilt wird, nicht die Gesamtanzahl der Zuteilungen
### Gewichtung
Die verschiedenen Fairness-Ziele werden gewichtet:
- **F1 (global): 40%** - Wichtig für Ausgleich über das Jahr (pro Diensttyp)
- **F2 (lokal): 60%** - Wichtiger für den aktuellen Monat (pro Diensttyp)
- **F3 (global): 10%** - Verhindert extreme Ungleichverteilung über Diensttypen im Jahr
- **F4 (lokal): 15%** - Verhindert extreme Ungleichverteilung über Diensttypen im Monat
- **P1/P2: niedrig** - Präferenzen werden berücksichtigt, wenn Fairness gewahrt ist
## Programmausgabe und Statistiken
Das Programm zeigt nach der Optimierung: Das Programm zeigt nach der Optimierung:
1. **Zuteilungen pro Eintrag**: Übersicht der zugeteilten Diensttypen für jeden Eintrag 1. **Dienste pro Eltern**: Übersicht der zugeteilten Dienste
2. **Dienstfaktoren**: Summe der Dienstfaktoren im Planungszeitraum 2. **Dienstfaktoren**: Summe im Planungszeitraum
3. **Verteilungsvergleich**: 3. **Verteilungsvergleich**: Soll (lokal/global) vs. Ist mit Abweichungen
- Soll-Werte (lokal und global) basierend auf Fairness 4. **Präferenz-Verletzungen**: Wie oft wurden Ablehnungen ignoriert
- Ist-Werte (tatsächlich zugeteilte Diensttypen)
- Abweichungen zwischen Soll und Ist
4. **Präferenz-Verletzungen**: Wie oft wurden abgelehnte Diensttypen (`-`) trotzdem zugeteilt
## Troubleshooting ## Troubleshooting
**"Keine optimale Lösung gefunden":** **"Keine optimale Lösung gefunden":**
- Zu viele Eltern nicht verfügbar - Zu viele Eltern nicht verfügbar
- Nicht genug Eltern für alle benötigten Diensttypen - Nicht genug Eltern für alle Dienste
- Widersprüchliche Präferenzen - Widersprüchliche Präferenzen
**"Unfaire Verteilung":** **"Unfaire Verteilung":**
- Prüfen Sie die Dienstfaktoren in `eltern.csv` - Prüfen Sie die Dienstfaktoren in `eltern.csv`
- Stellen Sie sicher, dass `vorherige.ausgaben.csv` korrekt ist - Stellen Sie sicher, dass `vorherige-ausgaben.csv` korrekt ist
- Mehr Eltern verfügbar machen - Mehr Eltern verfügbar machen

View File

@ -84,7 +84,7 @@ class ElterndienstAusgabe:
# Sammle Präferenzen strukturiert # Sammle Präferenzen strukturiert
# praeferenzen_pro_eltern_dienst[eltern][dienst] = {datum: präf_wert} # praeferenzen_pro_eltern_dienst[eltern][dienst] = {datum: präf_wert}
praeferenzen_pro_eltern_dienst = defaultdict(lambda: defaultdict(dict)) praeferenzen_pro_eltern_dienst = defaultdict(lambda: defaultdict(dict))
for (eltern, tag, dienst), präf in self.daten.praeferenzen.items(): for (eltern, tag, dienst), präf in self.daten.präferenzen.items():
praeferenzen_pro_eltern_dienst[eltern][dienst][tag] = präf praeferenzen_pro_eltern_dienst[eltern][dienst][tag] = präf
# Berechne Verletzungen # Berechne Verletzungen

View File

@ -60,8 +60,8 @@ class ElterndienstplanerDaten:
self.planungszeitraum: List[date] = [] self.planungszeitraum: List[date] = []
self.eltern: List[Eltern] = [] self.eltern: List[Eltern] = []
self.benoetigte_dienste: Dict[date, List[Dienst]] = {} self.benoetigte_dienste: Dict[date, List[Dienst]] = {}
self.verfuegbarkeit: Dict[Tuple[Eltern, date], bool] = {} self.verfügbarkeit: Dict[Tuple[Eltern, date], bool] = {}
self.praeferenzen: Dict[Tuple[Eltern, date, Dienst], int] = {} self.präferenzen: Dict[Tuple[Eltern, date, Dienst], int] = {}
# dienstfaktoren[eltern][tag] = faktor. # dienstfaktoren[eltern][tag] = faktor.
# Wenn es eltern nicht gibt -> keyerror # Wenn es eltern nicht gibt -> keyerror
@ -102,7 +102,7 @@ class ElterndienstplanerDaten:
vorherige_datei: Optionaler Pfad zur vorherige-ausgaben.csv für Fairness-Constraints vorherige_datei: Optionaler Pfad zur vorherige-ausgaben.csv für Fairness-Constraints
""" """
# Eingabe CSV: Termine, Präferenzen, Verfügbarkeit # Eingabe CSV: Termine, Präferenzen, Verfügbarkeit
self.eltern, self.planungszeitraum, self.benoetigte_dienste, self.verfuegbarkeit, self.praeferenzen = \ self.eltern, self.planungszeitraum, self.benoetigte_dienste, self.verfügbarkeit, self.präferenzen = \
EingabeParser.parse_eingabe_csv(eingabe_datei, self.get_dienst) EingabeParser.parse_eingabe_csv(eingabe_datei, self.get_dienst)
# Eltern CSV: Dienstfaktoren # Eltern CSV: Dienstfaktoren

View File

@ -17,31 +17,32 @@ from ausgabe import ElterndienstAusgabe
class Elterndienstplaner: class Elterndienstplaner:
"""Optimierungs-Engine fuer Elterndienstplanung""" """Optimierungs-Engine für Elterndienstplanung"""
def __init__(self, daten: ElterndienstplanerDaten, ausgabe: ElterndienstAusgabe) -> None: def __init__(self, daten: ElterndienstplanerDaten, ausgabe: ElterndienstAusgabe) -> None:
self.daten = daten self.daten = daten
self.ausgabe = ausgabe self.ausgabe = ausgabe
def berechne_faire_zielverteilung_global(self) -> Zielverteilung: def berechne_faire_zielverteilung_global(self) -> Zielverteilung:
"""Berechnet die faire Zielanzahl von Diensten fuer den Planungszeitraum """Berechnet die faire Zielanzahl von Diensten für den Planungszeitraum
basierend auf globaler Fairness (Historie + aktueller Planungszeitraum). basierend auf globaler Fairness (Historie + aktueller Planungszeitraum).
Gibt die Ziel-Dienstanzahl fuer den aktuellen Planungszeitraum zurueck, Gibt die Ziel-Dienstanzahl für den aktuellen Planungszeitraum zurück,
korrigiert um bereits geleistete Dienste. Kann negativ sein, wenn bereits korrigiert um bereits geleistete Dienste. Kann negativ sein, wenn bereits
mehr Dienste geleistet wurden als fair waere.""" mehr Dienste geleistet wurden als fair wäre."""
ziel_dienste: Zielverteilung = defaultdict(lambda: defaultdict(float)) ziel_dienste: Zielverteilung = defaultdict(lambda: defaultdict(float))
print("\nBerechne faire Zielverteilung basierend auf historischen Daten...") print("\nBerechne faire Zielverteilung basierend auf historischen Daten...")
# Historische Dienste nach Datum gruppieren
historische_tage = set(datum for datum, _, _ in self.daten.historische_dienste) if self.daten.historische_dienste else set() historische_tage = set(datum for datum, _, _ in self.daten.historische_dienste) if self.daten.historische_dienste else set()
print(f" Analysiere {len(historische_tage)} historische Tage mit {len(self.daten.historische_dienste)} Diensten") print(f" Analysiere {len(historische_tage)} historische Tage mit {len(self.daten.historische_dienste)} Diensten")
for dienst in self.daten.dienste: for dienst in self.daten.dienste:
print(f" Verarbeite Dienst {dienst.kuerzel}...") print(f" Verarbeite Dienst {dienst.kuerzel}...")
# 1. HISTORISCHE PERIODE: Faire Umverteilung # 1. HISTORISCHE PERIODE: Faire Umverteilung der tatsächlich geleisteten Dienste
historische_dienste_dieses_typs = [ historische_dienste_dieses_typs = [
(datum, eltern) for datum, eltern, d in self.daten.historische_dienste (datum, eltern) for datum, eltern, d in self.daten.historische_dienste
if d == dienst if d == dienst
@ -49,18 +50,22 @@ class Elterndienstplaner:
print(f" Gefundene historische {dienst.kuerzel}-Dienste: {len(historische_dienste_dieses_typs)}") print(f" Gefundene historische {dienst.kuerzel}-Dienste: {len(historische_dienste_dieses_typs)}")
# Gruppiere nach Datum
dienste_pro_tag = defaultdict(list) dienste_pro_tag = defaultdict(list)
for datum, eltern in historische_dienste_dieses_typs: for datum, eltern in historische_dienste_dieses_typs:
dienste_pro_tag[datum].append(eltern) dienste_pro_tag[datum].append(eltern)
# Für jeden historischen Tag faire Umverteilung berechnen
for tag, geleistete_eltern in dienste_pro_tag.items(): for tag, geleistete_eltern in dienste_pro_tag.items():
anzahl_dienste = len(geleistete_eltern) anzahl_dienste = len(geleistete_eltern) # Anzahl Dienste an diesem Tag
# Dienstfaktoren aller Eltern für diesen historischen Tag berechnen
gesamt_dienstfaktor_tag = 0 gesamt_dienstfaktor_tag = 0
for eltern in self.daten.eltern: for eltern in self.daten.eltern:
gesamt_dienstfaktor_tag += self.daten.dienstfaktoren[eltern][tag] gesamt_dienstfaktor_tag += self.daten.dienstfaktoren[eltern][tag]
# Faire Umverteilung der an diesem Tag geleisteten Dienste
if gesamt_dienstfaktor_tag > 0: if gesamt_dienstfaktor_tag > 0:
for eltern in self.daten.eltern: for eltern in self.daten.eltern:
if self.daten.dienstfaktoren[eltern][tag] > 0: if self.daten.dienstfaktoren[eltern][tag] > 0:
@ -68,19 +73,22 @@ class Elterndienstplaner:
faire_zuteilung = anteil * anzahl_dienste faire_zuteilung = anteil * anzahl_dienste
ziel_dienste[eltern][dienst] += faire_zuteilung ziel_dienste[eltern][dienst] += faire_zuteilung
if faire_zuteilung > 0.01: if faire_zuteilung > 0.01: # Debug nur für relevante Werte
print(f" {tag}: {eltern} Faktor={self.daten.dienstfaktoren[eltern][tag]} " print(f" {tag}: {eltern} Faktor={self.daten.dienstfaktoren[eltern][tag]} "
f"-> {faire_zuteilung:.2f} von {anzahl_dienste} Diensten") f"-> {faire_zuteilung:.2f} von {anzahl_dienste} Diensten")
# 2. AKTUELLER PLANUNGSZEITRAUM: Faire Verteilung # 2. AKTUELLER PLANUNGSZEITRAUM: Faire Verteilung der benötigten Dienste (tageweise wie bei historischen Diensten)
benoetigte_dienste_planungszeitraum = 0 benoetigte_dienste_planungszeitraum = 0
# Für jeden Tag im aktuellen Planungszeitraum faire Umverteilung berechnen
for tag in self.daten.planungszeitraum: for tag in self.daten.planungszeitraum:
# Prüfe ob an diesem Tag der Dienst benötigt wird
if dienst not in self.daten.benoetigte_dienste.get(tag, []): if dienst not in self.daten.benoetigte_dienste.get(tag, []):
continue continue
benoetigte_dienste_planungszeitraum += dienst.personen_anzahl benoetigte_dienste_planungszeitraum += dienst.personen_anzahl
# Dienstfaktoren aller Eltern für diesen Tag berechnen
dienstfaktoren = {} dienstfaktoren = {}
gesamt_dienstfaktor_tag = 0 gesamt_dienstfaktor_tag = 0
@ -89,6 +97,7 @@ class Elterndienstplaner:
dienstfaktoren[eltern] = faktor dienstfaktoren[eltern] = faktor
gesamt_dienstfaktor_tag += faktor gesamt_dienstfaktor_tag += faktor
# Faire Umverteilung der an diesem Tag benötigten Dienste
if gesamt_dienstfaktor_tag > 0: if gesamt_dienstfaktor_tag > 0:
for eltern in self.daten.eltern: for eltern in self.daten.eltern:
anteil = dienstfaktoren[eltern] / gesamt_dienstfaktor_tag anteil = dienstfaktoren[eltern] / gesamt_dienstfaktor_tag
@ -96,7 +105,9 @@ class Elterndienstplaner:
ziel_dienste[eltern][dienst] += faire_zuteilung ziel_dienste[eltern][dienst] += faire_zuteilung
# 3. ABZUG DER BEREITS GELEISTETEN DIENSTE # 3. ABZUG DER BEREITS GELEISTETEN DIENSTE
# Ziehe die tatsächlich geleisteten Dienste ab, um das Ziel für den Planungszeitraum zu erhalten
for eltern in self.daten.eltern: for eltern in self.daten.eltern:
# Berechne vorherige Dienste on-the-fly aus historischen Diensten
vorherige_anzahl = sum( vorherige_anzahl = sum(
1 for _, hist_eltern, hist_dienst in self.daten.historische_dienste 1 for _, hist_eltern, hist_dienst in self.daten.historische_dienste
if hist_eltern == eltern and hist_dienst == dienst if hist_eltern == eltern and hist_dienst == dienst
@ -107,21 +118,13 @@ class Elterndienstplaner:
def berechne_faire_zielverteilung_lokal(self) -> Zielverteilung: def berechne_faire_zielverteilung_lokal(self) -> Zielverteilung:
"""Berechnet die lokale faire Zielanzahl von Diensten pro Eltern-Dienst-Kombination """Berechnet die lokale faire Zielanzahl von Diensten pro Eltern-Dienst-Kombination
basierend auf Dienstfaktoren und benoetigten Diensten im aktuellen Planungszeitraum basierend auf Dienstfaktoren und benötigten Diensten im aktuellen Planungszeitraum"""
WICHTIG: Bei der lokalen Fairness werden Abwesenheitstage NICHT in die Dienstpflicht
eingerechnet (Dienstfaktor = 0 an Abwesenheitstagen). Das führt zu einer gleichmäßigeren
Verteilung im aktuellen Monat und verhindert, dass Familien mit längeren Abwesenheiten
in den wenigen verfügbaren Tagen überproportional viele Dienste bekommen.
Die "verpassten" Dienste werden dann über die globale Fairness (F1) im Jahresverlauf
ausgeglichen."""
ziel_dienste_lokal: Zielverteilung = defaultdict(lambda: defaultdict(float)) ziel_dienste_lokal: Zielverteilung = defaultdict(lambda: defaultdict(float))
print("\nBerechne lokale faire Zielverteilung für aktuellen Planungszeitraum...") print("\nBerechne lokale faire Zielverteilung für aktuellen Planungszeitraum...")
print(" (Abwesenheitstage werden aus der Dienstpflicht herausgerechnet)")
# Gesamtdienstfaktor für aktuellen Planungszeitraum berechnen
summe_dienstfaktor_planungszeitraum_alle_eltern = sum( summe_dienstfaktor_planungszeitraum_alle_eltern = sum(
sum(self.daten.dienstfaktoren[e][tag] for tag in self.daten.planungszeitraum) sum(self.daten.dienstfaktoren[e][tag] for tag in self.daten.planungszeitraum)
for e in self.daten.eltern for e in self.daten.eltern
@ -131,17 +134,21 @@ class Elterndienstplaner:
print(" WARNUNG: Gesamtdienstfaktor ist 0, keine lokale Zielverteilung möglich") print(" WARNUNG: Gesamtdienstfaktor ist 0, keine lokale Zielverteilung möglich")
return ziel_dienste_lokal return ziel_dienste_lokal
# Für jeden Dienst die lokale faire Verteilung berechnen
for dienst in self.daten.dienste: for dienst in self.daten.dienste:
# Anzahl benötigter Dienste im aktuellen Planungszeitraum
benoetigte_dienste_planungszeitraum = sum( benoetigte_dienste_planungszeitraum = sum(
1 for tag in self.daten.planungszeitraum 1 for tag in self.daten.planungszeitraum
if dienst in self.daten.benoetigte_dienste.get(tag, []) if dienst in self.daten.benoetigte_dienste.get(tag, [])
) )
# Multipliziere mit Anzahl benötigter Personen pro Dienst
benoetigte_dienste_planungszeitraum *= dienst.personen_anzahl benoetigte_dienste_planungszeitraum *= dienst.personen_anzahl
if benoetigte_dienste_planungszeitraum > 0: if benoetigte_dienste_planungszeitraum > 0:
print(f" {dienst.kuerzel}: {benoetigte_dienste_planungszeitraum} Dienste benötigt") print(f" {dienst.kuerzel}: {benoetigte_dienste_planungszeitraum} Dienste benötigt")
for eltern in self.daten.eltern: for eltern in self.daten.eltern:
# Dienstfaktor für diesen Elternteil im aktuellen Planungszeitraum
summe_dienstfaktor_planungszeitraum = sum( summe_dienstfaktor_planungszeitraum = sum(
self.daten.dienstfaktoren[eltern][tag] for tag in self.daten.planungszeitraum self.daten.dienstfaktoren[eltern][tag] for tag in self.daten.planungszeitraum
) )
@ -154,7 +161,7 @@ class Elterndienstplaner:
return ziel_dienste_lokal return ziel_dienste_lokal
def _erstelle_entscheidungsvariablen(self) -> Entscheidungsvariablen: def _erstelle_entscheidungsvariablen(self) -> Entscheidungsvariablen:
"""Erstellt die binaeren Entscheidungsvariablen x[eltern, tag, dienst]""" """Erstellt die binären Entscheidungsvariablen x[eltern, tag, dienst]"""
x: Entscheidungsvariablen = {} x: Entscheidungsvariablen = {}
for eltern in self.daten.eltern: for eltern in self.daten.eltern:
for tag in self.daten.planungszeitraum: for tag in self.daten.planungszeitraum:
@ -173,20 +180,21 @@ class Elterndienstplaner:
) -> None: ) -> None:
"""C1: Je Eltern und Dienst nur einmal die Woche (Woche = Montag bis Sonntag)""" """C1: Je Eltern und Dienst nur einmal die Woche (Woche = Montag bis Sonntag)"""
erster_tag = self.daten.planungszeitraum[0] erster_tag = self.daten.planungszeitraum[0]
# Finde Montag am oder vor dem ersten Planungstag # weekday(): 0=Montag, 6=Sonntag
# Finde Montag am oder vor dem ersten Planungstag (für historische Dienste)
woche_start = erster_tag - timedelta(days=erster_tag.weekday()) woche_start = erster_tag - timedelta(days=erster_tag.weekday())
woche_nr = 0 woche_nr = 0
letzter_tag = self.daten.planungszeitraum[-1] letzter_tag = self.daten.planungszeitraum[-1]
while woche_start <= letzter_tag: while woche_start <= letzter_tag:
woche_ende = woche_start + timedelta(days=6) woche_ende = woche_start + timedelta(days=6) # Sonntag
for eltern in self.daten.eltern: for eltern in self.daten.eltern:
for dienst in self.daten.dienste: for dienst in self.daten.dienste:
woche_vars = [] woche_vars = []
# Zaehle historische Dienste in dieser Woche (VOR Planungszeitraum) # Zähle historische Dienste in dieser Woche (VOR dem Planungszeitraum)
historische_dienste_in_woche = 0 historische_dienste_in_woche = 0
if woche_start < erster_tag: if woche_start < erster_tag:
for hist_datum, hist_eltern, hist_dienst in self.daten.historische_dienste: for hist_datum, hist_eltern, hist_dienst in self.daten.historische_dienste:
@ -195,11 +203,13 @@ class Elterndienstplaner:
woche_start <= hist_datum < erster_tag): woche_start <= hist_datum < erster_tag):
historische_dienste_in_woche += 1 historische_dienste_in_woche += 1
# Sammle Variablen für Planungszeitraum in dieser Woche
for tag in self.daten.planungszeitraum: for tag in self.daten.planungszeitraum:
if woche_start <= tag <= woche_ende: if woche_start <= tag <= woche_ende:
if (eltern, tag, dienst) in x: if (eltern, tag, dienst) in x:
woche_vars.append(x[eltern, tag, dienst]) woche_vars.append(x[eltern, tag, dienst])
# Constraint: Historische + geplante Dienste <= 1
if woche_vars: if woche_vars:
prob += pulp.lpSum(woche_vars) <= 1 - historische_dienste_in_woche, \ prob += pulp.lpSum(woche_vars) <= 1 - historische_dienste_in_woche, \
f"C1_{eltern.replace(' ', '_')}_{dienst.kuerzel}_w{woche_nr}" f"C1_{eltern.replace(' ', '_')}_{dienst.kuerzel}_w{woche_nr}"
@ -228,10 +238,10 @@ class Elterndienstplaner:
prob: pulp.LpProblem, prob: pulp.LpProblem,
x: Entscheidungsvariablen x: Entscheidungsvariablen
) -> None: ) -> None:
"""C3: Dienste nur verfuegbaren Eltern zuteilen""" """C3: Dienste nur verfügbaren Eltern zuteilen"""
for eltern in self.daten.eltern: for eltern in self.daten.eltern:
for tag in self.daten.planungszeitraum: for tag in self.daten.planungszeitraum:
if not self.daten.verfuegbarkeit.get((eltern, tag), True): if not self.daten.verfügbarkeit.get((eltern, tag), True):
for dienst in self.daten.dienste: for dienst in self.daten.dienste:
if (eltern, tag, dienst) in x: if (eltern, tag, dienst) in x:
prob += x[eltern, tag, dienst] == 0, \ prob += x[eltern, tag, dienst] == 0, \
@ -242,46 +252,41 @@ class Elterndienstplaner:
prob: pulp.LpProblem, prob: pulp.LpProblem,
x: Entscheidungsvariablen x: Entscheidungsvariablen
) -> None: ) -> None:
"""C4: Alle benoetigten Dienste muessen zugeteilt werden""" """C4: Alle benötigten Dienste müssen zugeteilt werden"""
for tag in self.daten.planungszeitraum: for tag in self.daten.planungszeitraum:
for dienst in self.daten.benoetigte_dienste.get(tag, []): for dienst in self.daten.benoetigte_dienste.get(tag, []):
dienst_vars = [] dienst_vars = []
for eltern in self.daten.eltern: for eltern in self.daten.eltern:
if (eltern, tag, dienst) in x: if (eltern, tag, dienst) in x:
if self.daten.verfuegbarkeit.get((eltern, tag), True): # Prüfe ob Eltern verfügbar
if self.daten.verfügbarkeit.get((eltern, tag), True):
dienst_vars.append(x[eltern, tag, dienst]) dienst_vars.append(x[eltern, tag, dienst])
if dienst_vars: if dienst_vars:
# Anzahl benoetigter Personen pro Dienst # Anzahl benötigter Personen pro Dienst (aus Dienst-Objekt)
benoetigte_personen = dienst.personen_anzahl benoetigte_personen = dienst.personen_anzahl
prob += pulp.lpSum(dienst_vars) == benoetigte_personen, \ prob += pulp.lpSum(dienst_vars) == benoetigte_personen, \
f"Bedarf_{tag}_{dienst.kuerzel}" f"Bedarf_{tag}_{dienst.kuerzel}"
def _add_constraint_fairness_diensttypspezifisch( def _add_fairness_constraints(
self, self,
prob: pulp.LpProblem, prob: pulp.LpProblem,
x: Entscheidungsvariablen, x: Entscheidungsvariablen,
ziel_dienste: Zielverteilung, ziel_dienste: Zielverteilung,
constraint_prefix: str constraint_prefix: str
) -> Dict: ) -> Dict:
"""F1/F2: Fairness pro Diensttyp - gleicht Anzahl je Diensttyp aus """Erstellt Fairness-Variablen und fügt Fairness-Constraints hinzu
Berechnet die Abweichung der zugeteilten Dienste vom fairen Ziel
fuer jeden Diensttyp separat. Dies sorgt dafuer, dass z.B. Kochdienste
und Putzdienste jeweils fair verteilt werden.
F1 (global): Basierend auf historischen Daten + aktuellem Planungszeitraum
F2 (lokal): Nur fuer den aktuellen Planungszeitraum
Args: Args:
prob: Das LP-Problem prob: Das LP-Problem
x: Die Entscheidungsvariablen x: Die Entscheidungsvariablen
ziel_dienste: Die Zielverteilung (global oder lokal) ziel_dienste: Die Zielverteilung der Dienste
constraint_prefix: Praefix fuer Constraint-Namen ('global' fuer F1, 'lokal' fuer F2) constraint_prefix: Präfix für Constraint-Namen ('lokal' oder 'global')
Returns: Returns:
Dictionary mit Fairness-Abweichungsvariablen pro Diensttyp Dictionary mit Fairness-Abweichungsvariablen
""" """
# Hilfsvariablen für Fairness-Abweichungen erstellen
fairness_abweichung = {} fairness_abweichung = {}
for eltern in self.daten.eltern: for eltern in self.daten.eltern:
@ -290,16 +295,17 @@ class Elterndienstplaner:
f"fair_{constraint_prefix}_{eltern.replace(' ', '_')}_{dienst.kuerzel}", f"fair_{constraint_prefix}_{eltern.replace(' ', '_')}_{dienst.kuerzel}",
lowBound=0) lowBound=0)
# Fairness-Constraints hinzufügen
for eltern in self.daten.eltern: for eltern in self.daten.eltern:
for dienst in self.daten.dienste: for dienst in self.daten.dienste:
# Tatsächliche Dienste im aktuellen Planungszeitraum
zugeteilte_dienste_planungszeitraum = pulp.lpSum( zugeteilte_dienste_planungszeitraum = pulp.lpSum(
x[eltern, tag, dienst] x[eltern, tag, dienst]
for tag in self.daten.planungszeitraum for tag in self.daten.planungszeitraum
if (eltern, tag, dienst) in x if (eltern, tag, dienst) in x
) )
# Ziel für diese Fairness-Variante
ziel = ziel_dienste[eltern][dienst] ziel = ziel_dienste[eltern][dienst]
prob += (zugeteilte_dienste_planungszeitraum - ziel <= prob += (zugeteilte_dienste_planungszeitraum - ziel <=
fairness_abweichung[eltern, dienst]) fairness_abweichung[eltern, dienst])
@ -308,30 +314,27 @@ class Elterndienstplaner:
return fairness_abweichung return fairness_abweichung
def _add_constraint_fairness_typuebergreifend( def _add_constraint_gesamtfairness(
self, self,
prob: pulp.LpProblem, prob: pulp.LpProblem,
x: Entscheidungsvariablen, x: Entscheidungsvariablen,
ziel_dienste: Zielverteilung, ziel_dienste: Zielverteilung,
constraint_prefix: str constraint_prefix: str
) -> Dict: ) -> Dict:
"""F3/F4: Diensttypuebergreifende Fairness - verhindert Haeufung bei einzelnen Familien """F3: Dienstübergreifende Fairness - verhindert Häufung bei einzelnen Eltern
Berechnet die Abweichung der Gesamtdienstanzahl (ueber alle Diensttypen) Berechnet die Abweichung der Gesamtdienstanzahl (über alle Diensttypen)
vom fairen Gesamtziel. Dies verhindert, dass einzelne Familien ueber alle vom fairen Gesamtziel. Dies verhindert, dass einzelne Eltern über alle
Diensttypen hinweg ueberproportional viele Dienste bekommen. Diensttypen hinweg überproportional viele Dienste bekommen.
F3 (global): Basierend auf historischen Daten + aktuellem Planungszeitraum
F4 (lokal): Nur fuer den aktuellen Planungszeitraum
Args: Args:
prob: Das LP-Problem prob: Das LP-Problem
x: Die Entscheidungsvariablen x: Die Entscheidungsvariablen
ziel_dienste: Die Zielverteilung (global oder lokal) ziel_dienste: Die Zielverteilung (global oder lokal)
constraint_prefix: Praefix fuer Constraint-Namen ('global' fuer F3, 'lokal' fuer F4) constraint_prefix: Präfix für Constraint-Namen ('lokal' oder 'global')
Returns: Returns:
Dictionary mit Gesamt-Fairness-Abweichungsvariablen ueber alle Diensttypen Dictionary mit Gesamt-Fairness-Abweichungsvariablen
""" """
fairness_abweichung_gesamt = {} fairness_abweichung_gesamt = {}
@ -340,7 +343,7 @@ class Elterndienstplaner:
f"fair_gesamt_{constraint_prefix}_{eltern.replace(' ', '_')}", f"fair_gesamt_{constraint_prefix}_{eltern.replace(' ', '_')}",
lowBound=0) lowBound=0)
# Tatsächliche Gesamtdienste für diesen Elternteil
tatsaechliche_dienste_gesamt = pulp.lpSum( tatsaechliche_dienste_gesamt = pulp.lpSum(
x[eltern, tag, dienst] x[eltern, tag, dienst]
for tag in self.daten.planungszeitraum for tag in self.daten.planungszeitraum
@ -348,8 +351,10 @@ class Elterndienstplaner:
if (eltern, tag, dienst) in x if (eltern, tag, dienst) in x
) )
# Ziel-Gesamtdienste für diesen Elternteil (Summe über alle Dienste)
ziel_gesamt = sum(ziel_dienste[eltern][dienst] for dienst in self.daten.dienste) ziel_gesamt = sum(ziel_dienste[eltern][dienst] for dienst in self.daten.dienste)
# Fairness-Constraints
prob += (tatsaechliche_dienste_gesamt - ziel_gesamt <= prob += (tatsaechliche_dienste_gesamt - ziel_gesamt <=
fairness_abweichung_gesamt[eltern]) fairness_abweichung_gesamt[eltern])
prob += (ziel_gesamt - tatsaechliche_dienste_gesamt <= prob += (ziel_gesamt - tatsaechliche_dienste_gesamt <=
@ -367,41 +372,46 @@ class Elterndienstplaner:
fairness_abweichung_gesamt_global: Dict, fairness_abweichung_gesamt_global: Dict,
fairness_abweichung_gesamt_lokal: Dict fairness_abweichung_gesamt_lokal: Dict
) -> None: ) -> None:
"""Erstellt die Zielfunktion mit Fairness und Praeferenzen""" """Erstellt die Zielfunktion mit Fairness und Präferenzen"""
objective_terms = [] objective_terms = []
# Fairness-Gewichtung
gewicht_global = 40 gewicht_global = 40
gewicht_lokal = 60 gewicht_lokal = 60
gewicht_f1 = gewicht_global gewicht_f1 = gewicht_global
gewicht_f2 = gewicht_lokal gewicht_f2 = gewicht_lokal
gewicht_f3_global = 0.25 * gewicht_global gewicht_f3_global = 0.25 * gewicht_global
gewicht_f4_lokal = 0.25 * gewicht_lokal gewicht_f3_lokal = 0.25 * gewicht_lokal
# Fairness-Terme zur Zielfunktion hinzufügen
for eltern in self.daten.eltern: for eltern in self.daten.eltern:
for dienst in self.daten.dienste: for dienst in self.daten.dienste:
objective_terms.append(gewicht_f1 * fairness_abweichung_global[eltern, dienst]) objective_terms.append(gewicht_f1 * fairness_abweichung_global[eltern, dienst])
objective_terms.append(gewicht_f2 * fairness_abweichung_lokal[eltern, dienst]) objective_terms.append(gewicht_f2 * fairness_abweichung_lokal[eltern, dienst])
# F3: Gesamtfairness (dienstübergreifend) - global und lokal
objective_terms.append(gewicht_f3_global * fairness_abweichung_gesamt_global[eltern]) objective_terms.append(gewicht_f3_global * fairness_abweichung_gesamt_global[eltern])
objective_terms.append(gewicht_f4_lokal * fairness_abweichung_gesamt_lokal[eltern]) objective_terms.append(gewicht_f3_lokal * fairness_abweichung_gesamt_lokal[eltern])
# P1: Bevorzugte Dienste # P1: Bevorzugte Dienste (positiv belohnen)
for (eltern, tag, dienst), praef in self.daten.praeferenzen.items(): for (eltern, tag, dienst), präf in self.daten.präferenzen.items():
if (eltern, tag, dienst) in x and praef == 1: if (eltern, tag, dienst) in x and präf == 1: # bevorzugt
objective_terms.append(-5 * x[eltern, tag, dienst]) objective_terms.append(-5 * x[eltern, tag, dienst])
# P2: Abgelehnte Dienste # P2: Abgelehnte Dienste (bestrafen)
for (eltern, tag, dienst), praef in self.daten.praeferenzen.items(): for (eltern, tag, dienst), präf in self.daten.präferenzen.items():
if (eltern, tag, dienst) in x and praef == -1: if (eltern, tag, dienst) in x and präf == -1: # abgelehnt
objective_terms.append(25 * x[eltern, tag, dienst]) objective_terms.append(25 * x[eltern, tag, dienst])
# Zielfunktion setzen
if objective_terms: if objective_terms:
prob += pulp.lpSum(objective_terms) prob += pulp.lpSum(objective_terms)
else: else:
# Fallback: Minimiere Gesamtanzahl Dienste
prob += pulp.lpSum([var for var in x.values()]) prob += pulp.lpSum([var for var in x.values()])
print(f"Verwende Gewichtung: F1 (global) = {gewicht_f1}, F2 (lokal) = {gewicht_f2}, " print(f"Verwende Gewichtung: F1 (global) = {gewicht_f1}, F2 (lokal) = {gewicht_f2}, "
f"F3 (global) = {gewicht_f3_global}, F4 (lokal) = {gewicht_f4_lokal}") f"F3_global = {gewicht_f3_global}, F3_lokal = {gewicht_f3_lokal}")
def erstelle_optimierungsmodell(self) -> Tuple[ def erstelle_optimierungsmodell(self) -> Tuple[
pulp.LpProblem, pulp.LpProblem,
@ -414,67 +424,76 @@ class Elterndienstplaner:
""" """
print("Erstelle Optimierungsmodell...") print("Erstelle Optimierungsmodell...")
print("\nDebug: Verfuegbarkeit analysieren...") # Debugging: Verfügbarkeit prüfen
for tag in self.daten.planungszeitraum[:5]: print("\nDebug: Verfügbarkeit analysieren...")
verfuegbare = [e for e in self.daten.eltern if self.daten.verfuegbarkeit.get((e, tag), True)] for tag in self.daten.planungszeitraum[:5]: # Erste 5 Tage
benoetigte = self.daten.benoetigte_dienste.get(tag, []) verfügbare = [e for e in self.daten.eltern if self.daten.verfügbarkeit.get((e, tag), True)]
print(f" {tag}: Benötigt {len(benoetigte)} Dienste {benoetigte}, verfügbar: {verfuegbare}") benötigte = self.daten.benoetigte_dienste.get(tag, [])
print(f" {tag}: Benötigt {len(benötigte)} Dienste {benötigte}, verfügbar: {verfügbare}")
# LP Problem erstellen
prob = pulp.LpProblem("Elterndienstplaner", pulp.LpMinimize) prob = pulp.LpProblem("Elterndienstplaner", pulp.LpMinimize)
# Entscheidungsvariablen erstellen
x = self._erstelle_entscheidungsvariablen() x = self._erstelle_entscheidungsvariablen()
# Grundlegende Constraints hinzufügen
self._add_constraint_ein_dienst_pro_woche(prob, x) self._add_constraint_ein_dienst_pro_woche(prob, x)
self._add_constraint_ein_dienst_pro_tag(prob, x) self._add_constraint_ein_dienst_pro_tag(prob, x)
self._add_constraint_verfuegbarkeit(prob, x) self._add_constraint_verfuegbarkeit(prob, x)
self._add_constraint_dienst_bedarf(prob, x) self._add_constraint_dienst_bedarf(prob, x)
# Fairness-Constraints
ziel_dienste_global = self.berechne_faire_zielverteilung_global() ziel_dienste_global = self.berechne_faire_zielverteilung_global()
ziel_dienste_lokal = self.berechne_faire_zielverteilung_lokal() ziel_dienste_lokal = self.berechne_faire_zielverteilung_lokal()
# Observer Pattern: Notify ausgabe about target distributions
self.ausgabe.setze_zielverteilungen(ziel_dienste_lokal, ziel_dienste_global) self.ausgabe.setze_zielverteilungen(ziel_dienste_lokal, ziel_dienste_global)
# F2: Lokale Fairness pro Diensttyp # F2: Lokale Fairness-Constraints
fairness_abweichung_lokal = self._add_constraint_fairness_diensttypspezifisch( fairness_abweichung_lokal = self._add_fairness_constraints(
prob, x, ziel_dienste_lokal, "lokal" prob, x, ziel_dienste_lokal, "lokal"
) )
# F1: Globale Fairness pro Diensttyp # F1: Globale Fairness-Constraints
fairness_abweichung_global = self._add_constraint_fairness_diensttypspezifisch( fairness_abweichung_global = self._add_fairness_constraints(
prob, x, ziel_dienste_global, "global" prob, x, ziel_dienste_global, "global"
) )
# F3: Diensttypuebergreifende Fairness (global) # F3: Dienstübergreifende Fairness - Global
fairness_abweichung_gesamt_global = self._add_constraint_fairness_typuebergreifend( fairness_abweichung_gesamt_global = self._add_constraint_gesamtfairness(
prob, x, ziel_dienste_global, "global" prob, x, ziel_dienste_global, "global"
) )
# F4: Diensttypuebergreifende Fairness (lokal) # F3: Dienstübergreifende Fairness - Lokal
fairness_abweichung_gesamt_lokal = self._add_constraint_fairness_typuebergreifend( fairness_abweichung_gesamt_lokal = self._add_constraint_gesamtfairness(
prob, x, ziel_dienste_lokal, "lokal" prob, x, ziel_dienste_lokal, "lokal"
) )
# Zielfunktion erstellen
self._erstelle_zielfunktion(prob, x, fairness_abweichung_lokal, fairness_abweichung_global, 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)
print(f"Modell erstellt mit {len(x)} Variablen und {len(prob.constraints)} Constraints") print(f"Modell erstellt mit {len(x)} Variablen und {len(prob.constraints)} Constraints")
return prob, x return prob, x
def loese_optimierung(self, prob: pulp.LpProblem, def löse_optimierung(self, prob: pulp.LpProblem,
x: Entscheidungsvariablen) -> Optional[Dict[date, Dict[Dienst, List[Eltern]]]]: x: Entscheidungsvariablen) -> Optional[Dict[date, Dict[Dienst, List[Eltern]]]]:
"""Loest das Optimierungsproblem""" """Löst das Optimierungsproblem"""
print("Löse Optimierungsproblem...") print("Löse Optimierungsproblem...")
# Solver wählen (verfügbare Solver testen)
solver = None solver = None
try: try:
print("Versuche CBC Solver...") print("Versuche CBC Solver...")
solver = pulp.PULP_CBC_CMD(msg=0, timeLimit=10) solver = pulp.PULP_CBC_CMD(msg=0, timeLimit=10) # Standard CBC Solver
except: except:
try: try:
print("Versuche GLPK Solver...") print("Versuche GLPK Solver...")
solver = pulp.GLPK_CMD(msg=0) solver = pulp.GLPK_CMD(msg=0) # GLPK falls verfügbar
except: except:
print("Kein spezifizierter Solver verfügbar, verwende Standard.") print("Kein spezifizierter Solver verfügbar, verwende Standard.")
solver = None solver = None # Default Solver
prob.solve(solver) prob.solve(solver)
@ -485,16 +504,17 @@ class Elterndienstplaner:
print("WARNUNG: Keine optimale Lösung gefunden!") print("WARNUNG: Keine optimale Lösung gefunden!")
return None return None
loesung: Dict[date, Dict[Dienst, List[Eltern]]] = {} # Lösung extrahieren
lösung: Dict[date, Dict[Dienst, List[Eltern]]] = {}
for (eltern, tag, dienst), var in x.items(): for (eltern, tag, dienst), var in x.items():
if var.varValue and var.varValue > 0.5: if var.varValue and var.varValue > 0.5: # Binary variable ist 1
if tag not in loesung: if tag not in lösung:
loesung[tag] = {} lösung[tag] = {}
if dienst not in loesung[tag]: if dienst not in lösung[tag]:
loesung[tag][dienst] = [] lösung[tag][dienst] = []
loesung[tag][dienst].append(eltern) lösung[tag][dienst].append(eltern)
return loesung return lösung
def main() -> None: def main() -> None:
@ -511,20 +531,28 @@ def main() -> None:
print("="*50) print("="*50)
try: try:
# Create data model and load data
daten = ElterndienstplanerDaten() daten = ElterndienstplanerDaten()
daten.lade_daten(eingabe_datei, eltern_datei, vorherige_datei) daten.lade_daten(eingabe_datei, eltern_datei, vorherige_datei)
# Create output handler and optimization engine
ausgabe = ElterndienstAusgabe(daten) ausgabe = ElterndienstAusgabe(daten)
planer = Elterndienstplaner(daten, ausgabe) planer = Elterndienstplaner(daten, ausgabe)
# Optimierung
prob, x = planer.erstelle_optimierungsmodell() prob, x = planer.erstelle_optimierungsmodell()
loesung = planer.loese_optimierung(prob, x) lösung = planer.se_optimierung(prob, x)
if loesung is not None: if lösung is not None:
ausgabe.schreibe_ausgabe_csv(ausgabe_datei, loesung) # Ergebnisse ausgeben
ausgabe.drucke_statistiken(loesung) ausgabe.schreibe_ausgabe_csv(ausgabe_datei, lösung)
ausgabe.visualisiere_verteilungen(loesung) ausgabe.drucke_statistiken(lösung)
ausgabe.visualisiere_praeferenz_verletzungen(loesung)
# Visualisierung der Verteilungen (uses Observer Pattern targets)
ausgabe.visualisiere_verteilungen(lösung)
# Visualisierung der Präferenz-Verletzungen
ausgabe.visualisiere_praeferenz_verletzungen(lösung)
print("\n✓ Planung erfolgreich abgeschlossen!") print("\n✓ Planung erfolgreich abgeschlossen!")
else: else: