Version 04 als eigenes Repository
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>
This commit is contained in:
commit
b7af2f1d9a
468 changed files with 262141 additions and 0 deletions
92
OR_HTML_04/programme/Monte_Carlo.py
Normal file
92
OR_HTML_04/programme/Monte_Carlo.py
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
#!/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)
|
||||
Loading…
Reference in a new issue