Compare commits

...

3 Commits

Author SHA1 Message Date
Jan Hoheisel
4e1b2291de Dokumentation 2026-01-17 23:42:47 +01:00
Jan Hoheisel
f8648313bf Dokumentation 2026-01-17 23:41:25 +01:00
Jan Hoheisel
a25f516d4c Readme, Kommentare, Umbenennungen
Readme ueberarbeitet
2026-01-17 23:19:41 +01:00
4 changed files with 243 additions and 209 deletions

224
README.md
View File

@ -2,52 +2,82 @@
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.
## Verwendung ## Wie funktioniert die Dienstplanung?
```bash Der Elterndienstplaner verteilt die anfallenden Dienstzuteilungen für einen Monat automatisch auf die Eltern. Dabei werden folgende Ziele berücksichtigt:
./elterndienstplaner.py <eingabe.csv> <eltern.csv> <ausgabe.csv> [<vorherige-ausgaben.csv>]
```
**Parameter:** ### 1. Regeln (Harte Constraints)
- `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
## Dienste Diese Regeln **müssen** immer eingehalten werden:
- **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 (nach Bedarf, 2 Personen) - **A** - Elternabend (ca. einmal im Monat, 2 Personen)
Die Planung erfolgt für einen Kalendermonat. Die Planung erfolgt für einen Kalendermonat.
## Eingabedateien ## Abwesenheiten und Präferenzen angeben (eingabe.csv)
### eingabe.csv Diese Datei enthält für jeden Tag des Planungsmonats, welche Dienste anfallen und welche Eltern verfügbar sind bzw. Präferenzen haben.
Informationen zu benötigten Diensten und Verfügbarkeit/Präferenzen der Eltern.
**Format:** **Format:**
``` ```
Datum,Wochentag,Dienste,Eltern1,Eltern2,... Datum, Wochentag, Dienste, Sarah & Tim, Leon, Maya, ...
2025-01-06,Montag,FPE,F+,x,... 2026-01-06, Montag, FPE, F+, x, , ...
2025-01-07,Dienstag,FPE,P-,F+P+,... 2026-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. Benötigte Dienste (z.B. "FPE" für Frühstück, Putzen, Essen) 3. **Diensttypen** - Welche Diensttypen an diesem Tag benötigt werden (z.B. "FPE" für Frühstück, Putzen, Essen)
4-n. Für jeden Elternteil: 4-n. **Eine Spalte pro Familie** - Abwesenheiten und Präferenzen:
- `x` = nicht verfügbar - `x` = nicht verfügbar (Urlaub, Krankheit, etc.)
- `F+` = Frühstücksdienst bevorzugt - `Kürzel+` = diesen Dienst bevorzugt (z.B. `F+` für Frühstücksdienst bevorzugt)
- `P-` = Putznotdienst nur notfalls - `Kürzel-` = diesen Dienst abgelehnt (z.B. `P-` für Putznotdienst abgelehnt)
- Mehrere Präferenzen kombinierbar: `F+P-E+` - Mehrere kombinierbar: `F+P-E+` (Frühstück bevorzugt, Putzen abgelehnt, Essen bevorzugt)
- 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.
@ -55,20 +85,25 @@ Dienstfaktoren (= Anzahl betreuter Kinder) pro Elternteil und Zeitraum.
**Format:** **Format:**
``` ```
Eltern, Beginn, Ende, Faktor, Beginn, Ende, Faktor, ... Eltern, Beginn, Ende, Faktor, Beginn, Ende, Faktor, ...
Müller,2024-09-01,2025-07-31,2 Sarah & Tim, 2024-09-01, 2025-07-31, 2
Schmidt,2024-09-01,2024-12-31,1,2025-01-01,2025-07-31,0 Leon, 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. Elternname (Kind-Name zur Identifikation) 1. **Kind-Name(n)** - Bei mehreren Kindern durch & verbunden (z.B. "Sarah & Tim")
2-4. Zeitraum 1: Beginn, Ende, Dienstfaktor 2-4. **Zeitraum 1:** Beginn (Datum), Ende (Datum), Dienstfaktor
5-7. Zeitraum 2: Beginn, Ende, Dienstfaktor (optional) 5-7. **Zeitraum 2:** Beginn, Ende, Dienstfaktor (optional, für Änderungen während des Jahres)
... ...
**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 (z.B. durch Vorstandsamt) - Faktor = 0 bedeutet: Befreiung von Diensten (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)
@ -78,86 +113,113 @@ 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 - Ab Oktober: Vorherige Ausgaben anhängen für kumulative Fairness über das Jahr
- Im Jahresverlauf sammeln sich die Ausgaben an - Im Jahresverlauf sammeln sich die Ausgaben an
## Ausgabe ## Ausgabedatei
### ausgabe.csv ### ausgabe.csv
Zugeteilte Dienste pro Tag. Dienstzuteilungen pro Tag. Diese Datei wird vom Programm erstellt.
**Format:** **Format:**
``` ```
Datum, Wochentag, Frühstücksdienst, Putznotdienst, Essensausgabenotdienst, Kochen, Elternabend Datum, Wochentag, Frühstücksdienst, Putznotdienst, Essensausgabenotdienst, Kochen, Elternabend
2025-01-06,Montag,Müller,Schmidt,Weber,, 2026-01-06, Montag, Sarah & Tim, Leon, Maya, ,
2025-01-07,Dienstag,Weber,Müller,Schmidt,, 2026-01-07, Dienstag, Maya, Sarah & Tim, Leon, ,
``` ```
## Constraints Jede Zeile entspricht einem Tag, die Spalten enthalten die Kindernamen, denen die jeweiligen Diensttypen zugeteilt wurden.
### Harte Constraints (müssen erfüllt sein) ## Verwendung
- **C1**: Pro Eltern und Dienst maximal **einmal pro Woche** (Mo-So) ```bash
- **C2**: Pro Eltern maximal **ein Dienst pro Tag** ./elterndienstplaner.py <eingabe.csv> <eltern.csv> <ausgabe.csv> [<vorherige-ausgaben.csv>]
- **C3**: Nur **verfügbare** Eltern einteilen ```
- **C4**: Alle **benötigten Dienste** müssen besetzt werden
### Weiche Constraints (werden optimiert) **Parameter:**
- `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
**Fairness** (nach Priorität): ## Wie werden die Constraints umgesetzt?
- **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)
- **F2 (Lokal)**: Dienste proportional zum Dienstfaktor im **aktuellen Monat** Dieser Abschnitt erklärt die technische Umsetzung für technisch interessierte Eltern.
- Nur aktueller Planungszeitraum
- Gewichtung: 60% (zu Jahresbeginn) → 40% (zu Jahresende)
- **F3 (Dienstübergreifend)**: Gleiche Gesamtanzahl über alle Diensttypen ### Mathematisches Optimierungsproblem
- Verhindert Häufung bei einzelnen Eltern
**Präferenzen** (niedrigere Priorität): 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.
- **P1**: Bevorzugte Dienste (`+`) werden bevorzugt zugeteilt
- **P2**: Abgelehnte Dienste (`-`) werden vermieden
### Fairness-Logik **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)
**Beispiel:** Familie Müller hat 2 Kinder, Familie Schmidt 1 Kind. **Constraints (Nebenbedingungen):** Diese schränken die möglichen Lösungen ein:
**Lokale Fairness (F2):** - **C1 (Wöchentliches Limit):** Für jeden Eintrag und jeden Diensttyp: Die Summe der Zuweisungen pro Woche ≤ 1
- Im Januar sollen beide verfügbar sein - **C2 (Tageslimit):** Für jeden Eintrag und jeden Tag: Die Summe aller Zuweisungen ≤ 1
- Müller sollte 2× so viele Dienste bekommen wie Schmidt - **C3 (Verfügbarkeit):** Wenn im Feld "x" steht, wird die entsprechende Variable auf 0 gesetzt
- Verhindert: Müller bekommt alle Dienste auf einmal - **C4 (Bedarfsdeckung):** Für jeden Tag und Diensttyp: Summe der Zuweisungen = benötigte Personenzahl
**Globale Fairness (F1):** ### Zielfunktion: Fairness und Präferenzen
- Müller war im Dezember im Urlaub → 0 Dienste
- Im Januar sollte Müller aufholen
- Über das Jahr: 2:1 Verhältnis wird ausgeglichen
**Gewichtung im Jahresverlauf:** Die **Zielfunktion** bewertet, wie gut eine Dienstverteilung ist. Das Programm minimiert Abweichungen von fairer Verteilung und berücksichtigt Präferenzen.
- **September-November**: F2 (lokal) stärker → sanftes Einführen
- **Dezember-Mai**: Ausgewogen
- **Juni-Juli**: F1 (global) stärker → Jahresausgleich
## Ausgabe-Statistiken **F1 (Globale Fairness):**
- 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. **Dienste pro Eltern**: Übersicht der zugeteilten Dienste 1. **Zuteilungen pro Eintrag**: Übersicht der zugeteilten Diensttypen für jeden Eintrag
2. **Dienstfaktoren**: Summe im Planungszeitraum 2. **Dienstfaktoren**: Summe der Dienstfaktoren im Planungszeitraum
3. **Verteilungsvergleich**: Soll (lokal/global) vs. Ist mit Abweichungen 3. **Verteilungsvergleich**:
4. **Präferenz-Verletzungen**: Wie oft wurden Ablehnungen ignoriert - Soll-Werte (lokal und global) basierend auf Fairness
- 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 Dienste - Nicht genug Eltern für alle benötigten Diensttypen
- Widersprüchliche Präferenzen
**"Unfaire Verteilung":** **"Unfaire Verteilung":**
- Prüfen Sie die Dienstfaktoren in `eltern.csv` - Dienstfaktoren in `eltern.csv` prüfen
- Stellen Sie sicher, dass `vorherige-ausgaben.csv` korrekt ist - Sicherstellen, dass `vorherige.ausgaben.csv` korrekt ist
- 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.präferenzen.items(): for (eltern, tag, dienst), präf in self.daten.praeferenzen.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.verfügbarkeit: Dict[Tuple[Eltern, date], bool] = {} self.verfuegbarkeit: Dict[Tuple[Eltern, date], bool] = {}
self.präferenzen: Dict[Tuple[Eltern, date, Dienst], int] = {} self.praeferenzen: 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.verfügbarkeit, self.präferenzen = \ self.eltern, self.planungszeitraum, self.benoetigte_dienste, self.verfuegbarkeit, self.praeferenzen = \
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,32 +17,31 @@ from ausgabe import ElterndienstAusgabe
class Elterndienstplaner: class Elterndienstplaner:
"""Optimierungs-Engine für Elterndienstplanung""" """Optimierungs-Engine fuer 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 für den Planungszeitraum """Berechnet die faire Zielanzahl von Diensten fuer den Planungszeitraum
basierend auf globaler Fairness (Historie + aktueller Planungszeitraum). basierend auf globaler Fairness (Historie + aktueller Planungszeitraum).
Gibt die Ziel-Dienstanzahl für den aktuellen Planungszeitraum zurück, Gibt die Ziel-Dienstanzahl fuer den aktuellen Planungszeitraum zurueck,
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 wäre.""" mehr Dienste geleistet wurden als fair waere."""
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 der tatsächlich geleisteten Dienste # 1. HISTORISCHE PERIODE: Faire Umverteilung
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
@ -50,22 +49,18 @@ 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 an diesem Tag anzahl_dienste = len(geleistete_eltern)
# 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:
@ -73,22 +68,19 @@ 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: # Debug nur für relevante Werte if faire_zuteilung > 0.01:
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 der benötigten Dienste (tageweise wie bei historischen Diensten) # 2. AKTUELLER PLANUNGSZEITRAUM: Faire Verteilung
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
@ -97,7 +89,6 @@ 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
@ -105,9 +96,7 @@ 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
@ -118,13 +107,21 @@ 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 benötigten Diensten im aktuellen Planungszeitraum""" basierend auf Dienstfaktoren und benoetigten 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
@ -134,21 +131,17 @@ 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
) )
@ -161,7 +154,7 @@ class Elterndienstplaner:
return ziel_dienste_lokal return ziel_dienste_lokal
def _erstelle_entscheidungsvariablen(self) -> Entscheidungsvariablen: def _erstelle_entscheidungsvariablen(self) -> Entscheidungsvariablen:
"""Erstellt die binären Entscheidungsvariablen x[eltern, tag, dienst]""" """Erstellt die binaeren 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:
@ -180,21 +173,20 @@ 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]
# weekday(): 0=Montag, 6=Sonntag # Finde Montag am oder vor dem ersten Planungstag
# 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) # Sonntag woche_ende = woche_start + timedelta(days=6)
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 = []
# Zähle historische Dienste in dieser Woche (VOR dem Planungszeitraum) # Zaehle historische Dienste in dieser Woche (VOR 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:
@ -203,13 +195,11 @@ 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}"
@ -238,10 +228,10 @@ class Elterndienstplaner:
prob: pulp.LpProblem, prob: pulp.LpProblem,
x: Entscheidungsvariablen x: Entscheidungsvariablen
) -> None: ) -> None:
"""C3: Dienste nur verfügbaren Eltern zuteilen""" """C3: Dienste nur verfuegbaren 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.verfügbarkeit.get((eltern, tag), True): if not self.daten.verfuegbarkeit.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, \
@ -252,41 +242,46 @@ class Elterndienstplaner:
prob: pulp.LpProblem, prob: pulp.LpProblem,
x: Entscheidungsvariablen x: Entscheidungsvariablen
) -> None: ) -> None:
"""C4: Alle benötigten Dienste müssen zugeteilt werden""" """C4: Alle benoetigten Dienste muessen 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:
# Prüfe ob Eltern verfügbar if self.daten.verfuegbarkeit.get((eltern, tag), True):
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 benötigter Personen pro Dienst (aus Dienst-Objekt) # Anzahl benoetigter Personen pro Dienst
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_fairness_constraints( def _add_constraint_fairness_diensttypspezifisch(
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:
"""Erstellt Fairness-Variablen und fügt Fairness-Constraints hinzu """F1/F2: Fairness pro Diensttyp - gleicht Anzahl je Diensttyp aus
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 der Dienste ziel_dienste: Die Zielverteilung (global oder lokal)
constraint_prefix: Präfix für Constraint-Namen ('lokal' oder 'global') constraint_prefix: Praefix fuer Constraint-Namen ('global' fuer F1, 'lokal' fuer F2)
Returns: Returns:
Dictionary mit Fairness-Abweichungsvariablen Dictionary mit Fairness-Abweichungsvariablen pro Diensttyp
""" """
# Hilfsvariablen für Fairness-Abweichungen erstellen
fairness_abweichung = {} fairness_abweichung = {}
for eltern in self.daten.eltern: for eltern in self.daten.eltern:
@ -295,17 +290,16 @@ 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])
@ -314,27 +308,30 @@ class Elterndienstplaner:
return fairness_abweichung return fairness_abweichung
def _add_constraint_gesamtfairness( def _add_constraint_fairness_typuebergreifend(
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: Dienstübergreifende Fairness - verhindert Häufung bei einzelnen Eltern """F3/F4: Diensttypuebergreifende Fairness - verhindert Haeufung bei einzelnen Familien
Berechnet die Abweichung der Gesamtdienstanzahl (über alle Diensttypen) Berechnet die Abweichung der Gesamtdienstanzahl (ueber alle Diensttypen)
vom fairen Gesamtziel. Dies verhindert, dass einzelne Eltern über alle vom fairen Gesamtziel. Dies verhindert, dass einzelne Familien ueber alle
Diensttypen hinweg überproportional viele Dienste bekommen. Diensttypen hinweg ueberproportional 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: Präfix für Constraint-Namen ('lokal' oder 'global') constraint_prefix: Praefix fuer Constraint-Namen ('global' fuer F3, 'lokal' fuer F4)
Returns: Returns:
Dictionary mit Gesamt-Fairness-Abweichungsvariablen Dictionary mit Gesamt-Fairness-Abweichungsvariablen ueber alle Diensttypen
""" """
fairness_abweichung_gesamt = {} fairness_abweichung_gesamt = {}
@ -343,7 +340,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
@ -351,10 +348,8 @@ 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 <=
@ -372,46 +367,41 @@ 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 Präferenzen""" """Erstellt die Zielfunktion mit Fairness und Praeferenzen"""
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_f3_lokal = 0.25 * gewicht_lokal gewicht_f4_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_f3_lokal * fairness_abweichung_gesamt_lokal[eltern]) objective_terms.append(gewicht_f4_lokal * fairness_abweichung_gesamt_lokal[eltern])
# P1: Bevorzugte Dienste (positiv belohnen) # P1: Bevorzugte Dienste
for (eltern, tag, dienst), präf in self.daten.präferenzen.items(): for (eltern, tag, dienst), praef in self.daten.praeferenzen.items():
if (eltern, tag, dienst) in x and präf == 1: # bevorzugt if (eltern, tag, dienst) in x and praef == 1:
objective_terms.append(-5 * x[eltern, tag, dienst]) objective_terms.append(-5 * x[eltern, tag, dienst])
# P2: Abgelehnte Dienste (bestrafen) # P2: Abgelehnte Dienste
for (eltern, tag, dienst), präf in self.daten.präferenzen.items(): for (eltern, tag, dienst), praef in self.daten.praeferenzen.items():
if (eltern, tag, dienst) in x and präf == -1: # abgelehnt if (eltern, tag, dienst) in x and praef == -1:
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}, F3_lokal = {gewicht_f3_lokal}") f"F3 (global) = {gewicht_f3_global}, F4 (lokal) = {gewicht_f4_lokal}")
def erstelle_optimierungsmodell(self) -> Tuple[ def erstelle_optimierungsmodell(self) -> Tuple[
pulp.LpProblem, pulp.LpProblem,
@ -424,76 +414,67 @@ class Elterndienstplaner:
""" """
print("Erstelle Optimierungsmodell...") print("Erstelle Optimierungsmodell...")
# Debugging: Verfügbarkeit prüfen print("\nDebug: Verfuegbarkeit analysieren...")
print("\nDebug: Verfügbarkeit analysieren...") for tag in self.daten.planungszeitraum[:5]:
for tag in self.daten.planungszeitraum[:5]: # Erste 5 Tage verfuegbare = [e for e in self.daten.eltern if self.daten.verfuegbarkeit.get((e, tag), True)]
verfügbare = [e for e in self.daten.eltern if self.daten.verfügbarkeit.get((e, tag), True)] benoetigte = self.daten.benoetigte_dienste.get(tag, [])
benötigte = self.daten.benoetigte_dienste.get(tag, []) print(f" {tag}: Benötigt {len(benoetigte)} Dienste {benoetigte}, verfügbar: {verfuegbare}")
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-Constraints # F2: Lokale Fairness pro Diensttyp
fairness_abweichung_lokal = self._add_fairness_constraints( fairness_abweichung_lokal = self._add_constraint_fairness_diensttypspezifisch(
prob, x, ziel_dienste_lokal, "lokal" prob, x, ziel_dienste_lokal, "lokal"
) )
# F1: Globale Fairness-Constraints # F1: Globale Fairness pro Diensttyp
fairness_abweichung_global = self._add_fairness_constraints( fairness_abweichung_global = self._add_constraint_fairness_diensttypspezifisch(
prob, x, ziel_dienste_global, "global" prob, x, ziel_dienste_global, "global"
) )
# F3: Dienstübergreifende Fairness - Global # F3: Diensttypuebergreifende Fairness (global)
fairness_abweichung_gesamt_global = self._add_constraint_gesamtfairness( fairness_abweichung_gesamt_global = self._add_constraint_fairness_typuebergreifend(
prob, x, ziel_dienste_global, "global" prob, x, ziel_dienste_global, "global"
) )
# F3: Dienstübergreifende Fairness - Lokal # F4: Diensttypuebergreifende Fairness (lokal)
fairness_abweichung_gesamt_lokal = self._add_constraint_gesamtfairness( fairness_abweichung_gesamt_lokal = self._add_constraint_fairness_typuebergreifend(
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 löse_optimierung(self, prob: pulp.LpProblem, def loese_optimierung(self, prob: pulp.LpProblem,
x: Entscheidungsvariablen) -> Optional[Dict[date, Dict[Dienst, List[Eltern]]]]: x: Entscheidungsvariablen) -> Optional[Dict[date, Dict[Dienst, List[Eltern]]]]:
"""Löst das Optimierungsproblem""" """Loest 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) # Standard CBC Solver solver = pulp.PULP_CBC_CMD(msg=0, timeLimit=10)
except: except:
try: try:
print("Versuche GLPK Solver...") print("Versuche GLPK Solver...")
solver = pulp.GLPK_CMD(msg=0) # GLPK falls verfügbar solver = pulp.GLPK_CMD(msg=0)
except: except:
print("Kein spezifizierter Solver verfügbar, verwende Standard.") print("Kein spezifizierter Solver verfügbar, verwende Standard.")
solver = None # Default Solver solver = None
prob.solve(solver) prob.solve(solver)
@ -504,17 +485,16 @@ class Elterndienstplaner:
print("WARNUNG: Keine optimale Lösung gefunden!") print("WARNUNG: Keine optimale Lösung gefunden!")
return None return None
# Lösung extrahieren loesung: Dict[date, Dict[Dienst, List[Eltern]]] = {}
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: # Binary variable ist 1 if var.varValue and var.varValue > 0.5:
if tag not in lösung: if tag not in loesung:
lösung[tag] = {} loesung[tag] = {}
if dienst not in lösung[tag]: if dienst not in loesung[tag]:
lösung[tag][dienst] = [] loesung[tag][dienst] = []
lösung[tag][dienst].append(eltern) loesung[tag][dienst].append(eltern)
return lösung return loesung
def main() -> None: def main() -> None:
@ -531,28 +511,20 @@ 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()
lösung = planer.se_optimierung(prob, x) loesung = planer.loese_optimierung(prob, x)
if lösung is not None: if loesung is not None:
# Ergebnisse ausgeben ausgabe.schreibe_ausgabe_csv(ausgabe_datei, loesung)
ausgabe.schreibe_ausgabe_csv(ausgabe_datei, lösung) ausgabe.drucke_statistiken(loesung)
ausgabe.drucke_statistiken(lösung) ausgabe.visualisiere_verteilungen(loesung)
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: