scheint zu funktionieren
This commit is contained in:
parent
cdb151d7b0
commit
7d131b9e72
95
README.md
95
README.md
@ -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
24
ausgabe.csv
Normal 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,,
|
||||||
|
24
eingabe.csv
Normal file
24
eingabe.csv
Normal 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+
|
||||||
|
7
eltern.csv
Normal file
7
eltern.csv
Normal 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,,,,,
|
||||||
|
665
elterndienstplaner.py
Executable file
665
elterndienstplaner.py
Executable 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
1
elterndienstplaner.sh
Executable file
@ -0,0 +1 @@
|
|||||||
|
/home/jwit/privat/elterndienstplaner/.venv/bin/python elterndienstplaner.py eingabe.csv eltern.csv ausgabe.csv vorherige-ausgaben.csv
|
||||||
102
prompt
Normal file
102
prompt
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
[Jan]
|
||||||
|
Erstelle Beispiel-CSV-Dateien für einen Monat und die Eltern von Erwin, Paula, Laura, Ben & Nele. Ben & Nele sind Geschwister, identifizieren also ein Elternpaar mit Dienstfaktor 2
|
||||||
|
|
||||||
|
[Copilot-Nachtrag]
|
||||||
|
- Erstellt eingabe.csv mit Januar 2026 Daten für 5 Eltern (Erwin, Paula, Laura, Ben, Nele)
|
||||||
|
- Ben & Nele als Geschwister mit Dienstfaktor 2 in eltern.csv konfiguriert
|
||||||
|
- Alle anderen Eltern mit Dienstfaktor 1
|
||||||
|
- ausgabe.csv als leere Vorlage erstellt
|
||||||
|
- vorherige-ausgaben.csv für historische Daten erstellt
|
||||||
|
|
||||||
|
[Jan]
|
||||||
|
Verwende in den spaltenüberschriften die langen Dienstnamen
|
||||||
|
|
||||||
|
[Copilot-Nachtrag]
|
||||||
|
- Spaltenüberschriften in ausgabe.csv und vorherige-ausgaben.csv von Kürzeln (F,P,E,K,A) auf lange Namen geändert:
|
||||||
|
- F → Frühstücksdienst
|
||||||
|
- P → Putznotdienst
|
||||||
|
- E → Essensausgabenotdienst
|
||||||
|
- K → Kochen
|
||||||
|
- A → Elternabend
|
||||||
|
- Programm entsprechend angepasst für automatische Erkennung beider Formate
|
||||||
|
|
||||||
|
[Jan]
|
||||||
|
Lege nun ein Programm elterndienstplan.py an, dass die in der Readme beschriebene Funktion ausführt. Verwende eine library zum einlesen der CSVs und Pulp als solver.
|
||||||
|
|
||||||
|
[Copilot-Nachtrag]
|
||||||
|
- Vollständiges Python-Programm elterndienstplaner.py erstellt
|
||||||
|
- CSV-Bibliothek für Datei-I/O implementiert
|
||||||
|
- PuLP-Solver für Optimierung integriert
|
||||||
|
- Grundlegende Constraint-Struktur (C1, C2, C3) implementiert
|
||||||
|
- Klassen-basierte Architektur mit modularen Methoden
|
||||||
|
|
||||||
|
[Jan]
|
||||||
|
Lege nun ein Programm elterndienstplan.py an, dass die in der Readme beschriebene Funktion ausführt. Verwende eine library zum einlesen der CSVs und Pulp als solver.
|
||||||
|
füge in die eingabedaen und eltern noch die kinder Jonas und Marie ein
|
||||||
|
|
||||||
|
[Copilot-Nachtrag]
|
||||||
|
- eingabe.csv erweitert um Jonas und Marie (6 Eltern total)
|
||||||
|
- eltern.csv um Jonas und Marie ergänzt (beide mit Dienstfaktor 1)
|
||||||
|
- Präferenzen für alle Eltern in eingabe.csv eingetragen
|
||||||
|
- Programm automatisch kompatibel durch dynamisches Einlesen der Eltern aus CSV-Header
|
||||||
|
|
||||||
|
[Jan]
|
||||||
|
erstelle optimierungen für F1 und F2. Überlege vorher, ob die formulierten Constraints ihren Zweck erfüllen
|
||||||
|
|
||||||
|
[Copilot-Nachtrag]
|
||||||
|
- Fairness-Constraints F1 (globale Fairness) und F2 (lokale Fairness) als Hilfsvariablen implementiert
|
||||||
|
- Zielfunktion erweitert um Fairness-Optimierung mit Gewichtung
|
||||||
|
- Adaptive Gewichtung je nach Zeitpunkt im Schuljahr (Jahresanfang vs. Jahresende)
|
||||||
|
- Präferenz-Constraints P1 (bevorzugt) und P2 (abgelehnt) mit niedrigerer Priorität
|
||||||
|
- Debugging der "Infeasible"-Probleme durch Constraint-Lockerung
|
||||||
|
|
||||||
|
[Jan]
|
||||||
|
Die globale Fairness soll die Dienstfaktoren und geleisteten Dienste der Vergangenheit berücksichtigen. Dazu müssen analog zur lokalen Fairness erwartete Dienste (anhand geleisteter vorheriger Dienste und Dienstfaktor) und tatsächlich geleistete Dienstet (vorherige und neu geplante) subtrahiert werden
|
||||||
|
|
||||||
|
[Copilot-Nachtrag]
|
||||||
|
- lade_vorherige_ausgaben_csv() implementiert zum Einlesen historischer Dienstdaten
|
||||||
|
- Globale Fairness-Berechnung erweitert um Vergangenheitsdaten
|
||||||
|
- Constraint F1 berücksichtigt jetzt: (vorherige_dienste + geplante_dienste) vs. erwartete_dienste_global
|
||||||
|
- Strukturierte Summierung historischer Dienste pro Eltern und Diensttyp
|
||||||
|
|
||||||
|
[Jan]
|
||||||
|
Das is nicht so einfach. total_dienstfaktor_alle berücksichtigt aktuell nur die dienstfaktoren des aktuellenmonats, da nur diese eingelesen werden (self.tage).
|
||||||
|
|
||||||
|
Beim einlesen der eltern.csv müssen alle Zeiträume vollständig eingelesen werden, nicht nur für self.tage.
|
||||||
|
|
||||||
|
Es muss aber folgendes Geschehen: Für die globale Fairness muss eine theoretische faire Verteilung über den Gesamten Zeitraum ermittelt werden. Dazu werden alle bereits geleisteten Dienste anhand der Dienstfaktoren des jeweiligen Tages auf die Eltern aufgeteilt und das über alle Tage summiert. Für den aktuellen Monat werden die zu leisteden Dienste anhand der Dienstfaktoren des auellen Tages auf die eltern aufgeteilt und summiert. So erhält man für alle Eltern-Dienst-Kombinationen die Zielanzahl der zu leistenden Dienste.
|
||||||
|
|
||||||
|
[Copilot-Nachtrag]
|
||||||
|
- lade_eltern_csv() erweitert zum Speichern ALLER Zeiträume in self.alle_zeitraeume
|
||||||
|
- berechne_dienstfaktor_an_datum() Funktion implementiert für historische Dienstfaktor-Berechnung
|
||||||
|
- berechne_faire_zielverteilung() Funktion erstellt für theoretische faire Verteilung
|
||||||
|
- Tag-genaue Berechnung der Dienstfaktoren über gesamten Zeitraum
|
||||||
|
- Summierung fairer Anteile aus Vergangenheit + Zukunft für globale Ziele
|
||||||
|
|
||||||
|
[Jan]
|
||||||
|
berechne_faire_zielverteilung() tut nicht das von mir verlangte.
|
||||||
|
"Es muss aber folgendes Geschehen: Für die globale Fairness muss eine theoretische faire Verteilung über den Gesamten Zeitraum ermittelt werden. Dazu werden alle bereits geleisteten Dienste anhand der Dienstfaktoren des jeweiligen Tages auf die Eltern aufgeteilt und das über alle Tage summiert. Für den aktuellen Monat werden die zu leisteden Dienste anhand der Dienstfaktoren des auellen Tages auf die eltern aufgeteilt und summiert. So erhält man für alle Eltern-Dienst-Kombinationen die Zielanzahl der zu leistenden Dienste."
|
||||||
|
|
||||||
|
dazu muss es die funktion berechne_dienstfaktor_an_datum() verwenden, um vergangene dienste nach faktor zu verteilen
|
||||||
|
|
||||||
|
[Copilot-Nachtrag]
|
||||||
|
- berechne_faire_zielverteilung() komplett neu geschrieben
|
||||||
|
- Verwendet jetzt berechne_dienstfaktor_an_datum() für historische Tage
|
||||||
|
- Tag-genaue Berechnung: für jeden historischen Tag die Dienstfaktoren aller Eltern ermitteln
|
||||||
|
- Theoretische Umverteilung der geleisteten Dienste basierend auf täglichen Dienstfaktoren
|
||||||
|
- Separate Berechnung für Vergangenheit und aktuellen Monat, dann Summierung
|
||||||
|
- Echte faire Zielverteilung ohne Annahmen über Dienstverteilung
|
||||||
|
|
||||||
|
[Jan]
|
||||||
|
hier keine annahme treffen, sondern aus den eingelesenen vorherigen diensten ermitteln, welche dienste geleistet wurden und diese umverteilen. Dazu muss in lade_vorherige_ausgaben_csv() nicht nur summiert werden, sondern gespeichert werden, welche Dienste an welchen Tagen von wem geleistet wurden.
|
||||||
|
|
||||||
|
[Copilot-Nachtrag]
|
||||||
|
- Neue Datenstruktur self.historische_dienste für (datum, eltern, dienst) Tupel hinzugefügt
|
||||||
|
- lade_vorherige_ausgaben_csv() erweitert um Datum-Parsing und detaillierte Speicherung
|
||||||
|
- berechne_faire_zielverteilung() verwendet jetzt nur echte historische Daten
|
||||||
|
- Für jeden historischen Tag: Ermittlung der tatsächlich geleisteten Dienste aus CSV
|
||||||
|
- Faire Umverteilung basierend auf Dienstfaktoren des jeweiligen historischen Tages
|
||||||
|
- Keine Annahmen mehr über Dienstverteilung - nur tatsächliche Daten aus vorherige-ausgaben.csv
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
37
vorherige-ausgaben.csv
Normal file
37
vorherige-ausgaben.csv
Normal 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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user