Phase 6.1: Chance Constraints - die Zusage "mit 95 % Sicherheit"

Neuer Abschnitt im Kapitel Unsicherheit plus Chance_Constraints.py (74.
Programm). Das Kapitel hatte Monte-Carlo, Zweistufigkeit und
Worst-Case-Robustheit; die Wahrscheinlichkeitszusage war die fehlende vierte
Antwort - und die, nach der das Management tatsaechlich fragt. Setzt Paket 4
aus Verbesserungen_02.md um.

Beide Wege an DERSELBEN Instanz (Kraftwerkspark, 500 MW gesicherte Zusage):
analytisch als Second-Order-Cone-Bedingung (CLARABEL) und szenariobasiert als
Big-M-MILP (SciPy/HiGHS) - beides in einem Prozess, ohne ortools- oder
highspy-Import.

Drei gemessene Befunde:

* Der Mittelwertplan haelt 50,08 %. Kein Fehler, sondern die Definition des
  Erwartungswerts.
* Sicherheit ist konvex bepreist: 56.289 EUR je Prozentpunkt auf dem Weg zu
  80 %, 253.848 EUR zwischen 95 und 99 % - das 4,5-fache. Das Programm
  rechnet die Tabelle selbst aus, statt sie zu behaupten.
* Die Zusage gilt nur fuer die unterstellte Verteilung: Der 95-%-Plan haelt
  gemessen 87,44 %, sobald die Testverteilung eine Kaeltewelle mit
  Dunkelflaute enthaelt, in der alles zugleich einbricht - auch das
  Gaskraftwerk. In einer reinen Normalwelt liefert derselbe Plan 94,97 %.

Zwei eigene Fehlgriffe, beide durch Messen aufgefallen und korrigiert: Die
erste Kostentarierung ergab eine entartete Loesung (alles ins Gaskraftwerk),
womit die Kovarianzmatrix wirkungslos war - und gerade sie begruendet die
Kegelform. Und der erste Kaelteeinbruch traf nur Wind und Sonne; der SOC-Plan
hatte die ohnehin herausgehalten und war zufaellig robust, das Argument trug
nicht.

Ehrlich berichtet statt geglaettet: Die Szenariomethode ueberanpasst. Ueber
zwoelf Laeufe (S = 200, 400, 800) lag die tatsaechliche Quote zwischen
93,14 % und 96,71 %, und die Spanne wurde von S = 400 auf 800 wieder
breiter. Beide Solver bestaetigen optimal bei identischen Kosten - echte
Ueberanpassung, kein Solverartefakt. Steht als Warnkasten im Abschnitt.

Die Laufzeitmessung ist aus der Ausgabe entfernt: Eine Wanduhrzeit ist nie
byteidentisch reproduzierbar (2,2 s / 2,3 s zwischen zwei Laeufen) und haette
Regel 4 dauerhaft gebrochen. Danach drei Laeufe zeichengleich, und die
abgedruckte Ausgabe stimmt mit dem Lauf des extrahierten Programms ueberein.

Mitgezogen: Kapitelkopf, Lernziele, Uebersichtstabelle (drei -> vier
Ansaetze), Selbsttest, Zusammenfassung, Vorwort-Programmverzeichnis, eine
neue Uebungsaufgabe und ihre Loesung in Anhang A.

pyproject.toml korrigiert: cvxpy lag in [finance], wird aber von 13
Programmen gebraucht, darunter dreien in diesem Kapitel - jetzt in der
Grundausstattung. Es zieht kein highspy nach, der Solverkonflikt bleibt auf
[large-scale] beschraenkt.

Stand: 293 Abschnitte, 712 Querverweise, 327 Indexmarken, 74 Programme (0
Fehler), 33 pytest-Tests, PDF 725 Seiten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
dschlueter 2026-09-08 01:57:19 +02:00
commit 6c67e126dd
23 changed files with 9089 additions and 6419 deletions

View file

@ -451,6 +451,251 @@
" print(\" als sie im Normalfall kostet.\")\n",
" print(\"=\" * 88)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Der Fall: ein Kraftwerkspark mit 500 MW Zusage\n",
"\n",
"`Chance_Constraints.py`\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#!/usr/bin/env python3\n",
"\n",
"# Chance_Constraints.py\n",
"\"\"\"\n",
"Kapitel Unsicherheit: Wahrscheinlichkeitsbeschraenkungen.\n",
"\n",
"Das Management fragt selten \"was ist im Mittel am besten?\", sondern \"mit\n",
"welcher Sicherheit haelt der Plan?\". Genau das formuliert eine Chance\n",
"Constraint: P(Versorgung >= Bedarf) >= 1 - alpha.\n",
"\n",
"Gezeigt werden beide Wege dorthin, an derselben Instanz:\n",
" (a) analytisch - unter Normalverteilungsannahme wird daraus eine\n",
" Second-Order-Cone-Bedingung, loesbar mit CVXPY\n",
" (b) szenariobasiert - Big-M mit Binaervariablen, fuer beliebige\n",
" empirische Verteilungen\n",
"\n",
"Und die beiden Messungen, auf die es ankommt: Was kostet ein Prozentpunkt\n",
"Versorgungssicherheit - und haelt die Zusage auch dann, wenn die Verteilung\n",
"nicht normal ist?\n",
"\n",
"Solver: CLARABEL (Kegel) und SciPy/HiGHS (gemischt-ganzzahlig). Weder ortools\n",
"noch ein direkter highspy-Import, damit alles in einem Prozess laeuft.\n",
"\"\"\"\n",
"\n",
"import cvxpy as cp\n",
"import numpy as np\n",
"from scipy.stats import norm\n",
"\n",
"# --- Der Kraftwerkspark ----------------------------------------------------\n",
"# Die Verfuegbarkeit ist der Anteil, den eine installierte MW im Mittel\n",
"# wirklich liefert: bei Wind und Sonne klein und stark schwankend, bei Gas und\n",
"# Biomasse gross und stabil. Die Ausbaugrenze ist der Standort - Flaeche,\n",
"# Genehmigung, Brennstoffversorgung.\n",
"TECHNIK = [\"Gaskraftwerk\", \"Windpark\", \"Solarpark\", \"Biomasse\"]\n",
"KOSTEN = np.array([65_000.0, 24_000.0, 12_000.0, 60_000.0]) # EUR je MW und Jahr\n",
"VERFUEGBAR = np.array([0.92, 0.35, 0.18, 0.85])\n",
"STREUUNG = np.array([0.05, 0.16, 0.10, 0.04])\n",
"GRENZE = np.array([400.0, 250.0, 600.0, 350.0]) # MW\n",
"\n",
"# Wind und Sonne sind leicht gegenlaeufig: Ein Tiefdruckgebiet bringt Wind und\n",
"# Wolken zugleich. Genau diese Korrelation macht die Bedingung zu einem Kegel\n",
"# und nicht zu einer Summe unabhaengiger Einzelzuschlaege.\n",
"KORRELATION = np.array([\n",
" [1.00, 0.00, 0.00, 0.05],\n",
" [0.00, 1.00, -0.25, 0.00],\n",
" [0.00, -0.25, 1.00, 0.00],\n",
" [0.05, 0.00, 0.00, 1.00],\n",
"])\n",
"SIGMA = np.diag(STREUUNG) @ KORRELATION @ np.diag(STREUUNG)\n",
"WURZEL = np.linalg.cholesky(SIGMA) # L mit L @ L.T == SIGMA\n",
"\n",
"BEDARF = 500.0 # MW, die gesichert bereitstehen muessen\n",
"N = len(TECHNIK)\n",
"\n",
"# Die Kaeltewelle mit Dunkelflaute: selten, aber sie trifft alles zugleich.\n",
"# Wind und Sonne brechen fast vollstaendig weg - und, das ist der Punkt, das\n",
"# Gaskraftwerk liefert ebenfalls weniger, weil bei Frost der Netzdruck faellt.\n",
"# Eine Kovarianzmatrix mit Korrelationen um null kann das nicht ausdruecken.\n",
"P_KAELTEWELLE = 0.08\n",
"EINBRUCH = np.array([0.72, 0.10, 0.15, 0.88])\n",
"\n",
"\n",
"def mittelwertplan():\n",
" \"\"\"Plant mit den Erwartungswerten - ignoriert die Streuung vollstaendig.\"\"\"\n",
" x = cp.Variable(N, nonneg=True)\n",
" problem = cp.Problem(cp.Minimize(KOSTEN @ x),\n",
" [VERFUEGBAR @ x >= BEDARF, x <= GRENZE])\n",
" problem.solve(solver=cp.CLARABEL)\n",
" return problem.value, x.value\n",
"\n",
"\n",
"def chance_constraint_analytisch(alpha):\n",
" \"\"\"\n",
" P(a^T x >= BEDARF) >= 1 - alpha unter a ~ N(VERFUEGBAR, SIGMA).\n",
"\n",
" Aequivalent zu: VERFUEGBAR^T x - z * ||L^T x||_2 >= BEDARF\n",
" mit z = Phi^-1(1 - alpha). Das ist eine Second-Order-Cone-Bedingung: Der\n",
" Sicherheitszuschlag ist eine Norm ueber x, kein fester Aufschlag je Anlage.\n",
" Deshalb belohnt sie Mischung - zwei gegenlaeufige Quellen schwanken\n",
" gemeinsam weniger als jede fuer sich.\n",
" \"\"\"\n",
" x = cp.Variable(N, nonneg=True)\n",
" z = norm.ppf(1 - alpha)\n",
" bedingung = VERFUEGBAR @ x - z * cp.norm(WURZEL.T @ x, 2) >= BEDARF\n",
" problem = cp.Problem(cp.Minimize(KOSTEN @ x), [bedingung, x <= GRENZE])\n",
" problem.solve(solver=cp.CLARABEL)\n",
" return problem.value, x.value\n",
"\n",
"\n",
"def chance_constraint_szenarien(a, alpha):\n",
" \"\"\"\n",
" Dieselbe Zusage ohne Verteilungsannahme: Fuer jedes Szenario s sagt eine\n",
" Binaervariable z_s, ob es verletzt werden darf.\n",
"\n",
" a_s^T x >= BEDARF - M * z_s fuer alle s\n",
" sum_s z_s <= alpha * S\n",
"\n",
" M = BEDARF ist die kleinstmoegliche gueltige Schranke, denn a_s^T x >= 0:\n",
" Groesser kann eine Verletzung gar nicht ausfallen. Ein unnoetig grosses M\n",
" wuerde die LP-Relaxierung aufweichen und die Suche verlangsamen - die\n",
" Big-M-Falle aus dem Kapitel Gemischt-ganzzahlige Optimierung.\n",
" \"\"\"\n",
" S = len(a)\n",
" x = cp.Variable(N, nonneg=True)\n",
" z = cp.Variable(S, boolean=True)\n",
" problem = cp.Problem(\n",
" cp.Minimize(KOSTEN @ x),\n",
" [a @ x >= BEDARF - BEDARF * z, cp.sum(z) <= alpha * S, x <= GRENZE])\n",
" problem.solve(solver=cp.SCIPY)\n",
" return problem.value, x.value\n",
"\n",
"\n",
"def ziehe_wetter(anzahl, seed):\n",
" \"\"\"Mischverteilung: normales Wetter, mit P_KAELTEWELLE ein Einbruch.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" a = rng.multivariate_normal(VERFUEGBAR, SIGMA, size=anzahl)\n",
" getroffen = rng.random(anzahl) < P_KAELTEWELLE\n",
" a[getroffen] *= EINBRUCH\n",
" return np.clip(a, 0.0, 1.0)\n",
"\n",
"\n",
"def sicherheit(x, a):\n",
" \"\"\"Anteil der Szenarien, in denen der Plan den Bedarf deckt.\"\"\"\n",
" return float(np.mean(a @ x >= BEDARF))\n",
"\n",
"\n",
"KOPF = (f\"{'Plan':<20} {'Kosten (EUR)':>13} {'Gas':>4} {'Wind':>4} \"\n",
" f\"{'Sol':>4} {'Bio':>4} {'Normalwelt':>11} {'echte Welt':>11}\")\n",
"\n",
"\n",
"def zeile(name, kosten, x, in_normal, in_echt):\n",
" mix = \" \".join(f\"{w:4.0f}\" for w in x)\n",
" return (f\"{name:<20} {kosten:>13,.0f} {mix} \"\n",
" f\"{in_normal * 100:>9.2f} % {in_echt * 100:>9.2f} %\")\n",
"\n",
"\n",
"if __name__ == \"__main__\":\n",
" print(\"=\" * 78)\n",
" print(\" WAHRSCHEINLICHKEITSBESCHRAENKUNGEN IM KRAFTWERKSPARK\")\n",
" print(\"=\" * 78)\n",
" print(f\"Gesichert bereitzustellen: {BEDARF:.0f} MW\\n\")\n",
" print(f\"{'Technologie':<14} {'EUR/MW':>8} {'verfuegbar':>11} {'Streuung':>9} \"\n",
" f\"{'Grenze':>8} {'EUR je erw. MW':>15}\")\n",
" print(\"-\" * 78)\n",
" for i, name in enumerate(TECHNIK):\n",
" print(f\"{name:<14} {KOSTEN[i]:>8,.0f} {VERFUEGBAR[i]:>10.2f} \"\n",
" f\"{STREUUNG[i]:>8.2f} {GRENZE[i]:>7.0f} \"\n",
" f\"{KOSTEN[i] / VERFUEGBAR[i]:>15,.0f}\")\n",
" print(f\"\\nVollausbau liefert im Mittel {VERFUEGBAR @ GRENZE:.0f} MW, \"\n",
" f\"in der Kaeltewelle {(VERFUEGBAR * EINBRUCH) @ GRENZE:.0f} MW.\")\n",
"\n",
" # Zwei grosse, unabhaengige Testmengen. An ihnen wird JEDER Plan gemessen -\n",
" # einmal in der unterstellten Normalwelt, einmal in der echten Verteilung.\n",
" echt = ziehe_wetter(200_000, seed=771)\n",
" normalwelt = np.random.default_rng(4711).multivariate_normal(\n",
" VERFUEGBAR, SIGMA, size=200_000)\n",
"\n",
" print(\"\\n\" + \"=\" * 78)\n",
" print(\" (1) Mittelwertplan und Chance Constraints im Vergleich\")\n",
" print(\"=\" * 78)\n",
" print(KOPF)\n",
" print(\"-\" * 78)\n",
"\n",
" k_mittel, x_mittel = mittelwertplan()\n",
" stufen = [(\"Mittelwert\", k_mittel, sicherheit(x_mittel, normalwelt))]\n",
" print(zeile(\"Mittelwert\", k_mittel, x_mittel,\n",
" sicherheit(x_mittel, normalwelt), sicherheit(x_mittel, echt)))\n",
"\n",
" plaene = {}\n",
" for alpha in (0.20, 0.10, 0.05, 0.01):\n",
" kosten, x = chance_constraint_analytisch(alpha)\n",
" plaene[alpha] = (kosten, x)\n",
" quote_normal = sicherheit(x, normalwelt)\n",
" stufen.append((f\"Zusage {(1 - alpha) * 100:.0f} %\", kosten, quote_normal))\n",
" print(zeile(f\"Zusage {(1 - alpha) * 100:.0f} %\", kosten, x,\n",
" quote_normal, sicherheit(x, echt)))\n",
"\n",
" print(\"\\n In der Normalwelt trifft jede Zusage ihren Wert - das Verfahren\")\n",
" print(\" rechnet richtig. Die Spalte 'echte Welt' kommt in Teil (3).\")\n",
"\n",
" print(\"\\n\" + \"=\" * 78)\n",
" print(\" (2) Was kostet ein Prozentpunkt Versorgungssicherheit?\")\n",
" print(\"=\" * 78)\n",
" print(f\"{'von -> nach':<26} {'Prozentpunkte':>13} {'Mehrkosten':>13} \"\n",
" f\"{'EUR je Punkt':>13}\")\n",
" print(\"-\" * 78)\n",
" for (n1, k1, q1), (n2, k2, q2) in zip(stufen, stufen[1:]):\n",
" d_punkte = (q2 - q1) * 100\n",
" d_kosten = k2 - k1\n",
" print(f\"{n1 + ' -> ' + n2:<26} {d_punkte:>13.2f} {d_kosten:>13,.0f} \"\n",
" f\"{d_kosten / d_punkte:>13,.0f}\")\n",
" erst = (stufen[1][1] - stufen[0][1]) / ((stufen[1][2] - stufen[0][2]) * 100)\n",
" letzt = (stufen[-1][1] - stufen[-2][1]) / ((stufen[-1][2] - stufen[-2][2]) * 100)\n",
" print(f\"\\n Der letzte Prozentpunkt kostet das {letzt / erst:.1f}-fache des ersten.\")\n",
" print(\" Sicherheit ist konvex bepreist - genau deshalb muss jemand\")\n",
" print(\" entscheiden, wie viel davon das Unternehmen kaufen will.\")\n",
"\n",
" print(\"\\n\" + \"=\" * 78)\n",
" print(\" (3) Und wenn die Verteilung nicht normal ist?\")\n",
" print(\"=\" * 78)\n",
" k_soc, x_soc = plaene[0.05]\n",
" print(f\"Die echte Welt kennt die Kaeltewelle mit Dunkelflaute: in \"\n",
" f\"{P_KAELTEWELLE * 100:.0f} % der Faelle liefern\")\n",
" print(f\"Wind {EINBRUCH[1] * 100:.0f} %, Sonne {EINBRUCH[2] * 100:.0f} % \"\n",
" f\"und - entscheidend - auch das Gaskraftwerk nur \"\n",
" f\"{EINBRUCH[0] * 100:.0f} %\")\n",
" print(\"des Ueblichen. Alles bricht gleichzeitig ein.\\n\")\n",
"\n",
" a_bau = ziehe_wetter(400, seed=20260908)\n",
" k_sz, x_sz = chance_constraint_szenarien(a_bau, 0.05)\n",
"\n",
" print(KOPF)\n",
" print(\"-\" * 78)\n",
" print(zeile(\"SOC, Zusage 95 %\", k_soc, x_soc,\n",
" sicherheit(x_soc, normalwelt), sicherheit(x_soc, echt)))\n",
" print(zeile(\"Szenarien, 95 %\", k_sz, x_sz,\n",
" sicherheit(x_sz, normalwelt), sicherheit(x_sz, echt)))\n",
"\n",
" print(f\"\\n Der SOC-Plan verspricht 95 % und haelt \"\n",
" f\"{sicherheit(x_soc, echt) * 100:.1f} %. Nicht das Verfahren ist\")\n",
" print(f\" falsch, sondern die Annahme: In der Normalwelt liefert derselbe\")\n",
" print(f\" Plan {sicherheit(x_soc, normalwelt) * 100:.1f} %.\")\n",
" print(f\"\\n Der Szenarioplan ({len(a_bau)} Szenarien, {len(a_bau)} \"\n",
" f\"Binaervariablen) kommt auf\")\n",
" print(f\" {sicherheit(x_sz, echt) * 100:.1f} % - und kostet dafuer \"\n",
" f\"{(k_sz / k_soc - 1) * 100:+.1f} %. Er kauft keine Windkraft mehr:\")\n",
" print(\" Was im Ernstfall ausfaellt, hilft der Zusage nicht.\")\n",
" print(\"=\" * 78)"
]
}
],
"metadata": {

View file

@ -451,6 +451,251 @@
" print(\" als sie im Normalfall kostet.\")\n",
" print(\"=\" * 88)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Der Fall: ein Kraftwerkspark mit 500 MW Zusage\n",
"\n",
"`Chance_Constraints.py`\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#!/usr/bin/env python3\n",
"\n",
"# Chance_Constraints.py\n",
"\"\"\"\n",
"Kapitel Unsicherheit: Wahrscheinlichkeitsbeschraenkungen.\n",
"\n",
"Das Management fragt selten \"was ist im Mittel am besten?\", sondern \"mit\n",
"welcher Sicherheit haelt der Plan?\". Genau das formuliert eine Chance\n",
"Constraint: P(Versorgung >= Bedarf) >= 1 - alpha.\n",
"\n",
"Gezeigt werden beide Wege dorthin, an derselben Instanz:\n",
" (a) analytisch - unter Normalverteilungsannahme wird daraus eine\n",
" Second-Order-Cone-Bedingung, loesbar mit CVXPY\n",
" (b) szenariobasiert - Big-M mit Binaervariablen, fuer beliebige\n",
" empirische Verteilungen\n",
"\n",
"Und die beiden Messungen, auf die es ankommt: Was kostet ein Prozentpunkt\n",
"Versorgungssicherheit - und haelt die Zusage auch dann, wenn die Verteilung\n",
"nicht normal ist?\n",
"\n",
"Solver: CLARABEL (Kegel) und SciPy/HiGHS (gemischt-ganzzahlig). Weder ortools\n",
"noch ein direkter highspy-Import, damit alles in einem Prozess laeuft.\n",
"\"\"\"\n",
"\n",
"import cvxpy as cp\n",
"import numpy as np\n",
"from scipy.stats import norm\n",
"\n",
"# --- Der Kraftwerkspark ----------------------------------------------------\n",
"# Die Verfuegbarkeit ist der Anteil, den eine installierte MW im Mittel\n",
"# wirklich liefert: bei Wind und Sonne klein und stark schwankend, bei Gas und\n",
"# Biomasse gross und stabil. Die Ausbaugrenze ist der Standort - Flaeche,\n",
"# Genehmigung, Brennstoffversorgung.\n",
"TECHNIK = [\"Gaskraftwerk\", \"Windpark\", \"Solarpark\", \"Biomasse\"]\n",
"KOSTEN = np.array([65_000.0, 24_000.0, 12_000.0, 60_000.0]) # EUR je MW und Jahr\n",
"VERFUEGBAR = np.array([0.92, 0.35, 0.18, 0.85])\n",
"STREUUNG = np.array([0.05, 0.16, 0.10, 0.04])\n",
"GRENZE = np.array([400.0, 250.0, 600.0, 350.0]) # MW\n",
"\n",
"# Wind und Sonne sind leicht gegenlaeufig: Ein Tiefdruckgebiet bringt Wind und\n",
"# Wolken zugleich. Genau diese Korrelation macht die Bedingung zu einem Kegel\n",
"# und nicht zu einer Summe unabhaengiger Einzelzuschlaege.\n",
"KORRELATION = np.array([\n",
" [1.00, 0.00, 0.00, 0.05],\n",
" [0.00, 1.00, -0.25, 0.00],\n",
" [0.00, -0.25, 1.00, 0.00],\n",
" [0.05, 0.00, 0.00, 1.00],\n",
"])\n",
"SIGMA = np.diag(STREUUNG) @ KORRELATION @ np.diag(STREUUNG)\n",
"WURZEL = np.linalg.cholesky(SIGMA) # L mit L @ L.T == SIGMA\n",
"\n",
"BEDARF = 500.0 # MW, die gesichert bereitstehen muessen\n",
"N = len(TECHNIK)\n",
"\n",
"# Die Kaeltewelle mit Dunkelflaute: selten, aber sie trifft alles zugleich.\n",
"# Wind und Sonne brechen fast vollstaendig weg - und, das ist der Punkt, das\n",
"# Gaskraftwerk liefert ebenfalls weniger, weil bei Frost der Netzdruck faellt.\n",
"# Eine Kovarianzmatrix mit Korrelationen um null kann das nicht ausdruecken.\n",
"P_KAELTEWELLE = 0.08\n",
"EINBRUCH = np.array([0.72, 0.10, 0.15, 0.88])\n",
"\n",
"\n",
"def mittelwertplan():\n",
" \"\"\"Plant mit den Erwartungswerten - ignoriert die Streuung vollstaendig.\"\"\"\n",
" x = cp.Variable(N, nonneg=True)\n",
" problem = cp.Problem(cp.Minimize(KOSTEN @ x),\n",
" [VERFUEGBAR @ x >= BEDARF, x <= GRENZE])\n",
" problem.solve(solver=cp.CLARABEL)\n",
" return problem.value, x.value\n",
"\n",
"\n",
"def chance_constraint_analytisch(alpha):\n",
" \"\"\"\n",
" P(a^T x >= BEDARF) >= 1 - alpha unter a ~ N(VERFUEGBAR, SIGMA).\n",
"\n",
" Aequivalent zu: VERFUEGBAR^T x - z * ||L^T x||_2 >= BEDARF\n",
" mit z = Phi^-1(1 - alpha). Das ist eine Second-Order-Cone-Bedingung: Der\n",
" Sicherheitszuschlag ist eine Norm ueber x, kein fester Aufschlag je Anlage.\n",
" Deshalb belohnt sie Mischung - zwei gegenlaeufige Quellen schwanken\n",
" gemeinsam weniger als jede fuer sich.\n",
" \"\"\"\n",
" x = cp.Variable(N, nonneg=True)\n",
" z = norm.ppf(1 - alpha)\n",
" bedingung = VERFUEGBAR @ x - z * cp.norm(WURZEL.T @ x, 2) >= BEDARF\n",
" problem = cp.Problem(cp.Minimize(KOSTEN @ x), [bedingung, x <= GRENZE])\n",
" problem.solve(solver=cp.CLARABEL)\n",
" return problem.value, x.value\n",
"\n",
"\n",
"def chance_constraint_szenarien(a, alpha):\n",
" \"\"\"\n",
" Dieselbe Zusage ohne Verteilungsannahme: Fuer jedes Szenario s sagt eine\n",
" Binaervariable z_s, ob es verletzt werden darf.\n",
"\n",
" a_s^T x >= BEDARF - M * z_s fuer alle s\n",
" sum_s z_s <= alpha * S\n",
"\n",
" M = BEDARF ist die kleinstmoegliche gueltige Schranke, denn a_s^T x >= 0:\n",
" Groesser kann eine Verletzung gar nicht ausfallen. Ein unnoetig grosses M\n",
" wuerde die LP-Relaxierung aufweichen und die Suche verlangsamen - die\n",
" Big-M-Falle aus dem Kapitel Gemischt-ganzzahlige Optimierung.\n",
" \"\"\"\n",
" S = len(a)\n",
" x = cp.Variable(N, nonneg=True)\n",
" z = cp.Variable(S, boolean=True)\n",
" problem = cp.Problem(\n",
" cp.Minimize(KOSTEN @ x),\n",
" [a @ x >= BEDARF - BEDARF * z, cp.sum(z) <= alpha * S, x <= GRENZE])\n",
" problem.solve(solver=cp.SCIPY)\n",
" return problem.value, x.value\n",
"\n",
"\n",
"def ziehe_wetter(anzahl, seed):\n",
" \"\"\"Mischverteilung: normales Wetter, mit P_KAELTEWELLE ein Einbruch.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" a = rng.multivariate_normal(VERFUEGBAR, SIGMA, size=anzahl)\n",
" getroffen = rng.random(anzahl) < P_KAELTEWELLE\n",
" a[getroffen] *= EINBRUCH\n",
" return np.clip(a, 0.0, 1.0)\n",
"\n",
"\n",
"def sicherheit(x, a):\n",
" \"\"\"Anteil der Szenarien, in denen der Plan den Bedarf deckt.\"\"\"\n",
" return float(np.mean(a @ x >= BEDARF))\n",
"\n",
"\n",
"KOPF = (f\"{'Plan':<20} {'Kosten (EUR)':>13} {'Gas':>4} {'Wind':>4} \"\n",
" f\"{'Sol':>4} {'Bio':>4} {'Normalwelt':>11} {'echte Welt':>11}\")\n",
"\n",
"\n",
"def zeile(name, kosten, x, in_normal, in_echt):\n",
" mix = \" \".join(f\"{w:4.0f}\" for w in x)\n",
" return (f\"{name:<20} {kosten:>13,.0f} {mix} \"\n",
" f\"{in_normal * 100:>9.2f} % {in_echt * 100:>9.2f} %\")\n",
"\n",
"\n",
"if __name__ == \"__main__\":\n",
" print(\"=\" * 78)\n",
" print(\" WAHRSCHEINLICHKEITSBESCHRAENKUNGEN IM KRAFTWERKSPARK\")\n",
" print(\"=\" * 78)\n",
" print(f\"Gesichert bereitzustellen: {BEDARF:.0f} MW\\n\")\n",
" print(f\"{'Technologie':<14} {'EUR/MW':>8} {'verfuegbar':>11} {'Streuung':>9} \"\n",
" f\"{'Grenze':>8} {'EUR je erw. MW':>15}\")\n",
" print(\"-\" * 78)\n",
" for i, name in enumerate(TECHNIK):\n",
" print(f\"{name:<14} {KOSTEN[i]:>8,.0f} {VERFUEGBAR[i]:>10.2f} \"\n",
" f\"{STREUUNG[i]:>8.2f} {GRENZE[i]:>7.0f} \"\n",
" f\"{KOSTEN[i] / VERFUEGBAR[i]:>15,.0f}\")\n",
" print(f\"\\nVollausbau liefert im Mittel {VERFUEGBAR @ GRENZE:.0f} MW, \"\n",
" f\"in der Kaeltewelle {(VERFUEGBAR * EINBRUCH) @ GRENZE:.0f} MW.\")\n",
"\n",
" # Zwei grosse, unabhaengige Testmengen. An ihnen wird JEDER Plan gemessen -\n",
" # einmal in der unterstellten Normalwelt, einmal in der echten Verteilung.\n",
" echt = ziehe_wetter(200_000, seed=771)\n",
" normalwelt = np.random.default_rng(4711).multivariate_normal(\n",
" VERFUEGBAR, SIGMA, size=200_000)\n",
"\n",
" print(\"\\n\" + \"=\" * 78)\n",
" print(\" (1) Mittelwertplan und Chance Constraints im Vergleich\")\n",
" print(\"=\" * 78)\n",
" print(KOPF)\n",
" print(\"-\" * 78)\n",
"\n",
" k_mittel, x_mittel = mittelwertplan()\n",
" stufen = [(\"Mittelwert\", k_mittel, sicherheit(x_mittel, normalwelt))]\n",
" print(zeile(\"Mittelwert\", k_mittel, x_mittel,\n",
" sicherheit(x_mittel, normalwelt), sicherheit(x_mittel, echt)))\n",
"\n",
" plaene = {}\n",
" for alpha in (0.20, 0.10, 0.05, 0.01):\n",
" kosten, x = chance_constraint_analytisch(alpha)\n",
" plaene[alpha] = (kosten, x)\n",
" quote_normal = sicherheit(x, normalwelt)\n",
" stufen.append((f\"Zusage {(1 - alpha) * 100:.0f} %\", kosten, quote_normal))\n",
" print(zeile(f\"Zusage {(1 - alpha) * 100:.0f} %\", kosten, x,\n",
" quote_normal, sicherheit(x, echt)))\n",
"\n",
" print(\"\\n In der Normalwelt trifft jede Zusage ihren Wert - das Verfahren\")\n",
" print(\" rechnet richtig. Die Spalte 'echte Welt' kommt in Teil (3).\")\n",
"\n",
" print(\"\\n\" + \"=\" * 78)\n",
" print(\" (2) Was kostet ein Prozentpunkt Versorgungssicherheit?\")\n",
" print(\"=\" * 78)\n",
" print(f\"{'von -> nach':<26} {'Prozentpunkte':>13} {'Mehrkosten':>13} \"\n",
" f\"{'EUR je Punkt':>13}\")\n",
" print(\"-\" * 78)\n",
" for (n1, k1, q1), (n2, k2, q2) in zip(stufen, stufen[1:]):\n",
" d_punkte = (q2 - q1) * 100\n",
" d_kosten = k2 - k1\n",
" print(f\"{n1 + ' -> ' + n2:<26} {d_punkte:>13.2f} {d_kosten:>13,.0f} \"\n",
" f\"{d_kosten / d_punkte:>13,.0f}\")\n",
" erst = (stufen[1][1] - stufen[0][1]) / ((stufen[1][2] - stufen[0][2]) * 100)\n",
" letzt = (stufen[-1][1] - stufen[-2][1]) / ((stufen[-1][2] - stufen[-2][2]) * 100)\n",
" print(f\"\\n Der letzte Prozentpunkt kostet das {letzt / erst:.1f}-fache des ersten.\")\n",
" print(\" Sicherheit ist konvex bepreist - genau deshalb muss jemand\")\n",
" print(\" entscheiden, wie viel davon das Unternehmen kaufen will.\")\n",
"\n",
" print(\"\\n\" + \"=\" * 78)\n",
" print(\" (3) Und wenn die Verteilung nicht normal ist?\")\n",
" print(\"=\" * 78)\n",
" k_soc, x_soc = plaene[0.05]\n",
" print(f\"Die echte Welt kennt die Kaeltewelle mit Dunkelflaute: in \"\n",
" f\"{P_KAELTEWELLE * 100:.0f} % der Faelle liefern\")\n",
" print(f\"Wind {EINBRUCH[1] * 100:.0f} %, Sonne {EINBRUCH[2] * 100:.0f} % \"\n",
" f\"und - entscheidend - auch das Gaskraftwerk nur \"\n",
" f\"{EINBRUCH[0] * 100:.0f} %\")\n",
" print(\"des Ueblichen. Alles bricht gleichzeitig ein.\\n\")\n",
"\n",
" a_bau = ziehe_wetter(400, seed=20260908)\n",
" k_sz, x_sz = chance_constraint_szenarien(a_bau, 0.05)\n",
"\n",
" print(KOPF)\n",
" print(\"-\" * 78)\n",
" print(zeile(\"SOC, Zusage 95 %\", k_soc, x_soc,\n",
" sicherheit(x_soc, normalwelt), sicherheit(x_soc, echt)))\n",
" print(zeile(\"Szenarien, 95 %\", k_sz, x_sz,\n",
" sicherheit(x_sz, normalwelt), sicherheit(x_sz, echt)))\n",
"\n",
" print(f\"\\n Der SOC-Plan verspricht 95 % und haelt \"\n",
" f\"{sicherheit(x_soc, echt) * 100:.1f} %. Nicht das Verfahren ist\")\n",
" print(f\" falsch, sondern die Annahme: In der Normalwelt liefert derselbe\")\n",
" print(f\" Plan {sicherheit(x_soc, normalwelt) * 100:.1f} %.\")\n",
" print(f\"\\n Der Szenarioplan ({len(a_bau)} Szenarien, {len(a_bau)} \"\n",
" f\"Binaervariablen) kommt auf\")\n",
" print(f\" {sicherheit(x_sz, echt) * 100:.1f} % - und kostet dafuer \"\n",
" f\"{(k_sz / k_soc - 1) * 100:+.1f} %. Er kauft keine Windkraft mehr:\")\n",
" print(\" Was im Ernstfall ausfaellt, hilft der Zusage nicht.\")\n",
" print(\"=\" * 78)"
]
}
],
"metadata": {

View file

@ -801,11 +801,12 @@
<hr />
<h2 id="sec:loesungen-unsicherheit">A.12 Lösungen zu Kapitel „Optimierung unter Unsicherheit — Monte-Carlo, Stochastik, Robustheit“</h2>
<p><strong>9.1 — Fluch des Durchschnitts.</strong> Weil sich Verzögerungen <strong>fortpflanzen</strong> und die Verteilung rechtsschief ist: Ein Gewerk, das früher fertig wird, beschleunigt selten den Gesamtablauf (der nächste Schritt ist noch nicht bereit), ein verspätetes verzögert alles. Weitere Beispiele: Wartezeiten in Warteschlangen (nichtlinear in der Auslastung), Projektkosten mit Nachträgen.</p>
<p><strong>9.2 — Ansatz wählen.</strong> (a) robust (Ausfall ist existenziell, Wahrscheinlichkeiten unzuverlässig). (b) stochastisch (Verteilung bekannt). (c) robust bzw. Extremwertstatistik (seltene Ereignisse, katastrophale Folgen). (d) stochastisch (Szenarien mit Wahrscheinlichkeiten liegen vor).</p>
<p><strong>9.2 — Ansatz wählen.</strong> (a) robust (Ausfall ist existenziell, Wahrscheinlichkeiten unzuverlässig). (b) stochastisch (Verteilung bekannt). (c) robust bzw. Extremwertstatistik (seltene Ereignisse, katastrophale Folgen). (d) stochastisch (Szenarien mit Wahrscheinlichkeiten liegen vor). (e) Chance Constraint — die Zusage ist bereits als Quote formuliert („an 99 % aller Wintertage“). Robust wäre hier zu teuer (es gibt immer einen kälteren Tag), der Erwartungswert zu schwach (er sagt nichts über die Zusage).</p>
<p><strong>9.3 — Zweistufig rechnen.</strong> (a) Mit Spot = 60: Ableitung bei <span class="math inline">x \in (100,250)</span>: <span class="math inline">40 - 60\cdot0{,}5 + 5\cdot0{,}5 = 40-30+2{,}5 = +12{,}5 &gt; 0</span> → Kapazität <strong>senken</strong>. Bei <span class="math inline">x &lt; 100</span>: <span class="math inline">40 - 60 = -20 &lt; 0</span> → erhöhen. Optimum daher <span class="math inline">x^* = 100</span>. (b) Billige Nachbesserung macht Vorhalten unattraktiv. (c) Bei <span class="math inline">x = 225</span> optimal müsste die Ableitung dort das Vorzeichen wechseln — das ist bei diskreten Szenarien nur zufällig der Fall; der Mittelwert ist nur bei symmetrischen Kosten und stetiger Verteilung optimal.</p>
<p><strong>9.4 — EVPI interpretieren.</strong> Selbst eine <strong>perfekte</strong> Prognose wäre nur 8 400 € pro Jahr wert. Eine Lösung für 15 000 € kann sich also <strong>niemals</strong> rechnen — unabhängig von ihrer Güte. Argument: „Der theoretische Maximalnutzen liegt unter dem Preis.“</p>
<p><strong>9.5 — Monte-Carlo erweitern.</strong> (a) Servicelevel 95 %: Kapazität = 95 %-Quantil des Bedarfs (ca. 485). Die Zusatzkosten gegenüber dem Kostenoptimum (180) betragen ein Vielfaches — Servicelevel ist teuer, und genau diese Zahl braucht die Geschäftsleitung für die Entscheidung. (b) <code>cvar = kosten[kosten &gt;= np.quantile(kosten, 0.95)].mean()</code>. (c) Erwartetes Bild: Bei kleiner Kapazität ist die Kostenverteilung stark rechtsschief (seltene, teure Notzukäufe); bei großer Kapazität schmal und nach rechts verschoben.</p>
<p><strong>9.6 — Budgeted Uncertainty.</strong></p>
<p><strong>9.6 — Den Preis der Zusage selbst bestimmen.</strong> (a) Die Kosten je Prozentpunkt steigen weiter steil an; der Mix verschiebt sich dabei fast nur noch zugunsten des Gaskraftwerks, weil Biomasse längst an ihrer Ausbaugrenze liegt. Wind und Sonne bleiben bei einem kleinen Rest — sie senken über ihre Gegenläufigkeit die Norm, tragen zur gesicherten Leistung aber kaum bei. (b) Der Unterschied ist <strong>nicht</strong> numerisch, sondern inhaltlich. Bei 200 MW Biomasse liefert selbst der <strong>Vollausbau</strong> in der Kältewelle nur rund 440 MW — 7,5 % der Szenarien sind schlicht nicht bedienbar, egal wie viel Geld man ausgibt. Das Szenariomodell sieht diese Fälle in den Daten und meldet korrekt <code>infeasible</code>, sobald die Zusage über 92 % steigt. Das analytische Modell kennt sie nicht: Für eine Normalverteilung ist jede Zusage unter 100 % erfüllbar, wenn man nur genug Streuung wegkauft. Es meldet <code>optimal</code> — und der Plan hält gemessen 87,4 %. Glauben sollte man dem Szenariomodell: Ein <code>infeasible</code> ist hier die <strong>richtige</strong> Antwort. Es sagt, dass die Zusage nicht am Budget scheitert, sondern am Kraftwerkspark, und dass nicht mehr Geld hilft, sondern nur eine andere Anlage oder eine kleinere Zusage. (c) Sinngemäß: „99,99 % kosten nicht 5 % mehr als 99 %, sondern ein Vielfaches — der letzte Prozentpunkt ist bereits 4,5-mal so teuer wie der erste. Sagen Sie mir, was ein Ausfalltag das Unternehmen kostet, dann rechne ich aus, welche Quote sich lohnt. Und die Zusage gilt nur für die Wetterlagen, die wir modelliert haben — die Kältewelle gehört hinein.“</p>
<p><strong>9.7 — Budgeted Uncertainty.</strong></p>
<div class="sourceCode" id="cb14"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb14-1"><a href="#cb14-1" aria-hidden="true" tabindex="-1"></a>abzug <span class="op">=</span> cp.sum_largest(cp.multiply(UNSICHERHEIT, w), Gamma)</span>
<span id="cb14-2"><a href="#cb14-2" aria-hidden="true" tabindex="-1"></a>ziel <span class="op">=</span> cp.Maximize(MU_SCHAETZUNG <span class="op">@</span> w <span class="op">-</span> abzug <span class="op">-</span> <span class="fl">0.5</span><span class="op">*</span>LAMBDA<span class="op">*</span>cp.quad_form(w, SIGMA))</span></code></pre></div>
<p><span class="math inline">\Gamma = 0</span> entspricht dem nominalen Fall, <span class="math inline">\Gamma = n</span> dem vollen Worst Case. Dazwischen steuert <span class="math inline">\Gamma</span> die Vorsicht stufenlos — der praktisch nützlichste Bereich liegt meist bei <span class="math inline">\Gamma \approx \sqrt{n}</span>.</p>
@ -852,6 +853,8 @@ F(11{,}33) = 1 - \frac{(18-11{,}33)^2}{(18-6)(18-10)} = 1 - \frac{44{,}4}{96} =
<li>Die Differenz zwischen Kosten unter Unsicherheit und Kosten bei perfektem Wissen — eine <strong>Obergrenze</strong> für den Wert jeder Prognoseverbesserung.</li>
<li>Weil sie nur eine <strong>Menge</strong> möglicher Werte braucht, nicht deren Verteilung — sie optimiert gegen das schlechteste Element dieser Menge.</li>
<li>Für einfache Mengen (Box, Ellipsoid) lässt sich das innere Maximum geschlossen ausrechnen und wird zu einem Abzugsterm; das Gesamtproblem bleibt konvex.</li>
<li>Weil die Streuung der Summe <span class="math inline">\mathbf{a}^\top\mathbf{x}</span> nicht die Summe der Streuungen ist: <span class="math inline">\sqrt{\mathbf{x}^\top\boldsymbol{\Sigma}\mathbf{x}}</span> enthält die <strong>Kovarianzen</strong>. Ein fester Zuschlag je Variable wäre linear und würde deshalb übersehen, dass sich gegenläufige Größen teilweise aufheben — die Norm belohnt Mischung, ein linearer Aufschlag nicht.</li>
<li>Weil die Zusage nur so gut ist wie die unterstellte Verteilung. Die Normalverteilung kennt keine Ereignisse, in denen alle Quellen <strong>gleichzeitig</strong> ausfallen; ihre schwachen Korrelationen unterschätzen genau den Fall, der die Zusage bricht. Die szenariobasierte Formulierung hat diese Fälle in den Daten und trifft die Quote — erkauft mit Mehrkosten und mit dem Risiko, sich an die verwendete Stichprobe anzupassen.</li>
</ol>
<hr />
<h2 id="sec:loesungen-dynamische-programmierung">A.13 Lösungen zu Kapitel „Dynamische Programmierung — Die Bellman-Gleichung und Order-Execution“</h2>

View file

@ -1 +1 @@
window.OR_PROGRAMME = ["Backtest_Fallen.py", "Bausteine_Vorlage.py", "Bellman_Minimalbeispiel.py", "Benchmark_Skalierung.py", "Betriebsueberwachung.py", "Big_M_Falle.py", "Bot_Allokation.py", "Brute_Force_Vergleich.py", "CP_SAT_Statusfaelle.py", "CP_SAT_Vertretungssystem.py", "CVaR_Portfolio.py", "Data_Snooping.py", "Diversifikation_Demo.py", "Dualitaet_Nachweis.py", "Ein_System_Vier_Ansaetze.py", "Entropie_Maximierte_Allokation.py", "Erklaerbarkeit.py", "Excel_Bruecke.py", "Finanzdaten_Ledoit_Wolf.py", "Fluch_des_Durchschnitts.py", "Infeasibility_Diagnose.py", "Installationstest.py", "JobShop_Intervalle.py", "KKT_Nachweis.py", "Konfliktsuche.py", "Konvexitaet_Demo.py", "Kovarianz_Falle.py", "Kraftwerkseinsatz.py", "Large_Neighborhood_Search.py", "Lokale_Optima_Multistart.py", "MILP_Portfolio_Fixgebuehren.py", "Markowitz_CVXPY.py", "Matrixform.py", "Mehrperiodige_Order_Execution.py", "Mehrziel_Pareto.py", "Metaheuristik_vs_Exakt.py", "Min_Cost_Flow.py", "Modellierungsschichten.py", "Monte_Carlo.py", "Mutationstest.py", "Optimierungsdienst.py", "Predict_then_Optimize.py", "Propagation_Demo.py", "QP_Grundlagen.py", "QuantitativeTradingEngine.py", "Renditen_Vergleich.py", "Renditeschaetzung_Falle.py", "Robuste_Optimierung.py", "Rucksack.py", "Runden_Gegenbeispiel.py", "Schaetzrauschen_Demo.py", "Sensitivitaetsanalyse.py", "Simplex_Tableau_LP.py", "Simulated_Annealing.py", "Skalierung_Kondition.py", "Solver_Wahl.py", "Solverstatus_und_Gap.py", "Solverwechsel_CPSAT_HiGHS.py", "Spaltengenerierung.py", "Stochastische_Optimierung.py", "Strafgewichte.py", "Strukturbruecke.py", "Toleranzen_und_Entartung.py", "VRP_Flotten_Routing.py", "VRP_Kapazitaetsfalle.py", "VaR_CVaR_Demo.py", "Vektorisierte_Modellgenerierung.py", "Visualisierung_Loesungsraum.py", "Vom_Wunsch_zum_Modell.py", "Warmstart_Effekt.py", "Zuordnung_Ungarisch.py", "or_kern.py", "test_or_kern.py"];
window.OR_PROGRAMME = ["Backtest_Fallen.py", "Bausteine_Vorlage.py", "Bellman_Minimalbeispiel.py", "Benchmark_Skalierung.py", "Betriebsueberwachung.py", "Big_M_Falle.py", "Bot_Allokation.py", "Brute_Force_Vergleich.py", "CP_SAT_Statusfaelle.py", "CP_SAT_Vertretungssystem.py", "CVaR_Portfolio.py", "Chance_Constraints.py", "Data_Snooping.py", "Diversifikation_Demo.py", "Dualitaet_Nachweis.py", "Ein_System_Vier_Ansaetze.py", "Entropie_Maximierte_Allokation.py", "Erklaerbarkeit.py", "Excel_Bruecke.py", "Finanzdaten_Ledoit_Wolf.py", "Fluch_des_Durchschnitts.py", "Infeasibility_Diagnose.py", "Installationstest.py", "JobShop_Intervalle.py", "KKT_Nachweis.py", "Konfliktsuche.py", "Konvexitaet_Demo.py", "Kovarianz_Falle.py", "Kraftwerkseinsatz.py", "Large_Neighborhood_Search.py", "Lokale_Optima_Multistart.py", "MILP_Portfolio_Fixgebuehren.py", "Markowitz_CVXPY.py", "Matrixform.py", "Mehrperiodige_Order_Execution.py", "Mehrziel_Pareto.py", "Metaheuristik_vs_Exakt.py", "Min_Cost_Flow.py", "Modellierungsschichten.py", "Monte_Carlo.py", "Mutationstest.py", "Optimierungsdienst.py", "Predict_then_Optimize.py", "Propagation_Demo.py", "QP_Grundlagen.py", "QuantitativeTradingEngine.py", "Renditen_Vergleich.py", "Renditeschaetzung_Falle.py", "Robuste_Optimierung.py", "Rucksack.py", "Runden_Gegenbeispiel.py", "Schaetzrauschen_Demo.py", "Sensitivitaetsanalyse.py", "Simplex_Tableau_LP.py", "Simulated_Annealing.py", "Skalierung_Kondition.py", "Solver_Wahl.py", "Solverstatus_und_Gap.py", "Solverwechsel_CPSAT_HiGHS.py", "Spaltengenerierung.py", "Stochastische_Optimierung.py", "Strafgewichte.py", "Strukturbruecke.py", "Toleranzen_und_Entartung.py", "VRP_Flotten_Routing.py", "VRP_Kapazitaetsfalle.py", "VaR_CVaR_Demo.py", "Vektorisierte_Modellgenerierung.py", "Visualisierung_Loesungsraum.py", "Vom_Wunsch_zum_Modell.py", "Warmstart_Effekt.py", "Zuordnung_Ungarisch.py", "or_kern.py", "test_or_kern.py"];

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,228 @@
#!/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)

File diff suppressed because one or more lines are too long

View file

@ -103,15 +103,16 @@
<div class="card card-blick">
<blockquote>
<p><strong>📌 Kapitel auf einen Blick</strong></p>
<p><strong>Worum geht es?</strong> Bisher waren alle Parameter bekannt. In der Realität sind Nachfrage, Rendite und Fahrzeit <strong>Zufallsgrößen</strong>. Dieses Kapitel zeigt drei Wege, damit umzugehen — und warum „einfach den Mittelwert einsetzen“ der schlechteste davon ist.</p>
<p><strong>Worum geht es?</strong> Bisher waren alle Parameter bekannt. In der Realität sind Nachfrage, Rendite und Fahrzeit <strong>Zufallsgrößen</strong>. Dieses Kapitel zeigt vier Wege, damit umzugehen — und warum „einfach den Mittelwert einsetzen“ der schlechteste davon ist.</p>
<p><strong>Voraussetzungen:</strong> <a href="lp.html#kap-lp">Kapitel 5</a>, <a href="qp-nlp.html#kap-qp-nlp">Kapitel 11</a>. Grundbegriffe der Statistik (Erwartungswert, Quantil).</p>
<p><strong>Danach können Sie:</strong> Ein zweistufiges stochastisches Modell aufstellen, Monte-Carlo zur Risikomessung einsetzen, ein robustes Modell für den Worst Case formulieren — und erkennen, wann Rechnen mit Mittelwerten zulässig ist und wann es systematisch danebengeht.</p>
<p><strong>Zeitbedarf:</strong> ca. 5,5 Stunden.</p>
<p><strong>Danach können Sie:</strong> 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.</p>
<p><strong>Zeitbedarf:</strong> ca. 6,5 Stunden.</p>
<p><strong>Programme:</strong><br />
<code>Fluch_des_Durchschnitts.py</code><br />
<code>Monte_Carlo.py</code><br />
<code>Stochastische_Optimierung.py</code><br />
<code>Robuste_Optimierung.py</code></p>
<code>Robuste_Optimierung.py</code><br />
<code>Chance_Constraints.py</code></p>
<p><strong>Notebook:</strong> <a href="Notebooks_04/unsicherheit.ipynb">unsicherheit.ipynb</a><br />
<a href="https://colab.research.google.com/github/dschlueter/or-mit-python/blob/main/Notebooks_04/unsicherheit.ipynb">In Google Colab öffnen</a></p>
</blockquote>
@ -220,7 +221,8 @@ q^{*} = \frac{c_{\text{zu wenig}}}{c_{\text{zu wenig}} + c_{\text{zu viel}}}
<li>… ein <strong>zweistufiges stochastisches Programm</strong> mit Recourse formulieren.</li>
<li>… Monte-Carlo einsetzen, um Verteilungen statt Punktschätzungen zu bewerten.</li>
<li>… ein <strong>robustes</strong> Modell mit Unsicherheitsmenge aufstellen und seinen Preis beziffern.</li>
<li>… begründen, welcher der drei Ansätze für ein gegebenes Problem passt.</li>
<li>… eine <strong>Wahrscheinlichkeitszusage</strong> („mit 95 % Sicherheit“) als Chance Constraint schreiben — analytisch als Kegelbedingung oder szenariobasiert mit Big-M — und den Preis eines Prozentpunkts Sicherheit beziffern.</li>
<li>… begründen, welcher der vier Ansätze für ein gegebenes Problem passt.</li>
<li>… das <strong>kritische Verhältnis</strong> eines Newsvendor-Problems aufstellen und daraus die optimale Bestellmenge als Quantil ablesen.</li>
<li>… eine Terminzusage als Quantil formulieren statt als Mittelwert.</li>
</ol>
@ -341,17 +343,18 @@ nicht beim Mittelwert 225 - typisch fuer asymmetrische Kostenfunktionen.</code><
<blockquote>
<p><strong>🎯 Merksatz</strong> Der Durchschnittskunde kauft nie. Das Durchschnittsszenario tritt nie ein. Wer mit Mittelwerten plant, optimiert eine Welt, die es nicht gibt.</p>
</blockquote>
<h3 id="die-drei-ansätze-im-überblick">Die drei Ansätze im Überblick</h3>
<h3 id="die-vier-ansätze-im-überblick">Die vier Ansätze im Überblick</h3>
<figure>
<img src="bilder_04/kap09_unsicherheit_ansaetze.svg" alt="Abb. 12.1: Ansätze für Optimierung unter Unsicherheit" />
<figcaption aria-hidden="true">Abb. 12.1: Ansätze für Optimierung unter Unsicherheit</figcaption>
</figure>
<table>
<colgroup>
<col style="width: 25%" />
<col style="width: 25%" />
<col style="width: 25%" />
<col style="width: 25%" />
<col style="width: 20%" />
<col style="width: 20%" />
<col style="width: 20%" />
<col style="width: 20%" />
<col style="width: 20%" />
</colgroup>
<thead>
<tr class="header">
@ -359,6 +362,7 @@ nicht beim Mittelwert 225 - typisch fuer asymmetrische Kostenfunktionen.</code><
<th><strong>Monte-Carlo</strong></th>
<th><strong>Stochastische Programmierung</strong></th>
<th><strong>Robuste Optimierung</strong></th>
<th><strong>Chance Constraints</strong></th>
</tr>
</thead>
<tbody>
@ -367,39 +371,46 @@ nicht beim Mittelwert 225 - typisch fuer asymmetrische Kostenfunktionen.</code><
<td>Verteilung</td>
<td>Szenarien <strong>mit Wahrscheinlichkeiten</strong></td>
<td>nur eine <strong>Menge</strong> möglicher Werte</td>
<td>Verteilung <strong>oder</strong> Szenarien</td>
</tr>
<tr class="even">
<td>Optimiert</td>
<td>nichts (bewertet nur)</td>
<td><strong>Erwartungswert</strong> über Szenarien</td>
<td><strong>Worst Case</strong> in der Menge</td>
<td>Kosten bei <strong>zugesicherter Quote</strong></td>
</tr>
<tr class="odd">
<td>Typische Frage</td>
<td>„Wie riskant ist dieser Plan?“</td>
<td>„Was ist im Mittel am besten?“</td>
<td>„Was hält auch im schlimmsten Fall?“</td>
<td>„Was hält in 95 % der Fälle?“</td>
</tr>
<tr class="even">
<td>Stärke</td>
<td>beliebige Kennzahlen, sehr flexibel</td>
<td>nutzt Wahrscheinlichkeiten voll aus</td>
<td>keine Verteilungsannahme nötig</td>
<td>die Zusage steht <strong>im Modell</strong></td>
</tr>
<tr class="odd">
<td>Schwäche</td>
<td>keine Optimierung</td>
<td>Wahrscheinlichkeiten müssen stimmen</td>
<td>oft übervorsichtig, kostet Ertrag</td>
<td>Zusage gilt nur für die unterstellte Verteilung</td>
</tr>
<tr class="even">
<td>Kapitel</td>
<td>9.2</td>
<td>9.3</td>
<td>9.4</td>
<td>Nachzulesen in</td>
<td><a href="#sec:unsicherheit-monte-carlo-simulation">Abschnitt 12.4</a></td>
<td><a href="#sec:unsicherheit-zweistufige-stochastische-programmierung">Abschnitt 12.5</a></td>
<td><a href="#sec:unsicherheit-robuste-optimierung-gegen-den-worst-case">Abschnitt 12.6</a></td>
<td><a href="#sec:unsicherheit-chance-constraints">Abschnitt 12.7</a></td>
</tr>
</tbody>
</table>
<p><strong>Der Unterschied zwischen den letzten beiden</strong> ist der, an dem die meisten Diskussionen hängenbleiben. Die robuste Optimierung sagt: <em>„Es darf <strong>nie</strong> schiefgehen“</em> — und zahlt dafür. Die Chance Constraint sagt: <em>„Es darf in höchstens 5 % der Fälle schiefgehen“</em> — und macht damit verhandelbar, was die Sicherheit kosten darf.</p>
<hr />
<h2 id="sec:unsicherheit-monte-carlo-simulation">12.4 Monte-Carlo-Simulation</h2>
<p><strong>Monte-Carlo</strong> heißt schlicht: Ziehe sehr viele Zufallsszenarien, werte für jedes den Plan aus, und betrachte die <strong>Verteilung</strong> der Ergebnisse statt eines einzigen Werts.</p>
@ -828,34 +839,375 @@ deutlich teurer ist als Leerlauf (5 EUR).
<p><strong>⚠️ Der Preis der Robustheit</strong> Robuste Modelle sind <strong>konservativ</strong>. Wer gegen den absoluten Worst Case absichert, zahlt im Normalfall drauf — oft erheblich. Drei Gegenmittel: 1. <strong>Unsicherheitsmenge realistisch wählen.</strong> Nicht „alles kann passieren“, sondern „Abweichungen bis zu einer Standardabweichung“. 2. <strong>Budgeted Uncertainty (Bertsimas/Sim)</strong>: Man nimmt an, dass höchstens <span class="math inline">\Gamma</span> von <span class="math inline">n</span> Parametern gleichzeitig ihren Worst Case annehmen. <span class="math inline">\Gamma</span> steuert die Vorsicht stufenlos. 3. <strong>Nutzen und Preis immer beziffern</strong> — genau wie im Programm oben. Ohne diese zwei Zahlen ist die Diskussion „robust oder nicht“ Glaubenssache.</p>
</blockquote>
<hr />
<h2 id="sec:unsicherheit-uebungsaufgaben">12.7 Übungsaufgaben</h2>
<h2 id="sec:unsicherheit-chance-constraints">12.7 Wahrscheinlichkeitsbeschränkungen: „mit 95 % Sicherheit“</h2>
<p>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 <strong>Wahrscheinlichkeit</strong>: <em>Mit welcher Sicherheit hält der Plan?</em></p>
<p>Die <strong>Wahrscheinlichkeitsbeschränkung</strong> schreibt genau das ins Modell:</p>
<p><span class="math display">
\mathbb{P}\!\left(\mathbf{a}^\top\mathbf{x} \ge b\right) \ge 1 - \alpha
</span></p>
<blockquote>
<p><strong>📐 Formel-Lesehilfe</strong> * <span class="math inline">\mathbf{a}</span> — der <strong>unsichere</strong> Vektor: Verfügbarkeiten, Erträge, Fahrzeiten. Er ist keine Zahl, sondern eine Zufallsgröße. * <span class="math inline">\mathbf{x}</span> — Ihre Entscheidung. Sie fällt, <strong>bevor</strong> <span class="math inline">\mathbf{a}</span> sich zeigt. * <span class="math inline">\alpha</span> — die Ausfallwahrscheinlichkeit, die Sie akzeptieren. <span class="math inline">\alpha = 0{,}05</span> heißt „in höchstens 5 % der Fälle darf es schiefgehen“.</p>
<p><strong>Ohne Formel gesagt:</strong> „Plane so, dass die Zusage in 95 von 100 Fällen hält — und sag mir, was das kostet.“</p>
</blockquote>
<p><strong>Das Problem daran.</strong> 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.</p>
<h3 id="weg-1-normalverteilung-die-bedingung-wird-ein-kegel">Weg 1: Normalverteilung — die Bedingung wird ein Kegel</h3>
<p>Ist <span class="math inline">\mathbf{a} \sim \mathcal{N}(\hat{\mathbf{a}}, \boldsymbol{\Sigma})</span>, dann ist <span class="math inline">\mathbf{a}^\top\mathbf{x}</span> für festes <span class="math inline">\mathbf{x}</span> selbst normalverteilt, mit Erwartungswert <span class="math inline">\hat{\mathbf{a}}^\top\mathbf{x}</span> und Standardabweichung <span class="math inline">\sqrt{\mathbf{x}^\top\boldsymbol{\Sigma}\mathbf{x}}</span>. Damit lässt sich die Wahrscheinlichkeit durch ein Quantil ersetzen:</p>
<p><span class="math display">
\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)
</span></p>
<p>Das ist eine <strong>Second-Order-Cone-Bedingung</strong> — dieselbe Bauform, die <a href="qp-nlp.html#kap-qp-nlp">Kapitel 11</a> eingeführt hat, und CVXPY löst sie ohne Umstände.</p>
<blockquote>
<p><strong>🎯 Warum eine Norm und keine Summe</strong> Der Sicherheitszuschlag <span class="math inline">\lVert\mathbf{L}^\top\mathbf{x}\rVert_2</span> ist <strong>keine</strong> Summe von Einzelzuschlägen je Anlage. Genau darin steckt der Nutzen der Mischung: Zwei gegenläufige Quellen schwanken <strong>gemeinsam</strong> 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.</p>
</blockquote>
<h3 id="weg-2-szenarien-big-m-ohne-verteilungsannahme">Weg 2: Szenarien — Big-M ohne Verteilungsannahme</h3>
<p>Liegen statt einer Verteilung nur <span class="math inline">S</span> beobachtete Szenarien vor, bekommt jedes eine Binärvariable <span class="math inline">z_s</span>, die sagt, ob es verletzt werden <strong>darf</strong>:</p>
<p><span class="math display">
\begin{aligned}
\mathbf{a}_s^\top\mathbf{x} &amp;\ge b - M z_s &amp;&amp; \forall s\\
\sum_{s} z_s &amp;\le \alpha S\\
z_s &amp;\in \{0,1\}
\end{aligned}
</span></p>
<p>Aus dem Kegelproblem wird ein MILP mit <span class="math inline">S</span> Binärvariablen. Für <span class="math inline">M</span> gilt, was <a href="milp.html#kap-milp">Kapitel 6</a> über die Big-M-Falle sagt: <strong>so klein wie möglich</strong>. Hier ist <span class="math inline">M = b</span> die kleinste gültige Schranke, denn <span class="math inline">\mathbf{a}_s^\top\mathbf{x} \ge 0</span> — größer kann eine Verletzung gar nicht ausfallen.</p>
<h3 id="der-fall-ein-kraftwerkspark-mit-500-mw-zusage">Der Fall: ein Kraftwerkspark mit 500 MW Zusage</h3>
<div class="sourceCode" id="cb10"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb10-2"><a href="#cb10-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-3"><a href="#cb10-3" aria-hidden="true" tabindex="-1"></a><span class="co"># Chance_Constraints.py</span></span>
<span id="cb10-4"><a href="#cb10-4" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb10-5"><a href="#cb10-5" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel Unsicherheit: Wahrscheinlichkeitsbeschraenkungen.</span></span>
<span id="cb10-6"><a href="#cb10-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-7"><a href="#cb10-7" aria-hidden="true" tabindex="-1"></a><span class="co">Das Management fragt selten &quot;was ist im Mittel am besten?&quot;, sondern &quot;mit</span></span>
<span id="cb10-8"><a href="#cb10-8" aria-hidden="true" tabindex="-1"></a><span class="co">welcher Sicherheit haelt der Plan?&quot;. Genau das formuliert eine Chance</span></span>
<span id="cb10-9"><a href="#cb10-9" aria-hidden="true" tabindex="-1"></a><span class="co">Constraint: P(Versorgung &gt;= Bedarf) &gt;= 1 - alpha.</span></span>
<span id="cb10-10"><a href="#cb10-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-11"><a href="#cb10-11" aria-hidden="true" tabindex="-1"></a><span class="co">Gezeigt werden beide Wege dorthin, an derselben Instanz:</span></span>
<span id="cb10-12"><a href="#cb10-12" aria-hidden="true" tabindex="-1"></a><span class="co"> (a) analytisch - unter Normalverteilungsannahme wird daraus eine</span></span>
<span id="cb10-13"><a href="#cb10-13" aria-hidden="true" tabindex="-1"></a><span class="co"> Second-Order-Cone-Bedingung, loesbar mit CVXPY</span></span>
<span id="cb10-14"><a href="#cb10-14" aria-hidden="true" tabindex="-1"></a><span class="co"> (b) szenariobasiert - Big-M mit Binaervariablen, fuer beliebige</span></span>
<span id="cb10-15"><a href="#cb10-15" aria-hidden="true" tabindex="-1"></a><span class="co"> empirische Verteilungen</span></span>
<span id="cb10-16"><a href="#cb10-16" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-17"><a href="#cb10-17" aria-hidden="true" tabindex="-1"></a><span class="co">Und die beiden Messungen, auf die es ankommt: Was kostet ein Prozentpunkt</span></span>
<span id="cb10-18"><a href="#cb10-18" aria-hidden="true" tabindex="-1"></a><span class="co">Versorgungssicherheit - und haelt die Zusage auch dann, wenn die Verteilung</span></span>
<span id="cb10-19"><a href="#cb10-19" aria-hidden="true" tabindex="-1"></a><span class="co">nicht normal ist?</span></span>
<span id="cb10-20"><a href="#cb10-20" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-21"><a href="#cb10-21" aria-hidden="true" tabindex="-1"></a><span class="co">Solver: CLARABEL (Kegel) und SciPy/HiGHS (gemischt-ganzzahlig). Weder ortools</span></span>
<span id="cb10-22"><a href="#cb10-22" aria-hidden="true" tabindex="-1"></a><span class="co">noch ein direkter highspy-Import, damit alles in einem Prozess laeuft.</span></span>
<span id="cb10-23"><a href="#cb10-23" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb10-24"><a href="#cb10-24" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-25"><a href="#cb10-25" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> cvxpy <span class="im">as</span> cp</span>
<span id="cb10-26"><a href="#cb10-26" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb10-27"><a href="#cb10-27" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> scipy.stats <span class="im">import</span> norm</span>
<span id="cb10-28"><a href="#cb10-28" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-29"><a href="#cb10-29" aria-hidden="true" tabindex="-1"></a><span class="co"># --- Der Kraftwerkspark ----------------------------------------------------</span></span>
<span id="cb10-30"><a href="#cb10-30" aria-hidden="true" tabindex="-1"></a><span class="co"># Die Verfuegbarkeit ist der Anteil, den eine installierte MW im Mittel</span></span>
<span id="cb10-31"><a href="#cb10-31" aria-hidden="true" tabindex="-1"></a><span class="co"># wirklich liefert: bei Wind und Sonne klein und stark schwankend, bei Gas und</span></span>
<span id="cb10-32"><a href="#cb10-32" aria-hidden="true" tabindex="-1"></a><span class="co"># Biomasse gross und stabil. Die Ausbaugrenze ist der Standort - Flaeche,</span></span>
<span id="cb10-33"><a href="#cb10-33" aria-hidden="true" tabindex="-1"></a><span class="co"># Genehmigung, Brennstoffversorgung.</span></span>
<span id="cb10-34"><a href="#cb10-34" aria-hidden="true" tabindex="-1"></a>TECHNIK <span class="op">=</span> [<span class="st">&quot;Gaskraftwerk&quot;</span>, <span class="st">&quot;Windpark&quot;</span>, <span class="st">&quot;Solarpark&quot;</span>, <span class="st">&quot;Biomasse&quot;</span>]</span>
<span id="cb10-35"><a href="#cb10-35" aria-hidden="true" tabindex="-1"></a>KOSTEN <span class="op">=</span> np.array([<span class="fl">65_000.0</span>, <span class="fl">24_000.0</span>, <span class="fl">12_000.0</span>, <span class="fl">60_000.0</span>]) <span class="co"># EUR je MW und Jahr</span></span>
<span id="cb10-36"><a href="#cb10-36" aria-hidden="true" tabindex="-1"></a>VERFUEGBAR <span class="op">=</span> np.array([<span class="fl">0.92</span>, <span class="fl">0.35</span>, <span class="fl">0.18</span>, <span class="fl">0.85</span>])</span>
<span id="cb10-37"><a href="#cb10-37" aria-hidden="true" tabindex="-1"></a>STREUUNG <span class="op">=</span> np.array([<span class="fl">0.05</span>, <span class="fl">0.16</span>, <span class="fl">0.10</span>, <span class="fl">0.04</span>])</span>
<span id="cb10-38"><a href="#cb10-38" aria-hidden="true" tabindex="-1"></a>GRENZE <span class="op">=</span> np.array([<span class="fl">400.0</span>, <span class="fl">250.0</span>, <span class="fl">600.0</span>, <span class="fl">350.0</span>]) <span class="co"># MW</span></span>
<span id="cb10-39"><a href="#cb10-39" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-40"><a href="#cb10-40" aria-hidden="true" tabindex="-1"></a><span class="co"># Wind und Sonne sind leicht gegenlaeufig: Ein Tiefdruckgebiet bringt Wind und</span></span>
<span id="cb10-41"><a href="#cb10-41" aria-hidden="true" tabindex="-1"></a><span class="co"># Wolken zugleich. Genau diese Korrelation macht die Bedingung zu einem Kegel</span></span>
<span id="cb10-42"><a href="#cb10-42" aria-hidden="true" tabindex="-1"></a><span class="co"># und nicht zu einer Summe unabhaengiger Einzelzuschlaege.</span></span>
<span id="cb10-43"><a href="#cb10-43" aria-hidden="true" tabindex="-1"></a>KORRELATION <span class="op">=</span> np.array([</span>
<span id="cb10-44"><a href="#cb10-44" aria-hidden="true" tabindex="-1"></a> [<span class="fl">1.00</span>, <span class="fl">0.00</span>, <span class="fl">0.00</span>, <span class="fl">0.05</span>],</span>
<span id="cb10-45"><a href="#cb10-45" aria-hidden="true" tabindex="-1"></a> [<span class="fl">0.00</span>, <span class="fl">1.00</span>, <span class="op">-</span><span class="fl">0.25</span>, <span class="fl">0.00</span>],</span>
<span id="cb10-46"><a href="#cb10-46" aria-hidden="true" tabindex="-1"></a> [<span class="fl">0.00</span>, <span class="op">-</span><span class="fl">0.25</span>, <span class="fl">1.00</span>, <span class="fl">0.00</span>],</span>
<span id="cb10-47"><a href="#cb10-47" aria-hidden="true" tabindex="-1"></a> [<span class="fl">0.05</span>, <span class="fl">0.00</span>, <span class="fl">0.00</span>, <span class="fl">1.00</span>],</span>
<span id="cb10-48"><a href="#cb10-48" aria-hidden="true" tabindex="-1"></a>])</span>
<span id="cb10-49"><a href="#cb10-49" aria-hidden="true" tabindex="-1"></a>SIGMA <span class="op">=</span> np.diag(STREUUNG) <span class="op">@</span> KORRELATION <span class="op">@</span> np.diag(STREUUNG)</span>
<span id="cb10-50"><a href="#cb10-50" aria-hidden="true" tabindex="-1"></a>WURZEL <span class="op">=</span> np.linalg.cholesky(SIGMA) <span class="co"># L mit L @ L.T == SIGMA</span></span>
<span id="cb10-51"><a href="#cb10-51" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-52"><a href="#cb10-52" aria-hidden="true" tabindex="-1"></a>BEDARF <span class="op">=</span> <span class="fl">500.0</span> <span class="co"># MW, die gesichert bereitstehen muessen</span></span>
<span id="cb10-53"><a href="#cb10-53" aria-hidden="true" tabindex="-1"></a>N <span class="op">=</span> <span class="bu">len</span>(TECHNIK)</span>
<span id="cb10-54"><a href="#cb10-54" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-55"><a href="#cb10-55" aria-hidden="true" tabindex="-1"></a><span class="co"># Die Kaeltewelle mit Dunkelflaute: selten, aber sie trifft alles zugleich.</span></span>
<span id="cb10-56"><a href="#cb10-56" aria-hidden="true" tabindex="-1"></a><span class="co"># Wind und Sonne brechen fast vollstaendig weg - und, das ist der Punkt, das</span></span>
<span id="cb10-57"><a href="#cb10-57" aria-hidden="true" tabindex="-1"></a><span class="co"># Gaskraftwerk liefert ebenfalls weniger, weil bei Frost der Netzdruck faellt.</span></span>
<span id="cb10-58"><a href="#cb10-58" aria-hidden="true" tabindex="-1"></a><span class="co"># Eine Kovarianzmatrix mit Korrelationen um null kann das nicht ausdruecken.</span></span>
<span id="cb10-59"><a href="#cb10-59" aria-hidden="true" tabindex="-1"></a>P_KAELTEWELLE <span class="op">=</span> <span class="fl">0.08</span></span>
<span id="cb10-60"><a href="#cb10-60" aria-hidden="true" tabindex="-1"></a>EINBRUCH <span class="op">=</span> np.array([<span class="fl">0.72</span>, <span class="fl">0.10</span>, <span class="fl">0.15</span>, <span class="fl">0.88</span>])</span>
<span id="cb10-61"><a href="#cb10-61" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-62"><a href="#cb10-62" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-63"><a href="#cb10-63" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> mittelwertplan():</span>
<span id="cb10-64"><a href="#cb10-64" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Plant mit den Erwartungswerten - ignoriert die Streuung vollstaendig.&quot;&quot;&quot;</span></span>
<span id="cb10-65"><a href="#cb10-65" aria-hidden="true" tabindex="-1"></a> x <span class="op">=</span> cp.Variable(N, nonneg<span class="op">=</span><span class="va">True</span>)</span>
<span id="cb10-66"><a href="#cb10-66" aria-hidden="true" tabindex="-1"></a> problem <span class="op">=</span> cp.Problem(cp.Minimize(KOSTEN <span class="op">@</span> x),</span>
<span id="cb10-67"><a href="#cb10-67" aria-hidden="true" tabindex="-1"></a> [VERFUEGBAR <span class="op">@</span> x <span class="op">&gt;=</span> BEDARF, x <span class="op">&lt;=</span> GRENZE])</span>
<span id="cb10-68"><a href="#cb10-68" aria-hidden="true" tabindex="-1"></a> problem.solve(solver<span class="op">=</span>cp.CLARABEL)</span>
<span id="cb10-69"><a href="#cb10-69" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> problem.value, x.value</span>
<span id="cb10-70"><a href="#cb10-70" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-71"><a href="#cb10-71" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-72"><a href="#cb10-72" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> chance_constraint_analytisch(alpha):</span>
<span id="cb10-73"><a href="#cb10-73" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;</span></span>
<span id="cb10-74"><a href="#cb10-74" aria-hidden="true" tabindex="-1"></a><span class="co"> P(a^T x &gt;= BEDARF) &gt;= 1 - alpha unter a ~ N(VERFUEGBAR, SIGMA).</span></span>
<span id="cb10-75"><a href="#cb10-75" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-76"><a href="#cb10-76" aria-hidden="true" tabindex="-1"></a><span class="co"> Aequivalent zu: VERFUEGBAR^T x - z * ||L^T x||_2 &gt;= BEDARF</span></span>
<span id="cb10-77"><a href="#cb10-77" aria-hidden="true" tabindex="-1"></a><span class="co"> mit z = Phi^-1(1 - alpha). Das ist eine Second-Order-Cone-Bedingung: Der</span></span>
<span id="cb10-78"><a href="#cb10-78" aria-hidden="true" tabindex="-1"></a><span class="co"> Sicherheitszuschlag ist eine Norm ueber x, kein fester Aufschlag je Anlage.</span></span>
<span id="cb10-79"><a href="#cb10-79" aria-hidden="true" tabindex="-1"></a><span class="co"> Deshalb belohnt sie Mischung - zwei gegenlaeufige Quellen schwanken</span></span>
<span id="cb10-80"><a href="#cb10-80" aria-hidden="true" tabindex="-1"></a><span class="co"> gemeinsam weniger als jede fuer sich.</span></span>
<span id="cb10-81"><a href="#cb10-81" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb10-82"><a href="#cb10-82" aria-hidden="true" tabindex="-1"></a> x <span class="op">=</span> cp.Variable(N, nonneg<span class="op">=</span><span class="va">True</span>)</span>
<span id="cb10-83"><a href="#cb10-83" aria-hidden="true" tabindex="-1"></a> z <span class="op">=</span> norm.ppf(<span class="dv">1</span> <span class="op">-</span> alpha)</span>
<span id="cb10-84"><a href="#cb10-84" aria-hidden="true" tabindex="-1"></a> bedingung <span class="op">=</span> VERFUEGBAR <span class="op">@</span> x <span class="op">-</span> z <span class="op">*</span> cp.norm(WURZEL.T <span class="op">@</span> x, <span class="dv">2</span>) <span class="op">&gt;=</span> BEDARF</span>
<span id="cb10-85"><a href="#cb10-85" aria-hidden="true" tabindex="-1"></a> problem <span class="op">=</span> cp.Problem(cp.Minimize(KOSTEN <span class="op">@</span> x), [bedingung, x <span class="op">&lt;=</span> GRENZE])</span>
<span id="cb10-86"><a href="#cb10-86" aria-hidden="true" tabindex="-1"></a> problem.solve(solver<span class="op">=</span>cp.CLARABEL)</span>
<span id="cb10-87"><a href="#cb10-87" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> problem.value, x.value</span>
<span id="cb10-88"><a href="#cb10-88" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-89"><a href="#cb10-89" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-90"><a href="#cb10-90" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> chance_constraint_szenarien(a, alpha):</span>
<span id="cb10-91"><a href="#cb10-91" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;</span></span>
<span id="cb10-92"><a href="#cb10-92" aria-hidden="true" tabindex="-1"></a><span class="co"> Dieselbe Zusage ohne Verteilungsannahme: Fuer jedes Szenario s sagt eine</span></span>
<span id="cb10-93"><a href="#cb10-93" aria-hidden="true" tabindex="-1"></a><span class="co"> Binaervariable z_s, ob es verletzt werden darf.</span></span>
<span id="cb10-94"><a href="#cb10-94" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-95"><a href="#cb10-95" aria-hidden="true" tabindex="-1"></a><span class="co"> a_s^T x &gt;= BEDARF - M * z_s fuer alle s</span></span>
<span id="cb10-96"><a href="#cb10-96" aria-hidden="true" tabindex="-1"></a><span class="co"> sum_s z_s &lt;= alpha * S</span></span>
<span id="cb10-97"><a href="#cb10-97" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-98"><a href="#cb10-98" aria-hidden="true" tabindex="-1"></a><span class="co"> M = BEDARF ist die kleinstmoegliche gueltige Schranke, denn a_s^T x &gt;= 0:</span></span>
<span id="cb10-99"><a href="#cb10-99" aria-hidden="true" tabindex="-1"></a><span class="co"> Groesser kann eine Verletzung gar nicht ausfallen. Ein unnoetig grosses M</span></span>
<span id="cb10-100"><a href="#cb10-100" aria-hidden="true" tabindex="-1"></a><span class="co"> wuerde die LP-Relaxierung aufweichen und die Suche verlangsamen - die</span></span>
<span id="cb10-101"><a href="#cb10-101" aria-hidden="true" tabindex="-1"></a><span class="co"> Big-M-Falle aus dem Kapitel Gemischt-ganzzahlige Optimierung.</span></span>
<span id="cb10-102"><a href="#cb10-102" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb10-103"><a href="#cb10-103" aria-hidden="true" tabindex="-1"></a> S <span class="op">=</span> <span class="bu">len</span>(a)</span>
<span id="cb10-104"><a href="#cb10-104" aria-hidden="true" tabindex="-1"></a> x <span class="op">=</span> cp.Variable(N, nonneg<span class="op">=</span><span class="va">True</span>)</span>
<span id="cb10-105"><a href="#cb10-105" aria-hidden="true" tabindex="-1"></a> z <span class="op">=</span> cp.Variable(S, boolean<span class="op">=</span><span class="va">True</span>)</span>
<span id="cb10-106"><a href="#cb10-106" aria-hidden="true" tabindex="-1"></a> problem <span class="op">=</span> cp.Problem(</span>
<span id="cb10-107"><a href="#cb10-107" aria-hidden="true" tabindex="-1"></a> cp.Minimize(KOSTEN <span class="op">@</span> x),</span>
<span id="cb10-108"><a href="#cb10-108" aria-hidden="true" tabindex="-1"></a> [a <span class="op">@</span> x <span class="op">&gt;=</span> BEDARF <span class="op">-</span> BEDARF <span class="op">*</span> z, cp.<span class="bu">sum</span>(z) <span class="op">&lt;=</span> alpha <span class="op">*</span> S, x <span class="op">&lt;=</span> GRENZE])</span>
<span id="cb10-109"><a href="#cb10-109" aria-hidden="true" tabindex="-1"></a> problem.solve(solver<span class="op">=</span>cp.SCIPY)</span>
<span id="cb10-110"><a href="#cb10-110" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> problem.value, x.value</span>
<span id="cb10-111"><a href="#cb10-111" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-112"><a href="#cb10-112" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-113"><a href="#cb10-113" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> ziehe_wetter(anzahl, seed):</span>
<span id="cb10-114"><a href="#cb10-114" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Mischverteilung: normales Wetter, mit P_KAELTEWELLE ein Einbruch.&quot;&quot;&quot;</span></span>
<span id="cb10-115"><a href="#cb10-115" aria-hidden="true" tabindex="-1"></a> rng <span class="op">=</span> np.random.default_rng(seed)</span>
<span id="cb10-116"><a href="#cb10-116" aria-hidden="true" tabindex="-1"></a> a <span class="op">=</span> rng.multivariate_normal(VERFUEGBAR, SIGMA, size<span class="op">=</span>anzahl)</span>
<span id="cb10-117"><a href="#cb10-117" aria-hidden="true" tabindex="-1"></a> getroffen <span class="op">=</span> rng.random(anzahl) <span class="op">&lt;</span> P_KAELTEWELLE</span>
<span id="cb10-118"><a href="#cb10-118" aria-hidden="true" tabindex="-1"></a> a[getroffen] <span class="op">*=</span> EINBRUCH</span>
<span id="cb10-119"><a href="#cb10-119" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> np.clip(a, <span class="fl">0.0</span>, <span class="fl">1.0</span>)</span>
<span id="cb10-120"><a href="#cb10-120" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-121"><a href="#cb10-121" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-122"><a href="#cb10-122" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> sicherheit(x, a):</span>
<span id="cb10-123"><a href="#cb10-123" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Anteil der Szenarien, in denen der Plan den Bedarf deckt.&quot;&quot;&quot;</span></span>
<span id="cb10-124"><a href="#cb10-124" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="bu">float</span>(np.mean(a <span class="op">@</span> x <span class="op">&gt;=</span> BEDARF))</span>
<span id="cb10-125"><a href="#cb10-125" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-126"><a href="#cb10-126" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-127"><a href="#cb10-127" aria-hidden="true" tabindex="-1"></a>KOPF <span class="op">=</span> (<span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;Plan&#39;</span><span class="sc">:&lt;20}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Kosten (EUR)&#39;</span><span class="sc">:&gt;13}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Gas&#39;</span><span class="sc">:&gt;4}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Wind&#39;</span><span class="sc">:&gt;4}</span><span class="ss"> &quot;</span></span>
<span id="cb10-128"><a href="#cb10-128" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;Sol&#39;</span><span class="sc">:&gt;4}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Bio&#39;</span><span class="sc">:&gt;4}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Normalwelt&#39;</span><span class="sc">:&gt;11}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;echte Welt&#39;</span><span class="sc">:&gt;11}</span><span class="ss">&quot;</span>)</span>
<span id="cb10-129"><a href="#cb10-129" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-130"><a href="#cb10-130" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-131"><a href="#cb10-131" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> zeile(name, kosten, x, in_normal, in_echt):</span>
<span id="cb10-132"><a href="#cb10-132" aria-hidden="true" tabindex="-1"></a> mix <span class="op">=</span> <span class="st">&quot; &quot;</span>.join(<span class="ss">f&quot;</span><span class="sc">{</span>w<span class="sc">:4.0f}</span><span class="ss">&quot;</span> <span class="cf">for</span> w <span class="kw">in</span> x)</span>
<span id="cb10-133"><a href="#cb10-133" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> (<span class="ss">f&quot;</span><span class="sc">{</span>name<span class="sc">:&lt;20}</span><span class="ss"> </span><span class="sc">{</span>kosten<span class="sc">:&gt;13,.0f}</span><span class="ss"> </span><span class="sc">{</span>mix<span class="sc">}</span><span class="ss"> &quot;</span></span>
<span id="cb10-134"><a href="#cb10-134" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>in_normal <span class="op">*</span> <span class="dv">100</span><span class="sc">:&gt;9.2f}</span><span class="ss"> % </span><span class="sc">{</span>in_echt <span class="op">*</span> <span class="dv">100</span><span class="sc">:&gt;9.2f}</span><span class="ss"> %&quot;</span>)</span>
<span id="cb10-135"><a href="#cb10-135" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-136"><a href="#cb10-136" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-137"><a href="#cb10-137" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> <span class="va">__name__</span> <span class="op">==</span> <span class="st">&quot;__main__&quot;</span>:</span>
<span id="cb10-138"><a href="#cb10-138" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-139"><a href="#cb10-139" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; WAHRSCHEINLICHKEITSBESCHRAENKUNGEN IM KRAFTWERKSPARK&quot;</span>)</span>
<span id="cb10-140"><a href="#cb10-140" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-141"><a href="#cb10-141" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Gesichert bereitzustellen: </span><span class="sc">{</span>BEDARF<span class="sc">:.0f}</span><span class="ss"> MW</span><span class="ch">\n</span><span class="ss">&quot;</span>)</span>
<span id="cb10-142"><a href="#cb10-142" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;Technologie&#39;</span><span class="sc">:&lt;14}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;EUR/MW&#39;</span><span class="sc">:&gt;8}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;verfuegbar&#39;</span><span class="sc">:&gt;11}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Streuung&#39;</span><span class="sc">:&gt;9}</span><span class="ss"> &quot;</span></span>
<span id="cb10-143"><a href="#cb10-143" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;Grenze&#39;</span><span class="sc">:&gt;8}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;EUR je erw. MW&#39;</span><span class="sc">:&gt;15}</span><span class="ss">&quot;</span>)</span>
<span id="cb10-144"><a href="#cb10-144" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-145"><a href="#cb10-145" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i, name <span class="kw">in</span> <span class="bu">enumerate</span>(TECHNIK):</span>
<span id="cb10-146"><a href="#cb10-146" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>name<span class="sc">:&lt;14}</span><span class="ss"> </span><span class="sc">{</span>KOSTEN[i]<span class="sc">:&gt;8,.0f}</span><span class="ss"> </span><span class="sc">{</span>VERFUEGBAR[i]<span class="sc">:&gt;10.2f}</span><span class="ss"> &quot;</span></span>
<span id="cb10-147"><a href="#cb10-147" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>STREUUNG[i]<span class="sc">:&gt;8.2f}</span><span class="ss"> </span><span class="sc">{</span>GRENZE[i]<span class="sc">:&gt;7.0f}</span><span class="ss"> &quot;</span></span>
<span id="cb10-148"><a href="#cb10-148" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>KOSTEN[i] <span class="op">/</span> VERFUEGBAR[i]<span class="sc">:&gt;15,.0f}</span><span class="ss">&quot;</span>)</span>
<span id="cb10-149"><a href="#cb10-149" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="ch">\n</span><span class="ss">Vollausbau liefert im Mittel </span><span class="sc">{</span>VERFUEGBAR <span class="op">@</span> GRENZE<span class="sc">:.0f}</span><span class="ss"> MW, &quot;</span></span>
<span id="cb10-150"><a href="#cb10-150" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;in der Kaeltewelle </span><span class="sc">{</span>(VERFUEGBAR <span class="op">*</span> EINBRUCH) <span class="op">@</span> GRENZE<span class="sc">:.0f}</span><span class="ss"> MW.&quot;</span>)</span>
<span id="cb10-151"><a href="#cb10-151" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-152"><a href="#cb10-152" aria-hidden="true" tabindex="-1"></a> <span class="co"># Zwei grosse, unabhaengige Testmengen. An ihnen wird JEDER Plan gemessen -</span></span>
<span id="cb10-153"><a href="#cb10-153" aria-hidden="true" tabindex="-1"></a> <span class="co"># einmal in der unterstellten Normalwelt, einmal in der echten Verteilung.</span></span>
<span id="cb10-154"><a href="#cb10-154" aria-hidden="true" tabindex="-1"></a> echt <span class="op">=</span> ziehe_wetter(<span class="dv">200_000</span>, seed<span class="op">=</span><span class="dv">771</span>)</span>
<span id="cb10-155"><a href="#cb10-155" aria-hidden="true" tabindex="-1"></a> normalwelt <span class="op">=</span> np.random.default_rng(<span class="dv">4711</span>).multivariate_normal(</span>
<span id="cb10-156"><a href="#cb10-156" aria-hidden="true" tabindex="-1"></a> VERFUEGBAR, SIGMA, size<span class="op">=</span><span class="dv">200_000</span>)</span>
<span id="cb10-157"><a href="#cb10-157" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-158"><a href="#cb10-158" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">&quot;</span> <span class="op">+</span> <span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-159"><a href="#cb10-159" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; (1) Mittelwertplan und Chance Constraints im Vergleich&quot;</span>)</span>
<span id="cb10-160"><a href="#cb10-160" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-161"><a href="#cb10-161" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(KOPF)</span>
<span id="cb10-162"><a href="#cb10-162" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-163"><a href="#cb10-163" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-164"><a href="#cb10-164" aria-hidden="true" tabindex="-1"></a> k_mittel, x_mittel <span class="op">=</span> mittelwertplan()</span>
<span id="cb10-165"><a href="#cb10-165" aria-hidden="true" tabindex="-1"></a> stufen <span class="op">=</span> [(<span class="st">&quot;Mittelwert&quot;</span>, k_mittel, sicherheit(x_mittel, normalwelt))]</span>
<span id="cb10-166"><a href="#cb10-166" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(zeile(<span class="st">&quot;Mittelwert&quot;</span>, k_mittel, x_mittel,</span>
<span id="cb10-167"><a href="#cb10-167" aria-hidden="true" tabindex="-1"></a> sicherheit(x_mittel, normalwelt), sicherheit(x_mittel, echt)))</span>
<span id="cb10-168"><a href="#cb10-168" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-169"><a href="#cb10-169" aria-hidden="true" tabindex="-1"></a> plaene <span class="op">=</span> {}</span>
<span id="cb10-170"><a href="#cb10-170" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> alpha <span class="kw">in</span> (<span class="fl">0.20</span>, <span class="fl">0.10</span>, <span class="fl">0.05</span>, <span class="fl">0.01</span>):</span>
<span id="cb10-171"><a href="#cb10-171" aria-hidden="true" tabindex="-1"></a> kosten, x <span class="op">=</span> chance_constraint_analytisch(alpha)</span>
<span id="cb10-172"><a href="#cb10-172" aria-hidden="true" tabindex="-1"></a> plaene[alpha] <span class="op">=</span> (kosten, x)</span>
<span id="cb10-173"><a href="#cb10-173" aria-hidden="true" tabindex="-1"></a> quote_normal <span class="op">=</span> sicherheit(x, normalwelt)</span>
<span id="cb10-174"><a href="#cb10-174" aria-hidden="true" tabindex="-1"></a> stufen.append((<span class="ss">f&quot;Zusage </span><span class="sc">{</span>(<span class="dv">1</span> <span class="op">-</span> alpha) <span class="op">*</span> <span class="dv">100</span><span class="sc">:.0f}</span><span class="ss"> %&quot;</span>, kosten, quote_normal))</span>
<span id="cb10-175"><a href="#cb10-175" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(zeile(<span class="ss">f&quot;Zusage </span><span class="sc">{</span>(<span class="dv">1</span> <span class="op">-</span> alpha) <span class="op">*</span> <span class="dv">100</span><span class="sc">:.0f}</span><span class="ss"> %&quot;</span>, kosten, x,</span>
<span id="cb10-176"><a href="#cb10-176" aria-hidden="true" tabindex="-1"></a> quote_normal, sicherheit(x, echt)))</span>
<span id="cb10-177"><a href="#cb10-177" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-178"><a href="#cb10-178" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st"> In der Normalwelt trifft jede Zusage ihren Wert - das Verfahren&quot;</span>)</span>
<span id="cb10-179"><a href="#cb10-179" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; rechnet richtig. Die Spalte &#39;echte Welt&#39; kommt in Teil (3).&quot;</span>)</span>
<span id="cb10-180"><a href="#cb10-180" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-181"><a href="#cb10-181" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">&quot;</span> <span class="op">+</span> <span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-182"><a href="#cb10-182" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; (2) Was kostet ein Prozentpunkt Versorgungssicherheit?&quot;</span>)</span>
<span id="cb10-183"><a href="#cb10-183" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-184"><a href="#cb10-184" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;von -&gt; nach&#39;</span><span class="sc">:&lt;26}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Prozentpunkte&#39;</span><span class="sc">:&gt;13}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Mehrkosten&#39;</span><span class="sc">:&gt;13}</span><span class="ss"> &quot;</span></span>
<span id="cb10-185"><a href="#cb10-185" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;EUR je Punkt&#39;</span><span class="sc">:&gt;13}</span><span class="ss">&quot;</span>)</span>
<span id="cb10-186"><a href="#cb10-186" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-187"><a href="#cb10-187" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> (n1, k1, q1), (n2, k2, q2) <span class="kw">in</span> <span class="bu">zip</span>(stufen, stufen[<span class="dv">1</span>:]):</span>
<span id="cb10-188"><a href="#cb10-188" aria-hidden="true" tabindex="-1"></a> d_punkte <span class="op">=</span> (q2 <span class="op">-</span> q1) <span class="op">*</span> <span class="dv">100</span></span>
<span id="cb10-189"><a href="#cb10-189" aria-hidden="true" tabindex="-1"></a> d_kosten <span class="op">=</span> k2 <span class="op">-</span> k1</span>
<span id="cb10-190"><a href="#cb10-190" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>n1 <span class="op">+</span> <span class="st">&#39; -&gt; &#39;</span> <span class="op">+</span> n2<span class="sc">:&lt;26}</span><span class="ss"> </span><span class="sc">{</span>d_punkte<span class="sc">:&gt;13.2f}</span><span class="ss"> </span><span class="sc">{</span>d_kosten<span class="sc">:&gt;13,.0f}</span><span class="ss"> &quot;</span></span>
<span id="cb10-191"><a href="#cb10-191" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>d_kosten <span class="op">/</span> d_punkte<span class="sc">:&gt;13,.0f}</span><span class="ss">&quot;</span>)</span>
<span id="cb10-192"><a href="#cb10-192" aria-hidden="true" tabindex="-1"></a> erst <span class="op">=</span> (stufen[<span class="dv">1</span>][<span class="dv">1</span>] <span class="op">-</span> stufen[<span class="dv">0</span>][<span class="dv">1</span>]) <span class="op">/</span> ((stufen[<span class="dv">1</span>][<span class="dv">2</span>] <span class="op">-</span> stufen[<span class="dv">0</span>][<span class="dv">2</span>]) <span class="op">*</span> <span class="dv">100</span>)</span>
<span id="cb10-193"><a href="#cb10-193" aria-hidden="true" tabindex="-1"></a> letzt <span class="op">=</span> (stufen[<span class="op">-</span><span class="dv">1</span>][<span class="dv">1</span>] <span class="op">-</span> stufen[<span class="op">-</span><span class="dv">2</span>][<span class="dv">1</span>]) <span class="op">/</span> ((stufen[<span class="op">-</span><span class="dv">1</span>][<span class="dv">2</span>] <span class="op">-</span> stufen[<span class="op">-</span><span class="dv">2</span>][<span class="dv">2</span>]) <span class="op">*</span> <span class="dv">100</span>)</span>
<span id="cb10-194"><a href="#cb10-194" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="ch">\n</span><span class="ss"> Der letzte Prozentpunkt kostet das </span><span class="sc">{</span>letzt <span class="op">/</span> erst<span class="sc">:.1f}</span><span class="ss">-fache des ersten.&quot;</span>)</span>
<span id="cb10-195"><a href="#cb10-195" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; Sicherheit ist konvex bepreist - genau deshalb muss jemand&quot;</span>)</span>
<span id="cb10-196"><a href="#cb10-196" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; entscheiden, wie viel davon das Unternehmen kaufen will.&quot;</span>)</span>
<span id="cb10-197"><a href="#cb10-197" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-198"><a href="#cb10-198" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">&quot;</span> <span class="op">+</span> <span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-199"><a href="#cb10-199" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; (3) Und wenn die Verteilung nicht normal ist?&quot;</span>)</span>
<span id="cb10-200"><a href="#cb10-200" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-201"><a href="#cb10-201" aria-hidden="true" tabindex="-1"></a> k_soc, x_soc <span class="op">=</span> plaene[<span class="fl">0.05</span>]</span>
<span id="cb10-202"><a href="#cb10-202" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Die echte Welt kennt die Kaeltewelle mit Dunkelflaute: in &quot;</span></span>
<span id="cb10-203"><a href="#cb10-203" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>P_KAELTEWELLE <span class="op">*</span> <span class="dv">100</span><span class="sc">:.0f}</span><span class="ss"> % der Faelle liefern&quot;</span>)</span>
<span id="cb10-204"><a href="#cb10-204" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Wind </span><span class="sc">{</span>EINBRUCH[<span class="dv">1</span>] <span class="op">*</span> <span class="dv">100</span><span class="sc">:.0f}</span><span class="ss"> %, Sonne </span><span class="sc">{</span>EINBRUCH[<span class="dv">2</span>] <span class="op">*</span> <span class="dv">100</span><span class="sc">:.0f}</span><span class="ss"> % &quot;</span></span>
<span id="cb10-205"><a href="#cb10-205" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;und - entscheidend - auch das Gaskraftwerk nur &quot;</span></span>
<span id="cb10-206"><a href="#cb10-206" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>EINBRUCH[<span class="dv">0</span>] <span class="op">*</span> <span class="dv">100</span><span class="sc">:.0f}</span><span class="ss"> %&quot;</span>)</span>
<span id="cb10-207"><a href="#cb10-207" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;des Ueblichen. Alles bricht gleichzeitig ein.</span><span class="ch">\n</span><span class="st">&quot;</span>)</span>
<span id="cb10-208"><a href="#cb10-208" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-209"><a href="#cb10-209" aria-hidden="true" tabindex="-1"></a> a_bau <span class="op">=</span> ziehe_wetter(<span class="dv">400</span>, seed<span class="op">=</span><span class="dv">20260908</span>)</span>
<span id="cb10-210"><a href="#cb10-210" aria-hidden="true" tabindex="-1"></a> k_sz, x_sz <span class="op">=</span> chance_constraint_szenarien(a_bau, <span class="fl">0.05</span>)</span>
<span id="cb10-211"><a href="#cb10-211" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-212"><a href="#cb10-212" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(KOPF)</span>
<span id="cb10-213"><a href="#cb10-213" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-214"><a href="#cb10-214" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(zeile(<span class="st">&quot;SOC, Zusage 95 %&quot;</span>, k_soc, x_soc,</span>
<span id="cb10-215"><a href="#cb10-215" aria-hidden="true" tabindex="-1"></a> sicherheit(x_soc, normalwelt), sicherheit(x_soc, echt)))</span>
<span id="cb10-216"><a href="#cb10-216" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(zeile(<span class="st">&quot;Szenarien, 95 %&quot;</span>, k_sz, x_sz,</span>
<span id="cb10-217"><a href="#cb10-217" aria-hidden="true" tabindex="-1"></a> sicherheit(x_sz, normalwelt), sicherheit(x_sz, echt)))</span>
<span id="cb10-218"><a href="#cb10-218" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-219"><a href="#cb10-219" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="ch">\n</span><span class="ss"> Der SOC-Plan verspricht 95 % und haelt &quot;</span></span>
<span id="cb10-220"><a href="#cb10-220" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>sicherheit(x_soc, echt) <span class="op">*</span> <span class="dv">100</span><span class="sc">:.1f}</span><span class="ss"> %. Nicht das Verfahren ist&quot;</span>)</span>
<span id="cb10-221"><a href="#cb10-221" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; falsch, sondern die Annahme: In der Normalwelt liefert derselbe&quot;</span>)</span>
<span id="cb10-222"><a href="#cb10-222" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Plan </span><span class="sc">{</span>sicherheit(x_soc, normalwelt) <span class="op">*</span> <span class="dv">100</span><span class="sc">:.1f}</span><span class="ss"> %.&quot;</span>)</span>
<span id="cb10-223"><a href="#cb10-223" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="ch">\n</span><span class="ss"> Der Szenarioplan (</span><span class="sc">{</span><span class="bu">len</span>(a_bau)<span class="sc">}</span><span class="ss"> Szenarien, </span><span class="sc">{</span><span class="bu">len</span>(a_bau)<span class="sc">}</span><span class="ss"> &quot;</span></span>
<span id="cb10-224"><a href="#cb10-224" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Binaervariablen) kommt auf&quot;</span>)</span>
<span id="cb10-225"><a href="#cb10-225" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; </span><span class="sc">{</span>sicherheit(x_sz, echt) <span class="op">*</span> <span class="dv">100</span><span class="sc">:.1f}</span><span class="ss"> % - und kostet dafuer &quot;</span></span>
<span id="cb10-226"><a href="#cb10-226" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>(k_sz <span class="op">/</span> k_soc <span class="op">-</span> <span class="dv">1</span>) <span class="op">*</span> <span class="dv">100</span><span class="sc">:+.1f}</span><span class="ss"> %. Er kauft keine Windkraft mehr:&quot;</span>)</span>
<span id="cb10-227"><a href="#cb10-227" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; Was im Ernstfall ausfaellt, hilft der Zusage nicht.&quot;</span>)</span>
<span id="cb10-228"><a href="#cb10-228" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span></code></pre></div>
<p><strong>Ausgabe:</strong></p>
<pre><code>==============================================================================
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 &#39;echte Welt&#39; kommt in Teil (3).
==============================================================================
(2) Was kostet ein Prozentpunkt Versorgungssicherheit?
==============================================================================
von -&gt; nach Prozentpunkte Mehrkosten EUR je Punkt
------------------------------------------------------------------------------
Mittelwert -&gt; Zusage 80 % 29.98 1,687,712 56,289
Zusage 80 % -&gt; Zusage 90 % 9.93 612,500 61,685
Zusage 90 % -&gt; Zusage 95 % 4.98 519,530 104,365
Zusage 95 % -&gt; 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.
==============================================================================</code></pre>
<p><strong>Drei Befunde, die man dem Plan nicht ansieht.</strong></p>
<p><strong>Erstens: Der Mittelwertplan fällt in der Hälfte aller Fälle aus.</strong> Das ist kein Fehler, sondern die Definition des Erwartungswerts — und dieselbe Falle wie in <a href="#sec:unsicherheit-der-fluch-des-durchschnitts">Abschnitt 12.3</a>, nur teurer. Er kauft die billigsten Quellen je <em>erwarteter</em> 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.</p>
<p><strong>Zweitens: Sicherheit ist konvex bepreist.</strong> Die ersten dreißig Prozentpunkte kosten rund 56 000 € pro Punkt, die letzten vier rund 254 000 € — das <strong>4,5-fache</strong>. Das ist die Zahl, die in die Vorlage gehört: Nicht „was kostet Versorgungssicherheit“, sondern „was kostet der <em>nächste</em> Prozentpunkt“. Wer 99,9 % fordert, ohne diese Kurve gesehen zu haben, fordert ins Blaue.</p>
<p><strong>Drittens — und das ist der unangenehme Befund: Die Zusage gilt nur für die unterstellte Verteilung.</strong> Der Plan verspricht 95 % und hält <strong>87,4 %</strong>. 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 <strong>alles zugleich</strong> ein — auch das Gaskraftwerk, weil bei Frost der Netzdruck fällt. Eine Kovarianzmatrix mit Korrelationen um null kann so etwas nicht ausdrücken.</p>
<p>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 <strong>keine Windkraft mehr</strong>. Die Logik dahinter ist hart und richtig: Was im Ernstfall ausfällt, hilft der Zusage nicht.</p>
<blockquote>
<p><strong>⚠️ Die Szenariomethode überanpasst — und mehr Szenarien helfen nicht verlässlich</strong> Bei <span class="math inline">S</span> Szenarien und <span class="math inline">\alpha = 5\,\%</span> darf das Modell genau <span class="math inline">0{,}05 \cdot S</span> davon ignorieren — und es sucht sich die <strong>teuersten</strong> aus. In der Stichprobe trifft es seine Quote dadurch immer; außerhalb streut sie.</p>
<p>Gemessen an zwölf Läufen dieses Modells (je vier Ziehungen bei <span class="math inline">S = 200</span>, <span class="math inline">400</span>, <span class="math inline">800</span>, geprüft an 200 000 unabhängigen Szenarien): Die tatsächliche Quote lag zwischen <strong>93,1 % und 96,7 %</strong>. Die Spanne wurde von <span class="math inline">S = 200</span> auf <span class="math inline">400</span> enger (3,5 → 1,2 Prozentpunkte), bei <span class="math inline">S = 800</span> aber wieder breiter (3,1) — die Zahl der Szenarien allein schließt die Lücke also nicht.</p>
<p><strong>Konsequenz für die Praxis:</strong> Eine mit Szenarien erkaufte Zusage immer an einer <strong>zurückgehaltenen</strong> 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 <a href="handelsmaschine.html#kap-handelsmaschine">Kapitel 21</a>.</p>
</blockquote>
<blockquote>
<p><strong>🎯 Merksatz</strong> 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.</p>
</blockquote>
<hr />
<h2 id="sec:unsicherheit-uebungsaufgaben">12.8 Übungsaufgaben</h2>
<blockquote>
<p>Lösungen: <a href="anhang-loesungen.html#sec:loesungen-unsicherheit">Abschnitt A.12</a>.</p>
</blockquote>
<p><strong>Aufgabe 12.1 ⭐ — Fluch des Durchschnitts erkennen.</strong> Ein Bauunternehmer plant mit der <em>durchschnittlichen</em> Bauzeit von 8 Monaten. Warum ist die <em>erwartete</em> Fertigstellung trotzdem später als 8 Monate, wenn Verzögerungen wahrscheinlicher sind als Beschleunigungen? Nennen Sie zwei weitere Alltagsbeispiele.</p>
<p><strong>Aufgabe 12.2 ⭐ — Ansatz wählen.</strong> Welcher der drei 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?</p>
<p><strong>Aufgabe 12.2 ⭐ — Ansatz wählen.</strong> 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?</p>
<p><strong>Aufgabe 12.3 ⭐⭐ — Zweistufiges Modell rechnen.</strong> Wiederholen Sie die Handrechnung <em>Warum der Mittelwert in die Irre führt</em> 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?</p>
<p><strong>Aufgabe 12.4 ⭐⭐ — EVPI interpretieren.</strong> 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?</p>
<p><strong>Aufgabe 12.5 ⭐⭐⭐ — Monte-Carlo erweitern.</strong> Erweitern Sie <code>Monte_Carlo.py</code>: (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.</p>
<p><strong>Aufgabe 12.6 ⭐⭐⭐ — Budgeted Uncertainty.</strong> Erweitern Sie <code>Robuste_Optimierung.py</code> um den Ansatz von Bertsimas/Sim: Höchstens <span class="math inline">\Gamma</span> Titel nehmen gleichzeitig ihren Worst Case an. Variieren Sie <span class="math inline">\Gamma \in \{0, 1, 2, 3, 4\}</span> und stellen Sie den Verlauf von Ertrag und Absicherung dar. (Hinweis: Bei <span class="math inline">\Gamma</span> ganzzahlig genügt es, die <span class="math inline">\Gamma</span> größten <span class="math inline">\delta_i w_i</span> abzuziehen — das lässt sich in CVXPY mit <code>cp.sum_largest</code> formulieren.)</p>
<p><strong>Aufgabe 12.6 ⭐⭐ — Den Preis der Zusage selbst bestimmen.</strong> <code>Chance_Constraints.py</code> 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 <strong>szenariobasierte</strong> Modell meldet dann bei 95 % <code>infeasible</code>, das <strong>analytische</strong> dagegen <code>optimal</code>. 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.</p>
<p><strong>Aufgabe 12.7 ⭐⭐⭐ — Budgeted Uncertainty.</strong> Erweitern Sie <code>Robuste_Optimierung.py</code> um den Ansatz von Bertsimas/Sim: Höchstens <span class="math inline">\Gamma</span> Titel nehmen gleichzeitig ihren Worst Case an. Variieren Sie <span class="math inline">\Gamma \in \{0, 1, 2, 3, 4\}</span> und stellen Sie den Verlauf von Ertrag und Absicherung dar. (Hinweis: Bei <span class="math inline">\Gamma</span> ganzzahlig genügt es, die <span class="math inline">\Gamma</span> größten <span class="math inline">\delta_i w_i</span> abzuziehen — das lässt sich in CVXPY mit <code>cp.sum_largest</code> formulieren.)</p>
<hr />
<h2 id="sec:unsicherheit-denkfehler">12.8 Finde den Denkfehler</h2>
<h2 id="sec:unsicherheit-denkfehler">12.9 Finde den Denkfehler</h2>
<div class="card card-denkfehler">
<blockquote>
<p><strong>🐛 Finde den Denkfehler 9.1: Warum jedes Projekt zu spät fertig wird</strong></p>
<p>Ein Projektleiter plant eine Umbaumaßnahme. Fünf Gewerke arbeiten <strong>parallel</strong> 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.</p>
<p>Er rechnet: <em>„Jedes Gewerk braucht im Mittel <span class="math inline">(6 + 10 + 18)/3 = 11{,}3</span> Tage. Alle laufen gleichzeitig. Also ist der Umbau nach 11,3 Tagen fertig — ich plane großzügig 12 ein.“</em></p>
<p>Nachgerechnet mit 200 000 simulierten Umbauten:</p>
<div class="sourceCode" id="cb10"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb10-2"><a href="#cb10-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-3"><a href="#cb10-3" aria-hidden="true" tabindex="-1"></a>rng <span class="op">=</span> np.random.default_rng(<span class="dv">3</span>)</span>
<span id="cb10-4"><a href="#cb10-4" aria-hidden="true" tabindex="-1"></a>dauer <span class="op">=</span> rng.triangular(<span class="dv">6</span>, <span class="dv">10</span>, <span class="dv">18</span>, size<span class="op">=</span>(<span class="dv">200_000</span>, <span class="dv">5</span>)) <span class="co"># 5 Gewerke, je eine Schaetzung</span></span>
<span id="cb10-5"><a href="#cb10-5" aria-hidden="true" tabindex="-1"></a>projekt <span class="op">=</span> dauer.<span class="bu">max</span>(axis<span class="op">=</span><span class="dv">1</span>) <span class="co"># fertig, wenn das LETZTE fertig ist</span></span>
<span id="cb10-6"><a href="#cb10-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-7"><a href="#cb10-7" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="ss">f&quot;Mittelwert einer Einzeldauer: </span><span class="sc">{</span>dauer<span class="sc">.</span>mean()<span class="sc">:.2f}</span><span class="ss"> Tage&quot;</span>)</span>
<span id="cb10-8"><a href="#cb10-8" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="ss">f&quot;Tatsaechliche Projektdauer: </span><span class="sc">{</span>projekt<span class="sc">.</span>mean()<span class="sc">:.2f}</span><span class="ss"> Tage&quot;</span>)</span>
<span id="cb10-9"><a href="#cb10-9" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="ss">f&quot;90-%-Quantil: </span><span class="sc">{</span>np<span class="sc">.</span>quantile(projekt, <span class="fl">0.9</span>)<span class="sc">:.2f}</span><span class="ss"> Tage&quot;</span>)</span>
<span id="cb10-10"><a href="#cb10-10" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="ss">f&quot;Anteil ueber 11,34 Tage: </span><span class="sc">{</span>(projekt <span class="op">&gt;</span> <span class="fl">11.34</span>)<span class="sc">.</span>mean() <span class="op">*</span> <span class="dv">100</span><span class="sc">:.1f}</span><span class="ss"> %&quot;</span>)</span></code></pre></div>
<div class="sourceCode" id="cb12"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb12-1"><a href="#cb12-1" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb12-2"><a href="#cb12-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-3"><a href="#cb12-3" aria-hidden="true" tabindex="-1"></a>rng <span class="op">=</span> np.random.default_rng(<span class="dv">3</span>)</span>
<span id="cb12-4"><a href="#cb12-4" aria-hidden="true" tabindex="-1"></a>dauer <span class="op">=</span> rng.triangular(<span class="dv">6</span>, <span class="dv">10</span>, <span class="dv">18</span>, size<span class="op">=</span>(<span class="dv">200_000</span>, <span class="dv">5</span>)) <span class="co"># 5 Gewerke, je eine Schaetzung</span></span>
<span id="cb12-5"><a href="#cb12-5" aria-hidden="true" tabindex="-1"></a>projekt <span class="op">=</span> dauer.<span class="bu">max</span>(axis<span class="op">=</span><span class="dv">1</span>) <span class="co"># fertig, wenn das LETZTE fertig ist</span></span>
<span id="cb12-6"><a href="#cb12-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-7"><a href="#cb12-7" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="ss">f&quot;Mittelwert einer Einzeldauer: </span><span class="sc">{</span>dauer<span class="sc">.</span>mean()<span class="sc">:.2f}</span><span class="ss"> Tage&quot;</span>)</span>
<span id="cb12-8"><a href="#cb12-8" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="ss">f&quot;Tatsaechliche Projektdauer: </span><span class="sc">{</span>projekt<span class="sc">.</span>mean()<span class="sc">:.2f}</span><span class="ss"> Tage&quot;</span>)</span>
<span id="cb12-9"><a href="#cb12-9" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="ss">f&quot;90-%-Quantil: </span><span class="sc">{</span>np<span class="sc">.</span>quantile(projekt, <span class="fl">0.9</span>)<span class="sc">:.2f}</span><span class="ss"> Tage&quot;</span>)</span>
<span id="cb12-10"><a href="#cb12-10" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="ss">f&quot;Anteil ueber 11,34 Tage: </span><span class="sc">{</span>(projekt <span class="op">&gt;</span> <span class="fl">11.34</span>)<span class="sc">.</span>mean() <span class="op">*</span> <span class="dv">100</span><span class="sc">:.1f}</span><span class="ss"> %&quot;</span>)</span></code></pre></div>
<p><strong>Ausgabe:</strong></p>
<pre><code>Mittelwert einer Einzeldauer: 11.34 Tage
Tatsaechliche Projektdauer: 14.38 Tage
@ -869,7 +1221,7 @@ Anteil ueber 11,34 Tage: 95.5 %</code></pre>
<p><strong>🎯 Merksatz</strong> Der Fluch des Durchschnitts hat zwei Gesichter. Beim Ersatzteil aus <a href="#sec:unsicherheit-schnellstart">Abschnitt 12.1</a> führt der Mittelwert in die Irre, weil die <strong>Kosten</strong> asymmetrisch sind. Hier führt er in die Irre, weil die <strong>Verknüpfung</strong> asymmetrisch ist: Ein Gewerk, das früher fertig wird, hilft niemandem — ein Gewerk, das sich verspätet, verzögert alles. Beide Male gilt: <em>Der Mittelwert einer Funktion ist nicht die Funktion des Mittelwerts.</em></p>
</blockquote>
<hr />
<h2 id="sec:unsicherheit-quiz">12.9 Micro-Quiz</h2>
<h2 id="sec:unsicherheit-quiz">12.10 Micro-Quiz</h2>
<div class="card card-quiz">
<blockquote>
<p><strong>❓ Micro-Quiz 9: Drei Fragen zum Selbstcheck</strong></p>
@ -880,7 +1232,7 @@ Anteil ueber 11,34 Tage: 95.5 %</code></pre>
</blockquote>
</div>
<hr />
<h2 id="sec:unsicherheit-selbsttest">12.10 Selbsttest</h2>
<h2 id="sec:unsicherheit-selbsttest">12.11 Selbsttest</h2>
<blockquote>
<p>Antworten: <a href="anhang-loesungen.html#selbsttest-loesung-unsicherheit">Anhang A</a>.</p>
</blockquote>
@ -890,9 +1242,11 @@ Anteil ueber 11,34 Tage: 95.5 %</code></pre>
<li>Was misst der EVPI, und wofür ist er praktisch nützlich?</li>
<li>Warum braucht robuste Optimierung keine Wahrscheinlichkeiten?</li>
<li>Wie macht man ein Min-Max-Problem mit Box-Unsicherheit lösbar?</li>
<li>Warum wird aus <span class="math inline">\mathbb{P}(\mathbf{a}^\top\mathbf{x} \ge b) \ge 1-\alpha</span> unter Normalverteilung eine <strong>Norm</strong> und nicht ein fester Zuschlag je Variable?</li>
<li>Ein Plan mit Chance Constraint verspricht 95 % und hält gemessen 87 %. Woran liegt das — und was ändert die szenariobasierte Formulierung daran?</li>
</ol>
<hr />
<h2 id="sec:unsicherheit-zusammenfassung">12.11 Zusammenfassung</h2>
<h2 id="sec:unsicherheit-zusammenfassung">12.12 Zusammenfassung</h2>
<ul>
<li><strong>Der Fluch des Durchschnitts</strong> ist kein Randphänomen und hat zwei Gesichter. Bei asymmetrischen <strong>Kosten</strong> liegt das Optimum systematisch neben dem Mittelwert — das Newsvendor-Verhältnis <span class="math inline">c_{-}/(c_{-}+c_{+})</span> sagt, wie weit. Bei asymmetrischer <strong>Verknüpfung</strong> (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).</li>
<li><strong>Eine Terminzusage ist ein Quantil, kein Mittelwert.</strong> Wer den Erwartungswert zusagt, reißt den Termin bei fünf parallelen Vorgängen in 95 % der Fälle.</li>
@ -900,6 +1254,9 @@ Anteil ueber 11,34 Tage: 95.5 %</code></pre>
<li><strong>Zweistufige stochastische Programme</strong> trennen die Festlegung von der Reaktion — ein <span class="math inline">\mathbf{x}</span>, aber je Szenario ein <span class="math inline">\mathbf{y}_s</span>.</li>
<li><strong>Der EVPI</strong> begrenzt, was eine perfekte Prognose wert sein darf.</li>
<li><strong>Robuste Optimierung</strong> braucht keine Wahrscheinlichkeiten, nur eine Unsicherheitsmenge — und wird für einfache Mengen zu einem gewöhnlichen Problem mit Abzugsterm.</li>
<li><strong>Chance Constraints</strong> schreiben die Zusage selbst ins Modell. Unter Normalverteilung wird daraus eine <strong>Kegelbedingung</strong> (der Sicherheitszuschlag ist eine Norm, deshalb belohnt sie Mischung), szenariobasiert ein <strong>MILP mit Big-M</strong> ohne jede Verteilungsannahme.</li>
<li><strong>Sicherheit ist konvex bepreist.</strong> Im Beispiel kostet der letzte Prozentpunkt das 4,5-fache des ersten. Die richtige Managementfrage lautet nicht „was kostet Sicherheit“, sondern „was kostet der <em>nächste</em> Prozentpunkt“.</li>
<li><strong>Eine Zusage gilt nur für die unterstellte Verteilung.</strong> Ein Plan, der unter Normalverteilung 95 % verspricht, hielt gemessen 87 %, weil das Modell die Kältewelle nicht kannte, in der <em>alles</em> zugleich ausfällt. Szenariobasierte Zusagen wiederum überanpassen — beide müssen an zurückgehaltenen Daten nachgemessen werden.</li>
<li><strong>Beziffern Sie immer Preis und Nutzen der Absicherung.</strong> Ohne beide Zahlen ist die Entscheidung nicht begründbar.</li>
</ul>
<p><strong>Ausblick.</strong> <a href="dynamische-programmierung.html#kap-dynamische-programmierung">Kapitel 13</a> fügt die Zeitdimension hinzu: Entscheidungen, die über viele Perioden aufeinander aufbauen — gelöst mit der Bellman-Gleichung.</p>

View file

@ -609,141 +609,146 @@ Alles bereit — Sie können mit dem ersten Kapitel beginnen.</code></pre>
<td><a href="unsicherheit.html#kap-unsicherheit">Kapitel 12</a></td>
</tr>
<tr class="even">
<td><code>Chance_Constraints.py</code></td>
<td>Zusage „mit 95 % Sicherheit“, SOC und Big-M</td>
<td><a href="unsicherheit.html#kap-unsicherheit">Kapitel 12</a></td>
</tr>
<tr class="odd">
<td><code>Bellman_Minimalbeispiel.py</code></td>
<td>Rückwärtsinduktion</td>
<td><a href="dynamische-programmierung.html#kap-dynamische-programmierung">Kapitel 13</a></td>
</tr>
<tr class="odd">
<tr class="even">
<td><code>Mehrziel_Pareto.py</code></td>
<td>Pareto-Front, ε-Constraint, Gewichtslücke</td>
<td><a href="mehrziel.html#kap-mehrziel">Kapitel 14</a></td>
</tr>
<tr class="even">
<tr class="odd">
<td><code>Predict_then_Optimize.py</code></td>
<td>MSE gegen Entscheidungskosten</td>
<td><a href="prognose.html#kap-prognose">Kapitel 15</a></td>
</tr>
<tr class="odd">
<tr class="even">
<td><code>Strukturbruecke.py</code></td>
<td>Derselbe Code über Werkstatt und Depot</td>
<td><a href="bruecke.html#kap-bruecke">Kapitel 16</a></td>
</tr>
<tr class="even">
<tr class="odd">
<td><code>Kraftwerkseinsatz.py</code></td>
<td>Unit Commitment unter Windunsicherheit</td>
<td><a href="supplychain.html#kap-supplychain">Kapitel 17</a></td>
</tr>
<tr class="odd">
<tr class="even">
<td><code>Mehrperiodige_Order_Execution.py</code></td>
<td>Almgren-Chriss</td>
<td><a href="dynamische-programmierung.html#kap-dynamische-programmierung">Kapitel 13</a></td>
</tr>
<tr class="even">
<tr class="odd">
<td><code>Renditen_Vergleich.py</code></td>
<td>Diskret vs. logarithmisch</td>
<td><a href="finanzdaten.html#kap-finanzdaten">Kapitel 18</a></td>
</tr>
<tr class="odd">
<tr class="even">
<td><code>Schaetzrauschen_Demo.py</code></td>
<td>Error-Maximizer messen</td>
<td><a href="finanzdaten.html#kap-finanzdaten">Kapitel 18</a></td>
</tr>
<tr class="even">
<tr class="odd">
<td><code>Finanzdaten_Ledoit_Wolf.py</code></td>
<td>Datenpipeline, Shrinkage</td>
<td><a href="finanzdaten.html#kap-finanzdaten">Kapitel 18</a></td>
</tr>
<tr class="odd">
<tr class="even">
<td><code>Kovarianz_Falle.py</code></td>
<td>Singuläre Kovarianz, Error-Maximizer</td>
<td><a href="finanzdaten.html#kap-finanzdaten">Kapitel 18</a></td>
</tr>
<tr class="even">
<tr class="odd">
<td><code>Markowitz_CVXPY.py</code></td>
<td>GMV, Max Sharpe, Frontier</td>
<td><a href="markowitz.html#kap-markowitz">Kapitel 19</a></td>
</tr>
<tr class="odd">
<tr class="even">
<td><code>Diversifikation_Demo.py</code></td>
<td>Korrelation und Portfoliorisiko</td>
<td><a href="markowitz.html#kap-markowitz">Kapitel 19</a></td>
</tr>
<tr class="even">
<tr class="odd">
<td><code>Renditeschaetzung_Falle.py</code></td>
<td>Schätzfehler in erwarteten Renditen</td>
<td><a href="markowitz.html#kap-markowitz">Kapitel 19</a></td>
</tr>
<tr class="odd">
<tr class="even">
<td><code>VaR_CVaR_Demo.py</code></td>
<td>Fat Tails, Subadditivität</td>
<td><a href="cvar.html#kap-cvar">Kapitel 20</a></td>
</tr>
<tr class="even">
<tr class="odd">
<td><code>CVaR_Portfolio.py</code></td>
<td>CVaR mit Reibung</td>
<td><a href="cvar.html#kap-cvar">Kapitel 20</a></td>
</tr>
<tr class="odd">
<tr class="even">
<td><code>QuantitativeTradingEngine.py</code></td>
<td>Walk-Forward-Backtest</td>
<td><a href="handelsmaschine.html#kap-handelsmaschine">Kapitel 21</a></td>
</tr>
<tr class="even">
<tr class="odd">
<td><code>Backtest_Fallen.py</code></td>
<td>Fünf Selbsttäuschungen</td>
<td><a href="handelsmaschine.html#kap-handelsmaschine">Kapitel 21</a></td>
</tr>
<tr class="odd">
<tr class="even">
<td><code>Data_Snooping.py</code></td>
<td>Bestes aus N Versuchen auf Rauschen</td>
<td><a href="handelsmaschine.html#kap-handelsmaschine">Kapitel 21</a></td>
</tr>
<tr class="even">
<tr class="odd">
<td><code>Infeasibility_Diagnose.py</code></td>
<td>Notfallplan statt Fehler</td>
<td><a href="praxisfallen.html#kap-praxisfallen">Kapitel 22</a></td>
</tr>
<tr class="odd">
<tr class="even">
<td><code>Erklaerbarkeit.py</code></td>
<td>Constraint-Trace, Was-wäre-wenn</td>
<td><a href="praxisfallen.html#kap-praxisfallen">Kapitel 22</a></td>
</tr>
<tr class="even">
<tr class="odd">
<td><code>Betriebsueberwachung.py</code></td>
<td>Status, Gap und Zeitausschöpfung</td>
<td><a href="praxisfallen.html#kap-praxisfallen">Kapitel 22</a></td>
</tr>
<tr class="odd">
<tr class="even">
<td><code>or_kern.py</code></td>
<td>Gemeinsamer Unterbau: Domäne, Status, Prüfung</td>
<td><a href="praxisfallen.html#kap-praxisfallen">Kapitel 22</a></td>
</tr>
<tr class="even">
<tr class="odd">
<td><code>Solverwechsel_CPSAT_HiGHS.py</code></td>
<td>Derselbe Fall in zwei Solvern</td>
<td><a href="praxisfallen.html#kap-praxisfallen">Kapitel 22</a></td>
</tr>
<tr class="odd">
<tr class="even">
<td><code>test_or_kern.py</code></td>
<td>Testsuite für ein Optimierungsmodell</td>
<td><a href="testing.html#kap-testing">Kapitel 23</a></td>
</tr>
<tr class="even">
<tr class="odd">
<td><code>Mutationstest.py</code></td>
<td>Findet die Lücken der eigenen Tests</td>
<td><a href="testing.html#kap-testing">Kapitel 23</a></td>
</tr>
<tr class="odd">
<tr class="even">
<td><code>Benchmark_Skalierung.py</code></td>
<td>Aufbau gegen Lösen, vier Bibliotheken</td>
<td><a href="testing.html#kap-testing">Kapitel 23</a></td>
</tr>
<tr class="even">
<tr class="odd">
<td><code>Optimierungsdienst.py</code></td>
<td>Das Modell als HTTP-Dienst</td>
<td><a href="testing.html#kap-testing">Kapitel 23</a></td>
</tr>
<tr class="odd">
<tr class="even">
<td><code>Konfliktsuche.py</code></td>
<td>Deletion Filter: den Widerspruch einkreisen</td>
<td><a href="anhang-fehlerdiagnose.html#anhang-fehlerdiagnose">Anhang C</a></td>

View file

@ -359,6 +359,7 @@ Alle Beispielprogramme dieses Buchs im Überblick, sortiert nach Kapitel — pra
| `Monte_Carlo.py` | Monte-Carlo-Bewertung | [Kapitel 12](#kap-unsicherheit) |
| `Stochastische_Optimierung.py` | Two-Stage mit Recourse | [Kapitel 12](#kap-unsicherheit) |
| `Robuste_Optimierung.py` | Worst-Case-Absicherung | [Kapitel 12](#kap-unsicherheit) |
| `Chance_Constraints.py` | Zusage „mit 95 % Sicherheit“, SOC und Big-M | [Kapitel 12](#kap-unsicherheit) |
| `Bellman_Minimalbeispiel.py` | Rückwärtsinduktion | [Kapitel 13](#kap-dynamische-programmierung) |
| `Mehrziel_Pareto.py` | Pareto-Front, ε-Constraint, Gewichtslücke | [Kapitel 14](#kap-mehrziel) |
| `Predict_then_Optimize.py` | MSE gegen Entscheidungskosten | [Kapitel 15](#kap-prognose) |
@ -12773,19 +12774,20 @@ else:
> **📌 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 drei Wege, damit umzugehen — und warum „einfach den Mittelwert einsetzen“ der schlechteste davon ist.
> **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](#kap-lp), [Kapitel 11](#kap-qp-nlp). 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 — und erkennen, wann Rechnen mit Mittelwerten zulässig ist und wann es systematisch danebengeht.
> **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. 5,5 Stunden.
> **Zeitbedarf:** ca. 6,5 Stunden.
>
> **Programme:**\
> `Fluch_des_Durchschnitts.py`\
> `Monte_Carlo.py`\
> `Stochastische_Optimierung.py`\
> `Robuste_Optimierung.py`
> `Robuste_Optimierung.py`\
> `Chance_Constraints.py`
>
> **Notebook:** `Notebooks_04/unsicherheit.ipynb`\
> [In Google Colab öffnen](https://colab.research.google.com/github/dschlueter/or-mit-python/blob/main/Notebooks_04/unsicherheit.ipynb)
@ -12867,9 +12869,10 @@ Nach diesem Kapitel können Sie …
2. … ein **zweistufiges stochastisches Programm** mit Recourse formulieren.
3. … Monte-Carlo einsetzen, um Verteilungen statt Punktschätzungen zu bewerten.
4. … ein **robustes** Modell mit Unsicherheitsmenge aufstellen und seinen Preis beziffern.
5. … begründen, welcher der drei Ansätze für ein gegebenes Problem passt.
6. … das **kritische Verhältnis** eines Newsvendor-Problems aufstellen und daraus die optimale Bestellmenge als Quantil ablesen.
7. … eine Terminzusage als Quantil formulieren statt als Mittelwert.
5. … eine **Wahrscheinlichkeitszusage** („mit 95 % Sicherheit“) als Chance Constraint schreiben — analytisch als Kegelbedingung oder szenariobasiert mit Big-M — und den Preis eines Prozentpunkts Sicherheit beziffern.
6. … begründen, welcher der vier Ansätze für ein gegebenes Problem passt.
7. … das **kritische Verhältnis** eines Newsvendor-Problems aufstellen und daraus die optimale Bestellmenge als Quantil ablesen.
8. … eine Terminzusage als Quantil formulieren statt als Mittelwert.
---
@ -12993,18 +12996,20 @@ Kein Rechentrick, sondern brute-force über alle ganzzahligen $x$ von 0 bis 500
> **🎯 Merksatz** Der Durchschnittskunde kauft nie. Das Durchschnittsszenario tritt nie ein. Wer mit Mittelwerten plant, optimiert eine Welt, die es nicht gibt.
### Die drei Ansätze im Überblick
### Die vier Ansätze im Überblick
![Abb. 12.1: Ansätze für Optimierung unter Unsicherheit](bilder_04/kap09_unsicherheit_ansaetze.svg)
| | **Monte-Carlo** | **Stochastische Programmierung** | **Robuste Optimierung** |
| --- | --- | --- | --- |
| Braucht | Verteilung | Szenarien **mit Wahrscheinlichkeiten** | nur eine **Menge** möglicher Werte |
| Optimiert | nichts (bewertet nur) | **Erwartungswert** über Szenarien | **Worst Case** in der Menge |
| Typische Frage | „Wie riskant ist dieser Plan?“ | „Was ist im Mittel am besten?“ | „Was hält auch im schlimmsten Fall?“ |
| Stärke | beliebige Kennzahlen, sehr flexibel | nutzt Wahrscheinlichkeiten voll aus | keine Verteilungsannahme nötig |
| Schwäche | keine Optimierung | Wahrscheinlichkeiten müssen stimmen | oft übervorsichtig, kostet Ertrag |
| Kapitel | 9.2 | 9.3 | 9.4 |
| | **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](#sec:unsicherheit-monte-carlo-simulation) | [Abschnitt 12.5](#sec:unsicherheit-zweistufige-stochastische-programmierung) | [Abschnitt 12.6](#sec:unsicherheit-robuste-optimierung-gegen-den-worst-case) | [Abschnitt 12.7](#sec:unsicherheit-chance-constraints) |
**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.
---
@ -13477,13 +13482,382 @@ if __name__ == "__main__":
---
## 12.7 Übungsaufgaben {#sec:unsicherheit-uebungsaufgaben}
## 12.7 Wahrscheinlichkeitsbeschränkungen: „mit 95 % Sicherheit“ {#sec:unsicherheit-chance-constraints}
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**`\index{Chance Constraint}`{=latex} 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**`\index{Konische Optimierung (SOCP, SDP)}`{=latex} — dieselbe Bauform, die [Kapitel 11](#kap-qp-nlp) 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](#kap-milp) ü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
```python
#!/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](#sec:unsicherheit-der-fluch-des-durchschnitts), 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](#kap-handelsmaschine).
> **🎯 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 {#sec:unsicherheit-uebungsaufgaben}
> Lösungen: [Abschnitt A.12](#sec:loesungen-unsicherheit).
**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 drei 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?
**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?
@ -13491,11 +13865,13 @@ if __name__ == "__main__":
**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 ⭐⭐⭐ — 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.)
**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.8 Finde den Denkfehler {#sec:unsicherheit-denkfehler}
## 12.9 Finde den Denkfehler {#sec:unsicherheit-denkfehler}
> **🐛 Finde den Denkfehler 9.1: Warum jedes Projekt zu spät fertig wird**
>
@ -13535,7 +13911,7 @@ if __name__ == "__main__":
---
## 12.9 Micro-Quiz {#sec:unsicherheit-quiz}
## 12.10 Micro-Quiz {#sec:unsicherheit-quiz}
> **❓ Micro-Quiz 9: Drei Fragen zum Selbstcheck**
>
@ -13549,7 +13925,7 @@ if __name__ == "__main__":
---
## 12.10 Selbsttest {#sec:unsicherheit-selbsttest}
## 12.11 Selbsttest {#sec:unsicherheit-selbsttest}
> Antworten: [Anhang A](#selbsttest-loesung-unsicherheit).
@ -13558,10 +13934,12 @@ if __name__ == "__main__":
3. Was misst der EVPI, und wofür ist er praktisch nützlich?
4. Warum braucht robuste Optimierung keine Wahrscheinlichkeiten?
5. Wie macht man ein Min-Max-Problem mit Box-Unsicherheit lösbar?
6. 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?
7. Ein Plan mit Chance Constraint verspricht 95 % und hält gemessen 87 %. Woran liegt das — und was ändert die szenariobasierte Formulierung daran?
---
## 12.11 Zusammenfassung {#sec:unsicherheit-zusammenfassung}
## 12.12 Zusammenfassung {#sec:unsicherheit-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.
@ -13569,6 +13947,9 @@ if __name__ == "__main__":
* **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](#kap-dynamische-programmierung) fügt die Zeitdimension hinzu: Entscheidungen, die über viele Perioden aufeinander aufbauen — gelöst mit der Bellman-Gleichung.
@ -24556,7 +24937,7 @@ Der Ausweg auf `scipy` hat das Warnsystem umgangen, nicht das Problem gelöst. R
**9.1 — Fluch des Durchschnitts.** Weil sich Verzögerungen **fortpflanzen** und die Verteilung rechtsschief ist: Ein Gewerk, das früher fertig wird, beschleunigt selten den Gesamtablauf (der nächste Schritt ist noch nicht bereit), ein verspätetes verzögert alles. Weitere Beispiele: Wartezeiten in Warteschlangen (nichtlinear in der Auslastung), Projektkosten mit Nachträgen.
**9.2 — Ansatz wählen.** (a) robust (Ausfall ist existenziell, Wahrscheinlichkeiten unzuverlässig). (b) stochastisch (Verteilung bekannt). (c) robust bzw. Extremwertstatistik (seltene Ereignisse, katastrophale Folgen). (d) stochastisch (Szenarien mit Wahrscheinlichkeiten liegen vor).
**9.2 — Ansatz wählen.** (a) robust (Ausfall ist existenziell, Wahrscheinlichkeiten unzuverlässig). (b) stochastisch (Verteilung bekannt). (c) robust bzw. Extremwertstatistik (seltene Ereignisse, katastrophale Folgen). (d) stochastisch (Szenarien mit Wahrscheinlichkeiten liegen vor). (e) Chance Constraint — die Zusage ist bereits als Quote formuliert („an 99 % aller Wintertage“). Robust wäre hier zu teuer (es gibt immer einen kälteren Tag), der Erwartungswert zu schwach (er sagt nichts über die Zusage).
**9.3 — Zweistufig rechnen.** (a) Mit Spot = 60: Ableitung bei $x \in (100,250)$: $40 - 60\cdot0{,}5 + 5\cdot0{,}5 = 40-30+2{,}5 = +12{,}5 > 0$ → Kapazität **senken**. Bei $x < 100$: $40 - 60 = -20 < 0$ → erhöhen. Optimum daher $x^* = 100$. (b) Billige Nachbesserung macht Vorhalten unattraktiv. (c) Bei $x = 225$ optimal müsste die Ableitung dort das Vorzeichen wechseln — das ist bei diskreten Szenarien nur zufällig der Fall; der Mittelwert ist nur bei symmetrischen Kosten und stetiger Verteilung optimal.
@ -24564,7 +24945,9 @@ Der Ausweg auf `scipy` hat das Warnsystem umgangen, nicht das Problem gelöst. R
**9.5 — Monte-Carlo erweitern.** (a) Servicelevel 95 %: Kapazität = 95 %-Quantil des Bedarfs (ca. 485). Die Zusatzkosten gegenüber dem Kostenoptimum (180) betragen ein Vielfaches — Servicelevel ist teuer, und genau diese Zahl braucht die Geschäftsleitung für die Entscheidung. (b) `cvar = kosten[kosten >= np.quantile(kosten, 0.95)].mean()`. (c) Erwartetes Bild: Bei kleiner Kapazität ist die Kostenverteilung stark rechtsschief (seltene, teure Notzukäufe); bei großer Kapazität schmal und nach rechts verschoben.
**9.6 — Budgeted Uncertainty.**
**9.6 — Den Preis der Zusage selbst bestimmen.** (a) Die Kosten je Prozentpunkt steigen weiter steil an; der Mix verschiebt sich dabei fast nur noch zugunsten des Gaskraftwerks, weil Biomasse längst an ihrer Ausbaugrenze liegt. Wind und Sonne bleiben bei einem kleinen Rest — sie senken über ihre Gegenläufigkeit die Norm, tragen zur gesicherten Leistung aber kaum bei. (b) Der Unterschied ist **nicht** numerisch, sondern inhaltlich. Bei 200 MW Biomasse liefert selbst der **Vollausbau** in der Kältewelle nur rund 440 MW — 7,5 % der Szenarien sind schlicht nicht bedienbar, egal wie viel Geld man ausgibt. Das Szenariomodell sieht diese Fälle in den Daten und meldet korrekt `infeasible`, sobald die Zusage über 92 % steigt. Das analytische Modell kennt sie nicht: Für eine Normalverteilung ist jede Zusage unter 100 % erfüllbar, wenn man nur genug Streuung wegkauft. Es meldet `optimal` — und der Plan hält gemessen 87,4 %. Glauben sollte man dem Szenariomodell: Ein `infeasible` ist hier die **richtige** Antwort. Es sagt, dass die Zusage nicht am Budget scheitert, sondern am Kraftwerkspark, und dass nicht mehr Geld hilft, sondern nur eine andere Anlage oder eine kleinere Zusage. (c) Sinngemäß: „99,99 % kosten nicht 5 % mehr als 99 %, sondern ein Vielfaches — der letzte Prozentpunkt ist bereits 4,5-mal so teuer wie der erste. Sagen Sie mir, was ein Ausfalltag das Unternehmen kostet, dann rechne ich aus, welche Quote sich lohnt. Und die Zusage gilt nur für die Wetterlagen, die wir modelliert haben — die Kältewelle gehört hinein.“
**9.7 — Budgeted Uncertainty.**
```python
abzug = cp.sum_largest(cp.multiply(UNSICHERHEIT, w), Gamma)
ziel = cp.Maximize(MU_SCHAETZUNG @ w - abzug - 0.5*LAMBDA*cp.quad_form(w, SIGMA))
@ -24628,6 +25011,8 @@ Und die Formulierung gegenüber dem Auftraggeber lautet nicht „der Umbau dauer
3. Die Differenz zwischen Kosten unter Unsicherheit und Kosten bei perfektem Wissen — eine **Obergrenze** für den Wert jeder Prognoseverbesserung.
4. Weil sie nur eine **Menge** möglicher Werte braucht, nicht deren Verteilung — sie optimiert gegen das schlechteste Element dieser Menge.
5. Für einfache Mengen (Box, Ellipsoid) lässt sich das innere Maximum geschlossen ausrechnen und wird zu einem Abzugsterm; das Gesamtproblem bleibt konvex.
6. Weil die Streuung der Summe $\mathbf{a}^\top\mathbf{x}$ nicht die Summe der Streuungen ist: $\sqrt{\mathbf{x}^\top\boldsymbol{\Sigma}\mathbf{x}}$ enthält die **Kovarianzen**. Ein fester Zuschlag je Variable wäre linear und würde deshalb übersehen, dass sich gegenläufige Größen teilweise aufheben — die Norm belohnt Mischung, ein linearer Aufschlag nicht.
7. Weil die Zusage nur so gut ist wie die unterstellte Verteilung. Die Normalverteilung kennt keine Ereignisse, in denen alle Quellen **gleichzeitig** ausfallen; ihre schwachen Korrelationen unterschätzen genau den Fall, der die Zusage bricht. Die szenariobasierte Formulierung hat diese Fälle in den Daten und trifft die Quote — erkauft mit Mehrkosten und mit dem Risiko, sich an die verwendete Stichprobe anzupassen.
---

View file

@ -449,6 +449,7 @@ Pfad E oder um gezielt nach einem Thema zu suchen:
| `Monte_Carlo.py` | Monte-Carlo-Bewertung | {ref:kap:unsicherheit} |
| `Stochastische_Optimierung.py` | Two-Stage mit Recourse | {ref:kap:unsicherheit} |
| `Robuste_Optimierung.py` | Worst-Case-Absicherung | {ref:kap:unsicherheit} |
| `Chance_Constraints.py` | Zusage „mit 95 % Sicherheit“, SOC und Big-M | {ref:kap:unsicherheit} |
| `Bellman_Minimalbeispiel.py` | Rückwärtsinduktion | {ref:kap:dynamische-programmierung} |
| `Mehrziel_Pareto.py` | Pareto-Front, ε-Constraint, Gewichtslücke | {ref:kap:mehrziel} |
| `Predict_then_Optimize.py` | MSE gegen Entscheidungskosten | {ref:kap:prognose} |

View file

@ -3,22 +3,25 @@
> **📌 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 drei Wege, damit umzugehen —
> Rendite und Fahrzeit **Zufallsgrößen**. Dieses Kapitel zeigt vier Wege, damit umzugehen —
> und warum „einfach den Mittelwert einsetzen“ der schlechteste davon ist.
>
> **Voraussetzungen:** {ref:kap:lp}, {ref:kap:qp-nlp}. 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 — und
> erkennen, wann Rechnen mit Mittelwerten zulässig ist und wann es systematisch danebengeht.
> 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. 5,5 Stunden.
> **Zeitbedarf:** ca. 6,5 Stunden.
>
> **Programme:**\
> `Fluch_des_Durchschnitts.py`\
> `Monte_Carlo.py`\
> `Stochastische_Optimierung.py`\
> `Robuste_Optimierung.py`
> `Robuste_Optimierung.py`\
> `Chance_Constraints.py`
---
@ -112,10 +115,13 @@ Nach diesem Kapitel können Sie …
2. … ein **zweistufiges stochastisches Programm** mit Recourse formulieren.
3. … Monte-Carlo einsetzen, um Verteilungen statt Punktschätzungen zu bewerten.
4. … ein **robustes** Modell mit Unsicherheitsmenge aufstellen und seinen Preis beziffern.
5. … begründen, welcher der drei Ansätze für ein gegebenes Problem passt.
6. … das **kritische Verhältnis** eines Newsvendor-Problems aufstellen und daraus die
5. … eine **Wahrscheinlichkeitszusage** („mit 95 % Sicherheit“) als Chance Constraint
schreiben — analytisch als Kegelbedingung oder szenariobasiert mit Big-M — und den Preis
eines Prozentpunkts Sicherheit beziffern.
6. … begründen, welcher der vier Ansätze für ein gegebenes Problem passt.
7. … das **kritische Verhältnis** eines Newsvendor-Problems aufstellen und daraus die
optimale Bestellmenge als Quantil ablesen.
7. … eine Terminzusage als Quantil formulieren statt als Mittelwert.
8. … eine Terminzusage als Quantil formulieren statt als Mittelwert.
---
@ -256,18 +262,23 @@ Erwartungswert der Kosten ist größer als die Kosten des Erwartungswerts.
> Der Durchschnittskunde kauft nie. Das Durchschnittsszenario tritt nie ein. Wer mit
> Mittelwerten plant, optimiert eine Welt, die es nicht gibt.
### Die drei Ansätze im Überblick
### Die vier Ansätze im Überblick
![Ansätze für Optimierung unter Unsicherheit](bilder_04/kap09_unsicherheit_ansaetze.svg)
| | **Monte-Carlo** | **Stochastische Programmierung** | **Robuste Optimierung** |
| --- | --- | --- | --- |
| Braucht | Verteilung | Szenarien **mit Wahrscheinlichkeiten** | nur eine **Menge** möglicher Werte |
| Optimiert | nichts (bewertet nur) | **Erwartungswert** über Szenarien | **Worst Case** in der Menge |
| Typische Frage | „Wie riskant ist dieser Plan?“ | „Was ist im Mittel am besten?“ | „Was hält auch im schlimmsten Fall?“ |
| Stärke | beliebige Kennzahlen, sehr flexibel | nutzt Wahrscheinlichkeiten voll aus | keine Verteilungsannahme nötig |
| Schwäche | keine Optimierung | Wahrscheinlichkeiten müssen stimmen | oft übervorsichtig, kostet Ertrag |
| Nachzulesen in | {ref:sec:unsicherheit-monte-carlo-simulation} | {ref:sec:unsicherheit-zweistufige-stochastische-programmierung} | {ref:sec:unsicherheit-robuste-optimierung-gegen-den-worst-case} |
| | **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 | {ref:sec:unsicherheit-monte-carlo-simulation} | {ref:sec:unsicherheit-zweistufige-stochastische-programmierung} | {ref:sec:unsicherheit-robuste-optimierung-gegen-den-worst-case} | {ref:sec:unsicherheit-chance-constraints} |
**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.
---
@ -791,6 +802,427 @@ if __name__ == "__main__":
---
## Wahrscheinlichkeitsbeschränkungen: „mit 95 % Sicherheit“ {#sec:unsicherheit-chance-constraints}
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**{idx:Chance Constraint} 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**{idx:Konische Optimierung (SOCP, SDP)} — dieselbe
Bauform, die {ref:kap:qp-nlp} 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
{ref:kap:milp} ü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
```python
#!/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
{ref:sec:unsicherheit-der-fluch-des-durchschnitts}, 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 {ref:kap:handelsmaschine}.
> **🎯 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.
---
## Übungsaufgaben {#sec:unsicherheit-uebungsaufgaben}
> Lösungen: {ref:sec:loesungen-unsicherheit}.
@ -801,11 +1233,12 @@ Ein Bauunternehmer plant mit der *durchschnittlichen* Bauzeit von 8 Monaten. War
sind als Beschleunigungen? Nennen Sie zwei weitere Alltagsbeispiele.
**Aufgabe ⭐ — Ansatz wählen.**
Welcher der drei Ansätze passt? Begründen Sie:
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 ⭐⭐ — Zweistufiges Modell rechnen.**
Wiederholen Sie die Handrechnung *Warum der Mittelwert in die Irre führt* mit geänderten Kosten: Spot 60 € statt 120 €.
@ -824,6 +1257,16 @@ Erweitern Sie `Monte_Carlo.py`:
(b) Berechnen Sie den CVaR der Kosten (Mittelwert der schlechtesten 5 %).
(c) Zeichnen Sie ein Histogramm der Kosten für drei Kapazitäten.
**Aufgabe ⭐⭐ — 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 ⭐⭐⭐ — 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
@ -931,6 +1374,10 @@ das lässt sich in CVXPY mit `cp.sum_largest` formulieren.)
3. Was misst der EVPI, und wofür ist er praktisch nützlich?
4. Warum braucht robuste Optimierung keine Wahrscheinlichkeiten?
5. Wie macht man ein Min-Max-Problem mit Box-Unsicherheit lösbar?
6. 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?
7. Ein Plan mit Chance Constraint verspricht 95 % und hält gemessen 87 %. Woran liegt das —
und was ändert die szenariobasierte Formulierung daran?
---
@ -951,6 +1398,16 @@ das lässt sich in CVXPY mit `cp.sum_largest` formulieren.)
* **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.

View file

@ -1492,6 +1492,9 @@ Projektkosten mit Nachträgen.
(b) stochastisch (Verteilung bekannt).
(c) robust bzw. Extremwertstatistik (seltene Ereignisse, katastrophale Folgen).
(d) stochastisch (Szenarien mit Wahrscheinlichkeiten liegen vor).
(e) Chance Constraint — die Zusage ist bereits als Quote formuliert („an 99 % aller
Wintertage“). Robust wäre hier zu teuer (es gibt immer einen kälteren Tag), der
Erwartungswert zu schwach (er sagt nichts über die Zusage).
**9.3 — Zweistufig rechnen.**
(a) Mit Spot = 60: Ableitung bei $x \in (100,250)$: $40 - 60\cdot0{,}5 + 5\cdot0{,}5 =
@ -1514,7 +1517,27 @@ genau diese Zahl braucht die Geschäftsleitung für die Entscheidung.
(c) Erwartetes Bild: Bei kleiner Kapazität ist die Kostenverteilung stark rechtsschief
(seltene, teure Notzukäufe); bei großer Kapazität schmal und nach rechts verschoben.
**9.6 — Budgeted Uncertainty.**
**9.6 — Den Preis der Zusage selbst bestimmen.**
(a) Die Kosten je Prozentpunkt steigen weiter steil an; der Mix verschiebt sich dabei fast
nur noch zugunsten des Gaskraftwerks, weil Biomasse längst an ihrer Ausbaugrenze liegt.
Wind und Sonne bleiben bei einem kleinen Rest — sie senken über ihre Gegenläufigkeit die
Norm, tragen zur gesicherten Leistung aber kaum bei.
(b) Der Unterschied ist **nicht** numerisch, sondern inhaltlich. Bei 200 MW Biomasse liefert
selbst der **Vollausbau** in der Kältewelle nur rund 440 MW — 7,5 % der Szenarien sind
schlicht nicht bedienbar, egal wie viel Geld man ausgibt. Das Szenariomodell sieht diese
Fälle in den Daten und meldet korrekt `infeasible`, sobald die Zusage über 92 % steigt. Das
analytische Modell kennt sie nicht: Für eine Normalverteilung ist jede Zusage unter 100 %
erfüllbar, wenn man nur genug Streuung wegkauft. Es meldet `optimal` — und der Plan hält
gemessen 87,4 %.
Glauben sollte man dem Szenariomodell: Ein `infeasible` ist hier die **richtige** Antwort.
Es sagt, dass die Zusage nicht am Budget scheitert, sondern am Kraftwerkspark, und dass
nicht mehr Geld hilft, sondern nur eine andere Anlage oder eine kleinere Zusage.
(c) Sinngemäß: „99,99 % kosten nicht 5 % mehr als 99 %, sondern ein Vielfaches — der letzte
Prozentpunkt ist bereits 4,5-mal so teuer wie der erste. Sagen Sie mir, was ein Ausfalltag
das Unternehmen kostet, dann rechne ich aus, welche Quote sich lohnt. Und die Zusage gilt
nur für die Wetterlagen, die wir modelliert haben — die Kältewelle gehört hinein.“
**9.7 — Budgeted Uncertainty.**
```python
abzug = cp.sum_largest(cp.multiply(UNSICHERHEIT, w), Gamma)
ziel = cp.Maximize(MU_SCHAETZUNG @ w - abzug - 0.5*LAMBDA*cp.quad_form(w, SIGMA))
@ -1625,6 +1648,16 @@ Fall, in dem es tatsächlich zulässig ist.
optimiert gegen das schlechteste Element dieser Menge.
5. Für einfache Mengen (Box, Ellipsoid) lässt sich das innere Maximum geschlossen ausrechnen
und wird zu einem Abzugsterm; das Gesamtproblem bleibt konvex.
6. Weil die Streuung der Summe $\mathbf{a}^\top\mathbf{x}$ nicht die Summe der Streuungen
ist: $\sqrt{\mathbf{x}^\top\boldsymbol{\Sigma}\mathbf{x}}$ enthält die **Kovarianzen**.
Ein fester Zuschlag je Variable wäre linear und würde deshalb übersehen, dass sich
gegenläufige Größen teilweise aufheben — die Norm belohnt Mischung, ein linearer
Aufschlag nicht.
7. Weil die Zusage nur so gut ist wie die unterstellte Verteilung. Die Normalverteilung
kennt keine Ereignisse, in denen alle Quellen **gleichzeitig** ausfallen; ihre schwachen
Korrelationen unterschätzen genau den Fall, der die Zusage bricht. Die szenariobasierte
Formulierung hat diese Fälle in den Daten und trifft die Quote — erkauft mit Mehrkosten
und mit dem Risiko, sich an die verwendete Stichprobe anzupassen.
---

View file

@ -0,0 +1,228 @@
#!/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)

View file

@ -1,6 +1,6 @@
# Beispielprogramme zu „Optimierte Entscheidungsfindung mit Python" (Version 04)
Diese 73 Dateien sind automatisch aus den Kapiteldateien in
Diese 74 Dateien sind automatisch aus den Kapiteldateien in
`../Operations_Research_mit_Python_Version_04/` extrahiert — jede Datei entspricht
genau einem im Buch vollständig abgedruckten Programm. Sie sind hier zusätzlich als
eigenständige, direkt ausführbare `.py`-Dateien verfügbar, damit man sie ohne
@ -15,7 +15,7 @@ python3 Operations_Research_mit_Python_Version_04/extract_programme_04.py
```
Das Verzeichnis „Verzeichnis der Beispielprogramme" im Vorwort des Buchs
(`00_Vorwort_und_Lesehilfe.md`) listet dieselben 73 Programme mit Kurzbeschreibung.
(`00_Vorwort_und_Lesehilfe.md`) listet dieselben 74 Programme mit Kurzbeschreibung.
Auch dieses README ist selbst generiert: `extract_programme.py` erzeugt es bei jedem
Lauf aus der Vorlage `README_Programme.md` im Quellverzeichnis und setzt die

View file

@ -77,14 +77,14 @@ sie unerwartet ab, ist etwas kaputtgegangen.
| Kapiteldateien | 22 | **31** |
| Kapitel | 15 | **23** |
| Anhänge | 4 | **5** |
| Zeilen im Gesamtdokument | 11 082 | **27 015** |
| Größe des Gesamtdokuments | 606 KB | **1 512 KB** |
| Hauptüberschriften | 131 | **276** |
| registrierte Abschnitte | 122 | **292** |
| aufgelöste Querverweise | 314 | **706** (0 unaufgelöst) |
| Indexmarken | 295 | **325** |
| Beispielprogramme | 41 | **73** (alle lauffähig) |
| PDF-Seiten | — | **715** |
| Zeilen im Gesamtdokument | 11 082 | **27 400** |
| Größe des Gesamtdokuments | 606 KB | **1 536 KB** |
| Hauptüberschriften | 131 | **289** |
| registrierte Abschnitte | 122 | **293** |
| aufgelöste Querverweise | 314 | **712** (0 unaufgelöst) |
| Indexmarken | 295 | **327** |
| Beispielprogramme | 41 | **74** (alle lauffähig) |
| PDF-Seiten | — | **725** |
| Notebooks | — | **25** |
| Plotly-Figuren | — | **4** |
| Diagramme (SVG) | 26 | **32**, davon **18** mit Generatorskript (15 Skripte) |
@ -1329,18 +1329,25 @@ pytest-Tests grün, Version 03 unberührt.
### Der konkret nächste Handgriff
**Alle sechs Phasen sind abgeschlossen, und Version 04 liegt getrennt in `Version_04/`.**
23 Kapitel, 5 Anhänge, 73 lauffähige Programme, 715 PDF-Seiten, Website mit vier
interaktiven Figuren.
**Alle sechs Phasen sind abgeschlossen. Seit dem 08.09.2026 ist `Version_04/` ein eigenes
Git-Repository**, und Phase 6 (Umsetzung von `Verbesserungen_02/_03`) hat begonnen.
23 Kapitel, 5 Anhänge, 74 lauffähige Programme, Website mit vier interaktiven Figuren.
Offen ist nur noch, was von außen kommt:
**Zum eigenen Repository — eine Korrektur der früheren Angabe hier:** `git subtree split -P
Version_04` hätte die Historie **nicht** mitgenommen. Der Pfad `Version_04/` existiert erst
seit der Verzeichnistrennung; der Split hätte 7 statt 41 einschlägiger Commits gebracht — eine
Teilhistorie, die vollständig aussieht und es nicht ist. Volle Historie ginge nur über
`git-filter-repo` (nicht installiert). Deshalb: frischer `git init -b main`, das
übergeordnete Repository bleibt das Archiv der 71 Commits und trägt `Version_04/` in seiner
`.gitignore`.
Offen ist weiterhin, was von außen kommt:
* **Veröffentlichen.** `Version_04/OR_HTML_04/` ist selbstgenügsam und direkt auf einen
Webserver kopierbar.
* **Eigenes Repository.** `git subtree split -P Version_04 -b v04-only` nimmt die Historie
mit. Danach **`COLAB_BASIS_URL`** in `build_version_04.py` auf Kontoname und
Repository-Name setzen — die 25 Colab-Badges hängen daran, und ein falscher Wert fällt
erst beim Klicken auf.
* **Remote-Repository anlegen.** Danach **`COLAB_BASIS_URL`** in `build_version_04.py` auf
Kontoname und Repository-Name setzen — die 25 Colab-Badges hängen daran, und ein falscher
Wert fällt erst beim Klicken auf.
Was einer späteren Sitzung als Erstes auffallen wird und **kein Fehler** ist:
@ -1363,6 +1370,106 @@ Was einer späteren Sitzung als Erstes auffallen wird und **kein Fehler** ist:
---
## 6d. Phase 6 — Umsetzung von `Verbesserungen_02` / `_03` 🔄
Grundlage: `Kritik_und_Verbesserungsvorschlaege/Verbesserungen_02.md` und `_03.md`, zwölf
Arbeitspakete. Nach Ertrag geordnet umgesetzt; **Streamlit-Apps sind auf Wunsch des Autors
zurückgestellt**, ebenso Folium-Karten (die VRP-Instanz ist ein synthetisches Raster ohne
Geokoordinaten) und Zielgruppen-Callouts in allen Kapiteln.
### ✅ Repository und `pyproject.toml`
`git init -b main` in `Version_04`, `Version_04` im übergeordneten Repository ausgetragen.
Neu: `pyproject.toml` mit den Gruppen `finance`, `large-scale`, `api`, `figures`, `dev`,
`empfehlungen` — die abgedruckte `requirements.txt` bleibt unverändert daneben bestehen.
**`ortools` steht in der Grundausstattung, `highspy` erst in `[large-scale]`.** Damit kann
der HiGHS-Symbolkonflikt bei der schlanken Installation gar nicht erst auftreten; `cvxpy`
zieht `highspy` nicht nach.
Zwei Funde beim Abgleich der Importe gegen `requirements.txt`:
* **`graphviz` fehlte** — `bilder_04/erzeuge_architektur_diagramme.py` importiert es, die
Leserliste kennt es nicht. Es gehört nicht in die abgedruckte Datei (kein
Beispielprogramm braucht es), steht jetzt aber in `[figures]`.
* **`pymoo` steht in `requirements.txt`, wird aber von keinem der 74 Programme importiert** —
es ist nur eine Empfehlung im Kapitel Metaheuristiken. Jetzt in `[empfehlungen]`.
Später korrigiert: **`cvxpy` lag zuerst in `[finance]`** — falsch, es wird von 13 Programmen
gebraucht, darunter dreien im Kapitel Unsicherheit, lange vor dem Finanzteil. Jetzt in der
Grundausstattung.
### ✅ 6.1 Chance Constraints ({ref:kap:unsicherheit})
Neuer Abschnitt {ref:sec:unsicherheit-chance-constraints} plus `Chance_Constraints.py`
(74. Programm). Das Kapitel hatte Monte-Carlo, Zweistufigkeit und Worst-Case-Robustheit; die
Wahrscheinlichkeitszusage war die fehlende vierte Antwort — und die, nach der das Management
tatsächlich fragt. Beide Wege an **derselben** Instanz: analytisch als
Second-Order-Cone-Bedingung (CLARABEL) und szenariobasiert als Big-M-MILP (SciPy/HiGHS),
beide in einem Prozess ohne `ortools`- oder `highspy`-Import.
Der Fall ist ein Kraftwerkspark mit 500 MW Zusage. **Drei Befunde, alle gemessen:**
1. **Der Mittelwertplan hält 50,08 %.** Kein Fehler, sondern die Definition des
Erwartungswerts — dieselbe Falle wie im Kapitelanfang, nur teurer.
2. **Sicherheit ist konvex bepreist.** 56 289 € je Prozentpunkt auf dem Weg zu 80 %,
253 848 € zwischen 95 % und 99 % — das **4,5-fache**. Das Programm rechnet die Tabelle
selbst aus, statt sie zu behaupten.
3. **Die Zusage gilt nur für die unterstellte Verteilung.** Der 95-%-Plan hält gemessen
**87,44 %**, sobald die Testverteilung eine Kältewelle mit Dunkelflaute enthält, in der
*alles zugleich* einbricht — auch das Gaskraftwerk. In einer reinen Normalwelt liefert
derselbe Plan 94,97 %. Nicht das Verfahren ist falsch, sondern die Annahme.
**Zwei eigene Fehlgriffe unterwegs, beide durch Messen aufgefallen:**
* Die erste Kostentarierung ergab eine **entartete Lösung** (alles ins Gaskraftwerk), womit
die Kovarianzmatrix wirkungslos war — und gerade sie ist der Grund für die Kegelform. Erst
Kosten, die je *erwarteter* MW vergleichbar sind, machen den Konflikt echt.
* Der erste Kälteeinbruch traf nur Wind und Sonne. Der SOC-Plan hatte die aber ohnehin fast
herausgehalten und war zufällig robust (94,40 %) — das Argument trug nicht. Physikalisch
richtig ist, dass bei Frost auch der Gasdruck fällt; damit trifft der Einbruch alles
zugleich, und genau das kann eine Kovarianzmatrix mit Korrelationen um null nicht
ausdrücken.
**Ehrlich berichtet statt geglättet:** Die Szenariomethode **überanpasst**. Über zwölf Läufe
(je vier Ziehungen bei S = 200, 400, 800, geprüft an 200 000 unabhängigen Szenarien) lag die
tatsächliche Quote zwischen **93,14 % und 96,71 %**, und die Spanne wurde von S = 400 auf
S = 800 wieder **breiter**. Beide Solver (SciPy und HiGHS) bestätigten `optimal` bei
identischen Kosten — es ist echte Überanpassung, kein Solverartefakt. Steht als
⚠️-Kasten im Abschnitt.
**Aus der Ausgabe entfernt: die Laufzeitmessung.** Eine Wanduhrzeit ist nie byteidentisch
reproduzierbar (2,2 s / 2,3 s zwischen zwei Läufen) und hätte Regel 4 dauerhaft gebrochen.
Die Beobachtung steht jetzt im Fließtext, wo eine Spanne ehrlich ist. Danach: drei Läufe
zeichengleich, und die abgedruckte Ausgabe stimmt mit dem Lauf des **extrahierten**
Programms überein.
Eine Aufgabenprämisse musste ich verwerfen: „Ab welcher Zusage wird das Modell unlösbar?“ —
das analytische Modell bleibt bis praktisch 100 % lösbar. Die Aufgabe fragt jetzt nach dem
Fall mit gedrosselter Biomasse, in dem das **szenariobasierte** Modell `infeasible` meldet
(7,50 % der Szenarien deckt selbst der Vollausbau nicht), das analytische aber `optimal`
liefert — und dann 87,39 % hält.
### ⏳ Offen: die Lösungsnummern in Anhang A — **sechste** harte Nummer
**Gefunden, noch nicht behoben.** Der Lösungsanhang trägt **97** feste Marken der Bauart
`**9.2 — Ansatz wählen.**`. Sie stammen aus Version 03, wo Unsicherheit Kapitel 9 war. Heute
ist es Kapitel 12, und die Aufgabe selbst wird korrekt als **„Aufgabe 12.2"** gesetzt.
Das ist keine Schönheitsfrage: **„Aufgabe 9.2" existiert heute wirklich** — sie gehört zum
Kapitel Metaheuristiken („Zuggröße und Temperatur"). Wer die Lösung zu 9.2 sucht, landet
beim falschen Thema.
Beide Prüfungen laufen daran vorbei: Vor der Zahl steht kein Schlüsselwort, und es ist keine
Tabellenzelle.
**Vorschlag zur Behebung** (eigenes Arbeitspaket, rührt an 97 Stellen plus Build):
Die Zuordnung ist herleitbar — der Abschnitt `{#sec:loesungen-<X>}` gehört zum Kapitel
`{#kap:<X>}`. Der Build kann die Marken also selbst vergeben, so wie er es für Aufgaben
bereits tut; im Quelltext bliebe nur noch der Titel stehen.
---
## 8. Commit-Historie des V04-Strangs
> **Zu den Kapitelnummern in dieser Tabelle:** Sie stammen aus den Commit-Betreffs und geben

View file

@ -17,9 +17,9 @@ daraus ab, und alle Befehle unten werden **hier** ausgeführt.
| `Operations_Research_mit_Python_Version_04/` | **Quelle**: 31 Kapiteldateien + Build-Skripte |
| `bilder_04/` | **Quelle**: Diagramme (SVG/PNG) + `erzeuge_*.py`-Generatoren |
| `Operations_Research_mit_Python_Version_04.md` | generiert: Gesamtdokument (Pandoc-Eingabe) |
| `Operations_Research_mit_Python_Version_04.pdf` | generiert: PDF, 715 Seiten |
| `Operations_Research_mit_Python_Version_04.pdf` | generiert: PDF, 725 Seiten |
| `OR_HTML_04/` | generiert: **Mehrseiten-Website** — dieser Ordner wird veröffentlicht |
| `Operations_Research_mit_Python_Version_04_Programme/` | generiert: 73 lauffähige Beispielprogramme |
| `Operations_Research_mit_Python_Version_04_Programme/` | generiert: 74 lauffähige Beispielprogramme |
| `Notebooks_04/` | generiert: ein Jupyter-Notebook je Kapitel |
| `PLAN.md` / `PROGRESS.md` | das Ziel des Umbaus und der erreichte Stand |
| `pyproject.toml` | Abhängigkeiten in Gruppen (`finance`, `large-scale`, `api`, …) |
@ -66,8 +66,8 @@ Wer eine schlanke Umgebung möchte, nimmt stattdessen [`pyproject.toml`](pyproje
seinen Gruppen — die Grundausstattung trägt den Großteil des Buchs:
```bash
pip install -e . # numpy, scipy, pandas, matplotlib, openpyxl, pydantic, ortools
pip install -e ".[finance]" # cvxpy, scikit-learn, yfinance
pip install -e . # numpy, scipy, pandas, matplotlib, openpyxl, pydantic, ortools, cvxpy
pip install -e ".[finance]" # scikit-learn, yfinance
pip install -e ".[large-scale]" # highspy, Pyomo, linopy, polars
pip install -e ".[api]" # fastapi, uvicorn, httpx
pip install -e ".[figures]" # plotly, graphviz — nur zum Neuerzeugen der Diagramme
@ -76,8 +76,9 @@ pip install -e ".[dev]" # pytest
**`ortools` steht in der Grundausstattung, `highspy` erst in `[large-scale]`.** Das ist
Absicht: Beide bringen eine eigene HiGHS-Kopie mit und vertragen sich nicht im selben
Prozess. Wer nur `pip install -e .` nutzt, kann in den Konflikt gar nicht geraten; `cvxpy`
aus `[finance]` zieht ein installiertes `highspy` bei der Solver-Erkennung selbst mit herein.
Prozess. Wer die Grundausstattung installiert, kann in den Konflikt gar nicht geraten —
`cvxpy` zieht `highspy` **nicht** nach, erkennt es aber, sobald `[large-scale]` es
mitgebracht hat.
`graphviz` fehlt in der `requirements.txt` — es wird allein von
`bilder_04/erzeuge_architektur_diagramme.py` gebraucht, nicht von den Beispielprogrammen,

View file

@ -50,12 +50,16 @@ dependencies = [
"openpyxl>=3.1.5", # kein direkter Import: Excel-Motor hinter pandas
"pydantic>=2.12",
"ortools>=9.15.6755",
# cvxpy gehoert in die Grundausstattung, nicht in [finance]: Es wird von
# 13 Programmen gebraucht, darunter dreien im Kapitel Unsicherheit, lange
# vor dem Finanzteil. Es zieht KEIN highspy nach - der Solverkonflikt
# entsteht erst mit [large-scale].
"cvxpy>=1.9.2",
]
[project.optional-dependencies]
# Teil "Anwendungen - Energiewirtschaft und Finanzmaerkte"
finance = [
"cvxpy>=1.9.2",
"scikit-learn>=1.8.0",
"yfinance>=1.2.0",
]