Umgang mit Unsicherheit: Von deterministisch zu robust

← Learning Hub Lektion 7 · Batterieoptimierungs-Kurs · Geschätzte Dauer: 25 Minuten

Jede bisherige Lektion nahm an, dass wir die Preise perfekt kennen. Tun wir nicht. Prognosen sind falsch. Wind fällt unerwartet ab. Ein Kraftwerk fällt aus. Diese Lektion zeigt drei Ansätze zum Umgang mit Preisunsicherheit — vom einfachsten zum anspruchsvollsten.

Das Problem mit deterministischer Optimierung

Dein Lektion-3-Optimierer nimmt einen einzelnen Preisvektor und findet den optimalen Fahrplan für genau diese Preise. Wenn die tatsächlichen Preise abweichen, kann dein „optimaler" Fahrplan suboptimal oder sogar verlustbringend sein.

# Beispiel: Prognose sagt Stunde 18 = €90/MWh → Optimierer entlädt
# Realität: Stunde 18 = €45/MWh (bewölkter Tag, Solar überproduiert)
# Ergebnis: Du hast zu €45 verkauft statt auf eine echte Spitze später zu warten
Die Illusion der Optimalität: Ein deterministischer Optimierer ist nur optimal bezüglich seiner Eingaben. Schlechte Prognose rein → schlechter Fahrplan raus. Das Ziel ist nicht perfekte Optimierung — sondern robuste Entscheidungsfindung.

Ansatz 1: Sensitivitätsanalyse (am einfachsten)

Führe deinen Optimierer mehrfach mit verschiedenen Preisszenarien aus und sieh, wie sich der Fahrplan ändert:

"""
sensitivitaet.py — Wie empfindlich ist der Fahrplan gegenüber Preisänderungen?
"""
import numpy as np

# Basisprognose
preise_basis = [28, 25, 22, 20, 19, 21, 35, 55,
                62, 58, 52, 48, 45, 44, 47, 53,
                68, 85, 92, 78, 60, 45, 35, 30]

# Szenarien generieren: Basis ± Rauschen
np.random.seed(42)
n_szenarien = 20
szenarien = []
for _ in range(n_szenarien):
    rauschen = np.random.normal(0, 8, 24)  # ±€8/MWh Standardabweichung
    szenario = np.clip(np.array(preise_basis) + rauschen, -50, 200)
    szenarien.append(szenario.tolist())

# Optimierer für jedes Szenario ausführen, Fahrpläne sammeln
# ergebnisse = [batterie_loesen(s) for s in szenarien]

# Analyse: Welche Stunden sind IMMER Laden/Entladen unabhängig vom Szenario?
# Das sind robuste Entscheidungen. Stunden die wechseln → riskant, konservativ bieten.
Praktischer Wert: Wenn Stunde 3 in allen 20 Szenarien immer „Laden" ist, ist das ein hochkonfidentes Gebot. Wenn Stunde 15 je nach Szenario zwischen Laden und Entladen wechselt, reduziere deine Gebotsmenge oder überspringe sie.

Ansatz 2: Szenariobasierte stochastische Optimierung

Statt für einen Preispfad zu optimieren, optimiere über mehrere Szenarien gleichzeitig. Die Zielfunktion wird zum erwarteten Erlös über alle Szenarien:

"""
stochastisch.py — Zweistufige stochastische Optimierung
"""
import pyomo.environ as pyo
import numpy as np

T = 24
P_max = 10.0
E_max = 20.0
eta = 0.90
eta_sqrt = np.sqrt(eta)
soc_init = 10.0
deg_kosten = 5.0

# Preisszenarien mit Wahrscheinlichkeiten
szenarien = {
    'basis':  {'prob': 0.5, 'preise': [28,25,22,20,19,21,35,55,62,58,52,48,45,44,47,53,68,85,92,78,60,45,35,30]},
    'hoch':   {'prob': 0.25, 'preise': [35,30,28,25,24,28,45,70,80,75,68,62,58,55,60,68,88,110,120,95,75,55,42,35]},
    'niedrig':{'prob': 0.25, 'preise': [22,20,18,16,15,17,28,42,48,45,40,38,35,34,36,40,52,65,70,58,45,35,28,22]},
}

model = pyo.ConcreteModel()
model.T = pyo.RangeSet(0, T - 1)
model.S = pyo.Set(initialize=szenarien.keys())

# ERSTE-STUFE-Entscheidungen: Gebotsmengen (festgelegt bevor Preise bekannt)
model.p_laden = pyo.Var(model.T, bounds=(0, P_max))
model.p_entladen = pyo.Var(model.T, bounds=(0, P_max))
model.soc = pyo.Var(model.T, bounds=(0.1*E_max, 0.9*E_max))
model.y = pyo.Var(model.T, within=pyo.Binary)

# Zielfunktion: erwarteter Erlös über Szenarien
def ziel_regel(m):
    gesamt = 0
    for s_name, s_daten in szenarien.items():
        prob = s_daten['prob']
        preise = s_daten['preise']
        erloes = sum(preise[t] * (m.p_entladen[t] - m.p_laden[t]) for t in m.T)
        deg = sum(deg_kosten * (m.p_laden[t] + m.p_entladen[t]) for t in m.T)
        gesamt += prob * (erloes - deg)
    return gesamt
model.ziel = pyo.Objective(rule=ziel_regel, sense=pyo.maximize)

# Nebenbedingungen (gleiche Struktur wie zuvor)
def soc_regel(m, t):
    vorher = soc_init if t == 0 else m.soc[t-1]
    return m.soc[t] == vorher + eta_sqrt * m.p_laden[t] - m.p_entladen[t] / eta_sqrt
model.soc_bil = pyo.Constraint(model.T, rule=soc_regel)

def kl(m, t): return m.p_laden[t] <= P_max * (1 - m.y[t])
def ke(m, t): return m.p_entladen[t] <= P_max * m.y[t]
model.kl = pyo.Constraint(model.T, rule=kl)
model.ke = pyo.Constraint(model.T, rule=ke)

solver = pyo.SolverFactory('highs')
ergebnis = solver.solve(model, tee=False)
print(f"Erwarteter Erlös: €{pyo.value(model.ziel):.2f}")

Ansatz 3: Robuste Optimierung (Worst-Case-Schutz)

Statt den Erwartungswert zu optimieren, garantiere eine Mindestperformance im schlimmsten Szenario:

"""
robust.py — Maximiere Worst-Case-Erlös über Szenarien
"""
import pyomo.environ as pyo

# ... gleiche Einrichtung wie oben ...
model = pyo.ConcreteModel()
model.T = pyo.RangeSet(0, T - 1)

model.p_laden = pyo.Var(model.T, bounds=(0, P_max))
model.p_entladen = pyo.Var(model.T, bounds=(0, P_max))
model.soc = pyo.Var(model.T, bounds=(0.1*E_max, 0.9*E_max))
model.y = pyo.Var(model.T, within=pyo.Binary)

# Hilfsvariable: Worst-Case-Erlös
model.worst_case = pyo.Var()

# Zielfunktion: Worst Case maximieren
model.ziel = pyo.Objective(expr=model.worst_case, sense=pyo.maximize)

# Worst Case muss ≤ Erlös in jedem Szenario sein
def szenario_schranke(m, s_name):
    preise = szenarien[s_name]['preise']
    erloes = sum(preise[t] * (m.p_entladen[t] - m.p_laden[t]) for t in m.T)
    deg = sum(deg_kosten * (m.p_laden[t] + m.p_entladen[t]) for t in m.T)
    return m.worst_case <= erloes - deg

model.S = pyo.Set(initialize=szenarien.keys())
model.szenarien_schranken = pyo.Constraint(model.S, rule=szenario_schranke)

Die robuste Lösung ist konservativer: sie jagt keinen Hochrisiko-Spitzen nach, die nur in einem Szenario auftauchen. Stattdessen findet sie Fahrpläne, die in allen Szenarien passabel abschneiden.

Wann welchen Ansatz verwenden

Ansatz Komplexität Am besten wenn
Deterministisch + Marge Niedrig Gute Prognosen, liquider Intraday zur Fehlerkorrektur
Sensitivitätsanalyse Niedrig Robuste vs. riskante Stunden für Gebotsmengen identifizieren
Stochastisch (Erwartungswert) Mittel Probabilistische Prognosen verfügbar, risikoneutraler Betreiber
Robust (Worst-Case) Mittel Hohe Prognoseunsicherheit, risikoaverser Betreiber
Produktionsrealität: Die meisten Batteriebetreiber beginnen mit deterministischer Optimierung + Intraday-Korrektur (Lektion 5). Sie ergänzen Szenarioanalyse für Gebotsmengen. Volle stochastische oder robuste Optimierung wird von anspruchsvollen Trading-Desks genutzt, erhöht aber die Komplexität. Beginne einfach, füge Anspruch hinzu wenn du misst, dass Prognosefehler dich tatsächlich Geld kosten.

Überprüfe dein Verständnis

Ein deterministischer Optimierer erzeugt einen Fahrplan für prognostizierte Preise. Die tatsächlichen Preise liegen 20% niedriger über alle Stunden. Was passiert mit dem realisierten Erlös?

Im stochastischen Modell, was bewirkt das Multiplizieren des Erlöses jedes Szenarios mit seiner Wahrscheinlichkeit?

Im Vergleich zu stochastischer Optimierung erzeugt robuste Optimierung typischerweise Fahrpläne die:

📖 Empfohlene Lektüre

Boyd & Vandenberghe, Kapitel 4.4 — „Robust Linear Programming" — formale Behandlung robuster Optimierung mit Unsicherheitsmengen. Erklärt die Dualität hinter der Max-Min-Formulierung.

💬 Fragen? Stochastisch vs. robust ist eine strategische Entscheidung für dein Produkt. Frag mich über Szenariogenerierung (wie erstellt man realistische Preisszenarien), CVaR (ein Mittelweg zwischen Erwartungswert und Worst-Case), oder wie viele Szenarien du tatsächlich brauchst.

Rechtliches: Impressum