Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| beea66ef2f | |||
| fc4182459b | |||
| f7b1267c98 | |||
| 9f7d3c6d4a | |||
| 08e5cf11bd | |||
|
|
b524edc2ba | ||
|
|
3b94f460a8 | ||
|
|
e6dc8ff9d4 | ||
|
|
07a1fbf002 |
224
README.md
224
README.md
@ -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
80
anleitung-eltern.md
Normal 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.
|
||||||
|
|
||||||
148
ausgabe.py
148
ausgabe.py
@ -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")
|
||||||
|
|||||||
59
csv_io.py
59
csv_io.py
@ -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)
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user