Kapitel 12: Optimierung unter Unsicherheit — Monte-Carlo, Stochastik, Robustheit
📌 Kapitel auf einen Blick
Worum geht es? Bisher waren alle Parameter bekannt. In der Realität sind Nachfrage, Rendite und Fahrzeit Zufallsgrößen. Dieses Kapitel zeigt vier Wege, damit umzugehen — und warum „einfach den Mittelwert einsetzen“ der schlechteste davon ist.
Voraussetzungen: Kapitel 5, Kapitel 11. Grundbegriffe der Statistik (Erwartungswert, Quantil).
Danach können Sie: Ein zweistufiges stochastisches Modell aufstellen, Monte-Carlo zur Risikomessung einsetzen, ein robustes Modell für den Worst Case formulieren, eine Wahrscheinlichkeitszusage („mit 95 % Sicherheit“) als lösbare Nebenbedingung schreiben — und erkennen, wann Rechnen mit Mittelwerten zulässig ist und wann es systematisch danebengeht.
Zeitbedarf: ca. 6,5 Stunden.
Programme:
Fluch_des_Durchschnitts.py
Monte_Carlo.py
Stochastische_Optimierung.py
Robuste_Optimierung.py
Chance_Constraints.pyNotebook: unsicherheit.ipynb
In Google Colab öffnen
12.1 In 5 Minuten gelöst
🚀 In 5 Minuten gelöst: Wie viele Ersatzteile bestellen?
Ein Betrieb bestellt vor der Wintersaison Ersatzteile — einmal, danach ist die Lieferkette bis zum Frühjahr zu. Der Bedarf schwankt, im Mittel werden 30 Stück gebraucht (Poisson-verteilt).
Fall Kosten je Stück Zu viel bestellt — Teil bleibt liegen und wird abgeschrieben 120 € Zu wenig bestellt — Maschine steht bis zur Nachlieferung 1 400 € Wie viele bestellen Sie?
import numpy as np KOSTEN_ZUVIEL, KOSTEN_ZUWENIG = 120.0, 1400.0 bedarf = np.random.default_rng(7).poisson(30, 200_000) # 200.000 Winter def kosten(menge): zuviel = np.maximum(menge - bedarf, 0) zuwenig = np.maximum(bedarf - menge, 0) return (KOSTEN_ZUVIEL * zuviel + KOSTEN_ZUWENIG * zuwenig).mean() kandidaten = np.arange(20, 61) werte = np.array([kosten(m) for m in kandidaten]) print("Optimum:", kandidaten[werte.argmin()], "Stueck,", f"{werte.min():,.0f} EUR") print("Mittlerer Bedarf 30:", f"{kosten(30):,.0f} EUR")Ausgabe:
Optimum: 38 Stueck, 1,270 EUR Mittlerer Bedarf 30: 3,306 EUR
Die naheliegende Antwort — „im Mittel brauchen wir 30, also bestellen wir 30“ — kostet das 2,6-Fache. 3 306 € statt 1 270 €, ein Aufschlag von 160 %.
Der Grund liegt in der Asymmetrie: Ein Teil zu viel kostet 120 €, ein Teil zu wenig kostet 1 400 €. Beides ist nicht gleich schlimm, also darf man auch nicht in der Mitte landen. Man muss sich bewusst auf die günstigere Seite des Fehlers stellen.
Wie weit? Dafür gibt es eine geschlossene Formel — das kritische Verhältnis:
q^{*} = \frac{c_{\text{zu wenig}}}{c_{\text{zu wenig}} + c_{\text{zu viel}}} = \frac{1400}{1400 + 120} = 0{,}921
🔤 Formel-Übersetzer
Mathematik Alltagssprache c_{\text{zu wenig}} „Was kostet mich ein Stück, das ich gebraucht hätte und nicht habe?“ c_{\text{zu viel}} „Was kostet mich ein Stück, das ich habe und nicht brauche?“ q^{*} = 0{,}921 „Bestelle so viel, dass der Bedarf in 92,1 % aller Winter gedeckt ist.“ Bestellmenge = F^{-1}(q^{*}) „Nimm das 92,1-%-Quantil der Bedarfsverteilung“ — hier 38 Stück. In einem Satz: Je teurer der Engpass gegenüber dem Überbestand, desto weiter über den Mittelwert hinaus muss man bestellen.
Das Quantil zu 0,921 liegt bei genau 38 Stück — dasselbe Ergebnis, das die Simulation oben gefunden hat. Zwei völlig verschiedene Wege, dieselbe Zahl.
🎯 Merksatz Die optimale Entscheidung unter Unsicherheit ist fast nie die Entscheidung, die für den Mittelwert optimal wäre. Sie hängt davon ab, welcher der beiden Fehler teurer ist — und verschiebt sich zu der Seite, auf der Irren billiger ist. Wer mit dem Mittelwert plant, hat diese Frage nie gestellt.
Dieses Muster heißt Newsvendor-Problem (deutsch: Zeitungsjungen-Problem) und taucht überall dort auf, wo einmal entschieden und danach beobachtet wird: Ersatzteile, Frischwaren, Saisonware, Personalreserve, Kraftwerksvorhaltung. Der Rest dieses Kapitels verallgemeinert es — von einer Zahl auf ganze Modelle, und von einer bekannten Verteilung auf den Fall, dass man nicht einmal die kennt.
12.2 Lernziele
Nach diesem Kapitel können Sie …
- … den Fluch des Durchschnitts (Flaw of Averages) an einem Beispiel erklären.
- … ein zweistufiges stochastisches Programm mit Recourse formulieren.
- … Monte-Carlo einsetzen, um Verteilungen statt Punktschätzungen zu bewerten.
- … ein robustes Modell mit Unsicherheitsmenge aufstellen und seinen Preis beziffern.
- … eine Wahrscheinlichkeitszusage („mit 95 % Sicherheit“) als Chance Constraint schreiben — analytisch als Kegelbedingung oder szenariobasiert mit Big-M — und den Preis eines Prozentpunkts Sicherheit beziffern.
- … begründen, welcher der vier Ansätze für ein gegebenes Problem passt.
- … das kritische Verhältnis eines Newsvendor-Problems aufstellen und daraus die optimale Bestellmenge als Quantil ablesen.
- … eine Terminzusage als Quantil formulieren statt als Mittelwert.
12.3 Der Fluch des Durchschnitts
Der naheliegende Umgang mit Unsicherheit lautet: „Wir setzen den Erwartungswert ein und rechnen deterministisch.“ Das ist fast immer falsch — und zwar nicht ungenau, sondern systematisch verzerrt.
✏️ Handrechnung 12.1: Warum der Mittelwert in die Irre führt
Ein Betreiber braucht Serverkapazität. Der Bedarf ist:
Szenario Wahrscheinlichkeit Bedarf Ruhig 50 % 100 Volatil 30 % 250 Crash 20 % 500 Erwartungswert: 0{,}5\cdot100 + 0{,}3\cdot250 + 0{,}2\cdot500 = 50+75+100 = 225.
Kosten: Vorabkauf 40 €/Einheit; fehlende Kapazität muss kurzfristig für 120 € zugekauft werden; überschüssige Kapazität kostet 5 € Verwaltung.
Naive Planung mit x = 225: * Ruhig (Bedarf 100): 125 zu viel → 40\cdot225 + 5\cdot125 = 9000 + 625 = 9625 * Volatil (250): 25 zu wenig → 9000 + 120\cdot25 = 9000 + 3000 = 12\,000 * Crash (500): 275 zu wenig → 9000 + 120\cdot275 = 9000 + 33\,000 = 42\,000 * Erwartete Kosten: 0{,}5\cdot9625 + 0{,}3\cdot12\,000 + 0{,}2\cdot42\,000 = 4812{,}5 + 3600 + 8400 = \mathbf{16\,812{,}50}
Stochastisch optimale Planung mit x = 250: * Ruhig: 150 zu viel → 10\,000 + 750 = 10\,750 * Volatil: passt genau → 10\,000 * Crash: 250 zu wenig → 10\,000 + 30\,000 = 40\,000 * Erwartete Kosten: 0{,}5\cdot10\,750 + 0{,}3\cdot10\,000 + 0{,}2\cdot40\,000 = 5375 + 3000 + 8000 = \mathbf{16\,375{,}00}
Die naive Planung kostet 437,50 € mehr — rund 2,7 % Aufschlag für das Einsetzen eines Mittelwerts, den es als Szenario gar nicht gibt. Beachten Sie: Der Aufschlag wirkt klein, weil bei nur drei Szenarien 225 und 250 nahe beieinanderliegen. Entscheidend ist die Richtung: Das Optimum liegt systematisch über dem Mittelwert, und zwar bei genau jenem Szenariowert, an dem die Kostenbalance kippt.
Dass x = 250 tatsächlich das Minimum ist und nicht nur eine plausible Vermutung, zeigt ein vollständiger Scan über alle möglichen Kapazitäten:
#!/usr/bin/env python3
# Fluch_des_Durchschnitts.py
"""
Kapitel Unsicherheit: Der Fluch des Durchschnitts (die Handrechnung dazu) als Scan ueber alle
moeglichen Kapazitaeten - zeigt, dass das Optimum nicht beim Mittelwert liegt.
"""
import numpy as np
SZENARIEN = np.array([100, 250, 500])
WAHRSCHEINLICHKEITEN = np.array([0.5, 0.3, 0.2])
PREIS_VORAB = 40.0
PREIS_ZUKAUF = 120.0
PREIS_VERWALTUNG = 5.0
ERWARTUNGSWERT = float(SZENARIEN @ WAHRSCHEINLICHKEITEN)
def erwartete_kosten(x):
kosten = np.where(
SZENARIEN >= x,
PREIS_VORAB * x + PREIS_ZUKAUF * (SZENARIEN - x),
PREIS_VORAB * x + PREIS_VERWALTUNG * (x - SZENARIEN),
)
return float(kosten @ WAHRSCHEINLICHKEITEN)
if __name__ == "__main__":
print("=" * 70)
print(" DER FLUCH DES DURCHSCHNITTS")
print("=" * 70)
print(f"Erwarteter Bedarf (Mittelwert): {ERWARTUNGSWERT:.1f}")
print(f"Kosten bei naiver Planung x={ERWARTUNGSWERT:.0f}: "
f"{erwartete_kosten(ERWARTUNGSWERT):,.2f} EUR\n")
x_werte = np.arange(0, 501, 1)
kosten_werte = np.array([erwartete_kosten(x) for x in x_werte])
x_optimal = x_werte[np.argmin(kosten_werte)]
kosten_optimal = kosten_werte.min()
print(f"Optimales x (durch Scan gefunden): {x_optimal}")
print(f"Kosten beim Optimum: {kosten_optimal:,.2f} EUR")
print(f"Ersparnis gegenueber naiver Planung: "
f"{erwartete_kosten(ERWARTUNGSWERT) - kosten_optimal:,.2f} EUR\n")
print("Kosten fuer ausgewaehlte x zum Vergleich:")
for x in [100, 225, 250, 300, 500]:
markierung = " <- Mittelwert" if x == 225 else (" <- Optimum" if x == 250 else "")
print(f" x={x:4d}: {erwartete_kosten(x):>12,.2f} EUR{markierung}")
print("\nDas Optimum liegt exakt auf einem Szenariowert (250 = 'Volatil'),")
print("nicht beim Mittelwert 225 - typisch fuer asymmetrische Kostenfunktionen.")Erwartete Ausgabe:
======================================================================
DER FLUCH DES DURCHSCHNITTS
======================================================================
Erwarteter Bedarf (Mittelwert): 225.0
Kosten bei naiver Planung x=225: 16,812.50 EUR
Optimales x (durch Scan gefunden): 250
Kosten beim Optimum: 16,375.00 EUR
Ersparnis gegenueber naiver Planung: 437.50 EUR
Kosten fuer ausgewaehlte x zum Vergleich:
x= 100: 19,000.00 EUR
x= 225: 16,812.50 EUR <- Mittelwert
x= 250: 16,375.00 EUR <- Optimum
x= 300: 17,375.00 EUR
x= 500: 21,375.00 EUR
Das Optimum liegt exakt auf einem Szenariowert (250 = 'Volatil'),
nicht beim Mittelwert 225 - typisch fuer asymmetrische Kostenfunktionen.
Kein Rechentrick, sondern brute-force über alle ganzzahligen x von 0 bis 500 — das Optimum landet exakt dort, wo es die Handrechnung vorhersagt.
Warum? Die Kostenfunktion ist asymmetrisch: Unterdeckung kostet 120, Überdeckung nur 5. Bei asymmetrischen Kosten liegt das Optimum nie beim Mittelwert, sondern verschoben in die „billigere“ Richtung. Formal ist das die Jensensche Ungleichung: Für konvexe Kostenfunktionen gilt \mathbb{E}[f(X)] \ge f(\mathbb{E}[X]) — der Erwartungswert der Kosten ist größer als die Kosten des Erwartungswerts.
🎯 Merksatz Der Durchschnittskunde kauft nie. Das Durchschnittsszenario tritt nie ein. Wer mit Mittelwerten plant, optimiert eine Welt, die es nicht gibt.
Die vier Ansätze im Überblick
| Monte-Carlo | Stochastische Programmierung | Robuste Optimierung | Chance Constraints | |
|---|---|---|---|---|
| Braucht | Verteilung | Szenarien mit Wahrscheinlichkeiten | nur eine Menge möglicher Werte | Verteilung oder Szenarien |
| Optimiert | nichts (bewertet nur) | Erwartungswert über Szenarien | Worst Case in der Menge | Kosten bei zugesicherter Quote |
| Typische Frage | „Wie riskant ist dieser Plan?“ | „Was ist im Mittel am besten?“ | „Was hält auch im schlimmsten Fall?“ | „Was hält in 95 % der Fälle?“ |
| Stärke | beliebige Kennzahlen, sehr flexibel | nutzt Wahrscheinlichkeiten voll aus | keine Verteilungsannahme nötig | die Zusage steht im Modell |
| Schwäche | keine Optimierung | Wahrscheinlichkeiten müssen stimmen | oft übervorsichtig, kostet Ertrag | Zusage gilt nur für die unterstellte Verteilung |
| Nachzulesen in | Abschnitt 12.4 | Abschnitt 12.5 | Abschnitt 12.6 | Abschnitt 12.7 |
Der Unterschied zwischen den letzten beiden ist der, an dem die meisten Diskussionen hängenbleiben. Die robuste Optimierung sagt: „Es darf nie schiefgehen“ — und zahlt dafür. Die Chance Constraint sagt: „Es darf in höchstens 5 % der Fälle schiefgehen“ — und macht damit verhandelbar, was die Sicherheit kosten darf.
12.4 Monte-Carlo-Simulation
Monte-Carlo heißt schlicht: Ziehe sehr viele Zufallsszenarien, werte für jedes den Plan aus, und betrachte die Verteilung der Ergebnisse statt eines einzigen Werts.
#!/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)Erwartete Ausgabe (gekürzt):
========================================================================================
MONTE-CARLO-BEWERTUNG (100,000 Szenarien)
========================================================================================
Bedarfsverteilung: Mittelwert 195.7 | Median 159.2 | 95%-Quantil 485.2 | Maximum 753.3
Der Median liegt deutlich unter dem Mittelwert - die Verteilung ist
rechtsschief. Genau hier fuehrt Planung mit dem Mittelwert in die Irre.
Kapazitaet | Erw. Kosten | Median | 95%-Quantil | Unterdeckung
----------------------------------------------------------------------------------------
150 | 13,169 | 7,104 | 46,220 | 57.8 %
200 | 12,989 | 8,345 | 42,220 | 24.1 %
225 | 13,475 | 9,432 | 40,220 | 17.8 %
250 | 14,085 | 10,546 | 38,220 | 15.6 %
300 | 15,391 | 12,791 | 34,220 | 14.8 %
350 | 16,746 | 15,033 | 30,220 | 13.6 %
400 | 18,214 | 17,266 | 26,220 | 11.1 %
----------------------------------------------------------------------------------------
Bester Kandidat: Kapazitaet 200 mit erwarteten Kosten 12,989 EUR
Feinsuche (Raster 100..500): Optimum bei Kapazitaet 180, Kosten 12,810 EUR
--- Fluch des Durchschnitts, gemessen ---
Planung mit Mittelwert (196): 12,934 EUR
Monte-Carlo-Optimum (180): 12,810 EUR
Mehrkosten der naiven Planung: 124 EUR (1.0 %)
========================================================================================
🎯 Eine wichtige Einschränkung — und was man daraus lernt
Hier kostet die naive Mittelwertplanung nur 1 %. In der Handrechnung zum Fluch des Durchschnitts waren es 2,7 %, in Lehrbüchern liest man von 20 % und mehr. Wie groß der Effekt ist, hängt von der Verteilung ab — und das ist selbst die Lektion.
Bei dieser Mischverteilung liegt zwischen dem Normalbetrieb (um 150) und den Lastspitzen (um 450) ein Bereich, in dem kaum Wahrscheinlichkeitsmasse liegt. Zusätzliche Kapazität zwischen 200 und 400 nützt daher wenig: Sie ist im Normalfall Leerlauf und reicht im Krisenfall trotzdem nicht. Deshalb ist die Kostenkurve dort flach, und die Wahl zwischen 180 und 196 macht kaum einen Unterschied.
Was Sie mitnehmen sollten: Die Frage „Wie teuer ist Mittelwertplanung?“ hat keine allgemeine Antwort — rechnen Sie es aus. Genau dafür ist Monte-Carlo da. Und beachten Sie die Spalte „Unterdeckung“: Bei Kapazität 200 wird in 24 % der Fälle nachgekauft. Ob das akzeptabel ist, entscheidet kein Optimierer, sondern ein Servicelevel-Ziel (Übung 9.5).
💡 Wann Monte-Carlo das richtige Werkzeug ist Immer dann, wenn Sie eine Kennzahl brauchen, die keine Formel hat: die Wahrscheinlichkeit, ein Servicelevel zu verfehlen; der erwartete Verlust in den schlechtesten 5 % der Fälle; die Verteilung der Projektdauer. Der Preis ist, dass Monte-Carlo nur bewertet — die Optimierung müssen Sie durch Rastersuche oder ein eigenes Modell ergänzen.
12.5 Zweistufige stochastische Programmierung
Das zweistufige Modell mit Recourse bildet ab, dass Entscheidungen zeitlich gestaffelt sind:
- Stufe 1 (here and now, \mathbf{x}): muss jetzt getroffen werden, bevor der Zufall sich zeigt — Serverkapazität kaufen, Produktion planen, Portfolio aufsetzen.
- Stufe 2 (wait and see, \mathbf{y}_s): Korrekturmaßnahmen, nachdem Szenario s eingetreten ist — Spot-Zukauf, Überstunden, Notverkauf.
\min_{\mathbf{x}} \quad \mathbf{c}^\top\mathbf{x} \;+\; \sum_{s=1}^S p_s\,\mathbf{q}_s^\top\mathbf{y}_s
\begin{aligned} \mathbf{A}\mathbf{x} &\le \mathbf{b} && \text{(Stufe-1-Restriktionen)}\\ \mathbf{T}_s\mathbf{x} + \mathbf{W}_s\mathbf{y}_s &\le \mathbf{h}_s \quad \forall s && \text{(Kopplung je Szenario)}\\ \mathbf{x} \ge \mathbf{0},\ \mathbf{y}_s &\ge \mathbf{0} \quad \forall s \end{aligned}
📐 Formel-Lesehilfe * \mathbf{c}^\top\mathbf{x} — die Kosten der Vorabentscheidung. Sie fallen immer an. * p_s — Wahrscheinlichkeit von Szenario s; \sum_s p_s = 1. * \mathbf{q}_s^\top\mathbf{y}_s — die Korrekturkosten in Szenario s. * \mathbf{T}_s\mathbf{x} + \mathbf{W}_s\mathbf{y}_s \le \mathbf{h}_s — die Kopplungsbedingung: Sie verbindet die Vorabentscheidung mit dem, was danach möglich ist. Genau hier steckt die Modellierungsarbeit.
Ohne Formel gesagt: „Entscheide jetzt so, dass die Summe aus heutigen Kosten und durchschnittlich zu erwartenden Nachbesserungskosten minimal wird.“
Der entscheidende Punkt: Es gibt ein \mathbf{x}, aber je Szenario ein eigenes \mathbf{y}_s. Das ist keine Rechentrickserei, sondern bildet die Realität ab: Man legt sich einmal fest und reagiert dann unterschiedlich.
#!/usr/bin/env python3
# Stochastische_Optimierung.py
"""
Kapitel Unsicherheit: Two-Stage Stochastic Programming mit CVXPY.
Eigenschaften:
* Die Analyse am Ende wird BERECHNET statt fest verdrahtet
* Vergleich gegen drei Alternativen: Mittelwert, Worst Case, perfekte Voraussicht
* Kennzahl EVPI (Wert perfekter Information) wird ausgewiesen
"""
import cvxpy as cp
import numpy as np
import pandas as pd
SZENARIEN = ["Ruhig", "Volatil", "Crash"]
WAHRSCHEINLICHKEIT = np.array([0.50, 0.30, 0.20])
BEDARF = np.array([100.0, 250.0, 500.0])
KOSTEN_VORAB = 40.0
KOSTEN_SPOT = 120.0
KOSTEN_LEERLAUF = 5.0
S = len(SZENARIEN)
def loese_stochastisch():
"""Zweistufiges Modell: eine Vorabentscheidung, szenarioabhaengige Korrektur."""
x = cp.Variable(nonneg=True, name="Basiskapazitaet") # Stufe 1
y_spot = cp.Variable(S, nonneg=True, name="Spot_Zukauf") # Stufe 2
y_leer = cp.Variable(S, nonneg=True, name="Leerlauf") # Stufe 2
# Kopplung: Basis + Zukauf - Leerlauf == Bedarf (je Szenario)
nebenbedingungen = [x + y_spot[s] - y_leer[s] == BEDARF[s] for s in range(S)]
erwartete_korrektur = sum(
WAHRSCHEINLICHKEIT[s] * (KOSTEN_SPOT * y_spot[s] + KOSTEN_LEERLAUF * y_leer[s])
for s in range(S))
problem = cp.Problem(cp.Minimize(KOSTEN_VORAB * x + erwartete_korrektur),
nebenbedingungen)
problem.solve()
return problem, x, y_spot, y_leer
def kosten_bei(kapazitaet):
"""Erwartete Gesamtkosten fuer eine fest vorgegebene Kapazitaet."""
unter = np.maximum(BEDARF - kapazitaet, 0.0)
ueber = np.maximum(kapazitaet - BEDARF, 0.0)
je_szenario = KOSTEN_VORAB * kapazitaet + KOSTEN_SPOT * unter + KOSTEN_LEERLAUF * ueber
return float(WAHRSCHEINLICHKEIT @ je_szenario)
if __name__ == "__main__":
problem, x, y_spot, y_leer = loese_stochastisch()
kapazitaet = float(x.value)
mittelwert = float(WAHRSCHEINLICHKEIT @ BEDARF)
print("=" * 82)
print(" STOCHASTISCHE TWO-STAGE OPTIMIERUNG (KAPAZITAETSPLANUNG)")
print("=" * 82)
print(f"Status: {problem.status}")
print(f"Erwarteter Bedarf (Mittelwert): {mittelwert:.1f} Einheiten")
print(f"Optimale Stufe-1-Kapazitaet x*: {kapazitaet:.1f} Einheiten")
print(f"Minimale erwartete Gesamtkosten: {problem.value:,.2f} EUR\n")
tabelle = pd.DataFrame({
"Szenario": SZENARIEN,
"Wahrsch.": [f"{p*100:.0f} %" for p in WAHRSCHEINLICHKEIT],
"Bedarf": BEDARF,
"Basis genutzt": [min(kapazitaet, b) for b in BEDARF],
"Spot-Zukauf": np.round(y_spot.value, 1),
"Leerlauf": np.round(y_leer.value, 1),
"Kosten (EUR)": [f"{KOSTEN_VORAB*kapazitaet + KOSTEN_SPOT*y_spot.value[s] + KOSTEN_LEERLAUF*y_leer.value[s]:,.0f}"
for s in range(S)],
})
print(tabelle.to_string(index=False))
# --- Vergleich mit Alternativstrategien (berechnet, nicht behauptet) --
print("\n" + "-" * 82)
print("Vergleich verschiedener Planungsstrategien:")
print(f"{'Strategie':<34} {'Kapazitaet':>11} {'Erw. Kosten':>14} {'Mehrkosten':>13}")
print("-" * 82)
optimal = problem.value
strategien = [
("Stochastisch optimal", kapazitaet),
("Naiv: Mittelwert einsetzen", mittelwert),
("Vorsichtig: Worst Case abdecken", float(BEDARF.max())),
("Optimistisch: Bestfall", float(BEDARF.min())),
]
for name, kap in strategien:
kosten = kosten_bei(kap)
print(f"{name:<34} {kap:>11.1f} {kosten:>14,.0f} "
f"{kosten - optimal:>+13,.0f}")
# --- EVPI: Was waere perfekte Voraussicht wert? ----------------------
# Bei perfekter Information wuerde man je Szenario genau den Bedarf kaufen.
kosten_perfekt = float(WAHRSCHEINLICHKEIT @ (KOSTEN_VORAB * BEDARF))
evpi = optimal - kosten_perfekt
print("-" * 82)
print(f"Kosten bei perfekter Voraussicht: {kosten_perfekt:>10,.0f} EUR")
print(f"Wert perfekter Information (EVPI): {evpi:>10,.0f} EUR "
f"({evpi/optimal*100:.1f} % der Kosten)")
print(" -> So viel duerfte eine perfekte Bedarfsprognose hoechstens kosten.")
# --- Automatische Interpretation --------------------------------------
print("-" * 82)
if kapazitaet > mittelwert + 1e-6:
print(f"Analyse: Der Solver waehlt {kapazitaet:.0f} Einheiten und damit MEHR als")
print(f"den Mittelwert ({mittelwert:.0f}), weil Unterdeckung ({KOSTEN_SPOT:.0f} EUR)")
print(f"deutlich teurer ist als Leerlauf ({KOSTEN_LEERLAUF:.0f} EUR).")
elif kapazitaet < mittelwert - 1e-6:
print(f"Analyse: Der Solver waehlt {kapazitaet:.0f} und damit WENIGER als den")
print(f"Mittelwert ({mittelwert:.0f}) - Leerlauf ist hier teurer als Zukauf.")
else:
print("Analyse: Kapazitaet entspricht dem Mittelwert (symmetrische Kosten).")
print("=" * 82)Erwartete Ausgabe:
==================================================================================
STOCHASTISCHE TWO-STAGE OPTIMIERUNG (KAPAZITAETSPLANUNG)
==================================================================================
Status: optimal
Erwarteter Bedarf (Mittelwert): 225.0 Einheiten
Optimale Stufe-1-Kapazitaet x*: 250.0 Einheiten
Minimale erwartete Gesamtkosten: 16,375.00 EUR
Szenario Wahrsch. Bedarf Basis genutzt Spot-Zukauf Leerlauf Kosten (EUR)
Ruhig 50 % 100.0 100.0 0.0 150.0 10,750
Volatil 30 % 250.0 250.0 0.0 0.0 10,000
Crash 20 % 500.0 250.0 250.0 0.0 40,000
----------------------------------------------------------------------------------
Vergleich verschiedener Planungsstrategien:
Strategie Kapazitaet Erw. Kosten Mehrkosten
----------------------------------------------------------------------------------
Stochastisch optimal 250.0 16,375 -0
Naiv: Mittelwert einsetzen 225.0 16,813 +437
Vorsichtig: Worst Case abdecken 500.0 21,375 +5,000
Optimistisch: Bestfall 100.0 19,000 +2,625
----------------------------------------------------------------------------------
Kosten bei perfekter Voraussicht: 9,000 EUR
Wert perfekter Information (EVPI): 7,375 EUR (45.0 % der Kosten)
-> So viel duerfte eine perfekte Bedarfsprognose hoechstens kosten.
----------------------------------------------------------------------------------
Analyse: Der Solver waehlt 250 Einheiten und damit MEHR als
den Mittelwert (225), weil Unterdeckung (120 EUR)
deutlich teurer ist als Leerlauf (5 EUR).
==================================================================================
Lesen Sie die Vergleichstabelle von unten nach oben. Die beiden extremen Haltungen sind die teuersten: Wer den Worst Case abdeckt (500 Einheiten), zahlt 5 000 € zu viel für Kapazität, die in 80 % der Fälle brachliegt. Wer optimistisch plant (100), zahlt 2 625 € Strafe für ständige Notzukäufe. Die naive Mittelwertplanung liegt dazwischen — aber eben auch nicht optimal.
Der EVPI von 7 375 € ist bemerkenswert hoch: 45 % der Gesamtkosten entstehen allein daraus, dass man die Zukunft nicht kennt. In so einem Fall lohnt sich Investition in bessere Prognosen tatsächlich — anders als in dem Beispiel aus Übung 9.4.
💡 Was ist der EVPI? Der Expected Value of Perfect Information beziffert, wie viel eine perfekte Prognose wert wäre: die Differenz zwischen den Kosten unter Unsicherheit und den Kosten bei vollständigem Wissen. Er ist eine Obergrenze für jedes Prognoseprojekt. Wenn der EVPI bei 12 000 € pro Jahr liegt, lohnt sich keine Prognosesoftware für 50 000 € — selbst wenn sie perfekt wäre. Diese Zahl bewahrt Projekte vor teuren Fehlinvestitionen.
12.6 Robuste Optimierung: gegen den Worst Case absichern
Wenn Wahrscheinlichkeiten unbekannt oder instabil sind — Marktcrashs, Lieferkettenabrisse, Pandemien —, hilft die robuste Optimierung. Sie fragt nicht nach dem Mittel, sondern nach dem Schlimmsten:
\min_{\mathbf{x}\in\mathcal{X}}\ \max_{\mathbf{u}\in\mathcal{U}}\ f(\mathbf{x},\mathbf{u})
📐 Formel-Lesehilfe Zwei ineinandergeschachtelte Optimierungen: * Das innere \max ist der „Gegner“: Er wählt aus der Unsicherheitsmenge \mathcal{U} die für Sie ungünstigsten Parameter. * Das äußere \min sind Sie: Sie wählen \mathbf{x} so, dass Sie selbst gegen diesen Gegner am besten dastehen.
Ohne Formel gesagt: „Entscheide so, dass du auch dann noch gut dastehst, wenn alles gegen dich läuft.“ Das ist Spieltheorie gegen die Natur.
Der Trick, der es lösbar macht. Ein Min-Max-Problem klingt unlösbar — man müsste ja unendlich viele Parameterkombinationen prüfen. Für einfache Unsicherheitsmengen lässt sich das innere Maximum aber geschlossen ausrechnen. Beispiel Box-Unsicherheit: Die Renditen liegen in Intervallen \mu_i \in [\hat\mu_i - \delta_i,\ \hat\mu_i + \delta_i]. Dann gilt für \mathbf{w} \ge 0:
\min_{\boldsymbol{\mu}\in\mathcal{U}} \boldsymbol{\mu}^\top\mathbf{w} = \sum_i (\hat\mu_i - \delta_i)w_i = \hat{\boldsymbol{\mu}}^\top\mathbf{w} - \boldsymbol{\delta}^\top\mathbf{w}
Das Min-Max-Problem wird damit zu einem gewöhnlichen Optimierungsproblem mit einem Abzugsterm. Genau das macht robuste Optimierung praktikabel.
#!/usr/bin/env python3
# Robuste_Optimierung.py
"""
Kapitel Unsicherheit: Robuste Portfolio-Optimierung.
Vergleicht drei Haltungen zur Unsicherheit:
(a) nominal - vertraut den Punktschaetzungen blind
(b) robust - sichert gegen Box-Unsicherheit ab (Worst Case)
(c) stochastisch - optimiert den Erwartungswert ueber Szenarien
und misst den "Preis der Robustheit": Wie viel Ertrag kostet die Absicherung
im Normalfall - und wie viel Verlust erspart sie im Ernstfall?
"""
import cvxpy as cp
import numpy as np
ASSETS = ["Aktien Welt", "Anleihen", "Rohstoffe", "Immobilien"]
MU_SCHAETZUNG = np.array([0.085, 0.030, 0.055, 0.060]) # Punktschaetzung
UNSICHERHEIT = np.array([0.040, 0.008, 0.045, 0.025]) # +/- delta je Titel
VOLA = np.array([0.17, 0.05, 0.22, 0.12])
KORR = np.array([
[1.00, -0.15, 0.35, 0.55],
[-0.15, 1.00, -0.05, 0.10],
[0.35, -0.05, 1.00, 0.25],
[0.55, 0.10, 0.25, 1.00],
])
SIGMA = np.diag(VOLA) @ KORR @ np.diag(VOLA)
LAMBDA = 4.0 # Risikoaversion
N = len(ASSETS)
def optimiere(mu_effektiv):
"""Standard-Mean-Variance mit vorgegebenem Renditevektor."""
w = cp.Variable(N, nonneg=True)
ziel = cp.Maximize(mu_effektiv @ w - 0.5 * LAMBDA * cp.quad_form(w, SIGMA))
problem = cp.Problem(ziel, [cp.sum(w) == 1])
problem.solve()
return w.value
def kennzahlen(w, mu):
ertrag = float(mu @ w)
risiko = float(np.sqrt(w @ SIGMA @ w))
return ertrag, risiko
if __name__ == "__main__":
print("=" * 88)
print(" ROBUSTE vs. NOMINALE PORTFOLIO-OPTIMIERUNG")
print("=" * 88)
print(f"{'Asset':<14} {'Erw. Rendite':>14} {'Unsicherheit':>14} "
f"{'Worst Case':>12} {'Volatilitaet':>13}")
print("-" * 88)
for i, name in enumerate(ASSETS):
print(f"{name:<14} {MU_SCHAETZUNG[i]*100:>13.1f} % "
f"{'+/- ' + format(UNSICHERHEIT[i]*100, '.1f') + ' %':>14} "
f"{(MU_SCHAETZUNG[i]-UNSICHERHEIT[i])*100:>11.1f} % "
f"{VOLA[i]*100:>12.1f} %")
# (a) nominal: vertraut den Schaetzungen
w_nominal = optimiere(MU_SCHAETZUNG)
# (b) robust: rechnet mit dem Worst Case der Box-Unsicherheitsmenge
w_robust = optimiere(MU_SCHAETZUNG - UNSICHERHEIT)
print("\n" + "-" * 88)
print(f"{'':<14} {'nominal':>22} {'robust':>22}")
print("-" * 88)
for i, name in enumerate(ASSETS):
print(f"{name:<14} {w_nominal[i]*100:>21.1f} % {w_robust[i]*100:>21.1f} %")
# --- Bewertung in beiden Welten --------------------------------------
mu_worst = MU_SCHAETZUNG - UNSICHERHEIT
e_nom_gut, r_nom = kennzahlen(w_nominal, MU_SCHAETZUNG)
e_rob_gut, r_rob = kennzahlen(w_robust, MU_SCHAETZUNG)
e_nom_schlecht, _ = kennzahlen(w_nominal, mu_worst)
e_rob_schlecht, _ = kennzahlen(w_robust, mu_worst)
print("\n" + "-" * 88)
print(f"{'Bewertung':<34} {'nominales Portfolio':>22} {'robustes Portfolio':>22}")
print("-" * 88)
print(f"{'Ertrag, wenn Schaetzung stimmt':<34} {e_nom_gut*100:>21.2f} % "
f"{e_rob_gut*100:>21.2f} %")
print(f"{'Ertrag im Worst Case':<34} {e_nom_schlecht*100:>21.2f} % "
f"{e_rob_schlecht*100:>21.2f} %")
print(f"{'Volatilitaet':<34} {r_nom*100:>21.2f} % {r_rob*100:>21.2f} %")
print("\n" + "-" * 88)
print(f"Preis der Robustheit (Ertragsverzicht im Normalfall): "
f"{(e_nom_gut - e_rob_gut)*100:+.2f} Prozentpunkte")
print(f"Nutzen der Robustheit (Vorteil im Worst Case): "
f"{(e_rob_schlecht - e_nom_schlecht)*100:+.2f} Prozentpunkte")
verhaeltnis = ((e_rob_schlecht - e_nom_schlecht)
/ max(e_nom_gut - e_rob_gut, 1e-9))
print(f"Verhaeltnis Nutzen/Preis: {verhaeltnis:.2f}")
print(" -> Werte > 1 bedeuten: Die Absicherung bringt im Ernstfall mehr,")
print(" als sie im Normalfall kostet.")
print("=" * 88)⚠️ Der Preis der Robustheit Robuste Modelle sind konservativ. Wer gegen den absoluten Worst Case absichert, zahlt im Normalfall drauf — oft erheblich. Drei Gegenmittel: 1. Unsicherheitsmenge realistisch wählen. Nicht „alles kann passieren“, sondern „Abweichungen bis zu einer Standardabweichung“. 2. Budgeted Uncertainty (Bertsimas/Sim): Man nimmt an, dass höchstens \Gamma von n Parametern gleichzeitig ihren Worst Case annehmen. \Gamma steuert die Vorsicht stufenlos. 3. Nutzen und Preis immer beziffern — genau wie im Programm oben. Ohne diese zwei Zahlen ist die Diskussion „robust oder nicht“ Glaubenssache.
12.7 Wahrscheinlichkeitsbeschränkungen: „mit 95 % Sicherheit“
Die drei bisherigen Ansätze beantworten Fragen, die im Sitzungssaal selten so gestellt werden. „Was ist im Mittel am besten?“ hört ein Vorstand als Ausflucht; „was hält auch im schlimmsten Fall?“ als Einladung, zu viel Geld auszugeben. Gefragt wird fast immer nach einer Wahrscheinlichkeit: Mit welcher Sicherheit hält der Plan?
Die Wahrscheinlichkeitsbeschränkung schreibt genau das ins Modell:
\mathbb{P}\!\left(\mathbf{a}^\top\mathbf{x} \ge b\right) \ge 1 - \alpha
📐 Formel-Lesehilfe * \mathbf{a} — der unsichere Vektor: Verfügbarkeiten, Erträge, Fahrzeiten. Er ist keine Zahl, sondern eine Zufallsgröße. * \mathbf{x} — Ihre Entscheidung. Sie fällt, bevor \mathbf{a} sich zeigt. * \alpha — die Ausfallwahrscheinlichkeit, die Sie akzeptieren. \alpha = 0{,}05 heißt „in höchstens 5 % der Fälle darf es schiefgehen“.
Ohne Formel gesagt: „Plane so, dass die Zusage in 95 von 100 Fällen hält — und sag mir, was das kostet.“
Das Problem daran. So aufgeschrieben ist die Bedingung nicht lösbar: Ein Solver kann nichts mit einem Wahrscheinlichkeitsoperator anfangen. Es braucht einen Weg, sie in etwas zu übersetzen, das ein Optimierer versteht. Es gibt zwei, und sie unterscheiden sich in dem, was sie über die Verteilung voraussetzen.
Weg 1: Normalverteilung — die Bedingung wird ein Kegel
Ist \mathbf{a} \sim \mathcal{N}(\hat{\mathbf{a}}, \boldsymbol{\Sigma}), dann ist \mathbf{a}^\top\mathbf{x} für festes \mathbf{x} selbst normalverteilt, mit Erwartungswert \hat{\mathbf{a}}^\top\mathbf{x} und Standardabweichung \sqrt{\mathbf{x}^\top\boldsymbol{\Sigma}\mathbf{x}}. Damit lässt sich die Wahrscheinlichkeit durch ein Quantil ersetzen:
\hat{\mathbf{a}}^\top\mathbf{x} \;-\; z_{1-\alpha}\,\lVert \mathbf{L}^\top\mathbf{x}\rVert_2 \;\ge\; b \qquad\text{mit}\qquad \boldsymbol{\Sigma} = \mathbf{L}\mathbf{L}^\top,\quad z_{1-\alpha} = \Phi^{-1}(1-\alpha)
Das ist eine Second-Order-Cone-Bedingung — dieselbe Bauform, die Kapitel 11 eingeführt hat, und CVXPY löst sie ohne Umstände.
🎯 Warum eine Norm und keine Summe Der Sicherheitszuschlag \lVert\mathbf{L}^\top\mathbf{x}\rVert_2 ist keine Summe von Einzelzuschlägen je Anlage. Genau darin steckt der Nutzen der Mischung: Zwei gegenläufige Quellen schwanken gemeinsam weniger als jede für sich, und die Norm rechnet das automatisch mit. Ein fester Aufschlag je Anlage könnte das nicht — er wäre linear und würde Diversifikation nicht belohnen.
Weg 2: Szenarien — Big-M ohne Verteilungsannahme
Liegen statt einer Verteilung nur S beobachtete Szenarien vor, bekommt jedes eine Binärvariable z_s, die sagt, ob es verletzt werden darf:
\begin{aligned} \mathbf{a}_s^\top\mathbf{x} &\ge b - M z_s && \forall s\\ \sum_{s} z_s &\le \alpha S\\ z_s &\in \{0,1\} \end{aligned}
Aus dem Kegelproblem wird ein MILP mit S Binärvariablen. Für M gilt, was Kapitel 6 über die Big-M-Falle sagt: so klein wie möglich. Hier ist M = b die kleinste gültige Schranke, denn \mathbf{a}_s^\top\mathbf{x} \ge 0 — größer kann eine Verletzung gar nicht ausfallen.
Der Fall: ein Kraftwerkspark mit 500 MW Zusage
#!/usr/bin/env python3
# Chance_Constraints.py
"""
Kapitel Unsicherheit: Wahrscheinlichkeitsbeschraenkungen.
Das Management fragt selten "was ist im Mittel am besten?", sondern "mit
welcher Sicherheit haelt der Plan?". Genau das formuliert eine Chance
Constraint: P(Versorgung >= Bedarf) >= 1 - alpha.
Gezeigt werden beide Wege dorthin, an derselben Instanz:
(a) analytisch - unter Normalverteilungsannahme wird daraus eine
Second-Order-Cone-Bedingung, loesbar mit CVXPY
(b) szenariobasiert - Big-M mit Binaervariablen, fuer beliebige
empirische Verteilungen
Und die beiden Messungen, auf die es ankommt: Was kostet ein Prozentpunkt
Versorgungssicherheit - und haelt die Zusage auch dann, wenn die Verteilung
nicht normal ist?
Solver: CLARABEL (Kegel) und SciPy/HiGHS (gemischt-ganzzahlig). Weder ortools
noch ein direkter highspy-Import, damit alles in einem Prozess laeuft.
"""
import cvxpy as cp
import numpy as np
from scipy.stats import norm
# --- Der Kraftwerkspark ----------------------------------------------------
# Die Verfuegbarkeit ist der Anteil, den eine installierte MW im Mittel
# wirklich liefert: bei Wind und Sonne klein und stark schwankend, bei Gas und
# Biomasse gross und stabil. Die Ausbaugrenze ist der Standort - Flaeche,
# Genehmigung, Brennstoffversorgung.
TECHNIK = ["Gaskraftwerk", "Windpark", "Solarpark", "Biomasse"]
KOSTEN = np.array([65_000.0, 24_000.0, 12_000.0, 60_000.0]) # EUR je MW und Jahr
VERFUEGBAR = np.array([0.92, 0.35, 0.18, 0.85])
STREUUNG = np.array([0.05, 0.16, 0.10, 0.04])
GRENZE = np.array([400.0, 250.0, 600.0, 350.0]) # MW
# Wind und Sonne sind leicht gegenlaeufig: Ein Tiefdruckgebiet bringt Wind und
# Wolken zugleich. Genau diese Korrelation macht die Bedingung zu einem Kegel
# und nicht zu einer Summe unabhaengiger Einzelzuschlaege.
KORRELATION = np.array([
[1.00, 0.00, 0.00, 0.05],
[0.00, 1.00, -0.25, 0.00],
[0.00, -0.25, 1.00, 0.00],
[0.05, 0.00, 0.00, 1.00],
])
SIGMA = np.diag(STREUUNG) @ KORRELATION @ np.diag(STREUUNG)
WURZEL = np.linalg.cholesky(SIGMA) # L mit L @ L.T == SIGMA
BEDARF = 500.0 # MW, die gesichert bereitstehen muessen
N = len(TECHNIK)
# Die Kaeltewelle mit Dunkelflaute: selten, aber sie trifft alles zugleich.
# Wind und Sonne brechen fast vollstaendig weg - und, das ist der Punkt, das
# Gaskraftwerk liefert ebenfalls weniger, weil bei Frost der Netzdruck faellt.
# Eine Kovarianzmatrix mit Korrelationen um null kann das nicht ausdruecken.
P_KAELTEWELLE = 0.08
EINBRUCH = np.array([0.72, 0.10, 0.15, 0.88])
def mittelwertplan():
"""Plant mit den Erwartungswerten - ignoriert die Streuung vollstaendig."""
x = cp.Variable(N, nonneg=True)
problem = cp.Problem(cp.Minimize(KOSTEN @ x),
[VERFUEGBAR @ x >= BEDARF, x <= GRENZE])
problem.solve(solver=cp.CLARABEL)
return problem.value, x.value
def chance_constraint_analytisch(alpha):
"""
P(a^T x >= BEDARF) >= 1 - alpha unter a ~ N(VERFUEGBAR, SIGMA).
Aequivalent zu: VERFUEGBAR^T x - z * ||L^T x||_2 >= BEDARF
mit z = Phi^-1(1 - alpha). Das ist eine Second-Order-Cone-Bedingung: Der
Sicherheitszuschlag ist eine Norm ueber x, kein fester Aufschlag je Anlage.
Deshalb belohnt sie Mischung - zwei gegenlaeufige Quellen schwanken
gemeinsam weniger als jede fuer sich.
"""
x = cp.Variable(N, nonneg=True)
z = norm.ppf(1 - alpha)
bedingung = VERFUEGBAR @ x - z * cp.norm(WURZEL.T @ x, 2) >= BEDARF
problem = cp.Problem(cp.Minimize(KOSTEN @ x), [bedingung, x <= GRENZE])
problem.solve(solver=cp.CLARABEL)
return problem.value, x.value
def chance_constraint_szenarien(a, alpha):
"""
Dieselbe Zusage ohne Verteilungsannahme: Fuer jedes Szenario s sagt eine
Binaervariable z_s, ob es verletzt werden darf.
a_s^T x >= BEDARF - M * z_s fuer alle s
sum_s z_s <= alpha * S
M = BEDARF ist die kleinstmoegliche gueltige Schranke, denn a_s^T x >= 0:
Groesser kann eine Verletzung gar nicht ausfallen. Ein unnoetig grosses M
wuerde die LP-Relaxierung aufweichen und die Suche verlangsamen - die
Big-M-Falle aus dem Kapitel Gemischt-ganzzahlige Optimierung.
"""
S = len(a)
x = cp.Variable(N, nonneg=True)
z = cp.Variable(S, boolean=True)
problem = cp.Problem(
cp.Minimize(KOSTEN @ x),
[a @ x >= BEDARF - BEDARF * z, cp.sum(z) <= alpha * S, x <= GRENZE])
problem.solve(solver=cp.SCIPY)
return problem.value, x.value
def ziehe_wetter(anzahl, seed):
"""Mischverteilung: normales Wetter, mit P_KAELTEWELLE ein Einbruch."""
rng = np.random.default_rng(seed)
a = rng.multivariate_normal(VERFUEGBAR, SIGMA, size=anzahl)
getroffen = rng.random(anzahl) < P_KAELTEWELLE
a[getroffen] *= EINBRUCH
return np.clip(a, 0.0, 1.0)
def sicherheit(x, a):
"""Anteil der Szenarien, in denen der Plan den Bedarf deckt."""
return float(np.mean(a @ x >= BEDARF))
KOPF = (f"{'Plan':<20} {'Kosten (EUR)':>13} {'Gas':>4} {'Wind':>4} "
f"{'Sol':>4} {'Bio':>4} {'Normalwelt':>11} {'echte Welt':>11}")
def zeile(name, kosten, x, in_normal, in_echt):
mix = " ".join(f"{w:4.0f}" for w in x)
return (f"{name:<20} {kosten:>13,.0f} {mix} "
f"{in_normal * 100:>9.2f} % {in_echt * 100:>9.2f} %")
if __name__ == "__main__":
print("=" * 78)
print(" WAHRSCHEINLICHKEITSBESCHRAENKUNGEN IM KRAFTWERKSPARK")
print("=" * 78)
print(f"Gesichert bereitzustellen: {BEDARF:.0f} MW\n")
print(f"{'Technologie':<14} {'EUR/MW':>8} {'verfuegbar':>11} {'Streuung':>9} "
f"{'Grenze':>8} {'EUR je erw. MW':>15}")
print("-" * 78)
for i, name in enumerate(TECHNIK):
print(f"{name:<14} {KOSTEN[i]:>8,.0f} {VERFUEGBAR[i]:>10.2f} "
f"{STREUUNG[i]:>8.2f} {GRENZE[i]:>7.0f} "
f"{KOSTEN[i] / VERFUEGBAR[i]:>15,.0f}")
print(f"\nVollausbau liefert im Mittel {VERFUEGBAR @ GRENZE:.0f} MW, "
f"in der Kaeltewelle {(VERFUEGBAR * EINBRUCH) @ GRENZE:.0f} MW.")
# Zwei grosse, unabhaengige Testmengen. An ihnen wird JEDER Plan gemessen -
# einmal in der unterstellten Normalwelt, einmal in der echten Verteilung.
echt = ziehe_wetter(200_000, seed=771)
normalwelt = np.random.default_rng(4711).multivariate_normal(
VERFUEGBAR, SIGMA, size=200_000)
print("\n" + "=" * 78)
print(" (1) Mittelwertplan und Chance Constraints im Vergleich")
print("=" * 78)
print(KOPF)
print("-" * 78)
k_mittel, x_mittel = mittelwertplan()
stufen = [("Mittelwert", k_mittel, sicherheit(x_mittel, normalwelt))]
print(zeile("Mittelwert", k_mittel, x_mittel,
sicherheit(x_mittel, normalwelt), sicherheit(x_mittel, echt)))
plaene = {}
for alpha in (0.20, 0.10, 0.05, 0.01):
kosten, x = chance_constraint_analytisch(alpha)
plaene[alpha] = (kosten, x)
quote_normal = sicherheit(x, normalwelt)
stufen.append((f"Zusage {(1 - alpha) * 100:.0f} %", kosten, quote_normal))
print(zeile(f"Zusage {(1 - alpha) * 100:.0f} %", kosten, x,
quote_normal, sicherheit(x, echt)))
print("\n In der Normalwelt trifft jede Zusage ihren Wert - das Verfahren")
print(" rechnet richtig. Die Spalte 'echte Welt' kommt in Teil (3).")
print("\n" + "=" * 78)
print(" (2) Was kostet ein Prozentpunkt Versorgungssicherheit?")
print("=" * 78)
print(f"{'von -> nach':<26} {'Prozentpunkte':>13} {'Mehrkosten':>13} "
f"{'EUR je Punkt':>13}")
print("-" * 78)
for (n1, k1, q1), (n2, k2, q2) in zip(stufen, stufen[1:]):
d_punkte = (q2 - q1) * 100
d_kosten = k2 - k1
print(f"{n1 + ' -> ' + n2:<26} {d_punkte:>13.2f} {d_kosten:>13,.0f} "
f"{d_kosten / d_punkte:>13,.0f}")
erst = (stufen[1][1] - stufen[0][1]) / ((stufen[1][2] - stufen[0][2]) * 100)
letzt = (stufen[-1][1] - stufen[-2][1]) / ((stufen[-1][2] - stufen[-2][2]) * 100)
print(f"\n Der letzte Prozentpunkt kostet das {letzt / erst:.1f}-fache des ersten.")
print(" Sicherheit ist konvex bepreist - genau deshalb muss jemand")
print(" entscheiden, wie viel davon das Unternehmen kaufen will.")
print("\n" + "=" * 78)
print(" (3) Und wenn die Verteilung nicht normal ist?")
print("=" * 78)
k_soc, x_soc = plaene[0.05]
print(f"Die echte Welt kennt die Kaeltewelle mit Dunkelflaute: in "
f"{P_KAELTEWELLE * 100:.0f} % der Faelle liefern")
print(f"Wind {EINBRUCH[1] * 100:.0f} %, Sonne {EINBRUCH[2] * 100:.0f} % "
f"und - entscheidend - auch das Gaskraftwerk nur "
f"{EINBRUCH[0] * 100:.0f} %")
print("des Ueblichen. Alles bricht gleichzeitig ein.\n")
a_bau = ziehe_wetter(400, seed=20260908)
k_sz, x_sz = chance_constraint_szenarien(a_bau, 0.05)
print(KOPF)
print("-" * 78)
print(zeile("SOC, Zusage 95 %", k_soc, x_soc,
sicherheit(x_soc, normalwelt), sicherheit(x_soc, echt)))
print(zeile("Szenarien, 95 %", k_sz, x_sz,
sicherheit(x_sz, normalwelt), sicherheit(x_sz, echt)))
print(f"\n Der SOC-Plan verspricht 95 % und haelt "
f"{sicherheit(x_soc, echt) * 100:.1f} %. Nicht das Verfahren ist")
print(f" falsch, sondern die Annahme: In der Normalwelt liefert derselbe")
print(f" Plan {sicherheit(x_soc, normalwelt) * 100:.1f} %.")
print(f"\n Der Szenarioplan ({len(a_bau)} Szenarien, {len(a_bau)} "
f"Binaervariablen) kommt auf")
print(f" {sicherheit(x_sz, echt) * 100:.1f} % - und kostet dafuer "
f"{(k_sz / k_soc - 1) * 100:+.1f} %. Er kauft keine Windkraft mehr:")
print(" Was im Ernstfall ausfaellt, hilft der Zusage nicht.")
print("=" * 78)Ausgabe:
==============================================================================
WAHRSCHEINLICHKEITSBESCHRAENKUNGEN IM KRAFTWERKSPARK
==============================================================================
Gesichert bereitzustellen: 500 MW
Technologie EUR/MW verfuegbar Streuung Grenze EUR je erw. MW
------------------------------------------------------------------------------
Gaskraftwerk 65,000 0.92 0.05 400 70,652
Windpark 24,000 0.35 0.16 250 68,571
Solarpark 12,000 0.18 0.10 600 66,667
Biomasse 60,000 0.85 0.04 350 70,588
Vollausbau liefert im Mittel 861 MW, in der Kaeltewelle 552 MW.
==============================================================================
(1) Mittelwertplan und Chance Constraints im Vergleich
==============================================================================
Plan Kosten (EUR) Gas Wind Sol Bio Normalwelt echte Welt
------------------------------------------------------------------------------
Mittelwert 34,694,565 8 250 600 350 50.08 % 46.18 %
Zusage 80 % 36,382,277 226 25 44 343 80.06 % 73.61 %
Zusage 90 % 36,994,777 233 20 35 349 89.99 % 82.82 %
Zusage 95 % 37,514,307 241 19 31 350 94.97 % 87.44 %
Zusage 99 % 38,529,698 258 18 28 350 98.97 % 91.05 %
In der Normalwelt trifft jede Zusage ihren Wert - das Verfahren
rechnet richtig. Die Spalte 'echte Welt' kommt in Teil (3).
==============================================================================
(2) Was kostet ein Prozentpunkt Versorgungssicherheit?
==============================================================================
von -> nach Prozentpunkte Mehrkosten EUR je Punkt
------------------------------------------------------------------------------
Mittelwert -> Zusage 80 % 29.98 1,687,712 56,289
Zusage 80 % -> Zusage 90 % 9.93 612,500 61,685
Zusage 90 % -> Zusage 95 % 4.98 519,530 104,365
Zusage 95 % -> Zusage 99 % 4.00 1,015,390 253,848
Der letzte Prozentpunkt kostet das 4.5-fache des ersten.
Sicherheit ist konvex bepreist - genau deshalb muss jemand
entscheiden, wie viel davon das Unternehmen kaufen will.
==============================================================================
(3) Und wenn die Verteilung nicht normal ist?
==============================================================================
Die echte Welt kennt die Kaeltewelle mit Dunkelflaute: in 8 % der Faelle liefern
Wind 10 %, Sonne 15 % und - entscheidend - auch das Gaskraftwerk nur 72 %
des Ueblichen. Alles bricht gleichzeitig ein.
Plan Kosten (EUR) Gas Wind Sol Bio Normalwelt echte Welt
------------------------------------------------------------------------------
SOC, Zusage 95 % 37,514,307 241 19 31 350 94.97 % 87.44 %
Szenarien, 95 % 43,760,709 350 0 0 350 100.00 % 94.84 %
Der SOC-Plan verspricht 95 % und haelt 87.4 %. Nicht das Verfahren ist
falsch, sondern die Annahme: In der Normalwelt liefert derselbe
Plan 95.0 %.
Der Szenarioplan (400 Szenarien, 400 Binaervariablen) kommt auf
94.8 % - und kostet dafuer +16.7 %. Er kauft keine Windkraft mehr:
Was im Ernstfall ausfaellt, hilft der Zusage nicht.
==============================================================================
Drei Befunde, die man dem Plan nicht ansieht.
Erstens: Der Mittelwertplan fällt in der Hälfte aller Fälle aus. Das ist kein Fehler, sondern die Definition des Erwartungswerts — und dieselbe Falle wie in Abschnitt 12.3, nur teurer. Er kauft die billigsten Quellen je erwarteter Megawattstunde und maximiert Wind und Sonne bis an die Ausbaugrenze. Sobald die Zusage im Modell steht, dreht sich das Bild: Gas und Biomasse tragen den Plan, Wind und Sonne schrumpfen auf einen Rest.
Zweitens: Sicherheit ist konvex bepreist. Die ersten dreißig Prozentpunkte kosten rund 56 000 € pro Punkt, die letzten vier rund 254 000 € — das 4,5-fache. Das ist die Zahl, die in die Vorlage gehört: Nicht „was kostet Versorgungssicherheit“, sondern „was kostet der nächste Prozentpunkt“. Wer 99,9 % fordert, ohne diese Kurve gesehen zu haben, fordert ins Blaue.
Drittens — und das ist der unangenehme Befund: Die Zusage gilt nur für die unterstellte Verteilung. Der Plan verspricht 95 % und hält 87,4 %. Nicht weil das Verfahren falsch rechnet: In einer reinen Normalwelt liefert derselbe Plan exakt seine 95,0 %. Sondern weil die Normalverteilung eine Kältewelle mit Dunkelflaute nicht kennt. In der bricht alles zugleich ein — auch das Gaskraftwerk, weil bei Frost der Netzdruck fällt. Eine Kovarianzmatrix mit Korrelationen um null kann so etwas nicht ausdrücken.
Der szenariobasierte Weg kennt diese Fälle, weil sie in den Daten stehen. Er kommt auf 94,8 %, kostet dafür 16,7 % mehr — und kauft keine Windkraft mehr. Die Logik dahinter ist hart und richtig: Was im Ernstfall ausfällt, hilft der Zusage nicht.
⚠️ Die Szenariomethode überanpasst — und mehr Szenarien helfen nicht verlässlich Bei S Szenarien und \alpha = 5\,\% darf das Modell genau 0{,}05 \cdot S davon ignorieren — und es sucht sich die teuersten aus. In der Stichprobe trifft es seine Quote dadurch immer; außerhalb streut sie.
Gemessen an zwölf Läufen dieses Modells (je vier Ziehungen bei S = 200, 400, 800, geprüft an 200 000 unabhängigen Szenarien): Die tatsächliche Quote lag zwischen 93,1 % und 96,7 %. Die Spanne wurde von S = 200 auf 400 enger (3,5 → 1,2 Prozentpunkte), bei S = 800 aber wieder breiter (3,1) — die Zahl der Szenarien allein schließt die Lücke also nicht.
Konsequenz für die Praxis: Eine mit Szenarien erkaufte Zusage immer an einer zurückgehaltenen Stichprobe nachmessen, so wie es das Programm oben tut. Wer nur die Trefferquote in den Baudaten berichtet, berichtet eine Tautologie — dieselbe Falle wie beim Backtest in Kapitel 21.
🎯 Merksatz Die robuste Optimierung fragt „was, wenn alles schiefgeht?“, die Chance Constraint „wie oft darf es schiefgehen?“. Nur die zweite Frage hat eine Antwort, über die man verhandeln kann — und nur sie zwingt dazu, den Preis eines Prozentpunkts Sicherheit zu beziffern.
12.8 Übungsaufgaben
Lösungen: Abschnitt A.12.
Aufgabe 12.1 ⭐ — Fluch des Durchschnitts erkennen. Ein Bauunternehmer plant mit der durchschnittlichen Bauzeit von 8 Monaten. Warum ist die erwartete Fertigstellung trotzdem später als 8 Monate, wenn Verzögerungen wahrscheinlicher sind als Beschleunigungen? Nennen Sie zwei weitere Alltagsbeispiele.
Aufgabe 12.2 ⭐ — Ansatz wählen. Welcher der vier Ansätze passt? Begründen Sie: (a) Wie viele Notstromaggregate für ein Krankenhaus? (b) Wie viel Weizen einkaufen bei bekannter Preisverteilung? (c) Wie hoch ein Deich gebaut werden muss? (d) Wie viele Saisonkräfte einstellen bei bekannten Nachfrageszenarien? (e) Wie viel Speicherkapazität, damit ein Gasnetz an 99 % aller Wintertage hält?
Aufgabe 12.3 ⭐⭐ — Zweistufiges Modell rechnen. Wiederholen Sie die Handrechnung Warum der Mittelwert in die Irre führt mit geänderten Kosten: Spot 60 € statt 120 €. (a) Wie ändert sich die optimale Kapazität? (b) Erklären Sie die Richtung der Änderung. (c) Bei welchem Spot-Preis wäre die Mittelwert-Planung optimal?
Aufgabe 12.4 ⭐⭐ — EVPI interpretieren. Ihr Modell liefert EVPI = 8 400 € pro Jahr. Ein Anbieter verlangt 15 000 € jährlich für eine Prognoselösung, die „80 % Treffsicherheit“ verspricht. Wie argumentieren Sie?
Aufgabe 12.5 ⭐⭐⭐ — Monte-Carlo erweitern. Erweitern Sie Monte_Carlo.py: (a) Fügen Sie ein Servicelevel-Kriterium hinzu: „Die Unterdeckung darf höchstens in 5 % der Fälle auftreten.“ Welche Kapazität ist dafür nötig, und was kostet sie zusätzlich? (b) Berechnen Sie den CVaR der Kosten (Mittelwert der schlechtesten 5 %). (c) Zeichnen Sie ein Histogramm der Kosten für drei Kapazitäten.
Aufgabe 12.6 ⭐⭐ — Den Preis der Zusage selbst bestimmen. Chance_Constraints.py beziffert die Kosten für 80, 90, 95 und 99 %. (a) Ergänzen Sie die Stufen 99,5 % und 99,9 % und setzen Sie die Kosten je Prozentpunkt fort. Was passiert mit dem Anlagenmix? (b) Drosseln Sie die Ausbaugrenze der Biomasse von 350 auf 200 MW. Das szenariobasierte Modell meldet dann bei 95 % infeasible, das analytische dagegen optimal. Erklären Sie den Unterschied. Welchem der beiden würden Sie glauben? (c) Ein Vorstand fordert „99,99 % Versorgungssicherheit“. Formulieren Sie in drei Sätzen, was Sie ihm mit dieser Kurve antworten.
Aufgabe 12.7 ⭐⭐⭐ — Budgeted Uncertainty. Erweitern Sie Robuste_Optimierung.py um den Ansatz von Bertsimas/Sim: Höchstens \Gamma Titel nehmen gleichzeitig ihren Worst Case an. Variieren Sie \Gamma \in \{0, 1, 2, 3, 4\} und stellen Sie den Verlauf von Ertrag und Absicherung dar. (Hinweis: Bei \Gamma ganzzahlig genügt es, die \Gamma größten \delta_i w_i abzuziehen — das lässt sich in CVXPY mit cp.sum_largest formulieren.)
12.9 Finde den Denkfehler
🐛 Finde den Denkfehler 9.1: Warum jedes Projekt zu spät fertig wird
Ein Projektleiter plant eine Umbaumaßnahme. Fünf Gewerke arbeiten parallel und unabhängig voneinander; fertig ist der Umbau, wenn das letzte fertig ist. Für jedes Gewerk liegt eine Schätzung vor: mindestens 6 Tage, wahrscheinlich 10, im schlechtesten Fall 18.
Er rechnet: „Jedes Gewerk braucht im Mittel (6 + 10 + 18)/3 = 11{,}3 Tage. Alle laufen gleichzeitig. Also ist der Umbau nach 11,3 Tagen fertig — ich plane großzügig 12 ein.“
Nachgerechnet mit 200 000 simulierten Umbauten:
import numpy as np rng = np.random.default_rng(3) dauer = rng.triangular(6, 10, 18, size=(200_000, 5)) # 5 Gewerke, je eine Schaetzung projekt = dauer.max(axis=1) # fertig, wenn das LETZTE fertig ist print(f"Mittelwert einer Einzeldauer: {dauer.mean():.2f} Tage") print(f"Tatsaechliche Projektdauer: {projekt.mean():.2f} Tage") print(f"90-%-Quantil: {np.quantile(projekt, 0.9):.2f} Tage") print(f"Anteil ueber 11,34 Tage: {(projekt > 11.34).mean() * 100:.1f} %")Ausgabe:
Mittelwert einer Einzeldauer: 11.34 Tage Tatsaechliche Projektdauer: 14.38 Tage 90-%-Quantil: 16.59 Tage Anteil ueber 11,34 Tage: 95.5 %Ihre Aufgabe: (a) Der Projektleiter hat richtig gerechnet — jedes Gewerk braucht tatsächlich im Mittel 11,34 Tage. Warum dauert das Projekt trotzdem 14,38 Tage? (b) Erklären Sie in einem Satz, warum ausgerechnet 95,5 % aller Umbauten den Plan reißen. Was müsste gelten, damit es 50 % wären? (c) Was passiert mit der Projektdauer, wenn statt fünf Gewerken zehn parallel arbeiten — bei unveränderter Einzelschätzung? (d) Welche Zahl gehört in den Projektplan, wenn der Auftraggeber eine Terminzusage mit 90 % Sicherheit verlangt?
Auflösung: Abschnitt A.12.
🎯 Merksatz Der Fluch des Durchschnitts hat zwei Gesichter. Beim Ersatzteil aus Abschnitt 12.1 führt der Mittelwert in die Irre, weil die Kosten asymmetrisch sind. Hier führt er in die Irre, weil die Verknüpfung asymmetrisch ist: Ein Gewerk, das früher fertig wird, hilft niemandem — ein Gewerk, das sich verspätet, verzögert alles. Beide Male gilt: Der Mittelwert einer Funktion ist nicht die Funktion des Mittelwerts.
12.10 Micro-Quiz
❓ Micro-Quiz 9: Drei Fragen zum Selbstcheck
Genau eine Antwort ist jeweils richtig. Auflösung in Anhang A.
1. Bei einem Ersatzteil kostet ein Stück zu wenig 1 400 €, ein Stück zu viel 120 €. Der Bedarf beträgt im Mittel 30 Stück. Wie viel bestellen Sie? (a) 30 — das ist der Erwartungswert des Bedarfs. (b) Deutlich mehr als 30, nämlich das 92,1-%-Quantil des Bedarfs. Das kritische Verhältnis 1400/(1400+120) sagt, wie weit man sich auf die günstigere Fehlerseite stellen soll. (c) Weniger als 30, weil Lagerhaltung Kapital bindet.
2. Was unterscheidet stochastische** von robuster Optimierung?** (a) Stochastische Optimierung ist genauer, robuste ist eine Näherung für schnelle Rechnungen. (b) Stochastische Optimierung braucht Wahrscheinlichkeiten und optimiert den Erwartungswert; robuste Optimierung braucht nur Bandbreiten und sichert den schlechtesten Fall darin ab. (c) Robuste Optimierung berücksichtigt mehr Szenarien.
3. Ein Kollege ersetzt in seinem Modell alle unsicheren Größen durch ihre Mittelwerte und rechnet deterministisch. Wann ist das unproblematisch? (a) Immer — der Erwartungswert ist die beste Einzelschätzung. (b) Nie. (c) Wenn Zielfunktion und Nebenbedingungen in den unsicheren Größen linear sind und keine Entscheidung erst nach Beobachtung fällt. Sobald Minimum, Maximum, Betrag oder eine Nachbesserungsentscheidung im Spiel ist, gilt es nicht mehr.
12.11 Selbsttest
Antworten: Anhang A.
- Warum ist Planung mit Erwartungswerten bei asymmetrischen Kosten systematisch falsch?
- Was unterscheidet Stufe-1- von Stufe-2-Variablen?
- Was misst der EVPI, und wofür ist er praktisch nützlich?
- Warum braucht robuste Optimierung keine Wahrscheinlichkeiten?
- Wie macht man ein Min-Max-Problem mit Box-Unsicherheit lösbar?
- Warum wird aus \mathbb{P}(\mathbf{a}^\top\mathbf{x} \ge b) \ge 1-\alpha unter Normalverteilung eine Norm und nicht ein fester Zuschlag je Variable?
- Ein Plan mit Chance Constraint verspricht 95 % und hält gemessen 87 %. Woran liegt das — und was ändert die szenariobasierte Formulierung daran?
12.12 Zusammenfassung
- Der Fluch des Durchschnitts ist kein Randphänomen und hat zwei Gesichter. Bei asymmetrischen Kosten liegt das Optimum systematisch neben dem Mittelwert — das Newsvendor-Verhältnis c_{-}/(c_{-}+c_{+}) sagt, wie weit. Bei asymmetrischer Verknüpfung (Maximum über parallele Vorgänge) ist der Erwartungswert des Ganzen größer als das Ganze der Erwartungswerte. Beide Male gilt: Der Mittelwert einer Funktion ist nicht die Funktion des Mittelwerts (Jensensche Ungleichung).
- Eine Terminzusage ist ein Quantil, kein Mittelwert. Wer den Erwartungswert zusagt, reißt den Termin bei fünf parallelen Vorgängen in 95 % der Fälle.
- Monte-Carlo bewertet, optimiert aber nicht. Sein Wert liegt in Kennzahlen, für die es keine Formel gibt.
- Zweistufige stochastische Programme trennen die Festlegung von der Reaktion — ein \mathbf{x}, aber je Szenario ein \mathbf{y}_s.
- Der EVPI begrenzt, was eine perfekte Prognose wert sein darf.
- Robuste Optimierung braucht keine Wahrscheinlichkeiten, nur eine Unsicherheitsmenge — und wird für einfache Mengen zu einem gewöhnlichen Problem mit Abzugsterm.
- Chance Constraints schreiben die Zusage selbst ins Modell. Unter Normalverteilung wird daraus eine Kegelbedingung (der Sicherheitszuschlag ist eine Norm, deshalb belohnt sie Mischung), szenariobasiert ein MILP mit Big-M ohne jede Verteilungsannahme.
- Sicherheit ist konvex bepreist. Im Beispiel kostet der letzte Prozentpunkt das 4,5-fache des ersten. Die richtige Managementfrage lautet nicht „was kostet Sicherheit“, sondern „was kostet der nächste Prozentpunkt“.
- Eine Zusage gilt nur für die unterstellte Verteilung. Ein Plan, der unter Normalverteilung 95 % verspricht, hielt gemessen 87 %, weil das Modell die Kältewelle nicht kannte, in der alles zugleich ausfällt. Szenariobasierte Zusagen wiederum überanpassen — beide müssen an zurückgehaltenen Daten nachgemessen werden.
- Beziffern Sie immer Preis und Nutzen der Absicherung. Ohne beide Zahlen ist die Entscheidung nicht begründbar.
Ausblick. Kapitel 13 fügt die Zeitdimension hinzu: Entscheidungen, die über viele Perioden aufeinander aufbauen — gelöst mit der Bellman-Gleichung.