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>
119 lines
4.8 KiB
Python
Generated
119 lines
4.8 KiB
Python
Generated
#!/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)
|