Kapitel 14: Mehrere Ziele — Pareto-Fronten statt Gewichte
📌 Kapitel auf einen Blick
Worum geht es? Um den Normalfall, den die bisherigen Kapitel umgangen haben: Es gibt nicht ein Ziel, sondern zwei, die gegeneinander stehen. Kosten und CO₂, Termintreue und Bestand, Rendite und Risiko.
Voraussetzungen: Kapitel 4 (die Zielgröße als Entscheidung) und Kapitel 6. Für den Denkfehler hilft Kapitel 19.
Danach können Sie: begründen, warum eine gewichtete Summe bei ganzzahligen Modellen Kompromisse grundsätzlich nicht erreichen kann; eine vollständige Pareto-Front mit dem ε-Constraint-Verfahren berechnen; und einer Geschäftsführung eine Entscheidung vorlegen, die sie tatsächlich treffen kann.
Zeitbedarf: ca. 4 Stunden.
Programme:
Mehrziel_Pareto.pyNotebook: mehrziel.ipynb
In Google Colab öffnen
14.1 In 5 Minuten gelöst
🚀 In 5 Minuten gelöst: Was kostet ein Kilogramm CO₂?
Eine Spedition vergibt fünf Sendungen an LKW, Bahn oder Kombinierten Verkehr. Der LKW ist billig und schmutzig, die Bahn teuer und sauber — und es sind nur zwei Trassen frei.
Statt zu fragen „wie wichtig ist uns CO₂?” fragen wir: Was bekommen wir für zwei Prozent mehr Geld?
import numpy as np from scipy.optimize import linprog # LKW Bahn Kombi LKW billig+schmutzig, Bahn teuer+sauber kosten = np.array([[ 900, 1150, 1020], [ 620, 790, 700], [1300, 1660, 1470], [ 480, 610, 545], [1050, 1340, 1180]], dtype=float) co2 = np.array([[ 1400, 310, 760], [ 980, 220, 530], [2000, 440, 1080], [ 760, 170, 410], [1620, 360, 880]], dtype=float) eine_pro_sendung = np.repeat(np.eye(5), 3, axis=1) bahn_trassen = [[1.0 if j == 1 else 0.0 for _ in range(5) for j in range(3)]] def plane(ziel, kostenbudget=None): A, b = list(bahn_trassen), [2.0] # nur zwei Trassen frei if kostenbudget is not None: A.append(kosten.reshape(-1)); b.append(kostenbudget) x = linprog(ziel.reshape(-1), A_ub=A, b_ub=b, A_eq=eine_pro_sendung, b_eq=np.ones(5), bounds=(0, 1), integrality=1, method="highs").x return kosten.reshape(-1) @ x, co2.reshape(-1) @ x k0, c0 = plane(kosten) # nur Kosten minimieren k1, c1 = plane(co2, kostenbudget=k0 * 1.02) # 2 % mehr ausgeben print(f"guenstigster Plan: {k0:>7,.0f} EUR {c0:>6,.0f} kg CO2") print(f"mit 2 % Aufpreis: {k1:>7,.0f} EUR {c1:>6,.0f} kg CO2 " f"({(c0-c1)/c0:.0%} weniger, {(k1-k0)/(c0-c1):.2f} EUR je kg)")Ausgabe:
guenstigster Plan: 4,350 EUR 6,760 kg CO2 mit 2 % Aufpreis: 4,430 EUR 6,310 kg CO2 (7% weniger, 0.18 EUR je kg)
Und jetzt der Punkt. 80 Euro mehr sparen 450 Kilogramm CO₂ — 18 Cent je Kilogramm. Der europäische Emissionshandel bewegt sich um ein Vielfaches davon. Die Entscheidung ist damit keine Wertefrage mehr, sondern eine Rechnung.
Beachten Sie, was hier nicht passiert ist: Niemand musste ein Gewicht festlegen. Die zweite Zeile setzt eine Schranke („höchstens 2 % teurer”) und optimiert dann das andere Ziel. Das Ergebnis ist eine Aussage, die ein Kaufmann prüfen kann.
🎯 Merksatz „Wie wichtig ist Ihnen CO₂?” ist keine beantwortbare Frage. „Wir geben zwei Prozent mehr aus — was bringt das?” ist eine.
Warum funktioniert das? Weil aus zwei Zielen ein Ziel und eine Nebenbedingung geworden sind. Das ist die Grundidee des ε-Constraint-Verfahrens, um das sich dieses Kapitel dreht — und es ist der Grund, warum es ohne jedes Gewicht auskommt.
14.2 Lernziele
Nach diesem Kapitel können Sie …
- … Pareto-Dominanz und Pareto-Front definieren und an einem Diagramm zeigen.
- … erklären, warum die gewichtete Summe nur gestützte Lösungen findet — und was das geometrisch heißt.
- … eine vollständige Pareto-Front mit dem ε-Constraint-Verfahren berechnen, mit so vielen Solverläufen, wie die Front Punkte hat.
- … lexikografische Optimierung einsetzen, wenn es eine klare Rangfolge der Ziele gibt.
- … einer Entscheiderin die Front so vorlegen, dass sie eine Wahl treffen kann.
14.3 Wann ein Kompromiss gut ist
Bei einem Ziel ist „besser” eindeutig. Bei zwei Zielen nicht mehr: Ein Plan kann billiger und schmutziger sein als ein anderer, und dann ist keiner von beiden besser.
Vergleichbar sind nur die Fälle, in denen ein Plan in keinem Ziel schlechter ist:
x \text{ dominiert } y \quad :\Longleftrightarrow \quad f_i(x) \le f_i(y)\ \text{ für alle } i \ \text{ und }\ f_j(x) < f_j(y)\ \text{ für mindestens ein } j
🔤 Formel-Übersetzer
Mathematik Alltagssprache f_i(x) \le f_i(y) für alle i „Plan x ist in keinem Ziel schlechter als y.“ f_j(x) < f_j(y) für ein j „… und in mindestens einem echt besser.“ x dominiert y „y kann man wegwerfen. Es gibt keinen Grund, ihn zu wählen.“ x ist pareto-optimal „Niemand dominiert x. Wer x verbessern will, muss anderswo schlechter werden.“ Pareto-Front „Die Speisekarte: alle Pläne, die man ernsthaft in Betracht ziehen kann.“ Ohne Formel gesagt: Wegwerfen darf man einen Plan nur, wenn ein anderer ihn in jeder Hinsicht schlägt. Alles Übrige ist eine Frage der Präferenz — und die gehört nicht dem Modellierer.
📖 Definition: Pareto-Front
Die Menge aller nicht dominierten Lösungen. Sie ist das vollständige Ergebnis eines Mehrzielproblems — nicht ein Punkt, sondern eine Liste.
Wer daraus einen einzelnen Punkt macht, trifft eine Entscheidung. Die Frage ist nur, ob er es merkt.
14.4 Der Reflex: alles in ein Ziel rühren
Der naheliegende Weg, zwei Ziele zu einem zu machen, ist die gewichtete Summe:
\min_{x \in X} \; c(x) + w \cdot e(x)
🔤 Formel-Übersetzer
Mathematik Alltagssprache c(x) Transportkosten des Plans x, in Euro. e(x) CO₂-Ausstoß desselben Plans, in Kilogramm. w „So viele Euro ist mir ein Kilogramm CO₂ wert.“ Ein Wechselkurs, kein Wichtigkeitsregler (Abschnitt 7.10). \min_{x \in X} über alle zulässigen Pläne. Der Ausdruck hat eine Einheit: Euro. Damit ist w keine freie Zahl zwischen 0 und 1, sondern ein CO₂-Preis in €/kg — und den kann man tatsächlich beziffern.
Das ist bequem, liefert zulässige Lösungen und wird in der Praxis fast immer so gemacht. Es hat nur einen Fehler, den man erst sieht, wenn man das Ergebnis mit der vollständigen Front vergleicht.
14.5 Das Programm
Zwölf Sendungen, drei Verkehrsträger, fünf freie Bahntrassen. Der Zielkonflikt entsteht nicht zwischen den Trägern — die Bahn ist immer billiger und sauberer als der LKW —, sondern durch die Knappheit der Trassen. Genau so sieht es in der Praxis aus.
#!/usr/bin/env python3
# Mehrziel_Pareto.py
"""
Kapitel Mehrziel: Kosten gegen CO2 - und warum Gewichte nicht genuegen.
Eine Spedition vergibt zwoelf Sendungen an drei Verkehrstraeger: LKW (schnell,
teuer, schmutzig), Bahn (billig und sauber, aber nur fuenf Trassen frei) und
Kombinierten Verkehr (dazwischen). Zwei Ziele stehen gegeneinander:
Transportkosten und CO2-Ausstoss.
Der uebliche Reflex ist, beide Ziele zu einem zusammenzuruehren:
minimiere Kosten + w * CO2
Das ist bequem, liefert zulaessige Loesungen - und ist unvollstaendig. Bei
ganzzahligen Entscheidungen gibt es Kompromisse, die auf diesem Weg
GRUNDSAETZLICH nicht erreichbar sind, egal welches w man waehlt. Nicht "schwer
zu finden", sondern beweisbar unerreichbar.
Das Programm zeigt in vier Teilen:
1. Die beiden Extreme - was jedes Ziel allein kostet.
2. Die lineare Skalarisierung ueber ein feines Gewichtsraster.
3. Die vollstaendige Pareto-Front ueber das eps-Constraint-Verfahren.
4. Den Nachweis, dass die Luecke keine Frage des Rasters ist, sondern
Geometrie: Die fehlenden Punkte liegen strikt oberhalb der konvexen
Huelle und koennen deshalb von keiner Geraden gestuetzt werden.
Benoetigt: numpy, scipy
"""
from __future__ import annotations
import numpy as np
from scipy.optimize import linprog
TRAEGER = ["LKW", "Bahn", "Kombiniert"]
BAHN_TRASSEN = 5 # so viele Sendungen passen hoechstens auf die Bahn
SAAT = 5
def erzeuge_sendungen(anzahl: int = 12, saat: int = SAAT):
"""Kosten und CO2 je Sendung und Verkehrstraeger.
Die Bahn ist immer billiger und sauberer als der LKW - der Zielkonflikt
entsteht nicht zwischen den Traegern, sondern durch die KNAPPHEIT der
Trassen. Genau so sieht es in der Praxis aus.
"""
rng = np.random.default_rng(saat)
kosten = np.zeros((anzahl, 3))
co2 = np.zeros((anzahl, 3))
for i in range(anzahl):
grund_kosten = rng.integers(600, 2400)
grund_co2 = rng.integers(400, 1800)
kosten[i] = [grund_kosten,
grund_kosten * rng.uniform(0.55, 0.85),
grund_kosten * rng.uniform(0.70, 1.00)]
co2[i] = [grund_co2,
grund_co2 * rng.uniform(0.15, 0.35),
grund_co2 * rng.uniform(0.40, 0.70)]
return np.round(kosten).astype(int), np.round(co2).astype(int)
KOSTEN, CO2 = erzeuge_sendungen()
N = len(KOSTEN)
def plane(ziel: np.ndarray, co2_grenze: float | None = None,
kosten_grenze: float | None = None):
"""Jede Sendung genau einem Traeger zuordnen; Trassen sind knapp.
'ziel' ist die zu minimierende Matrix (Kosten, CO2 oder eine Mischung).
Die beiden Grenzen sind das Werkzeug fuer eps-Constraint und
lexikografische Optimierung - sie machen aus einem Ziel eine Schranke.
"""
gleichungen = np.zeros((N, N * 3))
for i in range(N):
gleichungen[i, i * 3:(i + 1) * 3] = 1.0 # genau ein Traeger
ungleichungen = [[1.0 if j == 1 else 0.0 for _ in range(N) for j in range(3)]]
grenzen = [float(BAHN_TRASSEN)]
if co2_grenze is not None:
ungleichungen.append(CO2.reshape(-1).astype(float))
grenzen.append(float(co2_grenze))
if kosten_grenze is not None:
ungleichungen.append(KOSTEN.reshape(-1).astype(float))
grenzen.append(float(kosten_grenze))
ergebnis = linprog(ziel.reshape(-1).astype(float),
A_ub=ungleichungen, b_ub=grenzen,
A_eq=gleichungen, b_eq=np.ones(N),
bounds=(0, 1), integrality=1, method="highs")
if not ergebnis.success:
return None
plan = np.round(ergebnis.x).astype(int)
return (int(KOSTEN.reshape(-1) @ plan), int(CO2.reshape(-1) @ plan), plan)
def pareto_front():
"""Vollstaendige Front ueber das eps-Constraint-Verfahren.
Der Ablauf ist der eigentliche Inhalt dieser Funktion: Erst das
Kostenminimum bestimmen (der eine Rand der Front), dann die CO2-Schranke
schrittweise um genau ein Kilogramm unter den zuletzt erreichten Wert
druecken. Jeder Lauf liefert den naechsten Punkt - und wenn keiner mehr
zulaessig ist, ist die Front vollstaendig.
Das braucht so viele Solveraufrufe, wie die Front Punkte hat. Ein Raster
ueber alle moeglichen CO2-Werte braeuchte hier fast tausend.
"""
front = []
start = plane(KOSTEN)
grenze = start[1]
while True:
ergebnis = plane(KOSTEN, co2_grenze=grenze)
if ergebnis is None:
break
front.append((ergebnis[0], ergebnis[1]))
grenze = ergebnis[1] - 1
return front
def skalarisierung(gewichte):
"""Was 'Kosten + w * CO2' fuer viele w hergibt - als Menge von Punkten."""
gefunden = {}
for w in gewichte:
ergebnis = plane(KOSTEN + w * CO2)
if ergebnis:
gefunden.setdefault((ergebnis[0], ergebnis[1]), []).append(w)
return gefunden
def untere_huelle(punkte):
"""Untere linke konvexe Huelle - genau die Punkte, die eine Gerade stuetzt.
Der Zusammenhang, um den es geht: 'Kosten + w*CO2 minimieren' heisst
geometrisch, eine Gerade der Steigung -1/w von links unten an die
Punktwolke zu schieben. Sie beruehrt immer einen Eckpunkt der konvexen
Huelle. Punkte, die oberhalb liegen, werden nie beruehrt - fuer kein w.
"""
huelle = []
for punkt in sorted(punkte):
while len(huelle) >= 2:
(x1, y1), (x2, y2) = huelle[-2], huelle[-1]
if (x2 - x1) * (punkt[1] - y1) - (y2 - y1) * (punkt[0] - x1) <= 0:
huelle.pop()
else:
break
huelle.append(punkt)
return huelle
if __name__ == "__main__":
print("=" * 80)
print(" KOSTEN GEGEN CO2 - UND WARUM GEWICHTE NICHT GENUEGEN")
print("=" * 80)
print(f"{N} Sendungen, {len(TRAEGER)} Verkehrstraeger, "
f"{BAHN_TRASSEN} freie Bahntrassen.\n")
# --- 1. Die beiden Extreme -------------------------------------------
guenstigst = plane(KOSTEN)
saubersten = plane(CO2)
print("1. Was jedes Ziel allein ergibt\n")
print(f" {'':<22} {'Kosten':>10} {'CO2':>10}")
print(" " + "-" * 44)
print(f" {'nur Kosten minimal':<22} {guenstigst[0]:>10,} {guenstigst[1]:>9,} kg")
print(f" {'nur CO2 minimal':<22} {saubersten[0]:>10,} {saubersten[1]:>9,} kg")
print(f"\n Der Zielkonflikt ist echt, aber klein: "
f"{saubersten[0] - guenstigst[0]:,} EUR mehr")
print(f" ({(saubersten[0] - guenstigst[0]) / guenstigst[0] * 100:.1f} %) sparen "
f"{guenstigst[1] - saubersten[1]:,} kg CO2 "
f"({(guenstigst[1] - saubersten[1]) / guenstigst[1] * 100:.1f} %).")
print(" Genau solche Zahlen will die Geschaeftsfuehrung sehen - nicht ein")
print(" Gewicht, das niemand interpretieren kann.")
# --- 2. Die lineare Skalarisierung -----------------------------------
gewichte = np.concatenate([np.linspace(0.0, 3.0, 1201),
np.geomspace(3.0, 1000.0, 200)])
gefunden = skalarisierung(gewichte)
print("\n" + "-" * 80)
print(f"2. Lineare Skalarisierung: 'Kosten + w * CO2' fuer "
f"{len(gewichte):,} Gewichte\n")
print(f" {'Kosten':>10} {'CO2':>10} {'gefunden bei w':>16}")
print(" " + "-" * 46)
for (k, c), ws in sorted(gefunden.items()):
print(f" {k:>10,} {c:>9,} kg {min(ws):>7.3f} bis {max(ws):>7.3f}")
print(f"\n {len(gefunden)} verschiedene Plaene - fuer {len(gewichte):,} Gewichte.")
print(" Das Gewicht ist also gar keine Feineinstellung: Weite Bereiche")
print(" liefern dasselbe Ergebnis, und dazwischen springt es.")
# --- 3. Die vollstaendige Front --------------------------------------
front = pareto_front()
print("\n" + "-" * 80)
print(f"3. Die vollstaendige Pareto-Front ueber eps-Constraint "
f"({len(front)} Solverlaeufe)\n")
print(f" {'Kosten':>10} {'CO2':>10} {'Aufpreis':>9} {'CO2-Ersparnis':>14} "
f"{'EUR je kg':>10}")
print(" " + "-" * 60)
for k, c in front:
auf = k - guenstigst[0]
ersparnis = guenstigst[1] - c
preis = auf / ersparnis if ersparnis else 0.0
print(f" {k:>10,} {c:>9,} kg {auf:>8,} {ersparnis:>13,} "
f"{preis:>10.2f}")
# --- 4. Der Nachweis --------------------------------------------------
huelle = untere_huelle(front)
unerreichbar = [p for p in front if p not in huelle]
print("\n" + "-" * 80)
print("4. Was die Skalarisierung nicht findet\n")
erreicht = [p for p in front if p in gefunden]
print(f" Pareto-Punkte insgesamt: {len(front)}")
print(f" davon von der Skalarisierung gefunden: {len(erreicht)}")
print(f" nie gefunden: {len(front) - len(erreicht)}")
print()
print(" Diese Kompromisse sind fuer KEIN Gewicht erreichbar:\n")
print(f" {'Kosten':>10} {'CO2':>10} {'Aufpreis':>9} {'CO2-Ersparnis':>14}")
print(" " + "-" * 50)
for k, c in unerreichbar:
print(f" {k:>10,} {c:>9,} kg {k - guenstigst[0]:>8,} "
f"{guenstigst[1] - c:>13,}")
stimmt = sorted(unerreichbar) == sorted(p for p in front if p not in gefunden)
print(f"\n Gegenprobe ueber die Geometrie: {'bestanden' if stimmt else 'ABWEICHUNG'}")
print(" Genau die Punkte, die das Gewichtsraster verfehlt, liegen strikt")
print(" oberhalb der unteren konvexen Huelle. Das ist kein Rasterproblem -")
print(" eine Gerade, die von links unten an die Wolke geschoben wird,")
print(" beruehrt immer einen Eckpunkt der Huelle und nie einen Punkt")
print(" darueber. Ein feineres Raster aendert daran nichts.")
# --- 5. Lexikografisch -------------------------------------------------
print("\n" + "-" * 80)
print("5. Lexikografisch: erst Kosten, dann CO2 im Rahmen eines Budgets\n")
print(f" {'Kostenbudget':>14} {'Kosten':>10} {'CO2':>10} {'gegenueber Minimum':>20}")
print(" " + "-" * 58)
for aufschlag in (0.00, 0.01, 0.02, 0.05, 0.10):
budget = guenstigst[0] * (1 + aufschlag)
ergebnis = plane(CO2, kosten_grenze=budget)
if ergebnis is None:
print(f" {aufschlag:>13.0%} unzulaessig")
continue
print(f" {aufschlag:>13.0%} {ergebnis[0]:>10,} {ergebnis[1]:>9,} kg "
f"{guenstigst[1] - ergebnis[1]:>15,} kg weniger")
print("\n Das ist die Form, die im Betrieb am ehesten trifft: Nicht 'wie")
print(" wichtig ist CO2?', sondern 'wir geben zwei Prozent mehr aus - was")
print(" bringt das?'. Die Frage kann ein Kaufmann beantworten.")
print("\n" + "=" * 80)
print(" WAS MAN DARAUS MITNIMMT")
print("=" * 80)
print("Ein Gewicht zu setzen heisst, die Entscheidung heimlich zu treffen -")
print("und dabei einen Teil der Moeglichkeiten gar nicht erst zu sehen.")
print()
print("Die Pareto-Front ist die ehrlichere Antwort: Sie legt dem Betrieb")
print("alle sinnvollen Kompromisse vor und ueberlaesst ihm die Wahl. Die")
print("Spalte 'EUR je kg' macht sie entscheidbar - man vergleicht sie mit")
print("dem CO2-Preis, den das Unternehmen ohnehin ansetzt.")
print("=" * 80)Erwartete Ausgabe:
================================================================================
KOSTEN GEGEN CO2 - UND WARUM GEWICHTE NICHT GENUEGEN
================================================================================
12 Sendungen, 3 Verkehrstraeger, 5 freie Bahntrassen.
1. Was jedes Ziel allein ergibt
Kosten CO2
--------------------------------------------
nur Kosten minimal 13,543 6,475 kg
nur CO2 minimal 14,821 5,506 kg
Der Zielkonflikt ist echt, aber klein: 1,278 EUR mehr
(9.4 %) sparen 969 kg CO2 (15.0 %).
Genau solche Zahlen will die Geschaeftsfuehrung sehen - nicht ein
Gewicht, das niemand interpretieren kann.
--------------------------------------------------------------------------------
2. Lineare Skalarisierung: 'Kosten + w * CO2' fuer 1,401 Gewichte
Kosten CO2 gefunden bei w
----------------------------------------------
13,543 6,475 kg 0.000 bis 0.398
13,724 6,022 kg 0.400 bis 1.002
14,036 5,711 kg 1.005 bis 1.935
14,189 5,632 kg 1.938 bis 3.471
14,592 5,517 kg 3.574 bis 20.599
14,821 5,506 kg 21.210 bis 1000.000
6 verschiedene Plaene - fuer 1,401 Gewichte.
Das Gewicht ist also gar keine Feineinstellung: Weite Bereiche
liefern dasselbe Ergebnis, und dazwischen springt es.
--------------------------------------------------------------------------------
3. Die vollstaendige Pareto-Front ueber eps-Constraint (10 Solverlaeufe)
Kosten CO2 Aufpreis CO2-Ersparnis EUR je kg
------------------------------------------------------------
13,543 6,475 kg 0 0 0.00
13,635 6,317 kg 92 158 0.58
13,718 6,286 kg 175 189 0.93
13,724 6,022 kg 181 453 0.40
13,868 5,927 kg 325 548 0.59
13,978 5,912 kg 435 563 0.77
14,036 5,711 kg 493 764 0.65
14,189 5,632 kg 646 843 0.77
14,592 5,517 kg 1,049 958 1.09
14,821 5,506 kg 1,278 969 1.32
--------------------------------------------------------------------------------
4. Was die Skalarisierung nicht findet
Pareto-Punkte insgesamt: 10
davon von der Skalarisierung gefunden: 6
nie gefunden: 4
Diese Kompromisse sind fuer KEIN Gewicht erreichbar:
Kosten CO2 Aufpreis CO2-Ersparnis
--------------------------------------------------
13,635 6,317 kg 92 158
13,718 6,286 kg 175 189
13,868 5,927 kg 325 548
13,978 5,912 kg 435 563
Gegenprobe ueber die Geometrie: bestanden
Genau die Punkte, die das Gewichtsraster verfehlt, liegen strikt
oberhalb der unteren konvexen Huelle. Das ist kein Rasterproblem -
eine Gerade, die von links unten an die Wolke geschoben wird,
beruehrt immer einen Eckpunkt der Huelle und nie einen Punkt
darueber. Ein feineres Raster aendert daran nichts.
--------------------------------------------------------------------------------
5. Lexikografisch: erst Kosten, dann CO2 im Rahmen eines Budgets
Kostenbudget Kosten CO2 gegenueber Minimum
----------------------------------------------------------
0% 13,543 6,475 kg 0 kg weniger
1% 13,635 6,317 kg 158 kg weniger
2% 13,724 6,022 kg 453 kg weniger
5% 14,189 5,632 kg 843 kg weniger
10% 14,821 5,506 kg 969 kg weniger
Das ist die Form, die im Betrieb am ehesten trifft: Nicht 'wie
wichtig ist CO2?', sondern 'wir geben zwei Prozent mehr aus - was
bringt das?'. Die Frage kann ein Kaufmann beantworten.
================================================================================
WAS MAN DARAUS MITNIMMT
================================================================================
Ein Gewicht zu setzen heisst, die Entscheidung heimlich zu treffen -
und dabei einen Teil der Moeglichkeiten gar nicht erst zu sehen.
Die Pareto-Front ist die ehrlichere Antwort: Sie legt dem Betrieb
alle sinnvollen Kompromisse vor und ueberlaesst ihm die Wahl. Die
Spalte 'EUR je kg' macht sie entscheidbar - man vergleicht sie mit
dem CO2-Preis, den das Unternehmen ohnehin ansetzt.
================================================================================
14.6 Was die gewichtete Summe nicht sieht
Die Front hat zehn Punkte. Ein Raster aus 1 401 Gewichten findet sechs davon. Vier Kompromisse sind für kein Gewicht erreichbar — und das ist keine Frage der Rasterweite.
Warum es keine Frage des Rasters ist. „c(x) + w\,e(x) minimieren” heißt geometrisch: eine Gerade der Steigung -1/w von links unten an die Punktwolke schieben und schauen, welchen Punkt sie zuerst berührt. Eine Gerade berührt immer einen Eckpunkt der unteren konvexen Hülle. Punkte, die oberhalb der Hülle liegen, werden von keiner Geraden zuerst getroffen — für kein w, bei beliebig feiner Abstufung.
Das Programm rechnet diese Hülle in untere_huelle() unabhängig aus und vergleicht: Genau die vier Punkte, die das Gewichtsraster verfehlt, liegen strikt oberhalb. Die Gegenprobe steht in der Ausgabe.
⚠️ Der Grund liegt in der Ganzzahligkeit
Bei einem reinen LP ist der zulässige Bereich konvex, und die Pareto-Front liegt vollständig auf ihrer eigenen konvexen Hülle — dort findet die gewichtete Summe alles. Sobald Entscheidungen ganzzahlig werden (welcher Träger, welches Lager, welche Schicht), zerfällt der Bereich in einzelne Punkte, und zwischen ihnen entstehen Einbuchtungen.
Deshalb betrifft dieses Problem praktisch jedes betriebliche Mehrzielmodell. Wer mit Gewichten arbeitet, verliert nicht ein paar Nachkommastellen, sondern ganze Alternativen — im Beispiel 40 % der Front.
Das Gewicht ist keine Feineinstellung
Die zweite Beobachtung aus derselben Tabelle: 1 401 Gewichte erzeugen sechs verschiedene Pläne. Weite Gewichtsbereiche liefern dasselbe Ergebnis, und dazwischen springt es.
Das macht die verbreitete Vorgehensweise — „wir probieren ein paar Gewichte und schauen, was herauskommt” — zu einem Glücksspiel. Man weiß nie, ob man einen neuen Kompromiss gefunden hat oder nur denselben noch einmal.
14.7 Das ε-Constraint-Verfahren
Der Ausweg ist unspektakulär: Man macht aus dem zweiten Ziel eine Nebenbedingung.
\min_{x \in X} \; c(x) \qquad \text{unter} \qquad e(x) \le \varepsilon
Für jedes \varepsilon liefert das einen Punkt der Front. Die Kunst besteht nur darin, die \varepsilon geschickt zu wählen — und dafür gibt es ein Verfahren, das genau so viele Solverläufe braucht, wie die Front Punkte hat:
- Kostenminimum bestimmen. Das ist der eine Rand der Front; nebenbei fällt sein CO₂-Wert an.
- \varepsilon auf ein Kilogramm unter diesen Wert setzen und erneut lösen.
- Schritt 2 wiederholen, bis kein zulässiger Plan mehr existiert.
Im Beispiel sind das zehn Läufe. Ein Raster über alle möglichen CO₂-Werte bräuchte fast tausend, ein Gewichtsraster findet auch mit 1 401 Läufen nur sechs Punkte.
🎯 Merksatz ε-Constraint ist das Arbeitspferd der Mehrzieloptimierung: vollständig, mit minimal vielen Solverläufen, und ohne dass jemand ein Gewicht erfinden muss.
Wenn es eine klare Rangfolge gibt: lexikografisch
Manchmal ist ein Ziel wirklich vorrangig. Dann optimiert man lexikografisch: erst das erste Ziel, dann das zweite unter der Bedingung, dass das erste (fast) erhalten bleibt.
| Kostenbudget | Kosten | CO₂ | gegenüber dem Minimum |
|---|---|---|---|
| +0 % | 13 543 € | 6 475 kg | — |
| +1 % | 13 635 € | 6 317 kg | 158 kg weniger |
| +2 % | 13 724 € | 6 022 kg | 453 kg weniger |
| +5 % | 14 189 € | 5 632 kg | 843 kg weniger |
| +10 % | 14 821 € | 5 506 kg | 969 kg weniger |
Der Sprung von +1 % auf +2 % ist der interessante: Das zweite Prozent bringt fast dreimal so viel wie das erste. Solche Unregelmäßigkeiten sind bei ganzzahligen Modellen die Regel — und ein weiteres Argument gegen Gewichte, die eine gleichmäßige Abwägung suggerieren.
Beachten Sie außerdem: Die Zeile „+0 %” ist ein Sonderfall, der sich auszahlt. Sie sucht unter allen kostenminimalen Plänen den saubersten. Gibt es mehrere optimale Lösungen — und das ist bei ganzzahligen Modellen fast immer so —, bekommt man die CO₂-Ersparnis geschenkt. Ein Solver, der nur ein Ziel kennt, gibt einfach die erstbeste zurück.
14.8 Wie man die Front vorlegt
Eine Front mit zehn Punkten ist kein Ergebnis, sondern eine Speisekarte. Damit sie entscheidbar wird, gehört eine Spalte dazu, die im Programm EUR je kg heißt:
\text{Schattenpreis} = \frac{\text{Aufpreis gegenüber dem Kostenminimum}} {\text{eingesparte Kilogramm}}
Diese Zahl ist mit etwas vergleichbar, das das Unternehmen ohnehin kennt: dem internen CO₂-Preis oder dem Zertifikatspreis. Liegt der Schattenpreis darunter, ist die Entscheidung kaufmännisch schon getroffen.
💻 Code-Durchgang: die drei Bausteine
Stelle Was sie tut plane(ziel, co2_grenze, kosten_grenze)eine Funktion für alle Varianten. Welches Ziel und welche Schranke — mehr Unterschied gibt es zwischen den fünf Teilen des Programms nicht pareto_front()das ε-Verfahren: Grenze jeweils um 1 kg unter den zuletzt erreichten Wert drücken, bis nichts mehr geht untere_huelle()rechnet die Hülle unabhängig aus, damit die Aussage „unerreichbar“ nicht auf dem Gewichtsraster beruht, sondern auf Geometrie Der Vergleich am Ende von Teil 4 ist bewusst ein
assert-artiger Abgleich: Wenn Gewichtsraster und Hüllenrechnung verschiedene Punktmengen liefern, stimmt eines von beidem nicht — und das steht dann in der Ausgabe.
14.9 Übungsaufgaben
Lösungen: Abschnitt A.14.
Aufgabe 14.1 ⭐ — Dominanz prüfen. Plan P kostet 14 000 € und stößt 6 000 kg aus, Plan Q kostet 13 800 € und stößt 6 100 kg aus. Dominiert einer den anderen? Und wie steht es mit R (14 000 €, 6 100 kg)?
Aufgabe 14.2 ⭐ — Der Schattenpreis in der Praxis. Der interne CO₂-Preis des Unternehmens beträgt 0,90 € je Kilogramm. Welchen Punkt der Front würden Sie empfehlen — und wie begründen Sie ihn in einem Satz?
Aufgabe 14.3 ⭐⭐ — Mehr Trassen. Erhöhen Sie BAHN_TRASSEN von 5 auf 8. Was passiert mit der Länge der Front und mit der Zahl der unerreichbaren Punkte? Erklären Sie den Zusammenhang.
Aufgabe 14.4 ⭐⭐ — Die Front der Relaxation. Lassen Sie integrality weg, sodass Anteile erlaubt sind. Berechnen Sie die Front erneut (mit einem ε-Raster, da es nun unendlich viele Punkte gibt). Wie viele Punkte liegen jetzt oberhalb der konvexen Hülle — und warum?
Aufgabe 14.5 ⭐⭐⭐ — Drei Ziele. Ergänzen Sie die Laufzeit als drittes Ziel (LKW schnell, Bahn langsam). Die Front wird zu einer Fläche. Wie ändert sich das ε-Verfahren, und warum wächst der Aufwand so schnell?
14.10 Finde den Denkfehler
🐛 „Wir haben die Gewichte sauber kalibriert”
Ein Team baut ein Mehrzielmodell für die Tourenplanung: Kosten gegen CO₂. Um das Gewicht nicht willkürlich zu setzen, geht es methodisch vor:
„Wir haben w in 500 Schritten von 0 bis 5 durchgerechnet und für jeden Wert den > optimalen Plan bestimmt. Dann haben wir der Geschäftsführung alle so gefundenen Pläne > vorgelegt und sie hat einen ausgewählt. Damit ist die Entscheidung nicht von uns > getroffen worden, sondern vom Fachbereich — und wir haben den Lösungsraum vollständig > abgetastet.”
Das Vorgehen ist sorgfältig, die Rechnung stimmt, und die Beteiligung des Fachbereichs ist vorbildlich.
Trotzdem stimmt die letzte Behauptung nicht. Warum — und was hätte das Team stattdessen tun müssen?
Und ein zweiter Teil, der schwerer wiegt: Angenommen, unter den nicht gefundenen Plänen wäre einer gewesen, der die Geschäftsführung überzeugt hätte. Hätte irgendjemand das bemerkt?
14.11 Micro-Quiz
❓ Drei Fragen
1. Warum findet die gewichtete Summe nicht alle Pareto-Punkte? a) Weil das Gewichtsraster zwangsläufig zu grob ist. b) Weil sie nur Punkte auf der unteren konvexen Hülle erreichen kann — Punkte darüber werden von keiner Geraden gestützt. c) Weil Solver bei gemischten Zielfunktionen numerisch ungenau werden.
2. Wie viele Solverläufe braucht das ε-Constraint-Verfahren in der gezeigten Form? a) So viele, wie das ε-Raster Schritte hat. b) So viele, wie die Pareto-Front Punkte hat, plus einen. c) Genau zwei — einen je Ziel.
3. Die Zeile „+0 % Kostenbudget” in der lexikografischen Tabelle liefert weniger CO₂ als das reine Kostenminimum. Wie kann das sein? a) Ein Rundungsfehler im Solver. b) Es gibt mehrere kostenminimale Pläne; unter ihnen wird der sauberste gewählt. c) Das Kostenbudget ist durch die Prozentrechnung minimal größer.
14.12 Selbsttest
- Erklären Sie Pareto-Dominanz an einem Beispiel aus Ihrem Arbeitsumfeld.
- Warum ist „gewichtete Summe” bei einem reinen LP unproblematisch und bei einem MILP nicht?
- Ein Kollege sagt: „Ich nehme w = 0{,}5, das ist neutral.” Was ist daran falsch?
- Beschreiben Sie das ε-Constraint-Verfahren in drei Sätzen, ohne Formeln.
- Sie legen einer Geschäftsführung eine Front mit zwölf Punkten vor. Welche Spalte brauchen Sie, damit die Entscheidung fallen kann?
14.13 Zusammenfassung
- Bei mehreren Zielen ist „besser” nur noch über Dominanz definiert. Das vollständige Ergebnis ist keine Lösung, sondern die Pareto-Front.
- Die gewichtete Summe findet nur Punkte auf der unteren konvexen Hülle. Bei ganzzahligen Modellen liegen regelmäßig Pareto-Punkte darüber — im Beispiel 4 von 10, also 40 % der Alternativen. Das ist kein Rasterproblem, sondern Geometrie.
- Das Gewicht ist außerdem keine Feineinstellung: 1 401 Gewichte erzeugten sechs verschiedene Pläne, mit Sprüngen dazwischen.
- Das ε-Constraint-Verfahren macht aus dem zweiten Ziel eine Schranke und liefert die vollständige Front mit so vielen Solverläufen, wie sie Punkte hat.
- Lexikografisch optimiert man, wenn es eine echte Rangfolge gibt — „höchstens 2 % teurer, dafür so sauber wie möglich” ist eine Frage, die ein Kaufmann beantworten kann.
- Die Front wird erst entscheidbar durch die Spalte € je kg: Sie ist mit dem internen CO₂-Preis vergleichbar und verwandelt eine Wertefrage in eine Rechnung.