Version 04 als eigenes Repository

Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python"
(Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im
uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch
Version_03 (eingefroren) verwaltet und Version_04/ ignoriert.

Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit
der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen
Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist.

Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise,
325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren,
25 Notebooks, PDF mit 715 Seiten.

Zusaetzlich in diesem Commit:

* pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api,
  figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt
  unveraendert daneben bestehen. ortools steht in der Grundausstattung,
  highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der
  schlanken Installation gar nicht erst auftreten.

* Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py
  importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo
  steht in requirements.txt, wird aber von keinem Programm importiert,
  sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in
  [empfehlungen]).

* NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es
  ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden
  Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen.

* PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den
  tatsaechlichen aus erzeuge_titelseite.py korrigiert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
dschlueter 2026-09-08 01:20:09 +02:00
commit b7af2f1d9a
468 changed files with 262141 additions and 0 deletions

View file

@ -0,0 +1,119 @@
#!/usr/bin/env python3
# Stochastische_Optimierung.py
"""
Kapitel Unsicherheit: Two-Stage Stochastic Programming mit CVXPY.
Eigenschaften:
* Die Analyse am Ende wird BERECHNET statt fest verdrahtet
* Vergleich gegen drei Alternativen: Mittelwert, Worst Case, perfekte Voraussicht
* Kennzahl EVPI (Wert perfekter Information) wird ausgewiesen
"""
import cvxpy as cp
import numpy as np
import pandas as pd
SZENARIEN = ["Ruhig", "Volatil", "Crash"]
WAHRSCHEINLICHKEIT = np.array([0.50, 0.30, 0.20])
BEDARF = np.array([100.0, 250.0, 500.0])
KOSTEN_VORAB = 40.0
KOSTEN_SPOT = 120.0
KOSTEN_LEERLAUF = 5.0
S = len(SZENARIEN)
def loese_stochastisch():
"""Zweistufiges Modell: eine Vorabentscheidung, szenarioabhaengige Korrektur."""
x = cp.Variable(nonneg=True, name="Basiskapazitaet") # Stufe 1
y_spot = cp.Variable(S, nonneg=True, name="Spot_Zukauf") # Stufe 2
y_leer = cp.Variable(S, nonneg=True, name="Leerlauf") # Stufe 2
# Kopplung: Basis + Zukauf - Leerlauf == Bedarf (je Szenario)
nebenbedingungen = [x + y_spot[s] - y_leer[s] == BEDARF[s] for s in range(S)]
erwartete_korrektur = sum(
WAHRSCHEINLICHKEIT[s] * (KOSTEN_SPOT * y_spot[s] + KOSTEN_LEERLAUF * y_leer[s])
for s in range(S))
problem = cp.Problem(cp.Minimize(KOSTEN_VORAB * x + erwartete_korrektur),
nebenbedingungen)
problem.solve()
return problem, x, y_spot, y_leer
def kosten_bei(kapazitaet):
"""Erwartete Gesamtkosten fuer eine fest vorgegebene Kapazitaet."""
unter = np.maximum(BEDARF - kapazitaet, 0.0)
ueber = np.maximum(kapazitaet - BEDARF, 0.0)
je_szenario = KOSTEN_VORAB * kapazitaet + KOSTEN_SPOT * unter + KOSTEN_LEERLAUF * ueber
return float(WAHRSCHEINLICHKEIT @ je_szenario)
if __name__ == "__main__":
problem, x, y_spot, y_leer = loese_stochastisch()
kapazitaet = float(x.value)
mittelwert = float(WAHRSCHEINLICHKEIT @ BEDARF)
print("=" * 82)
print(" STOCHASTISCHE TWO-STAGE OPTIMIERUNG (KAPAZITAETSPLANUNG)")
print("=" * 82)
print(f"Status: {problem.status}")
print(f"Erwarteter Bedarf (Mittelwert): {mittelwert:.1f} Einheiten")
print(f"Optimale Stufe-1-Kapazitaet x*: {kapazitaet:.1f} Einheiten")
print(f"Minimale erwartete Gesamtkosten: {problem.value:,.2f} EUR\n")
tabelle = pd.DataFrame({
"Szenario": SZENARIEN,
"Wahrsch.": [f"{p*100:.0f} %" for p in WAHRSCHEINLICHKEIT],
"Bedarf": BEDARF,
"Basis genutzt": [min(kapazitaet, b) for b in BEDARF],
"Spot-Zukauf": np.round(y_spot.value, 1),
"Leerlauf": np.round(y_leer.value, 1),
"Kosten (EUR)": [f"{KOSTEN_VORAB*kapazitaet + KOSTEN_SPOT*y_spot.value[s] + KOSTEN_LEERLAUF*y_leer.value[s]:,.0f}"
for s in range(S)],
})
print(tabelle.to_string(index=False))
# --- Vergleich mit Alternativstrategien (berechnet, nicht behauptet) --
print("\n" + "-" * 82)
print("Vergleich verschiedener Planungsstrategien:")
print(f"{'Strategie':<34} {'Kapazitaet':>11} {'Erw. Kosten':>14} {'Mehrkosten':>13}")
print("-" * 82)
optimal = problem.value
strategien = [
("Stochastisch optimal", kapazitaet),
("Naiv: Mittelwert einsetzen", mittelwert),
("Vorsichtig: Worst Case abdecken", float(BEDARF.max())),
("Optimistisch: Bestfall", float(BEDARF.min())),
]
for name, kap in strategien:
kosten = kosten_bei(kap)
print(f"{name:<34} {kap:>11.1f} {kosten:>14,.0f} "
f"{kosten - optimal:>+13,.0f}")
# --- EVPI: Was waere perfekte Voraussicht wert? ----------------------
# Bei perfekter Information wuerde man je Szenario genau den Bedarf kaufen.
kosten_perfekt = float(WAHRSCHEINLICHKEIT @ (KOSTEN_VORAB * BEDARF))
evpi = optimal - kosten_perfekt
print("-" * 82)
print(f"Kosten bei perfekter Voraussicht: {kosten_perfekt:>10,.0f} EUR")
print(f"Wert perfekter Information (EVPI): {evpi:>10,.0f} EUR "
f"({evpi/optimal*100:.1f} % der Kosten)")
print(" -> So viel duerfte eine perfekte Bedarfsprognose hoechstens kosten.")
# --- Automatische Interpretation --------------------------------------
print("-" * 82)
if kapazitaet > mittelwert + 1e-6:
print(f"Analyse: Der Solver waehlt {kapazitaet:.0f} Einheiten und damit MEHR als")
print(f"den Mittelwert ({mittelwert:.0f}), weil Unterdeckung ({KOSTEN_SPOT:.0f} EUR)")
print(f"deutlich teurer ist als Leerlauf ({KOSTEN_LEERLAUF:.0f} EUR).")
elif kapazitaet < mittelwert - 1e-6:
print(f"Analyse: Der Solver waehlt {kapazitaet:.0f} und damit WENIGER als den")
print(f"Mittelwert ({mittelwert:.0f}) - Leerlauf ist hier teurer als Zukauf.")
else:
print("Analyse: Kapazitaet entspricht dem Mittelwert (symmetrische Kosten).")
print("=" * 82)