Compare commits
No commits in common. "4e1b2291dea0411a654af0dddba96fd8a0d3780e" and "f4791cf7bb6df15ddd40f7f0e2206e277d834759" have entirely different histories.
4e1b2291de
...
f4791cf7bb
228
README.md
228
README.md
@ -2,108 +2,73 @@
|
||||
|
||||
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:
|
||||
|
||||
- **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
|
||||
## Dienste
|
||||
|
||||
- **F** - Frühstücksdienst (täglich, 1 Person)
|
||||
- **P** - Putznotdienst (täglich, 1 Person)
|
||||
- **E** - Essensausgabenotdienst (täglich, 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.
|
||||
|
||||
## 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:**
|
||||
```
|
||||
Datum, Wochentag, Dienste, Sarah & Tim, Leon, Maya, ...
|
||||
2026-01-06, Montag, FPE, F+, x, , ...
|
||||
2026-01-07, Dienstag, FPE, P-, F+P+, , ...
|
||||
Datum,Wochentag,Dienste,Eltern1,Eltern2,...
|
||||
2025-01-06,Montag,FPE,F+,x,...
|
||||
2025-01-07,Dienstag,FPE,P-,F+P+,...
|
||||
```
|
||||
|
||||
**Spalten:**
|
||||
1. **Datum** (ISO-Format: YYYY-MM-DD)
|
||||
2. **Wochentag** (zur Information)
|
||||
3. **Diensttypen** - Welche Diensttypen an diesem Tag benötigt werden (z.B. "FPE" für Frühstück, Putzen, Essen)
|
||||
4-n. **Eine Spalte pro Familie** - Abwesenheiten und Präferenzen:
|
||||
- `x` = nicht verfügbar (Urlaub, Krankheit, etc.)
|
||||
- `Kürzel+` = diesen Dienst bevorzugt (z.B. `F+` für Frühstücksdienst bevorzugt)
|
||||
- `Kürzel-` = diesen Dienst abgelehnt (z.B. `P-` für Putznotdienst abgelehnt)
|
||||
- Mehrere kombinierbar: `F+P-E+` (Frühstück bevorzugt, Putzen abgelehnt, Essen bevorzugt)
|
||||
1. Datum (ISO-Format: YYYY-MM-DD)
|
||||
2. Wochentag (zur Information)
|
||||
3. Benötigte Dienste (z.B. "FPE" für Frühstück, Putzen, Essen)
|
||||
4-n. Für jeden Elternteil:
|
||||
- `x` = nicht verfügbar
|
||||
- `F+` = Frühstücksdienst bevorzugt
|
||||
- `P-` = Putznotdienst nur notfalls
|
||||
- Mehrere Präferenzen kombinierbar: `F+P-E+`
|
||||
- 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
|
||||
|
||||
Dienstfaktoren (= Anzahl betreuter Kinder) pro Elternteil und Zeitraum.
|
||||
|
||||
**Format:**
|
||||
```
|
||||
Eltern, Beginn, Ende, Faktor, Beginn, Ende, Faktor, ...
|
||||
Sarah & Tim, 2024-09-01, 2025-07-31, 2
|
||||
Leon, 2024-09-01, 2024-12-31, 1, 2025-01-01, 2025-07-31, 0
|
||||
Maya, 2024-09-01, 2025-07-31, 1
|
||||
Eltern,Beginn,Ende,Faktor,Beginn,Ende,Faktor,...
|
||||
Müller,2024-09-01,2025-07-31,2
|
||||
Schmidt,2024-09-01,2024-12-31,1,2025-01-01,2025-07-31,0
|
||||
```
|
||||
|
||||
**Spalten:**
|
||||
1. **Kind-Name(n)** - Bei mehreren Kindern durch & verbunden (z.B. "Sarah & Tim")
|
||||
2-4. **Zeitraum 1:** Beginn (Datum), Ende (Datum), Dienstfaktor
|
||||
5-7. **Zeitraum 2:** Beginn, Ende, Dienstfaktor (optional, für Änderungen während des Jahres)
|
||||
1. Elternname (Kind-Name zur Identifikation)
|
||||
2-4. Zeitraum 1: Beginn, Ende, Dienstfaktor
|
||||
5-7. Zeitraum 2: Beginn, Ende, Dienstfaktor (optional)
|
||||
...
|
||||
|
||||
**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
|
||||
- Außerhalb definierter Zeiträume: Faktor = 0 (keine Dienstpflicht)
|
||||
- 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).
|
||||
- Faktor = 0 bedeutet: Befreiung (z.B. durch Vorstandsamt)
|
||||
|
||||
### vorherige-ausgaben.csv (optional)
|
||||
|
||||
@ -113,113 +78,86 @@ Frühere Ausgaben des Programms zur Berechnung der Jahres-Fairness.
|
||||
|
||||
**Verwendung:**
|
||||
- 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
|
||||
|
||||
## Ausgabedatei
|
||||
## Ausgabe
|
||||
|
||||
### ausgabe.csv
|
||||
|
||||
Dienstzuteilungen pro Tag. Diese Datei wird vom Programm erstellt.
|
||||
Zugeteilte Dienste pro Tag.
|
||||
|
||||
**Format:**
|
||||
```
|
||||
Datum, Wochentag, Frühstücksdienst, Putznotdienst, Essensausgabenotdienst, Kochen, Elternabend
|
||||
2026-01-06, Montag, Sarah & Tim, Leon, Maya, ,
|
||||
2026-01-07, Dienstag, Maya, Sarah & Tim, Leon, ,
|
||||
Datum,Wochentag,Frühstücksdienst,Putznotdienst,Essensausgabenotdienst,Kochen,Elternabend
|
||||
2025-01-06,Montag,Müller,Schmidt,Weber,,
|
||||
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
|
||||
./elterndienstplaner.py <eingabe.csv> <eltern.csv> <ausgabe.csv> [<vorherige-ausgaben.csv>]
|
||||
```
|
||||
- **C1**: Pro Eltern und Dienst maximal **einmal pro Woche** (Mo-So)
|
||||
- **C2**: Pro Eltern maximal **ein Dienst pro Tag**
|
||||
- **C3**: Nur **verfügbare** Eltern einteilen
|
||||
- **C4**: Alle **benötigten Dienste** müssen besetzt werden
|
||||
|
||||
**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
|
||||
### Weiche Constraints (werden optimiert)
|
||||
|
||||
## 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
|
||||
- **C2 (Tageslimit):** Für jeden Eintrag und jeden Tag: Die Summe aller Zuweisungen ≤ 1
|
||||
- **C3 (Verfügbarkeit):** Wenn im Feld "x" steht, wird die entsprechende Variable auf 0 gesetzt
|
||||
- **C4 (Bedarfsdeckung):** Für jeden Tag und Diensttyp: Summe der Zuweisungen = benötigte Personenzahl
|
||||
**Lokale Fairness (F2):**
|
||||
- Im Januar sollen beide verfügbar sein
|
||||
- Müller sollte 2× so viele Dienste bekommen wie Schmidt
|
||||
- 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):**
|
||||
- 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
|
||||
## Ausgabe-Statistiken
|
||||
|
||||
Das Programm zeigt nach der Optimierung:
|
||||
|
||||
1. **Zuteilungen pro Eintrag**: Übersicht der zugeteilten Diensttypen für jeden Eintrag
|
||||
2. **Dienstfaktoren**: Summe der Dienstfaktoren im Planungszeitraum
|
||||
3. **Verteilungsvergleich**:
|
||||
- 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
|
||||
1. **Dienste pro Eltern**: Übersicht der zugeteilten Dienste
|
||||
2. **Dienstfaktoren**: Summe im Planungszeitraum
|
||||
3. **Verteilungsvergleich**: Soll (lokal/global) vs. Ist mit Abweichungen
|
||||
4. **Präferenz-Verletzungen**: Wie oft wurden Ablehnungen ignoriert
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"Keine optimale Lösung gefunden":**
|
||||
- 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
|
||||
|
||||
**"Unfaire Verteilung":**
|
||||
- Dienstfaktoren in `eltern.csv` prüfen
|
||||
- Sicherstellen, dass `vorherige.ausgaben.csv` korrekt ist
|
||||
|
||||
- Prüfen Sie die Dienstfaktoren in `eltern.csv`
|
||||
- Stellen Sie sicher, dass `vorherige-ausgaben.csv` korrekt ist
|
||||
- Mehr Eltern verfügbar machen
|
||||
|
||||
|
||||
@ -84,7 +84,7 @@ class ElterndienstAusgabe:
|
||||
# Sammle Präferenzen strukturiert
|
||||
# praeferenzen_pro_eltern_dienst[eltern][dienst] = {datum: präf_wert}
|
||||
praeferenzen_pro_eltern_dienst = defaultdict(lambda: defaultdict(dict))
|
||||
for (eltern, tag, dienst), präf in self.daten.praeferenzen.items():
|
||||
for (eltern, tag, dienst), präf in self.daten.präferenzen.items():
|
||||
praeferenzen_pro_eltern_dienst[eltern][dienst][tag] = präf
|
||||
|
||||
# Berechne Verletzungen
|
||||
|
||||
@ -60,8 +60,8 @@ class ElterndienstplanerDaten:
|
||||
self.planungszeitraum: List[date] = []
|
||||
self.eltern: List[Eltern] = []
|
||||
self.benoetigte_dienste: Dict[date, List[Dienst]] = {}
|
||||
self.verfuegbarkeit: Dict[Tuple[Eltern, date], bool] = {}
|
||||
self.praeferenzen: Dict[Tuple[Eltern, date, Dienst], int] = {}
|
||||
self.verfügbarkeit: Dict[Tuple[Eltern, date], bool] = {}
|
||||
self.präferenzen: Dict[Tuple[Eltern, date, Dienst], int] = {}
|
||||
|
||||
# dienstfaktoren[eltern][tag] = faktor.
|
||||
# Wenn es eltern nicht gibt -> keyerror
|
||||
@ -102,7 +102,7 @@ class ElterndienstplanerDaten:
|
||||
vorherige_datei: Optionaler Pfad zur vorherige-ausgaben.csv für Fairness-Constraints
|
||||
"""
|
||||
# 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)
|
||||
|
||||
# Eltern CSV: Dienstfaktoren
|
||||
|
||||
@ -17,31 +17,32 @@ from ausgabe import ElterndienstAusgabe
|
||||
|
||||
|
||||
class Elterndienstplaner:
|
||||
"""Optimierungs-Engine fuer Elterndienstplanung"""
|
||||
"""Optimierungs-Engine für Elterndienstplanung"""
|
||||
|
||||
def __init__(self, daten: ElterndienstplanerDaten, ausgabe: ElterndienstAusgabe) -> None:
|
||||
self.daten = daten
|
||||
self.ausgabe = ausgabe
|
||||
|
||||
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).
|
||||
|
||||
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
|
||||
mehr Dienste geleistet wurden als fair waere."""
|
||||
mehr Dienste geleistet wurden als fair wäre."""
|
||||
|
||||
ziel_dienste: Zielverteilung = defaultdict(lambda: defaultdict(float))
|
||||
|
||||
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()
|
||||
print(f" Analysiere {len(historische_tage)} historische Tage mit {len(self.daten.historische_dienste)} Diensten")
|
||||
|
||||
for dienst in self.daten.dienste:
|
||||
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 = [
|
||||
(datum, eltern) for datum, eltern, d in self.daten.historische_dienste
|
||||
if d == dienst
|
||||
@ -49,18 +50,22 @@ class Elterndienstplaner:
|
||||
|
||||
print(f" Gefundene historische {dienst.kuerzel}-Dienste: {len(historische_dienste_dieses_typs)}")
|
||||
|
||||
# Gruppiere nach Datum
|
||||
dienste_pro_tag = defaultdict(list)
|
||||
for datum, eltern in historische_dienste_dieses_typs:
|
||||
dienste_pro_tag[datum].append(eltern)
|
||||
|
||||
# Für jeden historischen Tag faire Umverteilung berechnen
|
||||
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
|
||||
|
||||
for eltern in self.daten.eltern:
|
||||
gesamt_dienstfaktor_tag += self.daten.dienstfaktoren[eltern][tag]
|
||||
|
||||
# Faire Umverteilung der an diesem Tag geleisteten Dienste
|
||||
if gesamt_dienstfaktor_tag > 0:
|
||||
for eltern in self.daten.eltern:
|
||||
if self.daten.dienstfaktoren[eltern][tag] > 0:
|
||||
@ -68,19 +73,22 @@ class Elterndienstplaner:
|
||||
faire_zuteilung = anteil * anzahl_dienste
|
||||
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]} "
|
||||
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
|
||||
|
||||
# Für jeden Tag im aktuellen Planungszeitraum faire Umverteilung berechnen
|
||||
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, []):
|
||||
continue
|
||||
|
||||
benoetigte_dienste_planungszeitraum += dienst.personen_anzahl
|
||||
|
||||
# Dienstfaktoren aller Eltern für diesen Tag berechnen
|
||||
dienstfaktoren = {}
|
||||
gesamt_dienstfaktor_tag = 0
|
||||
|
||||
@ -89,6 +97,7 @@ class Elterndienstplaner:
|
||||
dienstfaktoren[eltern] = faktor
|
||||
gesamt_dienstfaktor_tag += faktor
|
||||
|
||||
# Faire Umverteilung der an diesem Tag benötigten Dienste
|
||||
if gesamt_dienstfaktor_tag > 0:
|
||||
for eltern in self.daten.eltern:
|
||||
anteil = dienstfaktoren[eltern] / gesamt_dienstfaktor_tag
|
||||
@ -96,7 +105,9 @@ class Elterndienstplaner:
|
||||
ziel_dienste[eltern][dienst] += faire_zuteilung
|
||||
|
||||
# 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:
|
||||
# Berechne vorherige Dienste on-the-fly aus historischen Diensten
|
||||
vorherige_anzahl = sum(
|
||||
1 for _, hist_eltern, hist_dienst in self.daten.historische_dienste
|
||||
if hist_eltern == eltern and hist_dienst == dienst
|
||||
@ -107,21 +118,13 @@ class Elterndienstplaner:
|
||||
|
||||
def berechne_faire_zielverteilung_lokal(self) -> Zielverteilung:
|
||||
"""Berechnet die lokale faire Zielanzahl von Diensten pro Eltern-Dienst-Kombination
|
||||
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."""
|
||||
basierend auf Dienstfaktoren und benötigten Diensten im aktuellen Planungszeitraum"""
|
||||
|
||||
ziel_dienste_lokal: Zielverteilung = defaultdict(lambda: defaultdict(float))
|
||||
|
||||
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(
|
||||
sum(self.daten.dienstfaktoren[e][tag] for tag in self.daten.planungszeitraum)
|
||||
for e in self.daten.eltern
|
||||
@ -131,17 +134,21 @@ class Elterndienstplaner:
|
||||
print(" WARNUNG: Gesamtdienstfaktor ist 0, keine lokale Zielverteilung möglich")
|
||||
return ziel_dienste_lokal
|
||||
|
||||
# Für jeden Dienst die lokale faire Verteilung berechnen
|
||||
for dienst in self.daten.dienste:
|
||||
# Anzahl benötigter Dienste im aktuellen Planungszeitraum
|
||||
benoetigte_dienste_planungszeitraum = sum(
|
||||
1 for tag in self.daten.planungszeitraum
|
||||
if dienst in self.daten.benoetigte_dienste.get(tag, [])
|
||||
)
|
||||
# Multipliziere mit Anzahl benötigter Personen pro Dienst
|
||||
benoetigte_dienste_planungszeitraum *= dienst.personen_anzahl
|
||||
|
||||
if benoetigte_dienste_planungszeitraum > 0:
|
||||
print(f" {dienst.kuerzel}: {benoetigte_dienste_planungszeitraum} Dienste benötigt")
|
||||
|
||||
for eltern in self.daten.eltern:
|
||||
# Dienstfaktor für diesen Elternteil im aktuellen Planungszeitraum
|
||||
summe_dienstfaktor_planungszeitraum = sum(
|
||||
self.daten.dienstfaktoren[eltern][tag] for tag in self.daten.planungszeitraum
|
||||
)
|
||||
@ -154,7 +161,7 @@ class Elterndienstplaner:
|
||||
return ziel_dienste_lokal
|
||||
|
||||
def _erstelle_entscheidungsvariablen(self) -> Entscheidungsvariablen:
|
||||
"""Erstellt die binaeren Entscheidungsvariablen x[eltern, tag, dienst]"""
|
||||
"""Erstellt die binären Entscheidungsvariablen x[eltern, tag, dienst]"""
|
||||
x: Entscheidungsvariablen = {}
|
||||
for eltern in self.daten.eltern:
|
||||
for tag in self.daten.planungszeitraum:
|
||||
@ -173,20 +180,21 @@ class Elterndienstplaner:
|
||||
) -> None:
|
||||
"""C1: Je Eltern und Dienst nur einmal die Woche (Woche = Montag bis Sonntag)"""
|
||||
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_nr = 0
|
||||
letzter_tag = self.daten.planungszeitraum[-1]
|
||||
|
||||
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 dienst in self.daten.dienste:
|
||||
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
|
||||
if woche_start < erster_tag:
|
||||
for hist_datum, hist_eltern, hist_dienst in self.daten.historische_dienste:
|
||||
@ -195,11 +203,13 @@ class Elterndienstplaner:
|
||||
woche_start <= hist_datum < erster_tag):
|
||||
historische_dienste_in_woche += 1
|
||||
|
||||
# Sammle Variablen für Planungszeitraum in dieser Woche
|
||||
for tag in self.daten.planungszeitraum:
|
||||
if woche_start <= tag <= woche_ende:
|
||||
if (eltern, tag, dienst) in x:
|
||||
woche_vars.append(x[eltern, tag, dienst])
|
||||
|
||||
# Constraint: Historische + geplante Dienste <= 1
|
||||
if woche_vars:
|
||||
prob += pulp.lpSum(woche_vars) <= 1 - historische_dienste_in_woche, \
|
||||
f"C1_{eltern.replace(' ', '_')}_{dienst.kuerzel}_w{woche_nr}"
|
||||
@ -228,10 +238,10 @@ class Elterndienstplaner:
|
||||
prob: pulp.LpProblem,
|
||||
x: Entscheidungsvariablen
|
||||
) -> None:
|
||||
"""C3: Dienste nur verfuegbaren Eltern zuteilen"""
|
||||
"""C3: Dienste nur verfügbaren Eltern zuteilen"""
|
||||
for eltern in self.daten.eltern:
|
||||
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:
|
||||
if (eltern, tag, dienst) in x:
|
||||
prob += x[eltern, tag, dienst] == 0, \
|
||||
@ -242,46 +252,41 @@ class Elterndienstplaner:
|
||||
prob: pulp.LpProblem,
|
||||
x: Entscheidungsvariablen
|
||||
) -> None:
|
||||
"""C4: Alle benoetigten Dienste muessen zugeteilt werden"""
|
||||
"""C4: Alle benötigten Dienste müssen zugeteilt werden"""
|
||||
for tag in self.daten.planungszeitraum:
|
||||
for dienst in self.daten.benoetigte_dienste.get(tag, []):
|
||||
dienst_vars = []
|
||||
for eltern in self.daten.eltern:
|
||||
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])
|
||||
|
||||
if dienst_vars:
|
||||
# Anzahl benoetigter Personen pro Dienst
|
||||
# Anzahl benötigter Personen pro Dienst (aus Dienst-Objekt)
|
||||
benoetigte_personen = dienst.personen_anzahl
|
||||
prob += pulp.lpSum(dienst_vars) == benoetigte_personen, \
|
||||
f"Bedarf_{tag}_{dienst.kuerzel}"
|
||||
|
||||
def _add_constraint_fairness_diensttypspezifisch(
|
||||
def _add_fairness_constraints(
|
||||
self,
|
||||
prob: pulp.LpProblem,
|
||||
x: Entscheidungsvariablen,
|
||||
ziel_dienste: Zielverteilung,
|
||||
constraint_prefix: str
|
||||
) -> Dict:
|
||||
"""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
|
||||
"""Erstellt Fairness-Variablen und fügt Fairness-Constraints hinzu
|
||||
|
||||
Args:
|
||||
prob: Das LP-Problem
|
||||
x: Die Entscheidungsvariablen
|
||||
ziel_dienste: Die Zielverteilung (global oder lokal)
|
||||
constraint_prefix: Praefix fuer Constraint-Namen ('global' fuer F1, 'lokal' fuer F2)
|
||||
ziel_dienste: Die Zielverteilung der Dienste
|
||||
constraint_prefix: Präfix für Constraint-Namen ('lokal' oder 'global')
|
||||
|
||||
Returns:
|
||||
Dictionary mit Fairness-Abweichungsvariablen pro Diensttyp
|
||||
Dictionary mit Fairness-Abweichungsvariablen
|
||||
"""
|
||||
# Hilfsvariablen für Fairness-Abweichungen erstellen
|
||||
fairness_abweichung = {}
|
||||
|
||||
for eltern in self.daten.eltern:
|
||||
@ -290,16 +295,17 @@ class Elterndienstplaner:
|
||||
f"fair_{constraint_prefix}_{eltern.replace(' ', '_')}_{dienst.kuerzel}",
|
||||
lowBound=0)
|
||||
|
||||
# Fairness-Constraints hinzufügen
|
||||
for eltern in self.daten.eltern:
|
||||
for dienst in self.daten.dienste:
|
||||
|
||||
# Tatsächliche Dienste im aktuellen Planungszeitraum
|
||||
zugeteilte_dienste_planungszeitraum = pulp.lpSum(
|
||||
x[eltern, tag, dienst]
|
||||
for tag in self.daten.planungszeitraum
|
||||
if (eltern, tag, dienst) in x
|
||||
)
|
||||
|
||||
|
||||
# Ziel für diese Fairness-Variante
|
||||
ziel = ziel_dienste[eltern][dienst]
|
||||
prob += (zugeteilte_dienste_planungszeitraum - ziel <=
|
||||
fairness_abweichung[eltern, dienst])
|
||||
@ -308,30 +314,27 @@ class Elterndienstplaner:
|
||||
|
||||
return fairness_abweichung
|
||||
|
||||
def _add_constraint_fairness_typuebergreifend(
|
||||
def _add_constraint_gesamtfairness(
|
||||
self,
|
||||
prob: pulp.LpProblem,
|
||||
x: Entscheidungsvariablen,
|
||||
ziel_dienste: Zielverteilung,
|
||||
constraint_prefix: str
|
||||
) -> 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)
|
||||
vom fairen Gesamtziel. Dies verhindert, dass einzelne Familien ueber alle
|
||||
Diensttypen hinweg ueberproportional viele Dienste bekommen.
|
||||
|
||||
F3 (global): Basierend auf historischen Daten + aktuellem Planungszeitraum
|
||||
F4 (lokal): Nur fuer den aktuellen Planungszeitraum
|
||||
Berechnet die Abweichung der Gesamtdienstanzahl (über alle Diensttypen)
|
||||
vom fairen Gesamtziel. Dies verhindert, dass einzelne Eltern über alle
|
||||
Diensttypen hinweg überproportional viele Dienste bekommen.
|
||||
|
||||
Args:
|
||||
prob: Das LP-Problem
|
||||
x: Die Entscheidungsvariablen
|
||||
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:
|
||||
Dictionary mit Gesamt-Fairness-Abweichungsvariablen ueber alle Diensttypen
|
||||
Dictionary mit Gesamt-Fairness-Abweichungsvariablen
|
||||
"""
|
||||
fairness_abweichung_gesamt = {}
|
||||
|
||||
@ -340,7 +343,7 @@ class Elterndienstplaner:
|
||||
f"fair_gesamt_{constraint_prefix}_{eltern.replace(' ', '_')}",
|
||||
lowBound=0)
|
||||
|
||||
|
||||
# Tatsächliche Gesamtdienste für diesen Elternteil
|
||||
tatsaechliche_dienste_gesamt = pulp.lpSum(
|
||||
x[eltern, tag, dienst]
|
||||
for tag in self.daten.planungszeitraum
|
||||
@ -348,8 +351,10 @@ class Elterndienstplaner:
|
||||
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)
|
||||
|
||||
# Fairness-Constraints
|
||||
prob += (tatsaechliche_dienste_gesamt - ziel_gesamt <=
|
||||
fairness_abweichung_gesamt[eltern])
|
||||
prob += (ziel_gesamt - tatsaechliche_dienste_gesamt <=
|
||||
@ -367,41 +372,46 @@ class Elterndienstplaner:
|
||||
fairness_abweichung_gesamt_global: Dict,
|
||||
fairness_abweichung_gesamt_lokal: Dict
|
||||
) -> None:
|
||||
"""Erstellt die Zielfunktion mit Fairness und Praeferenzen"""
|
||||
"""Erstellt die Zielfunktion mit Fairness und Präferenzen"""
|
||||
objective_terms = []
|
||||
|
||||
# Fairness-Gewichtung
|
||||
gewicht_global = 40
|
||||
gewicht_lokal = 60
|
||||
gewicht_f1 = gewicht_global
|
||||
gewicht_f2 = gewicht_lokal
|
||||
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 dienst in self.daten.dienste:
|
||||
objective_terms.append(gewicht_f1 * fairness_abweichung_global[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_f4_lokal * fairness_abweichung_gesamt_lokal[eltern])
|
||||
objective_terms.append(gewicht_f3_lokal * fairness_abweichung_gesamt_lokal[eltern])
|
||||
|
||||
# P1: Bevorzugte Dienste
|
||||
for (eltern, tag, dienst), praef in self.daten.praeferenzen.items():
|
||||
if (eltern, tag, dienst) in x and praef == 1:
|
||||
# P1: Bevorzugte Dienste (positiv belohnen)
|
||||
for (eltern, tag, dienst), präf in self.daten.präferenzen.items():
|
||||
if (eltern, tag, dienst) in x and präf == 1: # bevorzugt
|
||||
objective_terms.append(-5 * x[eltern, tag, dienst])
|
||||
|
||||
# P2: Abgelehnte Dienste
|
||||
for (eltern, tag, dienst), praef in self.daten.praeferenzen.items():
|
||||
if (eltern, tag, dienst) in x and praef == -1:
|
||||
# P2: Abgelehnte Dienste (bestrafen)
|
||||
for (eltern, tag, dienst), präf in self.daten.präferenzen.items():
|
||||
if (eltern, tag, dienst) in x and präf == -1: # abgelehnt
|
||||
objective_terms.append(25 * x[eltern, tag, dienst])
|
||||
|
||||
# Zielfunktion setzen
|
||||
if objective_terms:
|
||||
prob += pulp.lpSum(objective_terms)
|
||||
else:
|
||||
# Fallback: Minimiere Gesamtanzahl Dienste
|
||||
prob += pulp.lpSum([var for var in x.values()])
|
||||
|
||||
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[
|
||||
pulp.LpProblem,
|
||||
@ -414,67 +424,76 @@ class Elterndienstplaner:
|
||||
"""
|
||||
print("Erstelle Optimierungsmodell...")
|
||||
|
||||
print("\nDebug: Verfuegbarkeit analysieren...")
|
||||
for tag in self.daten.planungszeitraum[:5]:
|
||||
verfuegbare = [e for e in self.daten.eltern if self.daten.verfuegbarkeit.get((e, tag), True)]
|
||||
benoetigte = self.daten.benoetigte_dienste.get(tag, [])
|
||||
print(f" {tag}: Benötigt {len(benoetigte)} Dienste {benoetigte}, verfügbar: {verfuegbare}")
|
||||
# Debugging: Verfügbarkeit prüfen
|
||||
print("\nDebug: Verfügbarkeit analysieren...")
|
||||
for tag in self.daten.planungszeitraum[:5]: # Erste 5 Tage
|
||||
verfügbare = [e for e in self.daten.eltern if self.daten.verfügbarkeit.get((e, tag), True)]
|
||||
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)
|
||||
|
||||
# Entscheidungsvariablen erstellen
|
||||
x = self._erstelle_entscheidungsvariablen()
|
||||
|
||||
# Grundlegende Constraints hinzufügen
|
||||
self._add_constraint_ein_dienst_pro_woche(prob, x)
|
||||
self._add_constraint_ein_dienst_pro_tag(prob, x)
|
||||
self._add_constraint_verfuegbarkeit(prob, x)
|
||||
self._add_constraint_dienst_bedarf(prob, x)
|
||||
|
||||
# Fairness-Constraints
|
||||
ziel_dienste_global = self.berechne_faire_zielverteilung_global()
|
||||
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)
|
||||
|
||||
# F2: Lokale Fairness pro Diensttyp
|
||||
fairness_abweichung_lokal = self._add_constraint_fairness_diensttypspezifisch(
|
||||
# F2: Lokale Fairness-Constraints
|
||||
fairness_abweichung_lokal = self._add_fairness_constraints(
|
||||
prob, x, ziel_dienste_lokal, "lokal"
|
||||
)
|
||||
|
||||
# F1: Globale Fairness pro Diensttyp
|
||||
fairness_abweichung_global = self._add_constraint_fairness_diensttypspezifisch(
|
||||
# F1: Globale Fairness-Constraints
|
||||
fairness_abweichung_global = self._add_fairness_constraints(
|
||||
prob, x, ziel_dienste_global, "global"
|
||||
)
|
||||
|
||||
# F3: Diensttypuebergreifende Fairness (global)
|
||||
fairness_abweichung_gesamt_global = self._add_constraint_fairness_typuebergreifend(
|
||||
# F3: Dienstübergreifende Fairness - Global
|
||||
fairness_abweichung_gesamt_global = self._add_constraint_gesamtfairness(
|
||||
prob, x, ziel_dienste_global, "global"
|
||||
)
|
||||
|
||||
# F4: Diensttypuebergreifende Fairness (lokal)
|
||||
fairness_abweichung_gesamt_lokal = self._add_constraint_fairness_typuebergreifend(
|
||||
# F3: Dienstübergreifende Fairness - Lokal
|
||||
fairness_abweichung_gesamt_lokal = self._add_constraint_gesamtfairness(
|
||||
prob, x, ziel_dienste_lokal, "lokal"
|
||||
)
|
||||
|
||||
# Zielfunktion erstellen
|
||||
self._erstelle_zielfunktion(prob, x, fairness_abweichung_lokal, fairness_abweichung_global,
|
||||
fairness_abweichung_gesamt_global, fairness_abweichung_gesamt_lokal)
|
||||
|
||||
print(f"Modell erstellt mit {len(x)} Variablen und {len(prob.constraints)} Constraints")
|
||||
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]]]]:
|
||||
"""Loest das Optimierungsproblem"""
|
||||
"""Löst das Optimierungsproblem"""
|
||||
print("Löse Optimierungsproblem...")
|
||||
|
||||
# Solver wählen (verfügbare Solver testen)
|
||||
solver = None
|
||||
try:
|
||||
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:
|
||||
try:
|
||||
print("Versuche GLPK Solver...")
|
||||
solver = pulp.GLPK_CMD(msg=0)
|
||||
solver = pulp.GLPK_CMD(msg=0) # GLPK falls verfügbar
|
||||
except:
|
||||
print("Kein spezifizierter Solver verfügbar, verwende Standard.")
|
||||
solver = None
|
||||
solver = None # Default Solver
|
||||
|
||||
prob.solve(solver)
|
||||
|
||||
@ -485,16 +504,17 @@ class Elterndienstplaner:
|
||||
print("WARNUNG: Keine optimale Lösung gefunden!")
|
||||
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():
|
||||
if var.varValue and var.varValue > 0.5:
|
||||
if tag not in loesung:
|
||||
loesung[tag] = {}
|
||||
if dienst not in loesung[tag]:
|
||||
loesung[tag][dienst] = []
|
||||
loesung[tag][dienst].append(eltern)
|
||||
if var.varValue and var.varValue > 0.5: # Binary variable ist 1
|
||||
if tag not in lösung:
|
||||
lösung[tag] = {}
|
||||
if dienst not in lösung[tag]:
|
||||
lösung[tag][dienst] = []
|
||||
lösung[tag][dienst].append(eltern)
|
||||
|
||||
return loesung
|
||||
return lösung
|
||||
|
||||
|
||||
def main() -> None:
|
||||
@ -511,20 +531,28 @@ def main() -> None:
|
||||
print("="*50)
|
||||
|
||||
try:
|
||||
# Create data model and load data
|
||||
daten = ElterndienstplanerDaten()
|
||||
daten.lade_daten(eingabe_datei, eltern_datei, vorherige_datei)
|
||||
|
||||
# Create output handler and optimization engine
|
||||
ausgabe = ElterndienstAusgabe(daten)
|
||||
planer = Elterndienstplaner(daten, ausgabe)
|
||||
|
||||
# Optimierung
|
||||
prob, x = planer.erstelle_optimierungsmodell()
|
||||
loesung = planer.loese_optimierung(prob, x)
|
||||
lösung = planer.löse_optimierung(prob, x)
|
||||
|
||||
if loesung is not None:
|
||||
ausgabe.schreibe_ausgabe_csv(ausgabe_datei, loesung)
|
||||
ausgabe.drucke_statistiken(loesung)
|
||||
ausgabe.visualisiere_verteilungen(loesung)
|
||||
ausgabe.visualisiere_praeferenz_verletzungen(loesung)
|
||||
if lösung is not None:
|
||||
# Ergebnisse ausgeben
|
||||
ausgabe.schreibe_ausgabe_csv(ausgabe_datei, lösung)
|
||||
ausgabe.drucke_statistiken(lösung)
|
||||
|
||||
# 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!")
|
||||
else:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user