Compare commits

..

2 Commits

Author SHA1 Message Date
Jan Hoheisel
5acef1e2ae Dokumentation 2026-01-17 23:38:11 +01:00
Jan Hoheisel
a25f516d4c Readme, Kommentare, Umbenennungen
Readme ueberarbeitet
2026-01-17 23:19:41 +01:00
4 changed files with 241 additions and 206 deletions

223
README.md
View File

@ -2,73 +2,108 @@
Automatische Zuteilung von Elterndiensten im Kinderladen unter Berücksichtigung von Fairness und Präferenzen.
## Verwendung
## Wie funktioniert die Dienstplanung?
```bash
./elterndienstplaner.py <eingabe.csv> <eltern.csv> <ausgabe.csv> [<vorherige-ausgaben.csv>]
```
Der Elterndienstplaner verteilt die anfallenden Dienstzuteilungen für einen Monat automatisch auf die Eltern. Dabei werden folgende Ziele berücksichtigt:
**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
### 1. Regeln (Harte Constraints)
## 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)
- **P** - Putznotdienst (täglich, 1 Person)
- **E** - Essensausgabenotdienst (täglich, 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.
## Eingabedateien
## Abwesenheiten und Präferenzen angeben (eingabe.csv)
### eingabe.csv
Informationen zu benötigten Diensten und Verfügbarkeit/Präferenzen der Eltern.
Diese Datei enthält für jeden Tag des Planungsmonats, welche Dienste anfallen und welche Eltern verfügbar sind bzw. Präferenzen haben.
**Format:**
```
Datum,Wochentag,Dienste,Eltern1,Eltern2,...
2025-01-06,Montag,FPE,F+,x,...
2025-01-07,Dienstag,FPE,P-,F+P+,...
Datum, Wochentag, Dienste, Sarah & Tim, Leon, Maya, ...
2026-01-06, Montag, FPE, F+, x, , ...
2026-01-07, Dienstag, FPE, P-, F+P+, , ...
```
**Spalten:**
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+`
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)
- 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,...
Müller,2024-09-01,2025-07-31,2
Schmidt,2024-09-01,2024-12-31,1,2025-01-01,2025-07-31,0
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
```
**Spalten:**
1. Elternname (Kind-Name zur Identifikation)
2-4. Zeitraum 1: Beginn, Ende, Dienstfaktor
5-7. Zeitraum 2: Beginn, Ende, Dienstfaktor (optional)
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)
...
**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 (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)
@ -78,86 +113,114 @@ 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
- Ab Oktober: Vorherige Ausgaben anhängen für kumulative Fairness über das Jahr
- Im Jahresverlauf sammeln sich die Ausgaben an
## Ausgabe
## Ausgabedatei
### ausgabe.csv
Zugeteilte Dienste pro Tag.
Dienstzuteilungen pro Tag. Diese Datei wird vom Programm erstellt.
**Format:**
```
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,,
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, ,
```
## 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)
- **C2**: Pro Eltern maximal **ein Dienst pro Tag**
- **C3**: Nur **verfügbare** Eltern einteilen
- **C4**: Alle **benötigten Dienste** müssen besetzt werden
```bash
./elterndienstplaner.py <eingabe.csv> <eltern.csv> <ausgabe.csv> [<vorherige-ausgaben.csv>]
```
### 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):
- **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)
## Wie werden die Constraints umgesetzt?
- **F2 (Lokal)**: Dienste proportional zum Dienstfaktor im **aktuellen Monat**
- Nur aktueller Planungszeitraum
- Gewichtung: 60% (zu Jahresbeginn) → 40% (zu Jahresende)
Dieser Abschnitt erklärt die technische Umsetzung für technisch interessierte Eltern.
- **F3 (Dienstübergreifend)**: Gleiche Gesamtanzahl über alle Diensttypen
- Verhindert Häufung bei einzelnen Eltern
### Mathematisches Optimierungsproblem
**Präferenzen** (niedrigere Priorität):
- **P1**: Bevorzugte Dienste (`+`) werden bevorzugt zugeteilt
- **P2**: Abgelehnte Dienste (`-`) werden vermieden
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.
### 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):**
- 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
- **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
**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
### Zielfunktion: Fairness und Präferenzen
**Gewichtung im Jahresverlauf:**
- **September-November**: F2 (lokal) stärker → sanftes Einführen
- **Dezember-Mai**: Ausgewogen
- **Juni-Juli**: F1 (global) stärker → Jahresausgleich
Die **Zielfunktion** bewertet, wie gut eine Dienstverteilung ist. Das Programm minimiert Abweichungen von fairer Verteilung und berücksichtigt Präferenzen.
## 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:
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
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
## Troubleshooting
**"Keine optimale Lösung gefunden":**
- 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":**
- Prüfen Sie die Dienstfaktoren in `eltern.csv`
- Stellen Sie sicher, dass `vorherige-ausgaben.csv` korrekt ist
- Stellen Sie sicher, dass `vorherige.ausgaben.csv` korrekt ist
- Mehr Eltern verfügbar machen

View File

@ -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.präferenzen.items():
for (eltern, tag, dienst), präf in self.daten.praeferenzen.items():
praeferenzen_pro_eltern_dienst[eltern][dienst][tag] = präf
# Berechne Verletzungen

View File

@ -60,8 +60,8 @@ class ElterndienstplanerDaten:
self.planungszeitraum: List[date] = []
self.eltern: List[Eltern] = []
self.benoetigte_dienste: Dict[date, List[Dienst]] = {}
self.verfügbarkeit: Dict[Tuple[Eltern, date], bool] = {}
self.präferenzen: Dict[Tuple[Eltern, date, Dienst], int] = {}
self.verfuegbarkeit: Dict[Tuple[Eltern, date], bool] = {}
self.praeferenzen: 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.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)
# Eltern CSV: Dienstfaktoren

View File

@ -17,32 +17,31 @@ from ausgabe import ElterndienstAusgabe
class Elterndienstplaner:
"""Optimierungs-Engine für Elterndienstplanung"""
"""Optimierungs-Engine fuer 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 für den Planungszeitraum
"""Berechnet die faire Zielanzahl von Diensten fuer den 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
mehr Dienste geleistet wurden als fair wäre."""
mehr Dienste geleistet wurden als fair waere."""
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 der tatsächlich geleisteten Dienste
# 1. HISTORISCHE PERIODE: Faire Umverteilung
historische_dienste_dieses_typs = [
(datum, eltern) for datum, eltern, d in self.daten.historische_dienste
if d == dienst
@ -50,22 +49,18 @@ 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 an diesem Tag
anzahl_dienste = len(geleistete_eltern)
# 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:
@ -73,22 +68,19 @@ class Elterndienstplaner:
faire_zuteilung = anteil * anzahl_dienste
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]} "
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
# 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
@ -97,7 +89,6 @@ 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
@ -105,9 +96,7 @@ 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
@ -118,13 +107,21 @@ class Elterndienstplaner:
def berechne_faire_zielverteilung_lokal(self) -> Zielverteilung:
"""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))
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
@ -134,21 +131,17 @@ 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
)
@ -161,7 +154,7 @@ class Elterndienstplaner:
return ziel_dienste_lokal
def _erstelle_entscheidungsvariablen(self) -> Entscheidungsvariablen:
"""Erstellt die binären Entscheidungsvariablen x[eltern, tag, dienst]"""
"""Erstellt die binaeren Entscheidungsvariablen x[eltern, tag, dienst]"""
x: Entscheidungsvariablen = {}
for eltern in self.daten.eltern:
for tag in self.daten.planungszeitraum:
@ -180,21 +173,20 @@ class Elterndienstplaner:
) -> None:
"""C1: Je Eltern und Dienst nur einmal die Woche (Woche = Montag bis Sonntag)"""
erster_tag = self.daten.planungszeitraum[0]
# weekday(): 0=Montag, 6=Sonntag
# Finde Montag am oder vor dem ersten Planungstag (für historische Dienste)
# Finde Montag am oder vor dem ersten Planungstag
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) # Sonntag
woche_ende = woche_start + timedelta(days=6)
for eltern in self.daten.eltern:
for dienst in self.daten.dienste:
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
if woche_start < erster_tag:
for hist_datum, hist_eltern, hist_dienst in self.daten.historische_dienste:
@ -203,13 +195,11 @@ 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}"
@ -238,10 +228,10 @@ class Elterndienstplaner:
prob: pulp.LpProblem,
x: Entscheidungsvariablen
) -> None:
"""C3: Dienste nur verfügbaren Eltern zuteilen"""
"""C3: Dienste nur verfuegbaren Eltern zuteilen"""
for eltern in self.daten.eltern:
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:
if (eltern, tag, dienst) in x:
prob += x[eltern, tag, dienst] == 0, \
@ -252,41 +242,46 @@ class Elterndienstplaner:
prob: pulp.LpProblem,
x: Entscheidungsvariablen
) -> None:
"""C4: Alle benötigten Dienste müssen zugeteilt werden"""
"""C4: Alle benoetigten Dienste muessen 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:
# Prüfe ob Eltern verfügbar
if self.daten.verfügbarkeit.get((eltern, tag), True):
if self.daten.verfuegbarkeit.get((eltern, tag), True):
dienst_vars.append(x[eltern, tag, dienst])
if dienst_vars:
# Anzahl benötigter Personen pro Dienst (aus Dienst-Objekt)
# Anzahl benoetigter Personen pro Dienst
benoetigte_personen = dienst.personen_anzahl
prob += pulp.lpSum(dienst_vars) == benoetigte_personen, \
f"Bedarf_{tag}_{dienst.kuerzel}"
def _add_fairness_constraints(
def _add_constraint_fairness_diensttypspezifisch(
self,
prob: pulp.LpProblem,
x: Entscheidungsvariablen,
ziel_dienste: Zielverteilung,
constraint_prefix: str
) -> 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:
prob: Das LP-Problem
x: Die Entscheidungsvariablen
ziel_dienste: Die Zielverteilung der Dienste
constraint_prefix: Präfix für Constraint-Namen ('lokal' oder 'global')
ziel_dienste: Die Zielverteilung (global oder lokal)
constraint_prefix: Praefix fuer Constraint-Namen ('global' fuer F1, 'lokal' fuer F2)
Returns:
Dictionary mit Fairness-Abweichungsvariablen
Dictionary mit Fairness-Abweichungsvariablen pro Diensttyp
"""
# Hilfsvariablen für Fairness-Abweichungen erstellen
fairness_abweichung = {}
for eltern in self.daten.eltern:
@ -295,17 +290,16 @@ 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])
@ -314,27 +308,30 @@ class Elterndienstplaner:
return fairness_abweichung
def _add_constraint_gesamtfairness(
def _add_constraint_fairness_typuebergreifend(
self,
prob: pulp.LpProblem,
x: Entscheidungsvariablen,
ziel_dienste: Zielverteilung,
constraint_prefix: str
) -> 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)
vom fairen Gesamtziel. Dies verhindert, dass einzelne Eltern über alle
Diensttypen hinweg überproportional viele Dienste bekommen.
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
Args:
prob: Das LP-Problem
x: Die Entscheidungsvariablen
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:
Dictionary mit Gesamt-Fairness-Abweichungsvariablen
Dictionary mit Gesamt-Fairness-Abweichungsvariablen ueber alle Diensttypen
"""
fairness_abweichung_gesamt = {}
@ -343,7 +340,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
@ -351,10 +348,8 @@ 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 <=
@ -372,46 +367,41 @@ class Elterndienstplaner:
fairness_abweichung_gesamt_global: Dict,
fairness_abweichung_gesamt_lokal: Dict
) -> None:
"""Erstellt die Zielfunktion mit Fairness und Präferenzen"""
"""Erstellt die Zielfunktion mit Fairness und Praeferenzen"""
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_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 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_f3_lokal * fairness_abweichung_gesamt_lokal[eltern])
objective_terms.append(gewicht_f4_lokal * fairness_abweichung_gesamt_lokal[eltern])
# 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
# P1: Bevorzugte Dienste
for (eltern, tag, dienst), praef in self.daten.praeferenzen.items():
if (eltern, tag, dienst) in x and praef == 1:
objective_terms.append(-5 * x[eltern, tag, dienst])
# 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
# P2: Abgelehnte Dienste
for (eltern, tag, dienst), praef in self.daten.praeferenzen.items():
if (eltern, tag, dienst) in x and praef == -1:
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}, F3_lokal = {gewicht_f3_lokal}")
f"F3 (global) = {gewicht_f3_global}, F4 (lokal) = {gewicht_f4_lokal}")
def erstelle_optimierungsmodell(self) -> Tuple[
pulp.LpProblem,
@ -424,76 +414,67 @@ class Elterndienstplaner:
"""
print("Erstelle Optimierungsmodell...")
# 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}")
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}")
# 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-Constraints
fairness_abweichung_lokal = self._add_fairness_constraints(
# F2: Lokale Fairness pro Diensttyp
fairness_abweichung_lokal = self._add_constraint_fairness_diensttypspezifisch(
prob, x, ziel_dienste_lokal, "lokal"
)
# F1: Globale Fairness-Constraints
fairness_abweichung_global = self._add_fairness_constraints(
# F1: Globale Fairness pro Diensttyp
fairness_abweichung_global = self._add_constraint_fairness_diensttypspezifisch(
prob, x, ziel_dienste_global, "global"
)
# F3: Dienstübergreifende Fairness - Global
fairness_abweichung_gesamt_global = self._add_constraint_gesamtfairness(
# F3: Diensttypuebergreifende Fairness (global)
fairness_abweichung_gesamt_global = self._add_constraint_fairness_typuebergreifend(
prob, x, ziel_dienste_global, "global"
)
# F3: Dienstübergreifende Fairness - Lokal
fairness_abweichung_gesamt_lokal = self._add_constraint_gesamtfairness(
# F4: Diensttypuebergreifende Fairness (lokal)
fairness_abweichung_gesamt_lokal = self._add_constraint_fairness_typuebergreifend(
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 löse_optimierung(self, prob: pulp.LpProblem,
def loese_optimierung(self, prob: pulp.LpProblem,
x: Entscheidungsvariablen) -> Optional[Dict[date, Dict[Dienst, List[Eltern]]]]:
"""Löst das Optimierungsproblem"""
"""Loest 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) # Standard CBC Solver
solver = pulp.PULP_CBC_CMD(msg=0, timeLimit=10)
except:
try:
print("Versuche GLPK Solver...")
solver = pulp.GLPK_CMD(msg=0) # GLPK falls verfügbar
solver = pulp.GLPK_CMD(msg=0)
except:
print("Kein spezifizierter Solver verfügbar, verwende Standard.")
solver = None # Default Solver
solver = None
prob.solve(solver)
@ -504,17 +485,16 @@ class Elterndienstplaner:
print("WARNUNG: Keine optimale Lösung gefunden!")
return None
# Lösung extrahieren
lösung: Dict[date, Dict[Dienst, List[Eltern]]] = {}
loesung: Dict[date, Dict[Dienst, List[Eltern]]] = {}
for (eltern, tag, dienst), var in x.items():
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)
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)
return lösung
return loesung
def main() -> None:
@ -531,28 +511,20 @@ 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()
lösung = planer.se_optimierung(prob, x)
loesung = planer.loese_optimierung(prob, x)
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)
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)
print("\n✓ Planung erfolgreich abgeschlossen!")
else: