scheint zu funktionieren

This commit is contained in:
Jan Hoheisel 2025-12-22 00:56:13 +01:00
parent cdb151d7b0
commit 095e022724
7 changed files with 853 additions and 0 deletions

View File

@ -0,0 +1,95 @@
Elterndienstplaner
syntax: ./elterndienstplaner.py <eingabe.csv> <eltern.csv> <ausgabe.csv> { <vorherige-ausgaben.csv> }
Der Elterndienstplaner hilft bei der Zuteilung von Elterndiensten zu Eltern.
Zur Identifizierung der Eltern dient der Name des Kindes/der Kinder.
Es gibt diese Dienste:
- F: Frühstücksdienst (täglich)
- P: Putznotdienst (täglich)
- E: Essensausgabenotdienst (täglich)
- K: Kochen (alle 2 wochen, Datum manuell festgelegt)
- A: Elternabend (2 Eltern, Datum manuell festgelegt)
Die Planung erfolgt immer für einen Kalendermonat.
Die Eltern geben an, an welchen Tagen sie abwesend sind, also nicht zur Verfügung stehen. Zudem können sie für jede Tag-Dienst-Kombination angeben, ob sie den Dienst an diesen Tag bevorzugt (+) oder nur notfalls (-) machen wollen.
Die Eingabe erfolgt über eine CSV-Datei eingabe.csv und eltern.csv
## eingabe.csv
Informationen zu notwendigen Diensten eines Monats und Zeiten/Praeferenzen der Eltern
1. Spalte: Datum in ISO-Format
2. Spalte: Wochentag (Hilfsinformation)
3. Spalte: Benötigte Dienste als aneinandergereihte Dienstkürzel
Folgende Spalten: Für alle Eltern: Verfügbarkeit und Präferenz:
- x, falls nicht verfügbar
- <Dienstkürzel>+, wenn Dienst an dem Tag bevorzugt.
- <Dienstkürzel>-, wenn Dienst an dem Tag abgelehnt.
Es können mehrere Präferenzen pro Tag angegeben werden.
1. Zeile header
folgende Zeilen je Tag.
## eltern.csv:
Informationen zur Diestpflicht der Eltern.
Die Dienstpflicht besteht, wenn Eltern Kinder im Kinderladen betreuen lassen.
Der Dienstfaktor entspricht der Anzahl der betreuten Kinder der Eltern.
Wenn Eltern ein Vorsandsamt im Kinderladen übernehmen, werden sie von der Dienstpflicht befreit.
1. Spalte Zeitraum Beginn
2. Spalte Zeitraum Ende
3. Spalte Dienstfaktor
4. Spalte ... nächster Zeitraum
1. Zeile Header
folgende Zeilen Eltern
Bei sich überschneidenden Zeiträumen gilt der letzte Eintrag.
An Tagen außerhalb der angegebenen Zeiträume ist der Dienstfaktor 0.
Die Datei eltern.csv enthält ggf. mehr Eltern als die Eingabe.csv,
da Kinder dazukommen oder den KiLa verlassen, die eltern.csv aber nur anwächst.
## ausgabe.csv
1. Spalte: Datum
2. Spalte: Wochentag
3. Spalte ... Dienste
Zeilen: für jeden Tag die zugeteilten Eltern in den jeweiligen Dienstspalten
## vorherige-ausgaben.csv
Hier werden die von früheren Läufen des Programms generierten ausgabe.csv-Datein wiedereingespielt.
Das Format entspricht der ausgabe.csv
## Constraints
Harte Constraints:
C1: Je Eltern und Dienst, Dienst nur einmal die Woche
C2: Je Eltern nur einen Dienst am Tag
C3: Dienste nur verfügbaren Eltern zuteilen
Weiche Constraints:
- F1: Alle Eltern erhalten Dienste im Verhältnis ihres Dienstfaktors (Gesamter vorliegender Zeitraum)
- F2: Alle Eltern erhalten Dienste im Verhältnis ihres aktuellen Dienstfaktors (Aktueller Monat)
- P1: Eltern erhalten bevorzugte Dienste
- P2: Eltern erhalten keine abgelehnten Dienste.
F1 und F2 sind Fairnessconstraints und gelten pro Dienst.
P1 und P2 sind Präferenzconstraints. Sie wiegen schwächer als die Fairnessconstaints.
Die vorherige-ausgaben.csv der vergangenen Monate dienen auch als Eingabe.
Sie werden für die Fairnessconstraints verwendet.
Im Laufe eines Kinderladenjahrs sammeln sich die Ausgaben der Monate an.
F2 stellt die Fairness im aktuellen Monat sicher -> lokale Fairness
F1 stellt die Fairness für das gesamte Jahr sicher -> globale Fairness
Wenn z.B. Eltern eine zeitlang nicht verfügbar sind, sollen sie nicht sofort
alle Dienste "nachholen" müssen (lokale Fairness stellt das sicher),
aber im Jahresverlauf die Dienste trotzdem nachholen (globale Fairness stellt das sicher).
F1 und F2 werden mit Faktoren gewichtet. Zu Beginn des Kinderladenjahrs ist F2 stärker,
zum Ende des Kinderladenjahres F1.

24
ausgabe.csv Normal file
View File

@ -0,0 +1,24 @@
Datum,Wochentag,Frühstücksdienst,Putznotdienst,Essensausgabenotdienst,Kochen,Elternabend
2026-01-01,Donnerstag,Paula,Marie,Jonas,,
2026-01-02,Freitag,Jonas,Ben & Nele,Laura,,
2026-01-03,Samstag,Marie,Jonas,Ben & Nele,,
2026-01-06,Dienstag,Laura,Paula,Erwin,,
2026-01-07,Mittwoch,Ben & Nele,Erwin,Marie,,
2026-01-08,Donnerstag,Jonas,Paula,Marie,Ben & Nele,
2026-01-09,Freitag,Erwin,Marie,Paula,,
2026-01-10,Samstag,Paula,Laura,Ben & Nele,,
2026-01-13,Dienstag,Ben & Nele,Jonas,Laura,,
2026-01-14,Mittwoch,Marie,Ben & Nele,Jonas,,
2026-01-15,Donnerstag,Laura,Jonas,Erwin,,
2026-01-16,Freitag,Marie,Laura,Ben & Nele,,
2026-01-17,Samstag,Jonas,Ben & Nele,Paula,,
2026-01-20,Dienstag,Ben & Nele,Erwin,Marie,,
2026-01-21,Mittwoch,Erwin,Marie,Jonas,,
2026-01-22,Donnerstag,Ben & Nele,Marie,Laura,Jonas,
2026-01-23,Freitag,Marie,Jonas,Ben & Nele,,
2026-01-24,Samstag,Laura,Erwin,Marie,,Ben & Nele Jonas
2026-01-27,Dienstag,Erwin,Ben & Nele,Jonas,,
2026-01-28,Mittwoch,Jonas,Paula,Erwin,,
2026-01-29,Donnerstag,Marie,Ben & Nele,Jonas,,
2026-01-30,Freitag,Ben & Nele,Jonas,Paula,,
2026-01-31,Samstag,Paula,Marie,Ben & Nele,,
1 Datum Wochentag Frühstücksdienst Putznotdienst Essensausgabenotdienst Kochen Elternabend
2 2026-01-01 Donnerstag Paula Marie Jonas
3 2026-01-02 Freitag Jonas Ben & Nele Laura
4 2026-01-03 Samstag Marie Jonas Ben & Nele
5 2026-01-06 Dienstag Laura Paula Erwin
6 2026-01-07 Mittwoch Ben & Nele Erwin Marie
7 2026-01-08 Donnerstag Jonas Paula Marie Ben & Nele
8 2026-01-09 Freitag Erwin Marie Paula
9 2026-01-10 Samstag Paula Laura Ben & Nele
10 2026-01-13 Dienstag Ben & Nele Jonas Laura
11 2026-01-14 Mittwoch Marie Ben & Nele Jonas
12 2026-01-15 Donnerstag Laura Jonas Erwin
13 2026-01-16 Freitag Marie Laura Ben & Nele
14 2026-01-17 Samstag Jonas Ben & Nele Paula
15 2026-01-20 Dienstag Ben & Nele Erwin Marie
16 2026-01-21 Mittwoch Erwin Marie Jonas
17 2026-01-22 Donnerstag Ben & Nele Marie Laura Jonas
18 2026-01-23 Freitag Marie Jonas Ben & Nele
19 2026-01-24 Samstag Laura Erwin Marie Ben & Nele Jonas
20 2026-01-27 Dienstag Erwin Ben & Nele Jonas
21 2026-01-28 Mittwoch Jonas Paula Erwin
22 2026-01-29 Donnerstag Marie Ben & Nele Jonas
23 2026-01-30 Freitag Ben & Nele Jonas Paula
24 2026-01-31 Samstag Paula Marie Ben & Nele

24
eingabe.csv Normal file
View File

@ -0,0 +1,24 @@
Datum , Wochentag , Dienste, Erwin, Paula, Laura, Ben & Nele, Jonas, Marie
2026-01-01, Mittwoch , FPE , x , F+ , P- , , E+ , P+
2026-01-02, Donnerstag, FPE , x , , E+ , P+ , F+ , P-
2026-01-03, Freitag , FPE , x , F+ , , F-E+ , P+ , E-
2026-01-06, Montag , FPE , F- , P+ , F+ , E+ , , F-
2026-01-07, Dienstag , FPE , P+ , F+ , P- , F+P+ , E+ , F-
2026-01-08, Mittwoch , FPEK , E+ , P+ , F+ , K+ , P- , E+
2026-01-09, Donnerstag, FPE , F+ , , E- , P+F+ , , P+
2026-01-10, Freitag , FPE , , F+ , P+ , E+ , F- ,
2026-01-13, Montag , FPE , F+ , P- , E+ , F+ , P+ , E-
2026-01-14, Dienstag , FPE , P+ , F+ , F- , P+E+ , E+ , P-
2026-01-15, Mittwoch , FPE , E+ , P+ , F+ , F- , , E+
2026-01-16, Donnerstag, FPE , F- , , P+ , E+F+ , P- , F+
2026-01-17, Freitag , FPE , F+ , F+ , E+ , P+ , , F-
2026-01-20, Montag , FPE , P+ , x , F- , F+E+ , F+ , P+
2026-01-21, Dienstag , FPE , F+ , x , P+ , P+ , E+ , F-
2026-01-22, Mittwoch , FPEK , , x , E+ , K+F+ , P+ , K-
2026-01-23, Donnerstag, FPE , F+ , x , F- , E+ , P+ , F+
2026-01-24, Freitag , FPEA , P+ , x , F+ , A+F+ , E- , A+
2026-01-27, Montag , FPE , F+ , P- , x , P+F+ , E+ , P-
2026-01-28, Dienstag , FPE , E+ , F+ , x , F- , P+ , E+
2026-01-29, Mittwoch , FPE , F- , P+ , x , E+P+ , , F+
2026-01-30, Donnerstag, FPE , F+ , , x , F+ , P+ , E-
2026-01-31, Freitag , FPE , P+ , F+ , x , E+ , F- , P+
1 Datum Wochentag Dienste Erwin Paula Laura Ben & Nele Jonas Marie
2 2026-01-01 Mittwoch FPE x F+ P- E+ P+
3 2026-01-02 Donnerstag FPE x E+ P+ F+ P-
4 2026-01-03 Freitag FPE x F+ F-E+ P+ E-
5 2026-01-06 Montag FPE F- P+ F+ E+ F-
6 2026-01-07 Dienstag FPE P+ F+ P- F+P+ E+ F-
7 2026-01-08 Mittwoch FPEK E+ P+ F+ K+ P- E+
8 2026-01-09 Donnerstag FPE F+ E- P+F+ P+
9 2026-01-10 Freitag FPE F+ P+ E+ F-
10 2026-01-13 Montag FPE F+ P- E+ F+ P+ E-
11 2026-01-14 Dienstag FPE P+ F+ F- P+E+ E+ P-
12 2026-01-15 Mittwoch FPE E+ P+ F+ F- E+
13 2026-01-16 Donnerstag FPE F- P+ E+F+ P- F+
14 2026-01-17 Freitag FPE F+ F+ E+ P+ F-
15 2026-01-20 Montag FPE P+ x F- F+E+ F+ P+
16 2026-01-21 Dienstag FPE F+ x P+ P+ E+ F-
17 2026-01-22 Mittwoch FPEK x E+ K+F+ P+ K-
18 2026-01-23 Donnerstag FPE F+ x F- E+ P+ F+
19 2026-01-24 Freitag FPEA P+ x F+ A+F+ E- A+
20 2026-01-27 Montag FPE F+ P- x P+F+ E+ P-
21 2026-01-28 Dienstag FPE E+ F+ x F- P+ E+
22 2026-01-29 Mittwoch FPE F- P+ x E+P+ F+
23 2026-01-30 Donnerstag FPE F+ x F+ P+ E-
24 2026-01-31 Freitag FPE P+ F+ x E+ F- P+

7
eltern.csv Normal file
View File

@ -0,0 +1,7 @@
Name_Kind(er),Zeitraum_Beginn,Zeitraum_Ende,Dienstfaktor,Zeitraum_Beginn2,Zeitraum_Ende2,Dienstfaktor2,Zeitraum_Beginn3,Zeitraum_Ende3,Dienstfaktor3
Erwin,2025-09-01,2026-07-31,1,,,,,
Paula,2025-09-01,2026-07-31,1,,,,,
Laura,2025-09-01,2026-07-31,1,,,,,
Ben & Nele,2025-09-01,2026-07-31,2,,,,,
Jonas,2025-09-01,2026-07-31,1,,,,,
Marie,2025-09-01,2026-07-31,1,,,,,
1 Name_Kind(er),Zeitraum_Beginn,Zeitraum_Ende,Dienstfaktor,Zeitraum_Beginn2,Zeitraum_Ende2,Dienstfaktor2,Zeitraum_Beginn3,Zeitraum_Ende3,Dienstfaktor3
2 Erwin,2025-09-01,2026-07-31,1,,,,,
3 Paula,2025-09-01,2026-07-31,1,,,,,
4 Laura,2025-09-01,2026-07-31,1,,,,,
5 Ben & Nele,2025-09-01,2026-07-31,2,,,,,
6 Jonas,2025-09-01,2026-07-31,1,,,,,
7 Marie,2025-09-01,2026-07-31,1,,,,,

665
elterndienstplaner.py Executable file
View File

@ -0,0 +1,665 @@
#!/usr/bin/env python3
"""
Elterndienstplaner - Optimale Zuteilung von Elterndiensten
Autor: Automatisch generiert
Datum: Dezember 2025
"""
import sys
import csv
import pulp
from datetime import datetime, timedelta
from collections import defaultdict
import calendar
class Elterndienstplaner:
def __init__(self):
self.dienste = ['F', 'P', 'E', 'K', 'A'] # Frühstück, Putz, Essen, Kochen, Elternabend
self.dienst_namen = {
'F': 'Frühstücksdienst',
'P': 'Putznotdienst',
'E': 'Essensausgabenotdienst',
'K': 'Kochen',
'A': 'Elternabend'
}
# Datenstrukturen
self.tage = []
self.eltern = []
self.benoetigte_dienste = {} # {datum: [dienste]}
self.verfügbarkeit = {} # {(eltern, datum): bool}
self.präferenzen = {} # {(eltern, datum, dienst): 1 (bevorzugt) oder -1 (abgelehnt)}
self.dienstfaktoren = {} # {eltern: {datum: faktor}}
self.alle_zeitraeume = {} # {eltern: [(beginn, ende, faktor), ...]} - ALLE Zeiträume
self.vorherige_dienste = defaultdict(lambda: defaultdict(int)) # {eltern: {dienst: anzahl}}
self.historische_dienste = [] # [(datum, eltern, dienst), ...] - Alle historischen Dienste mit Datum
def lade_eingabe_csv(self, datei):
"""Lädt die eingabe.csv mit Terminen und Präferenzen"""
print(f"Lade Eingabedaten aus {datei}...")
with open(datei, 'r', encoding='utf-8') as f:
reader = csv.reader(f)
header = next(reader)
# Eltern aus Header extrahieren (ab Spalte 3)
self.eltern = [name.strip() for name in header[3:] if name.strip()]
print(f"Gefundene Eltern: {self.eltern}")
for row in reader:
if len(row) < 3:
continue
datum = row[0].strip()
wochentag = row[1].strip()
dienste_str = row[2].strip()
# Datum parsen
try:
datum_obj = datetime.strptime(datum, '%Y-%m-%d').date()
self.tage.append(datum_obj)
except ValueError:
print(f"Warnung: Ungültiges Datum {datum}")
continue
# Benötigte Dienste
self.benoetigte_dienste[datum_obj] = list(dienste_str)
# Verfügbarkeit und Präferenzen der Eltern
for i, eltern_name in enumerate(self.eltern):
if i + 3 < len(row):
präf_str = row[i + 3].strip()
# Verfügbarkeit prüfen
if präf_str == 'x':
self.verfügbarkeit[(eltern_name, datum_obj)] = False
else:
self.verfügbarkeit[(eltern_name, datum_obj)] = True
# Präferenzen parsen
self._parse_präferenzen(eltern_name, datum_obj, präf_str)
else:
# Standard: verfügbar, keine Präferenzen
self.verfügbarkeit[(eltern_name, datum_obj)] = True
self.tage.sort()
print(f"Zeitraum: {self.tage[0]} bis {self.tage[-1]} ({len(self.tage)} Tage)")
def _parse_präferenzen(self, eltern, datum, präf_str):
"""Parst Präferenzstring wie 'F+P-E+' """
i = 0
while i < len(präf_str):
if i + 1 < len(präf_str) and präf_str[i] in self.dienste:
dienst = präf_str[i]
if i + 1 < len(präf_str):
if präf_str[i + 1] == '+':
self.präferenzen[(eltern, datum, dienst)] = 1
i += 2
elif präf_str[i + 1] == '-':
self.präferenzen[(eltern, datum, dienst)] = -1
i += 2
else:
i += 1
else:
i += 1
else:
i += 1
def lade_eltern_csv(self, datei):
"""Lädt die eltern.csv mit Dienstfaktoren"""
print(f"Lade Elterndaten aus {datei}...")
with open(datei, 'r', encoding='utf-8') as f:
reader = csv.reader(f)
header = next(reader)
for row in reader:
if len(row) < 4:
continue
eltern_name = row[0].strip()
# Initialisiere Datenstrukturen
self.dienstfaktoren[eltern_name] = {}
self.alle_zeitraeume[eltern_name] = []
# Alle Zeiträume einlesen (jeweils 3 Spalten: Beginn, Ende, Faktor)
for i in range(1, len(row), 3):
if i + 2 < len(row) and row[i].strip() and row[i + 1].strip():
try:
beginn = datetime.strptime(row[i].strip(), '%Y-%m-%d').date()
ende = datetime.strptime(row[i + 1].strip(), '%Y-%m-%d').date()
faktor = float(row[i + 2].strip()) if row[i + 2].strip() else 0
# Zeitraum speichern
self.alle_zeitraeume[eltern_name].append((beginn, ende, faktor))
# Faktor für Tage im aktuellen Planungsmonat setzen
for tag in self.tage:
if beginn <= tag <= ende:
self.dienstfaktoren[eltern_name][tag] = faktor
except (ValueError, IndexError):
continue
# Tage ohne expliziten Faktor auf 0 setzen
for tag in self.tage:
if tag not in self.dienstfaktoren[eltern_name]:
self.dienstfaktoren[eltern_name][tag] = 0
print(f"Dienstfaktoren geladen für {len(self.dienstfaktoren)} Eltern")
print(f"Zeiträume gespeichert für globale Fairness-Berechnung")
def lade_vorherige_ausgaben_csv(self, datei):
"""Lädt vorherige-ausgaben.csv für Fairness-Constraints"""
print(f"Lade vorherige Ausgaben aus {datei}...")
try:
with open(datei, 'r', encoding='utf-8') as f:
reader = csv.reader(f)
header = next(reader)
# Dienst-Spalten finden
dienst_spalten = {}
for i, col_name in enumerate(header[2:], 2): # Ab Spalte 2 (nach Datum, Wochentag)
for dienst_kürzel, dienst_name in self.dienst_namen.items():
if dienst_name.lower() in col_name.lower() or dienst_kürzel == col_name:
dienst_spalten[dienst_kürzel] = i
break
for row in reader:
if len(row) < 3:
continue
# Datum parsen
try:
datum = datetime.strptime(row[0].strip(), '%Y-%m-%d').date()
except ValueError:
continue
# Zugeteilte Dienste zählen UND mit Datum speichern
for dienst, spalte_idx in dienst_spalten.items():
if spalte_idx < len(row) and row[spalte_idx].strip():
# Mehrere Eltern können in einer Zelle stehen (durch Leerzeichen getrennt)
eltern_liste = row[spalte_idx].strip().split()
for eltern_name in eltern_liste:
if eltern_name in self.eltern:
# Summierung für Kompatibilität
self.vorherige_dienste[eltern_name][dienst] += 1
# Historische Dienste mit Datum speichern
self.historische_dienste.append((datum, eltern_name, dienst))
except FileNotFoundError:
print("Keine vorherigen Ausgaben gefunden - starte ohne historische Daten")
print(f"Vorherige Dienste geladen: {dict(self.vorherige_dienste)}")
print(f"Historische Dienste mit Datum: {len(self.historische_dienste)} Einträge")
def berechne_dienstfaktor_an_datum(self, eltern, datum):
"""Berechnet den Dienstfaktor eines Elternteils an einem bestimmten Datum"""
if eltern not in self.alle_zeitraeume:
return 0
for beginn, ende, faktor in self.alle_zeitraeume[eltern]:
if beginn <= datum <= ende:
return faktor
return 0
def berechne_faire_zielverteilung(self):
"""Berechnet die faire Zielanzahl von Diensten pro Eltern-Dienst-Kombination
basierend auf tatsächlich geleisteten historischen Diensten und deren fairer Umverteilung"""
ziel_dienste = defaultdict(lambda: defaultdict(float)) # {eltern: {dienst: anzahl}}
print("\nBerechne faire Zielverteilung basierend auf historischen Daten...")
# Wenn keine historischen Daten vorhanden, nur aktuellen Monat berechnen
if not self.historische_dienste:
print(" Keine historischen Daten - berechne nur für aktuellen Monat")
for dienst in self.dienste:
benoetigte_dienste_monat = sum(
1 for tag in self.tage
if dienst in self.benoetigte_dienste.get(tag, [])
)
if dienst == 'A':
benoetigte_dienste_monat *= 2
if benoetigte_dienste_monat > 0:
gesamt_dienstfaktor_monat = sum(
sum(self.dienstfaktoren.get(e, {}).get(tag, 0) for tag in self.tage)
for e in self.eltern
)
if gesamt_dienstfaktor_monat > 0:
for eltern in self.eltern:
monatsfaktor = sum(
self.dienstfaktoren.get(eltern, {}).get(tag, 0)
for tag in self.tage
)
if monatsfaktor > 0:
anteil = monatsfaktor / gesamt_dienstfaktor_monat
faire_zuteilung = anteil * benoetigte_dienste_monat
ziel_dienste[eltern][dienst] = faire_zuteilung
return ziel_dienste
# Historische Dienste nach Datum gruppieren
historische_tage = set(datum for datum, _, _ in self.historische_dienste)
print(f" Analysiere {len(historische_tage)} historische Tage mit {len(self.historische_dienste)} Diensten")
for dienst in self.dienste:
print(f" Verarbeite Dienst {dienst}...")
# 1. HISTORISCHE PERIODE: Faire Umverteilung der tatsächlich geleisteten Dienste
historische_dienste_dieses_typs = [
(datum, eltern) for datum, eltern, d in self.historische_dienste
if d == dienst
]
print(f" Gefundene historische {dienst}-Dienste: {len(historische_dienste_dieses_typs)}")
# Gruppiere nach Datum
dienste_pro_tag = defaultdict(list)
for datum, eltern in historische_dienste_dieses_typs:
dienste_pro_tag[datum].append(eltern)
# Für jeden historischen Tag faire Umverteilung berechnen
for hist_datum, geleistete_eltern in dienste_pro_tag.items():
anzahl_dienste = len(geleistete_eltern) # Anzahl Dienste an diesem Tag
# Dienstfaktoren aller Eltern für diesen historischen Tag berechnen
dienstfaktoren_tag = {}
gesamt_dienstfaktor_tag = 0
for eltern in self.eltern:
faktor = self.berechne_dienstfaktor_an_datum(eltern, hist_datum)
dienstfaktoren_tag[eltern] = faktor
gesamt_dienstfaktor_tag += faktor
# Faire Umverteilung der an diesem Tag geleisteten Dienste
if gesamt_dienstfaktor_tag > 0:
for eltern in self.eltern:
if dienstfaktoren_tag[eltern] > 0:
anteil = dienstfaktoren_tag[eltern] / gesamt_dienstfaktor_tag
faire_zuteilung = anteil * anzahl_dienste
ziel_dienste[eltern][dienst] += faire_zuteilung
if faire_zuteilung > 0.01: # Debug nur für relevante Werte
print(f" {hist_datum}: {eltern} Faktor={dienstfaktoren_tag[eltern]} "
f"-> {faire_zuteilung:.2f} von {anzahl_dienste} Diensten")
# 2. AKTUELLER MONAT: Faire Verteilung der benötigten Dienste
benoetigte_dienste_monat = sum(
1 for tag in self.tage
if dienst in self.benoetigte_dienste.get(tag, [])
)
if dienst == 'A':
benoetigte_dienste_monat *= 2
if benoetigte_dienste_monat > 0:
# Gesamtdienstfaktor für aktuellen Monat
gesamt_dienstfaktor_monat = sum(
sum(self.dienstfaktoren.get(e, {}).get(tag, 0) for tag in self.tage)
for e in self.eltern
)
if gesamt_dienstfaktor_monat > 0:
for eltern in self.eltern:
monatsfaktor = sum(
self.dienstfaktoren.get(eltern, {}).get(tag, 0)
for tag in self.tage
)
if monatsfaktor > 0:
anteil = monatsfaktor / gesamt_dienstfaktor_monat
faire_zuteilung = anteil * benoetigte_dienste_monat
ziel_dienste[eltern][dienst] += faire_zuteilung
# Debug-Ausgabe für diesen Dienst
total_historisch = sum(
ziel_dienste[e][dienst] for e in self.eltern
if dienst in [d for _, _, d in self.historische_dienste if d == dienst]
)
total_aktuell = benoetigte_dienste_monat
print(f" {dienst}: Historisch faire Summe={total_historisch:.1f}, "
f"Aktuell benötigt={total_aktuell}")
# Debug-Output: Detaillierte Zielverteilung
print("\n Berechnete Zielverteilung (basierend auf tatsächlichen historischen Diensten):")
for eltern in sorted(self.eltern):
for dienst in self.dienste:
if ziel_dienste[eltern][dienst] > 0.1: # Nur relevante Werte
ist = self.vorherige_dienste[eltern][dienst]
ziel = ziel_dienste[eltern][dienst]
print(f" {eltern} {dienst}: IST={ist}, FAIRE_ZIEL={ziel:.2f}, DIFF={ziel-ist:.2f}")
return ziel_dienste
def erstelle_optimierungsmodell(self):
"""Erstellt das PuLP Optimierungsmodell"""
print("Erstelle Optimierungsmodell...")
# Debugging: Verfügbarkeit prüfen
print("\nDebug: Verfügbarkeit analysieren...")
for tag in self.tage[:5]: # Erste 5 Tage
verfügbare = [e for e in self.eltern if self.verfügbarkeit.get((e, tag), True)]
benötigte = self.benoetigte_dienste.get(tag, [])
print(f" {tag}: Benötigt {len(benötigte)} Dienste {benötigte}, verfügbar: {verfügbare}")
# LP Problem erstellen
prob = pulp.LpProblem("Elterndienstplaner", pulp.LpMinimize)
# Entscheidungsvariablen: x[eltern, tag, dienst] ∈ {0,1}
x = {}
for eltern in self.eltern:
for tag in self.tage:
for dienst in self.dienste:
if dienst in self.benoetigte_dienste.get(tag, []):
x[eltern, tag, dienst] = pulp.LpVariable(
f"x_{eltern.replace(' ', '_')}_{tag}_{dienst}",
cat='Binary'
)
# Vereinfachtes Modell: Grundlegende Constraints
# C1: Je Eltern und Dienst nur einmal die Woche
woche_start = self.tage[0]
woche_nr = 0
while woche_start <= self.tage[-1]:
woche_ende = min(woche_start + timedelta(days=6), self.tage[-1])
woche_tage = [t for t in self.tage if woche_start <= t <= woche_ende]
for eltern in self.eltern:
for dienst in self.dienste:
woche_vars = []
for tag in woche_tage:
if (eltern, tag, dienst) in x:
woche_vars.append(x[eltern, tag, dienst])
if woche_vars:
prob += pulp.lpSum(woche_vars) <= 1, f"C1_{eltern.replace(' ', '_')}_{dienst}_w{woche_nr}"
woche_start += timedelta(days=7)
woche_nr += 1
# C2: Je Eltern nur einen Dienst am Tag
for eltern in self.eltern:
for tag in self.tage:
tag_vars = []
for dienst in self.dienste:
if (eltern, tag, dienst) in x:
tag_vars.append(x[eltern, tag, dienst])
if tag_vars:
prob += pulp.lpSum(tag_vars) <= 1, f"C2_{eltern.replace(' ', '_')}_{tag}"
# C3: Dienste nur verfügbaren Eltern zuteilen
for eltern in self.eltern:
for tag in self.tage:
if not self.verfügbarkeit.get((eltern, tag), True):
for dienst in self.dienste:
if (eltern, tag, dienst) in x:
prob += x[eltern, tag, dienst] == 0, f"C3_{eltern.replace(' ', '_')}_{tag}_{dienst}"
# Alle benötigten Dienste müssen zugeteilt werden (flexibel)
for tag in self.tage:
for dienst in self.benoetigte_dienste.get(tag, []):
dienst_vars = []
verfuegbare_eltern = 0
for eltern in self.eltern:
if (eltern, tag, dienst) in x:
# Prüfe ob Eltern verfügbar
if self.verfügbarkeit.get((eltern, tag), True):
dienst_vars.append(x[eltern, tag, dienst])
verfuegbare_eltern += 1
if dienst_vars:
# Genau 1 Person pro Dienst (außer Elternabend: genau 2)
benoetigte_personen = 2 if dienst == 'A' else 1
prob += pulp.lpSum(dienst_vars) == benoetigte_personen, f"Bedarf_{tag}_{dienst}"
# FAIRNESS-CONSTRAINTS UND ZIELFUNKTION
objective_terms = []
# Berechne faire Zielverteilung
ziel_dienste = self.berechne_faire_zielverteilung()
# Hilfsvariablen für Fairness-Abweichungen
fairness_abweichung_lokal = {} # F2
fairness_abweichung_global = {} # F1
for eltern in self.eltern:
for dienst in self.dienste:
fairness_abweichung_lokal[eltern, dienst] = pulp.LpVariable(
f"fair_lokal_{eltern.replace(' ', '_')}_{dienst}", lowBound=0)
fairness_abweichung_global[eltern, dienst] = pulp.LpVariable(
f"fair_global_{eltern.replace(' ', '_')}_{dienst}", lowBound=0)
# F1: Globale Fairness & F2: Lokale Fairness
for eltern in self.eltern:
for dienst in self.dienste:
# Dienstfaktor für aktuellen Monat
monatlicher_dienstfaktor = sum(
self.dienstfaktoren.get(eltern, {}).get(tag, 0) for tag in self.tage
)
if monatlicher_dienstfaktor > 0:
# Tatsächliche Dienste im aktuellen Monat
tatsaechliche_dienste_monat = pulp.lpSum(
x[eltern, tag, dienst]
for tag in self.tage
if (eltern, tag, dienst) in x
)
# F2: Lokale Fairness - nur aktueller Monat
benoetigte_dienste_monat = sum(
1 for tag in self.tage
if dienst in self.benoetigte_dienste.get(tag, [])
)
if dienst == 'A':
benoetigte_dienste_monat *= 2
gesamt_dienstfaktor_monat = sum(
sum(self.dienstfaktoren.get(e, {}).get(tag, 0) for tag in self.tage)
for e in self.eltern
)
if gesamt_dienstfaktor_monat > 0 and benoetigte_dienste_monat > 0:
erwartete_dienste_lokal = (
monatlicher_dienstfaktor / gesamt_dienstfaktor_monat
) * benoetigte_dienste_monat
# F2: Lokale Fairness-Constraints
prob += (tatsaechliche_dienste_monat - erwartete_dienste_lokal <=
fairness_abweichung_lokal[eltern, dienst])
prob += (erwartete_dienste_lokal - tatsaechliche_dienste_monat <=
fairness_abweichung_lokal[eltern, dienst])
# F1: Globale Fairness - basierend auf berechneter Zielverteilung
ziel_gesamt = ziel_dienste[eltern][dienst]
vorherige_dienste = self.vorherige_dienste[eltern][dienst]
if ziel_gesamt > 0:
# Tatsächliche Dienste global (Vergangenheit + geplant)
total_dienste_inkl_vergangenheit = tatsaechliche_dienste_monat + vorherige_dienste
# F1: Globale Fairness-Constraints
prob += (total_dienste_inkl_vergangenheit - ziel_gesamt <=
fairness_abweichung_global[eltern, dienst])
prob += (ziel_gesamt - total_dienste_inkl_vergangenheit <=
fairness_abweichung_global[eltern, dienst])
# Gewichtung: Jahresanfang F1 stärker, Jahresende F2 stärker
# Annahme: September = Jahresanfang, Juli = Jahresende
aktueller_monat = self.tage[0].month if self.tage else 1
if 9 <= aktueller_monat <= 12: # Sep-Dez: Jahresanfang
gewicht_f1 = 100 # Global wichtiger
gewicht_f2 = 50 # Lokal weniger wichtig
elif 1 <= aktueller_monat <= 3: # Jan-Mar: Jahresmitte
gewicht_f1 = 75
gewicht_f2 = 75
else: # Apr-Jul: Jahresende
gewicht_f1 = 50 # Global weniger wichtig
gewicht_f2 = 100 # Lokal wichtiger
# Fairness-Terme zur Zielfunktion hinzufügen
for eltern in self.eltern:
for dienst in self.dienste:
objective_terms.append(gewicht_f1 * fairness_abweichung_global[eltern, dienst])
objective_terms.append(gewicht_f2 * fairness_abweichung_lokal[eltern, dienst])
# P1: Bevorzugte Dienste (positiv belohnen)
for (eltern, tag, dienst), präf in self.präferenzen.items():
if (eltern, tag, dienst) in x and präf == 1: # bevorzugt
objective_terms.append(-5 * x[eltern, tag, dienst]) # Schwächer als Fairness
# P2: Abgelehnte Dienste (bestrafen)
for (eltern, tag, dienst), präf in self.präferenzen.items():
if (eltern, tag, dienst) in x and präf == -1: # abgelehnt
objective_terms.append(25 * x[eltern, tag, dienst]) # Schwächer als Fairness
# Zielfunktion setzen
if objective_terms:
prob += pulp.lpSum(objective_terms)
else:
# Fallback: Minimiere Gesamtanzahl Dienste
prob += pulp.lpSum([var for var in x.values()])
print(f"Verwende Gewichtung: F1 (global) = {gewicht_f1}, F2 (lokal) = {gewicht_f2}")
print(f"Modell erstellt mit {len(x)} Variablen und {len(prob.constraints)} Constraints")
return prob, x
def löse_optimierung(self, prob, x):
"""Löst das Optimierungsproblem"""
print("Löse Optimierungsproblem...")
# Solver wählen (verfügbare Solver testen)
solver = None
try:
solver = pulp.PULP_CBC_CMD(msg=0) # Standard CBC Solver
except:
try:
solver = pulp.GLPK_CMD(msg=0) # GLPK falls verfügbar
except:
solver = None # Default Solver
prob.solve(solver)
status = pulp.LpStatus[prob.status]
print(f"Optimierung abgeschlossen: {status}")
if prob.status != pulp.LpStatusOptimal:
print("WARNUNG: Keine optimale Lösung gefunden!")
return None
# Lösung extrahieren
lösung = {}
for (eltern, tag, dienst), var in x.items():
if var.varValue and var.varValue > 0.5: # Binary variable ist 1
if tag not in lösung:
lösung[tag] = {}
if dienst not in lösung[tag]:
lösung[tag][dienst] = []
lösung[tag][dienst].append(eltern)
return lösung
def schreibe_ausgabe_csv(self, datei, lösung):
"""Schreibt die Lösung in die ausgabe.csv"""
print(f"Schreibe Ergebnisse nach {datei}...")
with open(datei, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
# Header schreiben
header = ['Datum', 'Wochentag'] + [self.dienst_namen[d] for d in self.dienste]
writer.writerow(header)
# Daten schreiben
for tag in sorted(self.tage):
wochentag = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'][tag.weekday()]
row = [tag.strftime('%Y-%m-%d'), wochentag]
for dienst in self.dienste:
if tag in lösung and dienst in lösung[tag]:
eltern_str = ' '.join(lösung[tag][dienst])
else:
eltern_str = ''
row.append(eltern_str)
writer.writerow(row)
print("Ausgabe erfolgreich geschrieben!")
def drucke_statistiken(self, lösung):
"""Druckt Statistiken zur Lösung"""
print("\n" + "="*50)
print("STATISTIKEN")
print("="*50)
# Dienste pro Eltern zählen
dienste_pro_eltern = defaultdict(lambda: defaultdict(int))
for tag, tag_dienste in lösung.items():
for dienst, eltern_liste in tag_dienste.items():
for eltern in eltern_liste:
dienste_pro_eltern[eltern][dienst] += 1
# Gesamtübersicht
print("\nDienste pro Eltern:")
for eltern in sorted(self.eltern):
gesamt = sum(dienste_pro_eltern[eltern].values())
dienste_detail = ', '.join(f"{d}:{dienste_pro_eltern[eltern][d]}"
for d in self.dienste if dienste_pro_eltern[eltern][d] > 0)
print(f" {eltern:15} {gesamt:3d} ({dienste_detail})")
# Dienstfaktor-Analyse
print(f"\nDienstfaktoren im Planungszeitraum:")
for eltern in sorted(self.eltern):
faktor_summe = sum(self.dienstfaktoren.get(eltern, {}).get(tag, 0) for tag in self.tage)
print(f" {eltern:15} {faktor_summe:.1f}")
def main():
if len(sys.argv) < 4:
print("Usage: ./elterndienstplaner.py <eingabe.csv> <eltern.csv> <ausgabe.csv> [<vorherige-ausgaben.csv>]")
sys.exit(1)
eingabe_datei = sys.argv[1]
eltern_datei = sys.argv[2]
ausgabe_datei = sys.argv[3]
vorherige_datei = sys.argv[4] if len(sys.argv) > 4 else None
print("Elterndienstplaner gestartet")
print("="*50)
try:
planer = Elterndienstplaner()
# Daten laden
planer.lade_eingabe_csv(eingabe_datei)
planer.lade_eltern_csv(eltern_datei)
if vorherige_datei:
planer.lade_vorherige_ausgaben_csv(vorherige_datei)
# Optimierung
prob, x = planer.erstelle_optimierungsmodell()
lösung = planer.löse_optimierung(prob, x)
if lösung is not None:
# Ergebnisse ausgeben
planer.schreibe_ausgabe_csv(ausgabe_datei, lösung)
planer.drucke_statistiken(lösung)
print("\n✓ Planung erfolgreich abgeschlossen!")
else:
print("\n✗ Fehler: Keine gültige Lösung gefunden!")
sys.exit(1)
except Exception as e:
print(f"\n✗ Fehler: {e}")
sys.exit(1)
if __name__ == "__main__":
main()

1
elterndienstplaner.sh Executable file
View File

@ -0,0 +1 @@
/home/jwit/privat/elterndienstplaner/.venv/bin/python elterndienstplaner.py eingabe.csv eltern.csv ausgabe.csv vorherige-ausgaben.csv

37
vorherige-ausgaben.csv Normal file
View File

@ -0,0 +1,37 @@
Datum,Wochentag,Frühstücksdienst,Putznotdienst,Essensausgabenotdienst,Kochen,Elternabend
2025-12-02,Montag,Paula,Erwin,Laura,,
2025-12-03,Dienstag,Laura,Ben & Nele,Paula,,
2025-12-04,Mittwoch,Erwin,Paula,Ben & Nele,,
2025-12-05,Donnerstag,Ben & Nele,Laura,Erwin,,
2025-12-06,Freitag,Jonas,Marie,Laura,,
2025-12-09,Montag,Laura,Ben & Nele,Jonas,,
2025-12-10,Dienstag,Marie,Paula,Ben & Nele,,
2025-12-11,Mittwoch,Ben & Nele,Laura,Erwin,Paula,
2025-12-12,Donnerstag,Paula,Jonas,Marie,,
2025-12-13,Freitag,Laura,Jonas,Paula,,
2025-12-16,Montag,Erwin,Marie,Ben & Nele,,
2025-12-17,Dienstag,Ben & Nele,Laura,Jonas,,
2025-12-18,Mittwoch,Marie,Erwin,Laura,,
2025-12-19,Donnerstag,Laura,Ben & Nele,Paula,,
2025-12-20,Freitag,Jonas,Paula,Ben & Nele,Ben & Nele,Laura Erwin
2025-11-01,Freitag,Laura,Erwin,Ben & Nele,,
2025-11-04,Montag,Paula,Ben & Nele,Laura,,
2025-11-05,Dienstag,Erwin,Paula,Ben & Nele,,
2025-11-06,Mittwoch,Ben & Nele,Laura,Erwin,Erwin,
2025-11-07,Donnerstag,Paula,Erwin,Laura,,
2025-11-08,Freitag,Laura,Ben & Nele,Paula,,
2025-11-11,Montag,Erwin,Paula,Ben & Nele,,
2025-11-12,Dienstag,Ben & Nele,Laura,Erwin,,
2025-11-13,Mittwoch,Paula,Erwin,Laura,,
2025-11-14,Donnerstag,Laura,Ben & Nele,Paula,,
2025-11-15,Freitag,Erwin,Paula,Ben & Nele,,
2025-11-18,Montag,Ben & Nele,Laura,Erwin,,
2025-11-19,Dienstag,Paula,Erwin,Laura,,
2025-11-20,Mittwoch,Laura,Ben & Nele,Paula,Paula,
2025-11-21,Donnerstag,Erwin,Paula,Ben & Nele,,
2025-11-22,Freitag,Ben & Nele,Laura,Erwin,,
2025-11-25,Montag,Paula,Erwin,Laura,,
2025-11-26,Dienstag,Laura,Ben & Nele,Paula,,
2025-11-27,Mittwoch,Erwin,Paula,Ben & Nele,,
2025-11-28,Donnerstag,Ben & Nele,Laura,Erwin,,
2025-11-29,Freitag,Paula,Erwin,Laura,Laura,Ben & Nele Erwin
1 Datum Wochentag Frühstücksdienst Putznotdienst Essensausgabenotdienst Kochen Elternabend
2 2025-12-02 Montag Paula Erwin Laura
3 2025-12-03 Dienstag Laura Ben & Nele Paula
4 2025-12-04 Mittwoch Erwin Paula Ben & Nele
5 2025-12-05 Donnerstag Ben & Nele Laura Erwin
6 2025-12-06 Freitag Jonas Marie Laura
7 2025-12-09 Montag Laura Ben & Nele Jonas
8 2025-12-10 Dienstag Marie Paula Ben & Nele
9 2025-12-11 Mittwoch Ben & Nele Laura Erwin Paula
10 2025-12-12 Donnerstag Paula Jonas Marie
11 2025-12-13 Freitag Laura Jonas Paula
12 2025-12-16 Montag Erwin Marie Ben & Nele
13 2025-12-17 Dienstag Ben & Nele Laura Jonas
14 2025-12-18 Mittwoch Marie Erwin Laura
15 2025-12-19 Donnerstag Laura Ben & Nele Paula
16 2025-12-20 Freitag Jonas Paula Ben & Nele Ben & Nele Laura Erwin
17 2025-11-01 Freitag Laura Erwin Ben & Nele
18 2025-11-04 Montag Paula Ben & Nele Laura
19 2025-11-05 Dienstag Erwin Paula Ben & Nele
20 2025-11-06 Mittwoch Ben & Nele Laura Erwin Erwin
21 2025-11-07 Donnerstag Paula Erwin Laura
22 2025-11-08 Freitag Laura Ben & Nele Paula
23 2025-11-11 Montag Erwin Paula Ben & Nele
24 2025-11-12 Dienstag Ben & Nele Laura Erwin
25 2025-11-13 Mittwoch Paula Erwin Laura
26 2025-11-14 Donnerstag Laura Ben & Nele Paula
27 2025-11-15 Freitag Erwin Paula Ben & Nele
28 2025-11-18 Montag Ben & Nele Laura Erwin
29 2025-11-19 Dienstag Paula Erwin Laura
30 2025-11-20 Mittwoch Laura Ben & Nele Paula Paula
31 2025-11-21 Donnerstag Erwin Paula Ben & Nele
32 2025-11-22 Freitag Ben & Nele Laura Erwin
33 2025-11-25 Montag Paula Erwin Laura
34 2025-11-26 Dienstag Laura Ben & Nele Paula
35 2025-11-27 Mittwoch Erwin Paula Ben & Nele
36 2025-11-28 Donnerstag Ben & Nele Laura Erwin
37 2025-11-29 Freitag Paula Erwin Laura Laura Ben & Nele Erwin