#!/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)