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