Compare commits
No commits in common. "beea66ef2f9dee27e3690b9d661c416dd6c28a78" and "3b94f460a804d0a88f59c722d64f83a4a56ef0e6" have entirely different histories.
beea66ef2f
...
3b94f460a8
@ -54,23 +54,17 @@ Leon,2024-09-01,2024-12-31,1,2025-01-01,2025-07-31,0
|
||||
|
||||
### vorherige-ausgaben.csv (optional)
|
||||
|
||||
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.
|
||||
Historische Dienstzuteilungen für Jahres-Fairness. Format wie `ausgabe.csv`.
|
||||
|
||||
## Ausgabedatei
|
||||
|
||||
### ausgabe.csv
|
||||
|
||||
Die neu zugeteilten Dienste.
|
||||
```csv
|
||||
Datum,Wochentag,Frühstücksdienst,Putznotdienst,Essensausgabenotdienst,Kochen,Elternabend
|
||||
2026-01-06,Montag,Sarah & Tim,Leon,Erika,,
|
||||
```
|
||||
|
||||
|
||||
### 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
|
||||
|
||||
```bash
|
||||
|
||||
148
ausgabe.py
148
ausgabe.py
@ -6,7 +6,7 @@ Visualisierung und Export der Ergebnisse
|
||||
|
||||
from datetime import date
|
||||
from collections import defaultdict
|
||||
from typing import Dict, List, DefaultDict, Tuple
|
||||
from typing import Dict, List, DefaultDict
|
||||
|
||||
from datenmodell import ElterndienstplanerDaten, Dienst, Eltern, Zielverteilung
|
||||
from csv_io import AusgabeWriter
|
||||
@ -20,8 +20,6 @@ class ElterndienstAusgabe:
|
||||
# Zwischenergebnisse aus der Optimierung (über Observer-Pattern gesetzt)
|
||||
self.ziel_lokal: 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(
|
||||
self,
|
||||
@ -32,17 +30,9 @@ class ElterndienstAusgabe:
|
||||
self.ziel_lokal = ziel_lokal
|
||||
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:
|
||||
"""Schreibt die Lösung in die ausgabe.csv"""
|
||||
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)
|
||||
AusgabeWriter.schreibe_ausgabe_csv(datei, lösung, self.daten.planungszeitraum, self.daten.dienste)
|
||||
|
||||
def drucke_statistiken(self, lösung: Dict[date, Dict[Dienst, List[Eltern]]]) -> None:
|
||||
"""Druckt Statistiken zur Lösung"""
|
||||
@ -118,30 +108,28 @@ class ElterndienstAusgabe:
|
||||
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
|
||||
# Prüfe ob ALLE zugeteilten Dienste an nicht-präferierten Tagen sind
|
||||
for tag in zugeteilte_tage:
|
||||
if tag not in positive_praef_tage:
|
||||
# Dienst wurde an nicht-präferiertem Tag zugeteilt
|
||||
verletzungen[eltern][dienst]['positiv_nicht_erfuellt'] += 1
|
||||
|
||||
# Tabelle ausgeben (verbesserte Spaltenformatierung)
|
||||
col_width = 14 # Breite pro Dienst-Spalte (sichtbar)
|
||||
name_col = 20
|
||||
|
||||
# Header
|
||||
print(f"\n{'Eltern':<{name_col}}", end='')
|
||||
# Tabelle ausgeben
|
||||
print(f"\n{'Eltern':<20} ", end='')
|
||||
for dienst in self.daten.dienste:
|
||||
print(f"{dienst.kuerzel:^{col_width}}", end='')
|
||||
print(f"{dienst.kuerzel:>12}", end='')
|
||||
print()
|
||||
print(f"{'':{name_col}}", end='')
|
||||
for _ in self.daten.dienste:
|
||||
print(f"{'neg, pos':^{col_width}}", end='')
|
||||
print(f"{'':20} ", end='')
|
||||
for dienst in self.daten.dienste:
|
||||
print(f"{'neg, pos':>12}", end='')
|
||||
print()
|
||||
print("-" * (name_col + col_width * len(self.daten.dienste)))
|
||||
print("-" * (20 + 12 * len(self.daten.dienste)))
|
||||
|
||||
gesamt_negativ = defaultdict(int)
|
||||
gesamt_positiv = defaultdict(int)
|
||||
|
||||
for eltern in sorted(self.daten.eltern):
|
||||
print(f"{eltern:<{name_col}}", end='')
|
||||
print(f"{eltern:<20} ", end='')
|
||||
for dienst in self.daten.dienste:
|
||||
neg = verletzungen[eltern][dienst]['negativ']
|
||||
pos = verletzungen[eltern][dienst]['positiv_nicht_erfuellt']
|
||||
@ -149,28 +137,22 @@ class ElterndienstAusgabe:
|
||||
gesamt_negativ[dienst] += neg
|
||||
gesamt_positiv[dienst] += pos
|
||||
|
||||
# Inhalt vor Padding erstellen
|
||||
cell = f"{neg:>3}, {pos:>3}"
|
||||
cell_padded = cell.center(col_width)
|
||||
|
||||
# Farbcodierung (erst nach Padding anwenden)
|
||||
# Farbcodierung
|
||||
farbe = ""
|
||||
reset = ""
|
||||
if neg > 0 or pos > 0:
|
||||
farbe = "\033[91m" if neg > 0 else "\033[93m" # Rot für negativ, Gelb für positiv
|
||||
reset = "\033[0m"
|
||||
|
||||
print(f"{farbe}{cell_padded}{reset}", end='')
|
||||
print(f"{farbe}{neg:>3}, {pos:>3}{reset:>6}", end='')
|
||||
print()
|
||||
|
||||
# Summenzeile
|
||||
print("-" * (name_col + col_width * len(self.daten.dienste)))
|
||||
print(f"{'SUMME':<{name_col}}", end='')
|
||||
print("-" * (20 + 12 * len(self.daten.dienste)))
|
||||
print(f"{'SUMME':<20} ", end='')
|
||||
for dienst in self.daten.dienste:
|
||||
neg = gesamt_negativ[dienst]
|
||||
pos = gesamt_positiv[dienst]
|
||||
cell = f"{neg:>3}, {pos:>3}"
|
||||
cell_padded = cell.center(col_width)
|
||||
|
||||
farbe = ""
|
||||
reset = ""
|
||||
@ -178,7 +160,7 @@ class ElterndienstAusgabe:
|
||||
farbe = "\033[91m" if neg > 0 else "\033[93m"
|
||||
reset = "\033[0m"
|
||||
|
||||
print(f"{farbe}{cell_padded}{reset}", end='')
|
||||
print(f"{farbe}{neg:>3}, {pos:>3}{reset:>6}", end='')
|
||||
print()
|
||||
|
||||
print("\nLegende:")
|
||||
@ -267,99 +249,3 @@ class ElterndienstAusgabe:
|
||||
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("\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
|
||||
from datetime import datetime, date, timedelta
|
||||
from typing import Dict, List, Tuple, DefaultDict, Optional
|
||||
from typing import Dict, List, Tuple, DefaultDict
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
@ -213,9 +213,7 @@ class AusgabeWriter:
|
||||
datei: str,
|
||||
lösung: Dict[date, Dict[any, List[str]]], # Dienst-Objekt als Key
|
||||
tage: List[date],
|
||||
dienste: List, # List[Dienst]
|
||||
gesamt: bool = False,
|
||||
historische_dienste: List[Tuple[date, str, any]] = None
|
||||
dienste: List # List[Dienst]
|
||||
) -> None:
|
||||
"""
|
||||
Schreibt die Lösung in die ausgabe.csv
|
||||
@ -223,50 +221,10 @@ class AusgabeWriter:
|
||||
Args:
|
||||
datei: Pfad zur ausgabe.csv
|
||||
lösung: Dictionary mit Zuteilungen {datum: {dienst: [eltern]}}
|
||||
tage: Liste aller Planungstage (aktueller Planungszeitraum)
|
||||
tage: Liste aller Planungstage
|
||||
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}... (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)
|
||||
print(f"Schreibe Ergebnisse nach {datei}...")
|
||||
|
||||
with open(datei, 'w', newline='', encoding='utf-8') as f:
|
||||
writer = csv.writer(f)
|
||||
@ -276,16 +234,17 @@ class AusgabeWriter:
|
||||
writer.writerow(header)
|
||||
|
||||
# Daten schreiben
|
||||
for tag in output_dates:
|
||||
for tag in sorted(tage):
|
||||
wochentag = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag',
|
||||
'Freitag', 'Samstag', 'Sonntag'][tag.weekday()]
|
||||
|
||||
row = [tag.strftime('%Y-%m-%d'), wochentag]
|
||||
|
||||
for dienst in dienste:
|
||||
eltern_str = ''
|
||||
if tag in combined and dienst in combined[tag]:
|
||||
eltern_str = ' und '.join(combined[tag][dienst])
|
||||
if tag in lösung and dienst in lösung[tag]:
|
||||
eltern_str = ' und '.join(lösung[tag][dienst])
|
||||
else:
|
||||
eltern_str = ''
|
||||
row.append(eltern_str)
|
||||
|
||||
writer.writerow(row)
|
||||
|
||||
@ -15,17 +15,16 @@ from csv_io import EingabeParser
|
||||
class Dienst:
|
||||
"""Repräsentiert einen Diensttyp mit allen seinen Eigenschaften"""
|
||||
|
||||
def __init__(self, kuerzel: str, name: str, personen_anzahl: int = 1, aufwand: int = 1) -> None:
|
||||
def __init__(self, kuerzel: str, name: str, personen_anzahl: int = 1) -> None:
|
||||
self.kuerzel: str = kuerzel
|
||||
self.name: str = name
|
||||
self.personen_anzahl: int = personen_anzahl
|
||||
self.aufwand: int = aufwand
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.kuerzel} ({self.name}): {self.personen_anzahl} Person(en), Aufwand={self.aufwand}"
|
||||
return f"{self.kuerzel} ({self.name}): {self.personen_anzahl} Person(en)"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Dienst('{self.kuerzel}', '{self.name}', {self.personen_anzahl}, {self.aufwand})"
|
||||
return f"Dienst('{self.kuerzel}', '{self.name}', {self.personen_anzahl})"
|
||||
|
||||
def braucht_mehrere_personen(self) -> bool:
|
||||
"""Gibt True zurück, wenn mehr als eine Person benötigt wird"""
|
||||
@ -50,11 +49,11 @@ class ElterndienstplanerDaten:
|
||||
def __init__(self) -> None:
|
||||
# Dienste als Liste definieren
|
||||
self.dienste: List[Dienst] = [
|
||||
Dienst('F', 'Frühstücksdienst', 1, aufwand=3),
|
||||
Dienst('P', 'Putznotdienst', 1, aufwand=1),
|
||||
Dienst('E', 'Essensausgabenotdienst', 1, aufwand=1),
|
||||
Dienst('K', 'Kochen', 1, aufwand=3),
|
||||
Dienst('A', 'Elternabend', 2, aufwand=2)
|
||||
Dienst('F', 'Frühstücksdienst', 1),
|
||||
Dienst('P', 'Putznotdienst', 1),
|
||||
Dienst('E', 'Essensausgabenotdienst', 1),
|
||||
Dienst('K', 'Kochen', 1),
|
||||
Dienst('A', 'Elternabend', 2)
|
||||
]
|
||||
|
||||
# Datenstrukturen
|
||||
@ -103,17 +102,12 @@ class ElterndienstplanerDaten:
|
||||
vorherige_datei: Optionaler Pfad zur vorherige-ausgaben.csv für Fairness-Constraints
|
||||
"""
|
||||
# Eingabe CSV: Termine, Präferenzen, Verfügbarkeit
|
||||
# Eltern CSV: Dienstfaktoren (erst einlesen, damit self.eltern daraus abgeleitet wird)
|
||||
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 = \
|
||||
self.eltern, self.planungszeitraum, self.benoetigte_dienste, self.verfuegbarkeit, self.praeferenzen = \
|
||||
EingabeParser.parse_eingabe_csv(eingabe_datei, self.get_dienst)
|
||||
|
||||
# Eltern CSV: Dienstfaktoren
|
||||
self.dienstfaktoren = EingabeParser.parse_eltern_csv(eltern_datei)
|
||||
|
||||
# Vorherige Ausgaben CSV (optional): Historische Dienste für Fairness
|
||||
if vorherige_datei:
|
||||
self.historische_dienste = \
|
||||
|
||||
@ -8,7 +8,6 @@ Datum: Dezember 2025
|
||||
|
||||
import sys
|
||||
import pulp
|
||||
import multiprocessing
|
||||
from datetime import timedelta, date
|
||||
from collections import defaultdict
|
||||
from typing import Dict, List, Tuple, DefaultDict, Optional
|
||||
@ -69,9 +68,9 @@ class Elterndienstplaner:
|
||||
faire_zuteilung = anteil * anzahl_dienste
|
||||
ziel_dienste[eltern][dienst] += faire_zuteilung
|
||||
|
||||
#if faire_zuteilung > 0.01:
|
||||
# print(f" {tag}: {eltern} Faktor={self.daten.dienstfaktoren[eltern][tag]} "
|
||||
# f"-> {faire_zuteilung:.2f} von {anzahl_dienste} Diensten")
|
||||
if faire_zuteilung > 0.01:
|
||||
print(f" {tag}: {eltern} Faktor={self.daten.dienstfaktoren[eltern][tag]} "
|
||||
f"-> {faire_zuteilung:.2f} von {anzahl_dienste} Diensten")
|
||||
|
||||
# 2. AKTUELLER PLANUNGSZEITRAUM: Faire Verteilung
|
||||
benoetigte_dienste_planungszeitraum = 0
|
||||
@ -180,10 +179,6 @@ class Elterndienstplaner:
|
||||
woche_nr = 0
|
||||
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:
|
||||
woche_ende = woche_start + timedelta(days=6)
|
||||
|
||||
@ -193,13 +188,12 @@ class Elterndienstplaner:
|
||||
|
||||
# Zaehle historische Dienste in dieser Woche (VOR Planungszeitraum)
|
||||
historische_dienste_in_woche = 0
|
||||
#if woche_start < erster_tag:
|
||||
for hist_datum, hist_eltern, hist_dienst in self.daten.historische_dienste:
|
||||
if (hist_eltern == eltern and
|
||||
hist_dienst == dienst and
|
||||
woche_start <= hist_datum and
|
||||
hist_datum <= woche_ende):
|
||||
historische_dienste_in_woche += 1
|
||||
if woche_start < erster_tag:
|
||||
for hist_datum, hist_eltern, hist_dienst in self.daten.historische_dienste:
|
||||
if (hist_eltern == eltern and
|
||||
hist_dienst == dienst and
|
||||
woche_start <= hist_datum < erster_tag):
|
||||
historische_dienste_in_woche += 1
|
||||
|
||||
for tag in self.daten.planungszeitraum:
|
||||
if woche_start <= tag <= woche_ende:
|
||||
@ -207,11 +201,8 @@ class Elterndienstplaner:
|
||||
woche_vars.append(x[eltern, tag, dienst])
|
||||
|
||||
if woche_vars:
|
||||
if (1 - historische_dienste_in_woche) >= 0:
|
||||
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}")
|
||||
prob += pulp.lpSum(woche_vars) <= 1 - historische_dienste_in_woche, \
|
||||
f"C1_{eltern.replace(' ', '_')}_{dienst.kuerzel}_w{woche_nr}"
|
||||
|
||||
woche_start += timedelta(days=7)
|
||||
woche_nr += 1
|
||||
@ -225,18 +216,12 @@ class Elterndienstplaner:
|
||||
for eltern in self.daten.eltern:
|
||||
for tag in self.daten.planungszeitraum:
|
||||
tag_vars = []
|
||||
maximum = 1
|
||||
for dienst in self.daten.dienste:
|
||||
if (eltern, tag, dienst) in x:
|
||||
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:
|
||||
prob += pulp.lpSum(tag_vars) <= maximum, f"C2_{eltern.replace(' ', '_')}_{tag}"
|
||||
prob += pulp.lpSum(tag_vars) <= 1, f"C2_{eltern.replace(' ', '_')}_{tag}"
|
||||
|
||||
def _add_constraint_verfuegbarkeit(
|
||||
self,
|
||||
@ -356,16 +341,14 @@ class Elterndienstplaner:
|
||||
lowBound=0)
|
||||
|
||||
|
||||
# Zähle tatsächliche Dienste gewichtet mit dem Aufwand des Dienstes
|
||||
tatsaechliche_dienste_gesamt = pulp.lpSum(
|
||||
dienst.aufwand * x[eltern, tag, dienst]
|
||||
x[eltern, tag, dienst]
|
||||
for tag in self.daten.planungszeitraum
|
||||
for dienst in self.daten.dienste
|
||||
if (eltern, tag, dienst) in x
|
||||
)
|
||||
|
||||
# Zielgesamt ebenfalls mit Dienst-Aufwand gewichtet
|
||||
ziel_gesamt = sum(ziel_dienste[eltern][dienst] * dienst.aufwand for dienst in self.daten.dienste)
|
||||
ziel_gesamt = sum(ziel_dienste[eltern][dienst] for dienst in self.daten.dienste)
|
||||
|
||||
prob += (tatsaechliche_dienste_gesamt - ziel_gesamt <=
|
||||
fairness_abweichung_gesamt[eltern])
|
||||
@ -387,8 +370,8 @@ class Elterndienstplaner:
|
||||
"""Erstellt die Zielfunktion mit Fairness und Praeferenzen"""
|
||||
objective_terms = []
|
||||
|
||||
gewicht_global = 50
|
||||
gewicht_lokal = 50
|
||||
gewicht_global = 40
|
||||
gewicht_lokal = 60
|
||||
gewicht_f1 = gewicht_global
|
||||
gewicht_f2 = gewicht_lokal
|
||||
gewicht_f3_global = 0.25 * gewicht_global
|
||||
@ -396,23 +379,21 @@ class Elterndienstplaner:
|
||||
|
||||
for eltern in self.daten.eltern:
|
||||
for dienst in self.daten.dienste:
|
||||
# Skaliere diensttyp-spezifische Fairness mit dem Aufwand des Dienstes
|
||||
objective_terms.append(gewicht_f1 * fairness_abweichung_global[eltern, dienst] * dienst.aufwand)
|
||||
objective_terms.append(gewicht_f2 * fairness_abweichung_lokal[eltern, dienst] * dienst.aufwand)
|
||||
objective_terms.append(gewicht_f1 * fairness_abweichung_global[eltern, dienst])
|
||||
objective_terms.append(gewicht_f2 * fairness_abweichung_lokal[eltern, dienst])
|
||||
|
||||
# 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_f4_lokal * fairness_abweichung_gesamt_lokal[eltern])
|
||||
|
||||
# P1: Bevorzugte Dienste (stärker für aufwändigere Dienste)
|
||||
# P1: Bevorzugte Dienste
|
||||
for (eltern, tag, dienst), praef in self.daten.praeferenzen.items():
|
||||
if (eltern, tag, dienst) in x and praef == 1:
|
||||
objective_terms.append(-10 * dienst.aufwand * x[eltern, tag, dienst])
|
||||
objective_terms.append(-5 * x[eltern, tag, dienst])
|
||||
|
||||
# P2: Abgelehnte Dienste (stärker für aufwändigere Dienste)
|
||||
# P2: Abgelehnte Dienste
|
||||
for (eltern, tag, dienst), praef in self.daten.praeferenzen.items():
|
||||
if (eltern, tag, dienst) in x and praef == -1:
|
||||
objective_terms.append(20 * dienst.aufwand * x[eltern, tag, dienst])
|
||||
objective_terms.append(25 * x[eltern, tag, dienst])
|
||||
|
||||
if objective_terms:
|
||||
prob += pulp.lpSum(objective_terms)
|
||||
@ -485,15 +466,13 @@ class Elterndienstplaner:
|
||||
|
||||
solver = None
|
||||
try:
|
||||
cpu_count = multiprocessing.cpu_count()
|
||||
threads = max(1, cpu_count - 1)
|
||||
print(f"Versuche CBC Solver mit {threads} Threads...")
|
||||
solver = pulp.PULP_CBC_CMD(msg=0, timeLimit=20, threads=threads)
|
||||
except Exception:
|
||||
print("Versuche CBC Solver...")
|
||||
solver = pulp.PULP_CBC_CMD(msg=0, timeLimit=10)
|
||||
except:
|
||||
try:
|
||||
print("Versuche GLPK Solver...")
|
||||
solver = pulp.GLPK_CMD(msg=0)
|
||||
except Exception:
|
||||
except:
|
||||
print("Kein spezifizierter Solver verfügbar, verwende Standard.")
|
||||
solver = None
|
||||
|
||||
@ -544,7 +523,6 @@ def main() -> None:
|
||||
if loesung is not None:
|
||||
ausgabe.schreibe_ausgabe_csv(ausgabe_datei, loesung)
|
||||
ausgabe.drucke_statistiken(loesung)
|
||||
ausgabe.visualisiere_dienste_uebersicht(loesung)
|
||||
ausgabe.visualisiere_verteilungen(loesung)
|
||||
ausgabe.visualisiere_praeferenz_verletzungen(loesung)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user