Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python" (Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch Version_03 (eingefroren) verwaltet und Version_04/ ignoriert. Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist. Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise, 325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren, 25 Notebooks, PDF mit 715 Seiten. Zusaetzlich in diesem Commit: * pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api, figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt unveraendert daneben bestehen. ortools steht in der Grundausstattung, highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der schlanken Installation gar nicht erst auftreten. * Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo steht in requirements.txt, wird aber von keinem Programm importiert, sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in [empfehlungen]). * NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen. * PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den tatsaechlichen aus erzeuge_titelseite.py korrigiert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
92 lines
3.7 KiB
Python
Generated
92 lines
3.7 KiB
Python
Generated
#!/usr/bin/env python3
|
|
|
|
# Monte_Carlo.py
|
|
"""
|
|
Kapitel Unsicherheit: Monte-Carlo-Bewertung von Kapazitaetsplaenen.
|
|
|
|
Monte Carlo OPTIMIERT nicht - es BEWERTET. Der Nutzen liegt darin, dass man
|
|
beliebige Kennzahlen ablesen kann: Erwartungswert, Quantile, Ausfallwahr-
|
|
scheinlichkeit, Worst Case. Genau diese Groessen braucht man, um zwischen
|
|
Plaenen zu entscheiden.
|
|
"""
|
|
|
|
import numpy as np
|
|
|
|
KOSTEN_VORAB = 40.0 # EUR je Einheit, im Voraus gekauft
|
|
KOSTEN_SPOT = 120.0 # EUR je Einheit, kurzfristig zugekauft
|
|
KOSTEN_LEERLAUF = 5.0 # EUR je ungenutzter Einheit
|
|
|
|
ANZAHL_ZIEHUNGEN = 100_000
|
|
|
|
|
|
def ziehe_bedarf(rng, ziehungen):
|
|
"""
|
|
Bedarfsmodell: Mischverteilung aus Normalbetrieb und seltenen Lastspitzen.
|
|
Realistischer als eine reine Normalverteilung - Krisen sind selten,
|
|
aber extrem (fat tail).
|
|
"""
|
|
normal = rng.normal(loc=150, scale=40, size=ziehungen)
|
|
spitze = rng.normal(loc=450, scale=80, size=ziehungen)
|
|
ist_spitze = rng.random(ziehungen) < 0.15 # 15 % Lastspitzen
|
|
return np.maximum(np.where(ist_spitze, spitze, normal), 0.0)
|
|
|
|
|
|
def kosten_fuer(kapazitaet, bedarf):
|
|
"""Gesamtkosten je Szenario fuer eine gegebene Vorabkapazitaet."""
|
|
unterdeckung = np.maximum(bedarf - kapazitaet, 0.0)
|
|
ueberdeckung = np.maximum(kapazitaet - bedarf, 0.0)
|
|
return (KOSTEN_VORAB * kapazitaet
|
|
+ KOSTEN_SPOT * unterdeckung
|
|
+ KOSTEN_LEERLAUF * ueberdeckung)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
rng = np.random.default_rng(2026)
|
|
bedarf = ziehe_bedarf(rng, ANZAHL_ZIEHUNGEN)
|
|
|
|
print("=" * 88)
|
|
print(f" MONTE-CARLO-BEWERTUNG ({ANZAHL_ZIEHUNGEN:,} Szenarien)")
|
|
print("=" * 88)
|
|
print(f"Bedarfsverteilung: Mittelwert {bedarf.mean():.1f} | "
|
|
f"Median {np.median(bedarf):.1f} | "
|
|
f"95%-Quantil {np.percentile(bedarf, 95):.1f} | "
|
|
f"Maximum {bedarf.max():.1f}")
|
|
print("Der Median liegt deutlich unter dem Mittelwert - die Verteilung ist")
|
|
print("rechtsschief. Genau hier fuehrt Planung mit dem Mittelwert in die Irre.\n")
|
|
|
|
print(f"{'Kapazitaet':>10} | {'Erw. Kosten':>12} | {'Median':>10} | "
|
|
f"{'95%-Quantil':>12} | {'Unterdeckung':>12}")
|
|
print("-" * 88)
|
|
|
|
kandidaten = [150, 200, 225, 250, 300, 350, 400]
|
|
ergebnisse = []
|
|
for kapazitaet in kandidaten:
|
|
kosten = kosten_fuer(kapazitaet, bedarf)
|
|
p_unterdeckung = float(np.mean(bedarf > kapazitaet))
|
|
ergebnisse.append((kapazitaet, kosten.mean(), p_unterdeckung))
|
|
print(f"{kapazitaet:>10} | {kosten.mean():>12,.0f} | "
|
|
f"{np.median(kosten):>10,.0f} | {np.percentile(kosten, 95):>12,.0f} | "
|
|
f"{p_unterdeckung*100:>11.1f} %")
|
|
|
|
beste = min(ergebnisse, key=lambda t: t[1])
|
|
print("-" * 88)
|
|
print(f"Bester Kandidat: Kapazitaet {beste[0]} mit erwarteten Kosten "
|
|
f"{beste[1]:,.0f} EUR")
|
|
|
|
# --- Feinsuche ueber ein Raster ---------------------------------------
|
|
raster = np.arange(100, 500, 5)
|
|
erwartete = np.array([kosten_fuer(k, bedarf).mean() for k in raster])
|
|
optimum = raster[int(np.argmin(erwartete))]
|
|
print(f"Feinsuche (Raster 100..500): Optimum bei Kapazitaet {optimum}, "
|
|
f"Kosten {erwartete.min():,.0f} EUR")
|
|
|
|
# --- Vergleich mit der naiven Mittelwertplanung ----------------------
|
|
naiv = int(round(bedarf.mean()))
|
|
kosten_naiv = kosten_fuer(naiv, bedarf).mean()
|
|
kosten_opt = kosten_fuer(optimum, bedarf).mean()
|
|
print("\n--- Fluch des Durchschnitts, gemessen ---")
|
|
print(f" Planung mit Mittelwert ({naiv}): {kosten_naiv:,.0f} EUR")
|
|
print(f" Monte-Carlo-Optimum ({optimum}): {kosten_opt:,.0f} EUR")
|
|
print(f" Mehrkosten der naiven Planung: {kosten_naiv - kosten_opt:,.0f} EUR "
|
|
f"({(kosten_naiv/kosten_opt - 1)*100:.1f} %)")
|
|
print("=" * 88)
|