From a25f516d4c10ef3d310be0df655e8f8936b93113 Mon Sep 17 00:00:00 2001 From: Jan Hoheisel Date: Sat, 17 Jan 2026 23:19:41 +0100 Subject: [PATCH 1/2] Readme, Kommentare, Umbenennungen Readme ueberarbeitet --- README.md | 213 +++++++++++++++++++++++++++--------------- ausgabe.py | 2 +- datenmodell.py | 6 +- elterndienstplaner.py | 207 +++++++++++++++++----------------------- 4 files changed, 227 insertions(+), 201 deletions(-) diff --git a/README.md b/README.md index 4b8fac4..021ca5f 100644 --- a/README.md +++ b/README.md @@ -2,19 +2,36 @@ Automatische Zuteilung von Elterndiensten im Kinderladen unter Berücksichtigung von Fairness und Präferenzen. -## Verwendung +## Wie funktioniert die Dienstplanung? -```bash -./elterndienstplaner.py [] -``` +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). Um bei längeren Abwesenheiten für mehr Gleichmäßigkeit in der Verteilung zu sorgen, werden Abwesenheiten aus der Dienstpflicht herausgerechnet +- **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) @@ -24,30 +41,46 @@ Automatische Zuteilung von Elterndiensten im Kinderladen unter Berücksichtigung 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 +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 Kind** - Abwesenheiten und Präferenzen: + - `x` = nicht verfügbar (Urlaub, Krankheit, etc.) - `F+` = Frühstücksdienst bevorzugt - - `P-` = Putznotdienst nur notfalls - - Mehrere Präferenzen kombinierbar: `F+P-E+` + - `P-` = Putznotdienst abgelehnt + - `K+` = Kochen bevorzugt + - 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 + ### eltern.csv Dienstfaktoren (= Anzahl betreuter Kinder) pro Elternteil und Zeitraum. @@ -55,20 +88,25 @@ 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 +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,82 +116,107 @@ 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,, +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 [] +``` -### 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 +- Beispiel: Im Januar sollten Sarah & Tim ca. 2× so viele Zuteilungen erhalten wie Leon + +**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":** diff --git a/ausgabe.py b/ausgabe.py index 6cd8488..2195695 100644 --- a/ausgabe.py +++ b/ausgabe.py @@ -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 diff --git a/datenmodell.py b/datenmodell.py index 3a49f8f..75b5f6e 100644 --- a/datenmodell.py +++ b/datenmodell.py @@ -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 diff --git a/elterndienstplaner.py b/elterndienstplaner.py index 6429b90..6829a5b 100755 --- a/elterndienstplaner.py +++ b/elterndienstplaner.py @@ -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,12 @@ 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""" ziel_dienste_lokal: Zielverteilung = defaultdict(lambda: defaultdict(float)) print("\nBerechne lokale faire Zielverteilung für aktuellen Planungszeitraum...") - # 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 +122,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 +145,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 +164,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 +186,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 +219,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 +233,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 +281,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 +299,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 +331,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 +339,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 +358,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 +405,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 +476,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 +502,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.lö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: From f8648313bfb75ed1e5288f50176ecafbd8958057 Mon Sep 17 00:00:00 2001 From: Jan Hoheisel Date: Sat, 17 Jan 2026 23:38:11 +0100 Subject: [PATCH 2/2] Dokumentation --- README.md | 53 +++++++++++++++++++++---------------------- elterndienstplaner.py | 11 ++++++++- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 021ca5f..4aad1ef 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ 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). Um bei längeren Abwesenheiten für mehr Gleichmäßigkeit in der Verteilung zu sorgen, werden Abwesenheiten aus der Dienstpflicht herausgerechnet +- **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 @@ -37,7 +37,7 @@ Die Fairness bestimmt **wie viele** Zuteilungen jede Familie erhält, die Präfe - **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. @@ -47,29 +47,28 @@ Diese Datei enthält für jeden Tag des Planungsmonats, welche Dienste anfallen **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, 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. **Diensttypen** - Welche Diensttypen an diesem Tag benötigt werden (z.B. "FPE" für Frühstück, Putzen, Essen) -4-n. **Eine Spalte pro Kind** - Abwesenheiten und Präferenzen: +4-n. **Eine Spalte pro Familie** - Abwesenheiten und Präferenzen: - `x` = nicht verfügbar (Urlaub, Krankheit, etc.) - - `F+` = Frühstücksdienst bevorzugt - - `P-` = Putznotdienst abgelehnt - - `K+` = Kochen bevorzugt + - `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- +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 @@ -81,16 +80,14 @@ Datum,Wochentag,Dienste,Sarah & Tim,Leon,Maya ### eltern.csv -### 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, ... +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:** @@ -127,9 +124,9 @@ Dienstzuteilungen pro Tag. Diese Datei wird vom Programm erstellt. **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 +2026-01-06, Montag, Sarah & Tim, Leon, Maya, , +2026-01-07, Dienstag, Maya, Sarah & Tim, Leon, , ``` Jede Zeile entspricht einem Tag, die Spalten enthalten die Kindernamen, denen die jeweiligen Diensttypen zugeteilt wurden. @@ -174,7 +171,10 @@ Die **Zielfunktion** bewertet, wie gut eine Dienstverteilung ist. Das Programm m **F2 (Lokale Fairness):** - Berechnung: Nur für den aktuellen Planungsmonat - Ziel: Die Anzahl der Zuteilungen im aktuellen Monat soll proportional zum Dienstfaktor sein -- Beispiel: Im Januar sollten Sarah & Tim ca. 2× so viele Zuteilungen erhalten wie Leon +- **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 @@ -217,10 +217,9 @@ Das Programm zeigt nach der Optimierung: **"Keine optimale Lösung gefunden":** - Zu viele Eltern nicht verfügbar - 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 -- Mehr Eltern verfügbar machen +- Dienstfaktoren in `eltern.csv` prüfen +- Sicherstellen, dass `vorherige.ausgaben.csv` korrekt ist + diff --git a/elterndienstplaner.py b/elterndienstplaner.py index 6829a5b..107c518 100755 --- a/elterndienstplaner.py +++ b/elterndienstplaner.py @@ -107,11 +107,20 @@ 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""" + 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)") summe_dienstfaktor_planungszeitraum_alle_eltern = sum( sum(self.daten.dienstfaktoren[e][tag] for tag in self.daten.planungszeitraum)