Compare commits

...

9 Commits
v0.1 ... main

Author SHA1 Message Date
beea66ef2f Kila-jahr neigt sich dem Ende zu, globales gewicht erhoeht 2026-03-07 21:26:53 +01:00
fc4182459b Merge branch 'dev' 2026-02-01 21:49:04 +01:00
f7b1267c98 Dienstaufwand und paralleles Rechnen 2026-02-01 21:48:03 +01:00
9f7d3c6d4a Formatierung Ausgabe 2026-02-01 20:33:11 +01:00
08e5cf11bd Mehr Fuktionen fuer historische Dienste
Planungszeitraum und historischer Zeitraum koennen sich jetzt
ueberlappen. So lassen sich einzelne Dienste (z.B. von einer Familie)
nachtraeglich neu planen.

historische Dienste werden bei den Constraints 1 Dienst pro Tag und 1
Dienst pro Woche korrekt beruecksichtigt

elterndienstplaner.py erzeugt jetzt ausgaben-gesamt.csv, die fuer
spaetere Aufrufe als Eingabe vorherige-dienste.csv verwendet werden kann.
2026-01-28 20:30:14 +01:00
Jan Hoheisel
b524edc2ba Tabellarische Debugausgabe
Gibt die Anzahl zugeteilter Dienste (historisch + neu) nach Dienstart und Eltern aus, sowie abweichung zum Globalen Ziel
2026-01-25 22:07:39 +01:00
Jan Hoheisel
3b94f460a8 Anleitung Eltern 2026-01-18 21:25:35 +01:00
Jan Hoheisel
e6dc8ff9d4 Eltern-Anleitung 2026-01-18 21:23:31 +01:00
Jan Hoheisel
07a1fbf002 leerzeile 2026-01-18 00:03:02 +01:00
6 changed files with 398 additions and 217 deletions

224
README.md
View File

@ -2,34 +2,13 @@
Automatische Zuteilung von Elterndiensten im Kinderladen unter Berücksichtigung von Fairness und Präferenzen. Automatische Zuteilung von Elterndiensten im Kinderladen unter Berücksichtigung von Fairness und Präferenzen.
## Wie funktioniert die Dienstplanung? ## Überblick
Der Elterndienstplaner verteilt die anfallenden Dienstzuteilungen für einen Monat automatisch auf die Eltern. Dabei werden folgende Ziele berücksichtigt: Der Elterndienstplaner verteilt monatliche Dienstzuteilungen optimal auf die Eltern durch Lösung eines linearen Optimierungsproblems. Das System berücksichtigt:
### 1. Regeln (Harte Constraints) - **Harte Constraints (C1-C4):** Müssen immer eingehalten werden
- **Fairness-Constraints (F1-F4):** Werden optimiert
Diese Regeln **müssen** immer eingehalten werden: - **Präferenzen (P1-P2):** Werden berücksichtigt, wenn möglich
- **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 ## Diensttypen
@ -39,97 +18,58 @@ Die Fairness bestimmt **wie viele** Zuteilungen jede Familie erhält, die Präfe
- **K** - Kochen (ca. alle 2 Wochen, 1 Person) - **K** - Kochen (ca. alle 2 Wochen, 1 Person)
- **A** - Elternabend (ca. einmal im Monat, 2 Personen) - **A** - Elternabend (ca. einmal im Monat, 2 Personen)
Die Planung erfolgt für einen Kalendermonat. ## Eingabedateien
## Abwesenheiten und Präferenzen angeben (eingabe.csv) ### eingabe.csv
Diese Datei enthält für jeden Tag des Planungsmonats, welche Dienste anfallen und welche Eltern verfügbar sind bzw. Präferenzen haben. Tägliche Dienste und Eltern-Verfügbarkeiten/Präferenzen für den Planungsmonat.
**Format:** **Format:**
``` ```csv
Datum, Wochentag, Dienste, Sarah & Tim, Leon, Maya, ... Datum,Wochentag,Dienste,Kind1,Kind2,...
2026-01-06, Montag, FPE, F+, x, , ... 2026-01-06,Montag,FPE,x,F+,...
2026-01-07, Dienstag, FPE, P-, F+P+, , ...
``` ```
**Spalten:** **Spalten:**
1. **Datum** (ISO-Format: YYYY-MM-DD) - Datum (YYYY-MM-DD)
2. **Wochentag** (zur Information) - Wochentag
3. **Diensttypen** - Welche Diensttypen an diesem Tag benötigt werden (z.B. "FPE" für Frühstück, Putzen, Essen) - Dienste (Kombination aus F/P/E/K/A)
4-n. **Eine Spalte pro Familie** - Abwesenheiten und Präferenzen: - Pro Familie: `x` (abwesend), `Kürzel+` (bevorzugt), `Kürzel-` (abgelehnt), leer (verfügbar)
- `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 ### eltern.csv
Dienstfaktoren (= Anzahl betreuter Kinder) pro Elternteil und Zeitraum. Dienstfaktoren (Anzahl Kinder) pro Familie und Zeitraum.
**Format:** **Format:**
```csv
Name_Kind(er),Zeitraum_Beginn,Zeitraum_Ende,Dienstfaktor,...
Sarah & Tim,2024-09-01,2025-07-31,2
Leon,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. **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:** **Hinweise:**
- Der Dienstfaktor entspricht der Anzahl der Kinder in der Familie (z.B. 2 für "Sarah & Tim", 1 für "Leon") - Dienstfaktor = Anzahl Kinder
- Familien mit mehreren Kindern werden als ein Eintrag mit entsprechendem Dienstfaktor geführt - Faktor 0 = keine Dienstpflicht (z.B. Vorstand)
- Bei überlappenden Zeiträumen gilt der letzte Eintrag - Mehrere Zeiträume möglich für Änderungen im Jahr
- 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).
### vorherige-ausgaben.csv (optional) ### vorherige-ausgaben.csv (optional)
Frühere Ausgaben des Programms zur Berechnung der Jahres-Fairness. Historische Dienstzuteilungen für Jahres-Fairness. Format wie `ausgabe.csv` bzw. `ausgabe-gesamt.csv`.
Hier kann die `ausgabe-gesamt.csv`, die bei der letzten Planung generiert wurde eingespielt werden.
**Format:** Wie `ausgabe.csv` (siehe unten).
**Verwendung:**
- Zu Beginn des Kita-Jahres (September): Keine Datei nötig
- Ab Oktober: Vorherige Ausgaben anhängen für kumulative Fairness über das Jahr
- Im Jahresverlauf sammeln sich die Ausgaben an
## Ausgabedatei ## Ausgabedatei
### ausgabe.csv ### ausgabe.csv
Dienstzuteilungen pro Tag. Diese Datei wird vom Programm erstellt. Die neu zugeteilten Dienste.
```csv
**Format:** Datum,Wochentag,Frühstücksdienst,Putznotdienst,Essensausgabenotdienst,Kochen,Elternabend
``` 2026-01-06,Montag,Sarah & Tim,Leon,Erika,,
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.
### ausgabe-gesamt.csv
Wie `ausgabe.csv`, enthält aber neben den neu geplanten Diensten auch die historischen Dienste, die über `vorherige-ausgaben.csv` übergeben wurden. Die Datei `ausgabe-gesamt.csv` kann bei der nächsten Planung wieder als Eingabe `vorherige-ausgaben.csv` verwendet werden.
## Verwendung ## Verwendung
@ -137,89 +77,67 @@ Jede Zeile entspricht einem Tag, die Spalten enthalten die Kindernamen, denen di
./elterndienstplaner.py <eingabe.csv> <eltern.csv> <ausgabe.csv> [<vorherige-ausgaben.csv>] ./elterndienstplaner.py <eingabe.csv> <eltern.csv> <ausgabe.csv> [<vorherige-ausgaben.csv>]
``` ```
**Parameter:** ## Constraints
- `eingabe.csv`: Benötigte Diensttypen und Eltern-Präferenzen für den Planungsmonat
- `eltern.csv`: Dienstfaktoren der Eltern (Anzahl betreuter Kinder)
- `ausgabe.csv`: Hier werden die Zuteilungen geschrieben
- `vorherige-ausgaben.csv` (optional): Historische Zuteilungen für Fairness über das Jahr
## Wie werden die Constraints umgesetzt? ### Harte Constraints (müssen erfüllt sein)
Dieser Abschnitt erklärt die technische Umsetzung für technisch interessierte Eltern. - **C1:** Maximal 1× pro Woche pro Diensttyp
- **C2:** Maximal 1 Dienst pro Tag
- **C3:** Nur bei Verfügbarkeit (keine `x` in eingabe.csv)
- **C4:** Alle benötigten Dienste werden besetzt
### Mathematisches Optimierungsproblem ### Fairness (werden optimiert)
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. - **F1 (Global):** Faire Jahresverteilung pro Diensttyp
- **F2 (Lokal):** Faire Monatsverteilung pro Diensttyp
- **Besonderheit:** Abwesenheitstage = Dienstfaktor 0, werden nicht nachgeholt im selben Monat
- **F3 (Global):** Ausgewogene Gesamtdienste über Jahr
- **F4 (Lokal):** Ausgewogene Gesamtdienste im Monat
**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) ### Präferenzen (niedrige Gewichtung)
**Constraints (Nebenbedingungen):** Diese schränken die möglichen Lösungen ein: - **P1:** Bevorzugte Dienste (`+`) werden bevorzugt zugeteilt
- **P2:** Abgelehnte Dienste (`-`) werden vermieden
- **C1 (Wöchentliches Limit):** Für jeden Eintrag und jeden Diensttyp: Die Summe der Zuweisungen pro Woche ≤ 1 ## Technische Details
- **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
### Zielfunktion: Fairness und Präferenzen ### Optimierungsproblem
Die **Zielfunktion** bewertet, wie gut eine Dienstverteilung ist. Das Programm minimiert Abweichungen von fairer Verteilung und berücksichtigt Präferenzen. **Entscheidungsvariablen:**
- Binär: `x[eltern, tag, dienst]` = 1 wenn zugeteilt, 0 sonst
**F1 (Globale Fairness):** **Zielfunktion:**
- 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):** Minimiert gewichtete Summe der Abweichungen von fairer Verteilung unter Berücksichtigung von Präferenzen. Lokale Fairness (aktueller Monat) hat höchste Priorität, gefolgt von globaler Fairness (ganzes Jahr).
- 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):** ### Lokale vs. Globale Fairness
- 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):** **Lokal (F2/F4):**
- An bestimmten Tagen bevorzugte Diensttypen (`+`) bekommen einen Bonus in der Zielfunktion - Nur aktueller Planungsmonat
- An bestimmten Tagen abgelehnte Diensttypen (`-`) bekommen eine Strafe in der Zielfunktion - Abwesenheitstage: Dienstfaktor = 0
- Diese Effekte sind **schwächer** als die Fairness-Terme, d.h. Fairness hat Vorrang - Ziel: Gleichmäßige Verteilung im Monat
- **Wichtig:** Präferenzen beeinflussen nur, an welchen Tagen welcher Diensttyp zugeteilt wird, nicht die Gesamtanzahl der Zuteilungen
### Gewichtung **Global (F1/F3):**
- Historische Daten + aktueller Monat
- Abwesenheitstage: Dienstfaktor wie in eltern.csv
- Ziel: Ausgleich über das Kitajahr
- Bereits geleistete Dienste werden abgezogen
Die verschiedenen Fairness-Ziele werden gewichtet: ## Programmausgabe
- **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 1. **Zuteilungen pro Familie:** Anzahl je Diensttyp
2. **Dienstfaktoren-Summe:** Im Planungszeitraum
3. **Verteilungsvergleich:** Soll (lokal/global) vs. Ist
4. **Präferenz-Verletzungen:** Anzahl abgelehnte/nicht-erfüllte Präferenzen
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
## Troubleshooting ## Troubleshooting
**"Keine optimale Lösung gefunden":** **"Keine optimale Lösung gefunden":**
- Zu viele Eltern nicht verfügbar - Zu viele Abwesenheiten
- Nicht genug Eltern für alle benötigten Diensttypen - Nicht genug verfügbare Eltern für benötigte Dienste
**"Unfaire Verteilung":** **"Unfaire Verteilung":**
- Dienstfaktoren in `eltern.csv` prüfen - Dienstfaktoren in `eltern.csv` prüfen
- Sicherstellen, dass `vorherige.ausgaben.csv` korrekt ist - `vorherige-ausgaben.csv` auf Korrektheit prüfen

80
anleitung-eltern.md Normal file
View File

@ -0,0 +1,80 @@
# Anleitung: Abwesenheiten und Präferenzen angeben
Diese Anleitung erklärt, wie ihr eure Abwesenheiten und Präferenzen für die Elterndienst-Planung angebt.
## Was sind Präferenzen?
An welchen Tagen ihr welche Dienste gern (oder lieber nicht) machen möchtet.
**Berücksichtigung der Präferenzen**
- Präferenzen beeinflussen **an welchen Tagen** ihr Dienste bekommt.
- Sie beeinflussen wenig, wie viele Dienste ihr bekommt.
- Fairness hat Vorrang: Wenn es für eine faire Verteilung nötig ist, werden eure Präferenzen ggf. nicht berücksichtigt.
## Bearbeiten der Monatsvorlage
Ihr erhaltet eine Datei `eingabe-monat-vorlage.csv`.
**Änderungen:**
1. Spaltenüberschrift `Eingabe` durch euren Kindernamen ersetzen
2. In dieser Spalte eure Abwesenheiten/Präferenzen eintragen
**Programm zum Öffnen:**
- Computer: LibreOffice Calc, Excel oder Texteditor
- Android: App "CSV Editor"
- iOS: Dateien-App
## Eintragen
### Dienstkürzel
**F** = Frühstücksdienst, **P** = Putznotdienst, **E** = Essensausgabenotdienst, **K** = Kochen, **A** = Elternabend
### Was eintragen?
| Eingabe | Bedeutung |
|---------|-----------|
| `x` | Abwesend |
| `F+` | Frühstücksdienst gern |
| `P-` | Putznotdienst lieber nicht |
**Kombinierbar:** `F+P-K+` = "Frühstück/Kochen gern, Putzen nicht"
**Leer:** Verfügbar, keine Präferenz
## Beispiel
Angenommen, euer Kind heißt Erika und ihr seid vom 6. bis 10. April im Urlaub, wollt gern dienstags oder freitags Frühstücksdienst machen, und am 15., 22. und 29. lieber keinen Frühstücks-, Putz- oder Essensdienst.
Die Tabelle sieht dann so aus:
```csv
Datum , Wochentag , Dienste, Erika
2026-04-01, Mittwoch , FPE ,
2026-04-02, Donnerstag, FPE ,
2026-04-03, Freitag , , F+
2026-04-06, Montag , , x
2026-04-07, Dienstag , FPE , x
2026-04-08, Mittwoch , FPE , x
2026-04-09, Donnerstag, FPE , x
2026-04-10, Freitag , FPEK , x
2026-04-13, Montag , FPE ,
2026-04-14, Dienstag , FPE , F+
2026-04-15, Mittwoch , FPE , F-P-E-
2026-04-16, Donnerstag, FPEA ,
2026-04-17, Freitag , FPE , F+
2026-04-20, Montag , FPE ,
2026-04-21, Dienstag , FPE , F+
2026-04-22, Mittwoch , FPE , F-P-E-
2026-04-23, Donnerstag, FPE ,
2026-04-24, Freitag , FPEK , F+K+
2026-04-27, Montag , FPE ,
2026-04-28, Dienstag , FPE , F+
2026-04-29, Mittwoch , FPE , F-P-E-
2026-04-30, Donnerstag, FPE ,
```
**Anmerkungen zum Beispiel:**
- Am 3. April ist `F+` eingetragen, obwohl keine Dienste anfallen (Spalte "Dienste" ist leer). Das hat keine Auswirkung.
- Am 24. April sind sowohl Frühstücksdienst als auch Kochen gewünscht (`F+K+`). Ihr bekommt aber maximal einen davon, da pro Tag nur ein Dienst erlaubt ist.
- Die Präferenzen am 15., 22. und 29. (`F-P-E-`) werden nach Möglichkeit berücksichtigt, aber wenn es für eine faire Verteilung nötig ist, könntet ihr trotzdem einen dieser Dienste bekommen.

View File

@ -6,7 +6,7 @@ Visualisierung und Export der Ergebnisse
from datetime import date from datetime import date
from collections import defaultdict from collections import defaultdict
from typing import Dict, List, DefaultDict from typing import Dict, List, DefaultDict, Tuple
from datenmodell import ElterndienstplanerDaten, Dienst, Eltern, Zielverteilung from datenmodell import ElterndienstplanerDaten, Dienst, Eltern, Zielverteilung
from csv_io import AusgabeWriter from csv_io import AusgabeWriter
@ -20,6 +20,8 @@ class ElterndienstAusgabe:
# Zwischenergebnisse aus der Optimierung (über Observer-Pattern gesetzt) # Zwischenergebnisse aus der Optimierung (über Observer-Pattern gesetzt)
self.ziel_lokal: Zielverteilung = None self.ziel_lokal: Zielverteilung = None
self.ziel_global: Zielverteilung = None self.ziel_global: Zielverteilung = None
# Historische Dienste (kann über Observer gesetzt werden)
self.historische_dienste: List[Tuple[date, Eltern, Dienst]] = None
def setze_zielverteilungen( def setze_zielverteilungen(
self, self,
@ -30,9 +32,17 @@ class ElterndienstAusgabe:
self.ziel_lokal = ziel_lokal self.ziel_lokal = ziel_lokal
self.ziel_global = ziel_global self.ziel_global = ziel_global
def setze_historische_dienste(self, historische_dienste: List[Tuple[date, Eltern, Dienst]]) -> None:
"""Observer-Callback: Setzt historische Dienste für Ausgabe/Export"""
self.historische_dienste = historische_dienste
def schreibe_ausgabe_csv(self, datei: str, lösung: Dict[date, Dict[Dienst, List[Eltern]]]) -> None: def schreibe_ausgabe_csv(self, datei: str, lösung: Dict[date, Dict[Dienst, List[Eltern]]]) -> None:
"""Schreibt die Lösung in die ausgabe.csv""" """Schreibt die Lösung in die ausgabe.csv"""
AusgabeWriter.schreibe_ausgabe_csv(datei, lösung, self.daten.planungszeitraum, self.daten.dienste) AusgabeWriter.schreibe_ausgabe_csv(datei, lösung, self.daten.planungszeitraum, self.daten.dienste, False)
# Schreibe ergänzende Datei mit historischen Diensten (falls vorhanden).
hist_datei = datei.replace('.csv', '-gesamt.csv') if datei.endswith('.csv') else datei + '-gesamt.csv'
historische = self.historische_dienste if self.historische_dienste is not None else self.daten.historische_dienste
AusgabeWriter.schreibe_ausgabe_csv(hist_datei, lösung, self.daten.planungszeitraum, self.daten.dienste, True, historische)
def drucke_statistiken(self, lösung: Dict[date, Dict[Dienst, List[Eltern]]]) -> None: def drucke_statistiken(self, lösung: Dict[date, Dict[Dienst, List[Eltern]]]) -> None:
"""Druckt Statistiken zur Lösung""" """Druckt Statistiken zur Lösung"""
@ -108,28 +118,30 @@ class ElterndienstAusgabe:
positive_praef_tage = {tag for tag, präf in praeferenzen_dienst.items() if präf == 1} positive_praef_tage = {tag for tag, präf in praeferenzen_dienst.items() if präf == 1}
if positive_praef_tage: # Es gibt positive Präferenzen if positive_praef_tage: # Es gibt positive Präferenzen
# Prüfe ob ALLE zugeteilten Dienste an nicht-präferierten Tagen sind
for tag in zugeteilte_tage: for tag in zugeteilte_tage:
if tag not in positive_praef_tage: if tag not in positive_praef_tage:
# Dienst wurde an nicht-präferiertem Tag zugeteilt
verletzungen[eltern][dienst]['positiv_nicht_erfuellt'] += 1 verletzungen[eltern][dienst]['positiv_nicht_erfuellt'] += 1
# Tabelle ausgeben # Tabelle ausgeben (verbesserte Spaltenformatierung)
print(f"\n{'Eltern':<20} ", end='') col_width = 14 # Breite pro Dienst-Spalte (sichtbar)
name_col = 20
# Header
print(f"\n{'Eltern':<{name_col}}", end='')
for dienst in self.daten.dienste: for dienst in self.daten.dienste:
print(f"{dienst.kuerzel:>12}", end='') print(f"{dienst.kuerzel:^{col_width}}", end='')
print() print()
print(f"{'':20} ", end='') print(f"{'':{name_col}}", end='')
for dienst in self.daten.dienste: for _ in self.daten.dienste:
print(f"{'neg, pos':>12}", end='') print(f"{'neg, pos':^{col_width}}", end='')
print() print()
print("-" * (20 + 12 * len(self.daten.dienste))) print("-" * (name_col + col_width * len(self.daten.dienste)))
gesamt_negativ = defaultdict(int) gesamt_negativ = defaultdict(int)
gesamt_positiv = defaultdict(int) gesamt_positiv = defaultdict(int)
for eltern in sorted(self.daten.eltern): for eltern in sorted(self.daten.eltern):
print(f"{eltern:<20} ", end='') print(f"{eltern:<{name_col}}", end='')
for dienst in self.daten.dienste: for dienst in self.daten.dienste:
neg = verletzungen[eltern][dienst]['negativ'] neg = verletzungen[eltern][dienst]['negativ']
pos = verletzungen[eltern][dienst]['positiv_nicht_erfuellt'] pos = verletzungen[eltern][dienst]['positiv_nicht_erfuellt']
@ -137,22 +149,28 @@ class ElterndienstAusgabe:
gesamt_negativ[dienst] += neg gesamt_negativ[dienst] += neg
gesamt_positiv[dienst] += pos gesamt_positiv[dienst] += pos
# Farbcodierung # Inhalt vor Padding erstellen
cell = f"{neg:>3}, {pos:>3}"
cell_padded = cell.center(col_width)
# Farbcodierung (erst nach Padding anwenden)
farbe = "" farbe = ""
reset = "" reset = ""
if neg > 0 or pos > 0: if neg > 0 or pos > 0:
farbe = "\033[91m" if neg > 0 else "\033[93m" # Rot für negativ, Gelb für positiv farbe = "\033[91m" if neg > 0 else "\033[93m" # Rot für negativ, Gelb für positiv
reset = "\033[0m" reset = "\033[0m"
print(f"{farbe}{neg:>3}, {pos:>3}{reset:>6}", end='') print(f"{farbe}{cell_padded}{reset}", end='')
print() print()
# Summenzeile # Summenzeile
print("-" * (20 + 12 * len(self.daten.dienste))) print("-" * (name_col + col_width * len(self.daten.dienste)))
print(f"{'SUMME':<20} ", end='') print(f"{'SUMME':<{name_col}}", end='')
for dienst in self.daten.dienste: for dienst in self.daten.dienste:
neg = gesamt_negativ[dienst] neg = gesamt_negativ[dienst]
pos = gesamt_positiv[dienst] pos = gesamt_positiv[dienst]
cell = f"{neg:>3}, {pos:>3}"
cell_padded = cell.center(col_width)
farbe = "" farbe = ""
reset = "" reset = ""
@ -160,7 +178,7 @@ class ElterndienstAusgabe:
farbe = "\033[91m" if neg > 0 else "\033[93m" farbe = "\033[91m" if neg > 0 else "\033[93m"
reset = "\033[0m" reset = "\033[0m"
print(f"{farbe}{neg:>3}, {pos:>3}{reset:>6}", end='') print(f"{farbe}{cell_padded}{reset}", end='')
print() print()
print("\nLegende:") print("\nLegende:")
@ -249,3 +267,99 @@ class ElterndienstAusgabe:
print(f"Maximale Abweichung von Global-Ziel: {max_abw_global:.2f} Dienste") print(f"Maximale Abweichung von Global-Ziel: {max_abw_global:.2f} Dienste")
print(f"Maximale Abweichung von Lokal-Ziel: {max_abw_lokal:.2f} Dienste") print(f"Maximale Abweichung von Lokal-Ziel: {max_abw_lokal:.2f} Dienste")
print("\nLegende: Δ = Tatsächlich - Ziel (positiv = mehr als Ziel, negativ = weniger als Ziel)") print("\nLegende: Δ = Tatsächlich - Ziel (positiv = mehr als Ziel, negativ = weniger als Ziel)")
def visualisiere_dienste_uebersicht(
self,
lösung: Dict[date, Dict[Dienst, List[Eltern]]]
) -> None:
"""Visualisiert die Übersicht der zugeteilten Dienste nach Optimierung
Zeigt für jede Familie und jeden Diensttyp:
- Anzahl Dienste nach Optimierung (Historie + Planungszeitraum)
- Differenz zum globalen Ziel (Historie + Planungszeitraum)
Args:
lösung: Die Lösung der Optimierung
"""
if self.ziel_global is None:
print("FEHLER: Globale Zielverteilung wurde nicht gesetzt!")
return
# Berechne historische Dienste pro Eltern und Dienst
historisch = defaultdict(lambda: defaultdict(int))
for datum, eltern, dienst in self.daten.historische_dienste:
historisch[eltern][dienst] += 1
# Berechne geplante Dienste (aus Lösung)
geplant = defaultdict(lambda: defaultdict(int))
for tag_dienste in lösung.values():
for dienst, eltern_liste in tag_dienste.items():
for eltern in eltern_liste:
geplant[eltern][dienst] += 1
# Berechne globale Ziele für jeden Elternteil und Dienst
# Das globale Ziel ist: faire Verteilung über (Historie + Planungszeitraum) MINUS bereits geleistete Historie
# Also: ziel_global[eltern][dienst] ist die SOLL-Änderung im Planungszeitraum
# Tatsächliches Gesamt-Ziel = historisch[eltern][dienst] + ziel_global[eltern][dienst]
print("\n" + "="*120)
print("ÜBERSICHT: Dienst nach der Optimierung")
print("="*120)
# Tabelle: NACH der Optimierung (historisch + geplant)
print("\n>>> NACH OPTIMIERUNG (historische Dienste + Planungszeitraum) <<<\n")
# Header
print(f"{'Eltern':<20} ", end='')
for dienst in self.daten.dienste:
print(f"{dienst.kuerzel:>14} ", end='')
print(f"{'GESAMT':>14}")
print(f"{'':20} ", end='')
for dienst in self.daten.dienste:
print(f"{'Ist / Δ Ziel':>14} ", end='')
print(f"{'Ist / Δ Ziel':>14}")
print("-" * 120)
# Datenzeilen
for eltern in sorted(self.daten.eltern):
print(f"{eltern:<20} ", end='')
gesamt_ist = 0
gesamt_ziel = 0
for dienst in self.daten.dienste:
ist_dienste = historisch[eltern][dienst] + geplant[eltern][dienst]
gesamt_ist += ist_dienste
# Globales Ziel = historisch + ziel_global (das ist das faire Gesamt-Ziel)
ziel_gesamt = historisch[eltern][dienst] + self.ziel_global[eltern][dienst]
gesamt_ziel += ziel_gesamt
delta = ist_dienste - ziel_gesamt
# Farbcodierung
farbe = ""
reset = ""
if abs(delta) > 0.5:
farbe = "\033[93m" if abs(delta) <= 1.5 else "\033[91m"
reset = "\033[0m"
print(f"{farbe}{ist_dienste:>6} / {delta:>+5.1f}{reset} ", end='')
# Gesamt-Spalte
delta_gesamt = gesamt_ist - gesamt_ziel
farbe = ""
reset = ""
if abs(delta_gesamt) > 0.5:
farbe = "\033[93m" if abs(delta_gesamt) <= 1.5 else "\033[91m"
reset = "\033[0m"
print(f"{farbe}{gesamt_ist:>6} / {delta_gesamt:>+5.1f}{reset}")
print()
print("Legende:")
print(" Ist = Anzahl tatsächlich geleisteter Dienste")
print(" Δ Ziel = Differenz zum globalen fairen Ziel (positiv = mehr als fair, negativ = weniger)")
print(" \033[93mGelb\033[0m = Abweichung 0.5 - 1.5 Dienste")
print(" \033[91mRot\033[0m = Abweichung > 1.5 Dienste")

View File

@ -6,7 +6,7 @@ Trennt CSV-Parsing und -Schreiben von der Business-Logik
import csv import csv
from datetime import datetime, date, timedelta from datetime import datetime, date, timedelta
from typing import Dict, List, Tuple, DefaultDict from typing import Dict, List, Tuple, DefaultDict, Optional
from collections import defaultdict from collections import defaultdict
@ -213,7 +213,9 @@ class AusgabeWriter:
datei: str, datei: str,
lösung: Dict[date, Dict[any, List[str]]], # Dienst-Objekt als Key lösung: Dict[date, Dict[any, List[str]]], # Dienst-Objekt als Key
tage: List[date], tage: List[date],
dienste: List # List[Dienst] dienste: List, # List[Dienst]
gesamt: bool = False,
historische_dienste: List[Tuple[date, str, any]] = None
) -> None: ) -> None:
""" """
Schreibt die Lösung in die ausgabe.csv Schreibt die Lösung in die ausgabe.csv
@ -221,10 +223,50 @@ class AusgabeWriter:
Args: Args:
datei: Pfad zur ausgabe.csv datei: Pfad zur ausgabe.csv
lösung: Dictionary mit Zuteilungen {datum: {dienst: [eltern]}} lösung: Dictionary mit Zuteilungen {datum: {dienst: [eltern]}}
tage: Liste aller Planungstage tage: Liste aller Planungstage (aktueller Planungszeitraum)
dienste: Liste der Dienst-Objekte dienste: Liste der Dienst-Objekte
gesamt: Wenn True -> schreibe gesamte Dienste (inkl. historische_dienste).
Wenn False -> wie bisher nur die neu verplanten Dienste (lösung).
historische_dienste: Optional Liste von (datum, eltern, dienst) aus vorherige-ausgaben.csv
(wird nur ausgewertet, wenn gesamt==True)
""" """
print(f"Schreibe Ergebnisse nach {datei}...") print(f"Schreibe Ergebnisse nach {datei}... (gesamt={gesamt})")
# Bestimme alle zu schreibenden Tage
if gesamt and historische_dienste:
historische_dates = {hd[0] for hd in historische_dienste}
output_dates = sorted(set(tage) | historische_dates)
else:
output_dates = sorted(tage)
# Sicherstellen: Für alle Tage im Zeitraum (von min bis max) soll eine Zeile ausgegeben werden,
# auch wenn keine Informationen vorliegen.
if output_dates:
start_date = min(output_dates)
end_date = max(output_dates)
full_dates = []
current = start_date
while current <= end_date:
full_dates.append(current)
current += timedelta(days=1)
output_dates = full_dates
# Erstelle Mapping date -> dienst -> list[eltern]
combined: Dict[date, Dict[any, List[str]]] = {}
if gesamt and historische_dienste:
for datum, eltern_name, dienst in historische_dienste:
combined.setdefault(datum, {}).setdefault(dienst, [])
if eltern_name not in combined[datum][dienst]:
combined[datum][dienst].append(eltern_name)
# Füge neue (optimierte) Zuweisungen hinzu (überschreiben/ergänzen)
for datum, dienst_map in (lösung or {}).items():
combined.setdefault(datum, {})
for dienst, eltern_liste in dienst_map.items():
combined[datum].setdefault(dienst, [])
for eltern_name in eltern_liste:
if eltern_name not in combined[datum][dienst]:
combined[datum][dienst].append(eltern_name)
with open(datei, 'w', newline='', encoding='utf-8') as f: with open(datei, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f) writer = csv.writer(f)
@ -234,17 +276,16 @@ class AusgabeWriter:
writer.writerow(header) writer.writerow(header)
# Daten schreiben # Daten schreiben
for tag in sorted(tage): for tag in output_dates:
wochentag = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', wochentag = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag',
'Freitag', 'Samstag', 'Sonntag'][tag.weekday()] 'Freitag', 'Samstag', 'Sonntag'][tag.weekday()]
row = [tag.strftime('%Y-%m-%d'), wochentag] row = [tag.strftime('%Y-%m-%d'), wochentag]
for dienst in dienste: for dienst in dienste:
if tag in lösung and dienst in lösung[tag]: eltern_str = ''
eltern_str = ' und '.join(lösung[tag][dienst]) if tag in combined and dienst in combined[tag]:
else: eltern_str = ' und '.join(combined[tag][dienst])
eltern_str = ''
row.append(eltern_str) row.append(eltern_str)
writer.writerow(row) writer.writerow(row)

View File

@ -15,16 +15,17 @@ from csv_io import EingabeParser
class Dienst: class Dienst:
"""Repräsentiert einen Diensttyp mit allen seinen Eigenschaften""" """Repräsentiert einen Diensttyp mit allen seinen Eigenschaften"""
def __init__(self, kuerzel: str, name: str, personen_anzahl: int = 1) -> None: def __init__(self, kuerzel: str, name: str, personen_anzahl: int = 1, aufwand: int = 1) -> None:
self.kuerzel: str = kuerzel self.kuerzel: str = kuerzel
self.name: str = name self.name: str = name
self.personen_anzahl: int = personen_anzahl self.personen_anzahl: int = personen_anzahl
self.aufwand: int = aufwand
def __str__(self) -> str: def __str__(self) -> str:
return f"{self.kuerzel} ({self.name}): {self.personen_anzahl} Person(en)" return f"{self.kuerzel} ({self.name}): {self.personen_anzahl} Person(en), Aufwand={self.aufwand}"
def __repr__(self) -> str: def __repr__(self) -> str:
return f"Dienst('{self.kuerzel}', '{self.name}', {self.personen_anzahl})" return f"Dienst('{self.kuerzel}', '{self.name}', {self.personen_anzahl}, {self.aufwand})"
def braucht_mehrere_personen(self) -> bool: def braucht_mehrere_personen(self) -> bool:
"""Gibt True zurück, wenn mehr als eine Person benötigt wird""" """Gibt True zurück, wenn mehr als eine Person benötigt wird"""
@ -49,11 +50,11 @@ class ElterndienstplanerDaten:
def __init__(self) -> None: def __init__(self) -> None:
# Dienste als Liste definieren # Dienste als Liste definieren
self.dienste: List[Dienst] = [ self.dienste: List[Dienst] = [
Dienst('F', 'Frühstücksdienst', 1), Dienst('F', 'Frühstücksdienst', 1, aufwand=3),
Dienst('P', 'Putznotdienst', 1), Dienst('P', 'Putznotdienst', 1, aufwand=1),
Dienst('E', 'Essensausgabenotdienst', 1), Dienst('E', 'Essensausgabenotdienst', 1, aufwand=1),
Dienst('K', 'Kochen', 1), Dienst('K', 'Kochen', 1, aufwand=3),
Dienst('A', 'Elternabend', 2) Dienst('A', 'Elternabend', 2, aufwand=2)
] ]
# Datenstrukturen # Datenstrukturen
@ -102,11 +103,16 @@ class ElterndienstplanerDaten:
vorherige_datei: Optionaler Pfad zur vorherige-ausgaben.csv für Fairness-Constraints vorherige_datei: Optionaler Pfad zur vorherige-ausgaben.csv für Fairness-Constraints
""" """
# Eingabe CSV: Termine, Präferenzen, Verfügbarkeit # Eingabe CSV: Termine, Präferenzen, Verfügbarkeit
self.eltern, self.planungszeitraum, self.benoetigte_dienste, self.verfuegbarkeit, self.praeferenzen = \ # Eltern CSV: Dienstfaktoren (erst einlesen, damit self.eltern daraus abgeleitet wird)
EingabeParser.parse_eingabe_csv(eingabe_datei, self.get_dienst)
# Eltern CSV: Dienstfaktoren
self.dienstfaktoren = EingabeParser.parse_eltern_csv(eltern_datei) self.dienstfaktoren = EingabeParser.parse_eltern_csv(eltern_datei)
# Fülle self.eltern aus den Einträgen in eltern.csv (Vertrauensquelle für Elternnamen)
self.eltern = list(self.dienstfaktoren.keys())
# Eingabe CSV: Termine, Präferenzen, Verfügbarkeit
# Wir verwenden die Elterndefinition aus eltern.csv; die von parse_eingabe_csv
# zurückgegebene Eltern-Liste wird ignoriert, damit die Quell-of-truth konsistent bleibt.
_, self.planungszeitraum, self.benoetigte_dienste, self.verfuegbarkeit, self.praeferenzen = \
EingabeParser.parse_eingabe_csv(eingabe_datei, self.get_dienst)
# Vorherige Ausgaben CSV (optional): Historische Dienste für Fairness # Vorherige Ausgaben CSV (optional): Historische Dienste für Fairness
if vorherige_datei: if vorherige_datei:

View File

@ -8,6 +8,7 @@ Datum: Dezember 2025
import sys import sys
import pulp import pulp
import multiprocessing
from datetime import timedelta, date from datetime import timedelta, date
from collections import defaultdict from collections import defaultdict
from typing import Dict, List, Tuple, DefaultDict, Optional from typing import Dict, List, Tuple, DefaultDict, Optional
@ -68,9 +69,9 @@ class Elterndienstplaner:
faire_zuteilung = anteil * anzahl_dienste faire_zuteilung = anteil * anzahl_dienste
ziel_dienste[eltern][dienst] += faire_zuteilung ziel_dienste[eltern][dienst] += faire_zuteilung
if faire_zuteilung > 0.01: #if faire_zuteilung > 0.01:
print(f" {tag}: {eltern} Faktor={self.daten.dienstfaktoren[eltern][tag]} " # print(f" {tag}: {eltern} Faktor={self.daten.dienstfaktoren[eltern][tag]} "
f"-> {faire_zuteilung:.2f} von {anzahl_dienste} Diensten") # f"-> {faire_zuteilung:.2f} von {anzahl_dienste} Diensten")
# 2. AKTUELLER PLANUNGSZEITRAUM: Faire Verteilung # 2. AKTUELLER PLANUNGSZEITRAUM: Faire Verteilung
benoetigte_dienste_planungszeitraum = 0 benoetigte_dienste_planungszeitraum = 0
@ -179,6 +180,10 @@ class Elterndienstplaner:
woche_nr = 0 woche_nr = 0
letzter_tag = self.daten.planungszeitraum[-1] letzter_tag = self.daten.planungszeitraum[-1]
print ("\n Erster Tag im Planungszeitraum:", erster_tag)
print ("\n Letzter Tag im Planungszeitraum:", letzter_tag)
while woche_start <= letzter_tag: while woche_start <= letzter_tag:
woche_ende = woche_start + timedelta(days=6) woche_ende = woche_start + timedelta(days=6)
@ -188,12 +193,13 @@ class Elterndienstplaner:
# Zaehle historische Dienste in dieser Woche (VOR Planungszeitraum) # Zaehle historische Dienste in dieser Woche (VOR Planungszeitraum)
historische_dienste_in_woche = 0 historische_dienste_in_woche = 0
if woche_start < erster_tag: #if woche_start < erster_tag:
for hist_datum, hist_eltern, hist_dienst in self.daten.historische_dienste: for hist_datum, hist_eltern, hist_dienst in self.daten.historische_dienste:
if (hist_eltern == eltern and if (hist_eltern == eltern and
hist_dienst == dienst and hist_dienst == dienst and
woche_start <= hist_datum < erster_tag): woche_start <= hist_datum and
historische_dienste_in_woche += 1 hist_datum <= woche_ende):
historische_dienste_in_woche += 1
for tag in self.daten.planungszeitraum: for tag in self.daten.planungszeitraum:
if woche_start <= tag <= woche_ende: if woche_start <= tag <= woche_ende:
@ -201,8 +207,11 @@ class Elterndienstplaner:
woche_vars.append(x[eltern, tag, dienst]) woche_vars.append(x[eltern, tag, dienst])
if woche_vars: if woche_vars:
prob += pulp.lpSum(woche_vars) <= 1 - historische_dienste_in_woche, \ if (1 - historische_dienste_in_woche) >= 0:
f"C1_{eltern.replace(' ', '_')}_{dienst.kuerzel}_w{woche_nr}" prob += pulp.lpSum(woche_vars) <= 1 - historische_dienste_in_woche, \
f"C1_{eltern.replace(' ', '_')}_{dienst.kuerzel}_w{woche_nr}"
else:
print (f" Hinweis: {eltern} hat in Woche {woche_nr} bereits {historische_dienste_in_woche} mal {dienst.name}")
woche_start += timedelta(days=7) woche_start += timedelta(days=7)
woche_nr += 1 woche_nr += 1
@ -216,12 +225,18 @@ class Elterndienstplaner:
for eltern in self.daten.eltern: for eltern in self.daten.eltern:
for tag in self.daten.planungszeitraum: for tag in self.daten.planungszeitraum:
tag_vars = [] tag_vars = []
maximum = 1
for dienst in self.daten.dienste: for dienst in self.daten.dienste:
if (eltern, tag, dienst) in x: if (eltern, tag, dienst) in x:
tag_vars.append(x[eltern, tag, dienst]) tag_vars.append(x[eltern, tag, dienst])
for hist_datum, hist_eltern, hist_dienst in self.daten.historische_dienste:
if (hist_eltern == eltern and
hist_datum == tag):
maximum = 0
if tag_vars: if tag_vars:
prob += pulp.lpSum(tag_vars) <= 1, f"C2_{eltern.replace(' ', '_')}_{tag}" prob += pulp.lpSum(tag_vars) <= maximum, f"C2_{eltern.replace(' ', '_')}_{tag}"
def _add_constraint_verfuegbarkeit( def _add_constraint_verfuegbarkeit(
self, self,
@ -341,14 +356,16 @@ class Elterndienstplaner:
lowBound=0) lowBound=0)
# Zähle tatsächliche Dienste gewichtet mit dem Aufwand des Dienstes
tatsaechliche_dienste_gesamt = pulp.lpSum( tatsaechliche_dienste_gesamt = pulp.lpSum(
x[eltern, tag, dienst] dienst.aufwand * x[eltern, tag, dienst]
for tag in self.daten.planungszeitraum for tag in self.daten.planungszeitraum
for dienst in self.daten.dienste for dienst in self.daten.dienste
if (eltern, tag, dienst) in x if (eltern, tag, dienst) in x
) )
ziel_gesamt = sum(ziel_dienste[eltern][dienst] for dienst in self.daten.dienste) # Zielgesamt ebenfalls mit Dienst-Aufwand gewichtet
ziel_gesamt = sum(ziel_dienste[eltern][dienst] * dienst.aufwand for dienst in self.daten.dienste)
prob += (tatsaechliche_dienste_gesamt - ziel_gesamt <= prob += (tatsaechliche_dienste_gesamt - ziel_gesamt <=
fairness_abweichung_gesamt[eltern]) fairness_abweichung_gesamt[eltern])
@ -370,8 +387,8 @@ class Elterndienstplaner:
"""Erstellt die Zielfunktion mit Fairness und Praeferenzen""" """Erstellt die Zielfunktion mit Fairness und Praeferenzen"""
objective_terms = [] objective_terms = []
gewicht_global = 40 gewicht_global = 50
gewicht_lokal = 60 gewicht_lokal = 50
gewicht_f1 = gewicht_global gewicht_f1 = gewicht_global
gewicht_f2 = gewicht_lokal gewicht_f2 = gewicht_lokal
gewicht_f3_global = 0.25 * gewicht_global gewicht_f3_global = 0.25 * gewicht_global
@ -379,21 +396,23 @@ class Elterndienstplaner:
for eltern in self.daten.eltern: for eltern in self.daten.eltern:
for dienst in self.daten.dienste: for dienst in self.daten.dienste:
objective_terms.append(gewicht_f1 * fairness_abweichung_global[eltern, dienst]) # Skaliere diensttyp-spezifische Fairness mit dem Aufwand des Dienstes
objective_terms.append(gewicht_f2 * fairness_abweichung_lokal[eltern, dienst]) objective_terms.append(gewicht_f1 * fairness_abweichung_global[eltern, dienst] * dienst.aufwand)
objective_terms.append(gewicht_f2 * fairness_abweichung_lokal[eltern, dienst] * dienst.aufwand)
# Gesamt-Fairness (bereits dienstabhängig in den Constraints) — keine zusätzliche Mean-Skalierung mehr
objective_terms.append(gewicht_f3_global * fairness_abweichung_gesamt_global[eltern]) objective_terms.append(gewicht_f3_global * fairness_abweichung_gesamt_global[eltern])
objective_terms.append(gewicht_f4_lokal * fairness_abweichung_gesamt_lokal[eltern]) objective_terms.append(gewicht_f4_lokal * fairness_abweichung_gesamt_lokal[eltern])
# P1: Bevorzugte Dienste # P1: Bevorzugte Dienste (stärker für aufwändigere Dienste)
for (eltern, tag, dienst), praef in self.daten.praeferenzen.items(): for (eltern, tag, dienst), praef in self.daten.praeferenzen.items():
if (eltern, tag, dienst) in x and praef == 1: if (eltern, tag, dienst) in x and praef == 1:
objective_terms.append(-5 * x[eltern, tag, dienst]) objective_terms.append(-10 * dienst.aufwand * x[eltern, tag, dienst])
# P2: Abgelehnte Dienste # P2: Abgelehnte Dienste (stärker für aufwändigere Dienste)
for (eltern, tag, dienst), praef in self.daten.praeferenzen.items(): for (eltern, tag, dienst), praef in self.daten.praeferenzen.items():
if (eltern, tag, dienst) in x and praef == -1: if (eltern, tag, dienst) in x and praef == -1:
objective_terms.append(25 * x[eltern, tag, dienst]) objective_terms.append(20 * dienst.aufwand * x[eltern, tag, dienst])
if objective_terms: if objective_terms:
prob += pulp.lpSum(objective_terms) prob += pulp.lpSum(objective_terms)
@ -466,13 +485,15 @@ class Elterndienstplaner:
solver = None solver = None
try: try:
print("Versuche CBC Solver...") cpu_count = multiprocessing.cpu_count()
solver = pulp.PULP_CBC_CMD(msg=0, timeLimit=10) threads = max(1, cpu_count - 1)
except: print(f"Versuche CBC Solver mit {threads} Threads...")
solver = pulp.PULP_CBC_CMD(msg=0, timeLimit=20, threads=threads)
except Exception:
try: try:
print("Versuche GLPK Solver...") print("Versuche GLPK Solver...")
solver = pulp.GLPK_CMD(msg=0) solver = pulp.GLPK_CMD(msg=0)
except: except Exception:
print("Kein spezifizierter Solver verfügbar, verwende Standard.") print("Kein spezifizierter Solver verfügbar, verwende Standard.")
solver = None solver = None
@ -523,6 +544,7 @@ def main() -> None:
if loesung is not None: if loesung is not None:
ausgabe.schreibe_ausgabe_csv(ausgabe_datei, loesung) ausgabe.schreibe_ausgabe_csv(ausgabe_datei, loesung)
ausgabe.drucke_statistiken(loesung) ausgabe.drucke_statistiken(loesung)
ausgabe.visualisiere_dienste_uebersicht(loesung)
ausgabe.visualisiere_verteilungen(loesung) ausgabe.visualisiere_verteilungen(loesung)
ausgabe.visualisiere_praeferenz_verletzungen(loesung) ausgabe.visualisiere_praeferenz_verletzungen(loesung)