operations_research/OR_HTML_04/qp-nlp.html

1313 lines
168 KiB
HTML
Raw Normal View History

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>
2026-09-08 01:20:09 +02:00
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Kapitel 11: Quadratische und nichtlineare Optimierung — KKT, Lagrange, Konvexität · Optimierte Entscheidungsfindung mit Python</title>
<script>
(function () {
try {
var t = localStorage.getItem("or-theme");
if (t) document.documentElement.setAttribute("data-theme", t);
} catch (e) {}
})();
</script>
<link rel="stylesheet" href="assets/highlight.css" />
<link rel="stylesheet" href="katex/katex.min.css" />
<script defer="" src="katex/katex.min.js"></script>
<script>document.addEventListener("DOMContentLoaded", function () {
var mathElements = document.getElementsByClassName("math");
var macros = [];
for (var i = 0; i < mathElements.length; i++) {
var texText = mathElements[i].firstChild;
if (mathElements[i].tagName == "SPAN") {
katex.render(texText.data, mathElements[i], {
displayMode: mathElements[i].classList.contains('display'),
throwOnError: false,
macros: macros,
fleqn: false
});
}}
// Der Browser springt zu einem #anker in der URL schon beim ersten Rendern
// an, BEVOR die KaTeX-Formeln oben im Text ihre finale Hoehe bekommen -
// durch den Reflow landet der Anker danach zu weit unten. Nach dem
// Formel-Rendering hier erneut zum Anker springen, das behebt es.
if (location.hash) {
var ziel = document.getElementById(decodeURIComponent(location.hash.slice(1)));
if (ziel) ziel.scrollIntoView({behavior: "instant", block: "start"});
}
});
</script>
<link rel="stylesheet" href="assets/site.css" />
</head>
<body>
<svg style="display:none" aria-hidden="true"><symbol id="icon-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/>
</symbol>
<symbol id="icon-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.2" y2="16.2"/>
</symbol>
<symbol id="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="4.5"/>
<line x1="12" y1="1.5" x2="12" y2="4"/><line x1="12" y1="20" x2="12" y2="22.5"/>
<line x1="1.5" y1="12" x2="4" y2="12"/><line x1="20" y1="12" x2="22.5" y2="12"/>
<line x1="4.5" y1="4.5" x2="6.2" y2="6.2"/><line x1="17.8" y1="17.8" x2="19.5" y2="19.5"/>
<line x1="19.5" y1="4.5" x2="17.8" y2="6.2"/><line x1="6.2" y1="17.8" x2="4.5" y2="19.5"/>
</symbol>
<symbol id="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 14.5A8.5 8.5 0 1 1 9.5 4a6.8 6.8 0 0 0 10.5 10.5z"/>
</symbol>
<symbol id="icon-chevron-left" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="15 4 7 12 15 20"/>
</symbol>
<symbol id="icon-chevron-right" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="9 4 17 12 9 20"/>
</symbol>
<symbol id="icon-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="4 13 9.5 18.5 20 6"/>
</symbol>
<symbol id="icon-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 13.5V19a1.5 1.5 0 0 1-1.5 1.5H5A1.5 1.5 0 0 1 3.5 19V7A1.5 1.5 0 0 1 5 5.5h5.5"/>
<polyline points="14.5 3.5 20.5 3.5 20.5 9.5"/><line x1="11" y1="13" x2="20" y2="4"/>
</symbol>
<symbol id="icon-book" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 5.5A2 2 0 0 1 6 4h6v16H6a2 2 0 0 0-2 1.5z"/>
<path d="M20 5.5A2 2 0 0 0 18 4h-6v16h6a2 2 0 0 1 2 1.5z"/>
</symbol>
<symbol id="icon-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</symbol>
<symbol id="icon-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 3v12"/><polyline points="7 10 12 15 17 10"/><path d="M4 19.5h16"/>
</symbol></svg>
<header class="site-header">
<button type="button" class="icon-btn" id="sidebar-toggle" aria-label="Menü öffnen"><svg class="icon" aria-hidden="true"><use href="#icon-menu"></use></svg></button>
<a class="brand" href="index.html"><svg class="icon" aria-hidden="true"><use href="#icon-book"></use></svg> <span>Optimierte Entscheidungsfindung mit Python</span></a>
<div class="site-search">
<input id="suche-eingabe" type="search" placeholder="Suchen …" aria-label="Suche" autocomplete="off" />
<svg class="icon such-icon" aria-hidden="true"><use href="#icon-search"></use></svg>
<div id="suche-ergebnisse" class="suche-ergebnisse" hidden></div>
</div>
<button type="button" class="icon-btn" id="theme-toggle" aria-label="Darstellung umschalten">
<svg class="icon icon-sun" aria-hidden="true"><use href="#icon-sun"></use></svg><svg class="icon icon-moon" aria-hidden="true"><use href="#icon-moon"></use></svg>
</button>
</header>
<div class="site-body">
<div class="sidebar-overlay" id="sidebar-overlay" hidden></div>
Phase 8.1: Synthese-Seiten je Teil - und die gebrochene Lesekette Setzt Paket 5 aus Verbesserungen_02.md um (den Teil, der nicht zurueckgestellt wurde). Fuenf neue Dateien 19_/29_/39_/49_/52_Synthese_*.md, je eine am Ende eines Teils, mit eigener Website-Seite ueber SONDERSEITEN - sie tragen bewusst keine "# Kapitel:"-Ueberschrift, weil sie keine Kapitel sind, sondern der Rueckblick auf einen Teil. Der Entwurf musste sich abgrenzen: Die Teil-Einleitungen haben bereits Entscheidungsdiagramme. Eine zweite Matrix am Teil-Ende waere eine Dopplung gewesen. Die Synthesen leisten deshalb, was eine Einleitung nicht kann - den Vergleich ueber die Kapitel hinweg (Verfahren nebeneinander, mit der Spalte "wo es aufhoert"), eine Tabelle "was dieser Teil gemessen hat" (Behauptung gegen Messung gegen Fundstelle) und drei Fehler, die der Teil verhindert. Zitiert wird ausschliesslich, was im Buch tatsaechlich gerechnet wird. Drei Funde beim Einbau: * Teil III sagte "die drei Kapitel dieses Teils", hat aber fuenf. Phase 3 hatte Mehrziel und Predict-then-Optimize hinzugefuegt, die Einleitung blieb stehen. * 50_Praxis.md verwies auf die Projektwerkstatt mit "acht eigene Anwendungen" - sie hat elf. * Und der eigentliche Fund: Die Lesekette der Quelldateien fuehrte an ACHT Kapiteln vorbei. 12_Python_Oekosystem zeigte direkt auf 20_Lineare_Programmierung, 23_Graphen direkt auf 30_QP, 32_Dynamische direkt auf 40_Finanzdaten, 50_Praxis direkt auf die Projektwerkstatt. Wer der Kette folgte, uebersprang acht von 23 Kapiteln - darunter Metaheuristiken, Spaltengenerierung, Strukturbruecke, Supply-Chain und das ganze Testing-Kapitel. Zehn weitere Dateien hatten gar keine Navigationszeile. Zur Reichweite, damit sie nicht ueberschaetzt wird: Diese Zeilen stehen nur in den Quelldateien. entferne_navigation() streicht sie aus dem Gesamtdokument, und die Website baut ihre Vor/Zurueck-Knoepfe selbst aus DATEIEN. PDF und Website waren nie betroffen - wohl aber jeder, der die Markdown-Dateien im Repository liest, und das wird nach der Veroeffentlichung der Normalfall sein. Die Kette ist jetzt ueber alle 35 Uebergaenge geschlossen, und --check bewacht sie: Fehlt eine Zeile oder zeigt sie an der in DATEIEN folgenden Datei vorbei, ist der Lauf rot. Gegengetestet mit beiden Bruchformen. Stand: 36 Dateien, 296 Abschnitte, 815 Querverweise, 328 Indexmarken, 76 Programme (unveraendert), 33 pytest-Tests, PDF 758 Seiten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 12:10:02 +02:00
<nav class="sidebar" id="sidebar" aria-label="Kapitelnavigation"><div class="sidebar-inhalt"><details class="sidebar-gruppe"><summary>Einstieg</summary><ul><li data-kapitel="vorwort.html"><a href="vorwort.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Vorwort & Lesehilfe</span></a></li><li data-kapitel="notation.html"><a href="notation.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Notation & Abkürzungen</span></a></li></ul></details><details class="sidebar-gruppe"><summary>Teil I: Grundlagen des Operations Research</summary><ul><li data-kapitel="einfuehrung.html"><a href="einfuehrung.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 1: Einführung in Operations Research — Vom Ursprung zur mathematischen Entscheidungsfindung</span></a></li><li data-kapitel="fundament.html"><a href="fundament.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 2: Das mathematische Fundament — Vektoren, Matrizen, Konvexität</span></a></li><li data-kapitel="oekosystem.html"><a href="oekosystem.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 3: Das Python-Ökosystem für OR — Solver, Bindings und Modellierungsschichten</span></a></li><li data-kapitel="modellierung.html"><a href="modellierung.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 4: Vom Management-Wunsch zum Modell</span></a></li><li data-kapitel="synthese-grundlagen.html"><a href="synthese-grundlagen.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Synthese Teil I</span></a></li></ul></details><details class="sidebar-gruppe"><summary>Teil II: Die Kernverfahren der deterministischen Optimierung</summary><ul><li data-kapitel="lp.html"><a href="lp.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 5: Lineare Programmierung — Simplex, Dualität und Schattenpreise</span></a></li><li data-kapitel="milp.html"><a href="milp.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 6: Gemischt-ganzzahlige Optimierung — Diskrete Entscheidungen und Branch-and-Bound</span></a></li><li data-kapitel="cpsat.html"><a href="cpsat.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 7: Constraint Programming mit CP-SAT — Logik, Scheduling und Zuweisung</span></a></li><li data-kapitel="graphen.html"><a href="graphen.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 8: Graphen, Flüsse und Touren — Min-Cost-Flow, Matching und VRP</span></a></li><li data-kapitel="metaheuristiken.html"><a href="metaheuristiken.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 9: Metaheuristiken — wenn der exakte Solver aussteigt</span></a></li><li data-kapitel="dekomposition.html"><a href="dekomposition.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 10: Spaltengenerierung — das Modell umbauen statt die Lösung raten</span></a></li><li data-kapitel="synthese-kernverfahren.html"><a href="synthese-kernverfahren.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Synthese Teil II</span></a></li></ul></details><details class="sidebar-gruppe" open><summary>Teil III: Nichtlinearität, Unsicherheit und mehrperiod
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>
2026-09-08 01:20:09 +02:00
<main class="content">
<nav class="breadcrumb" aria-label="Breadcrumb"><a href="index.html">Start</a> <span>Teil III</span> <span>Kapitel 11: Quadratische und nichtlineare Optimierung — KKT, Lagrange, Konvexität</span></nav>
Phase 8.1: Synthese-Seiten je Teil - und die gebrochene Lesekette Setzt Paket 5 aus Verbesserungen_02.md um (den Teil, der nicht zurueckgestellt wurde). Fuenf neue Dateien 19_/29_/39_/49_/52_Synthese_*.md, je eine am Ende eines Teils, mit eigener Website-Seite ueber SONDERSEITEN - sie tragen bewusst keine "# Kapitel:"-Ueberschrift, weil sie keine Kapitel sind, sondern der Rueckblick auf einen Teil. Der Entwurf musste sich abgrenzen: Die Teil-Einleitungen haben bereits Entscheidungsdiagramme. Eine zweite Matrix am Teil-Ende waere eine Dopplung gewesen. Die Synthesen leisten deshalb, was eine Einleitung nicht kann - den Vergleich ueber die Kapitel hinweg (Verfahren nebeneinander, mit der Spalte "wo es aufhoert"), eine Tabelle "was dieser Teil gemessen hat" (Behauptung gegen Messung gegen Fundstelle) und drei Fehler, die der Teil verhindert. Zitiert wird ausschliesslich, was im Buch tatsaechlich gerechnet wird. Drei Funde beim Einbau: * Teil III sagte "die drei Kapitel dieses Teils", hat aber fuenf. Phase 3 hatte Mehrziel und Predict-then-Optimize hinzugefuegt, die Einleitung blieb stehen. * 50_Praxis.md verwies auf die Projektwerkstatt mit "acht eigene Anwendungen" - sie hat elf. * Und der eigentliche Fund: Die Lesekette der Quelldateien fuehrte an ACHT Kapiteln vorbei. 12_Python_Oekosystem zeigte direkt auf 20_Lineare_Programmierung, 23_Graphen direkt auf 30_QP, 32_Dynamische direkt auf 40_Finanzdaten, 50_Praxis direkt auf die Projektwerkstatt. Wer der Kette folgte, uebersprang acht von 23 Kapiteln - darunter Metaheuristiken, Spaltengenerierung, Strukturbruecke, Supply-Chain und das ganze Testing-Kapitel. Zehn weitere Dateien hatten gar keine Navigationszeile. Zur Reichweite, damit sie nicht ueberschaetzt wird: Diese Zeilen stehen nur in den Quelldateien. entferne_navigation() streicht sie aus dem Gesamtdokument, und die Website baut ihre Vor/Zurueck-Knoepfe selbst aus DATEIEN. PDF und Website waren nie betroffen - wohl aber jeder, der die Markdown-Dateien im Repository liest, und das wird nach der Veroeffentlichung der Normalfall sein. Die Kette ist jetzt ueber alle 35 Uebergaenge geschlossen, und --check bewacht sie: Fehlt eine Zeile oder zeigt sie an der in DATEIEN folgenden Datei vorbei, ist der Lauf rot. Gegengetestet mit beiden Bruchformen. Stand: 36 Dateien, 296 Abschnitte, 815 Querverweise, 328 Indexmarken, 76 Programme (unveraendert), 33 pytest-Tests, PDF 758 Seiten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 12:10:02 +02:00
<nav class="prev-next"><a class="prev-next-knopf prev-next-prev" href="synthese-kernverfahren.html"><svg class="icon" aria-hidden="true"><use href="#icon-chevron-left"></use></svg><span><small>Zurück</small>Synthese Teil II</span></a><a class="prev-next-knopf prev-next-next" href="unsicherheit.html"><span><small>Weiter</small>Kapitel 12: Optimierung unter Unsicherheit — Monte-Carlo, Stochastik, Robustheit</span><svg class="icon" aria-hidden="true"><use href="#icon-chevron-right"></use></svg></a></nav>
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>
2026-09-08 01:20:09 +02:00
<article>
<h1 id="teil-nichtlinear">Teil III: Nichtlinearität, Unsicherheit und mehrperiodige Dynamik</h1>
<p><a href="lp.html#teil-kernverfahren">Teil II</a> ging von festen Daten und linearen Zusammenhängen aus. Beides gilt in der Praxis oft nicht: Risiko wächst <strong>quadratisch</strong> mit dem Einsatz, Nachfrage ist erst morgen bekannt, und eine Entscheidung heute verändert, welche Möglichkeiten übermorgen noch offenstehen.</p>
Phase 8.1: Synthese-Seiten je Teil - und die gebrochene Lesekette Setzt Paket 5 aus Verbesserungen_02.md um (den Teil, der nicht zurueckgestellt wurde). Fuenf neue Dateien 19_/29_/39_/49_/52_Synthese_*.md, je eine am Ende eines Teils, mit eigener Website-Seite ueber SONDERSEITEN - sie tragen bewusst keine "# Kapitel:"-Ueberschrift, weil sie keine Kapitel sind, sondern der Rueckblick auf einen Teil. Der Entwurf musste sich abgrenzen: Die Teil-Einleitungen haben bereits Entscheidungsdiagramme. Eine zweite Matrix am Teil-Ende waere eine Dopplung gewesen. Die Synthesen leisten deshalb, was eine Einleitung nicht kann - den Vergleich ueber die Kapitel hinweg (Verfahren nebeneinander, mit der Spalte "wo es aufhoert"), eine Tabelle "was dieser Teil gemessen hat" (Behauptung gegen Messung gegen Fundstelle) und drei Fehler, die der Teil verhindert. Zitiert wird ausschliesslich, was im Buch tatsaechlich gerechnet wird. Drei Funde beim Einbau: * Teil III sagte "die drei Kapitel dieses Teils", hat aber fuenf. Phase 3 hatte Mehrziel und Predict-then-Optimize hinzugefuegt, die Einleitung blieb stehen. * 50_Praxis.md verwies auf die Projektwerkstatt mit "acht eigene Anwendungen" - sie hat elf. * Und der eigentliche Fund: Die Lesekette der Quelldateien fuehrte an ACHT Kapiteln vorbei. 12_Python_Oekosystem zeigte direkt auf 20_Lineare_Programmierung, 23_Graphen direkt auf 30_QP, 32_Dynamische direkt auf 40_Finanzdaten, 50_Praxis direkt auf die Projektwerkstatt. Wer der Kette folgte, uebersprang acht von 23 Kapiteln - darunter Metaheuristiken, Spaltengenerierung, Strukturbruecke, Supply-Chain und das ganze Testing-Kapitel. Zehn weitere Dateien hatten gar keine Navigationszeile. Zur Reichweite, damit sie nicht ueberschaetzt wird: Diese Zeilen stehen nur in den Quelldateien. entferne_navigation() streicht sie aus dem Gesamtdokument, und die Website baut ihre Vor/Zurueck-Knoepfe selbst aus DATEIEN. PDF und Website waren nie betroffen - wohl aber jeder, der die Markdown-Dateien im Repository liest, und das wird nach der Veroeffentlichung der Normalfall sein. Die Kette ist jetzt ueber alle 35 Uebergaenge geschlossen, und --check bewacht sie: Fehlt eine Zeile oder zeigt sie an der in DATEIEN folgenden Datei vorbei, ist der Lauf rot. Gegengetestet mit beiden Bruchformen. Stand: 36 Dateien, 296 Abschnitte, 815 Querverweise, 328 Indexmarken, 76 Programme (unveraendert), 33 pytest-Tests, PDF 758 Seiten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 12:10:02 +02:00
<p>Drei Kapitel dieses Teils behandeln genau diese drei Abweichungen. Welches Werkzeug Sie brauchen, hängt davon ab, <strong>welche</strong> davon bei Ihnen vorliegt:</p>
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>
2026-09-08 01:20:09 +02:00
<figure>
<img src="bilder_04/teil3_solverwahl.svg" alt="Entscheidungsdiagramm: Welches Werkzeug passt zu meinem Problem? (Teil III)" />
<figcaption aria-hidden="true">Entscheidungsdiagramm: Welches Werkzeug passt zu meinem Problem? (Teil III)</figcaption>
</figure>
<blockquote>
<p><strong>🎯 Merksatz zum Diagramm</strong> Die erste Frage trennt zwei Welten, die man nicht vermischen darf. Ist die <strong>Nichtlinearität</strong> das Problem, geht es um Konvexität — und damit darum, ob Sie eine Optimalitätsgarantie bekommen oder nur ein lokales Optimum. Ist die <strong>Unsicherheit</strong> das Problem, geht es um etwas ganz anderes: Nicht der Solver ist die Schwierigkeit, sondern die Frage, was „optimal“ überhaupt heißen soll, wenn man die Daten nicht kennt.</p>
<p>Der häufigste Fehler an dieser Stelle ist, Unsicherheit durch <strong>Mittelwerte</strong> zu ersetzen und dann deterministisch zu rechnen. Warum das systematisch danebengeht, zeigt <a href="unsicherheit.html#kap-unsicherheit">Kapitel 12</a> unter dem Namen <em>Fluch des Durchschnitts</em>.</p>
</blockquote>
Phase 8.1: Synthese-Seiten je Teil - und die gebrochene Lesekette Setzt Paket 5 aus Verbesserungen_02.md um (den Teil, der nicht zurueckgestellt wurde). Fuenf neue Dateien 19_/29_/39_/49_/52_Synthese_*.md, je eine am Ende eines Teils, mit eigener Website-Seite ueber SONDERSEITEN - sie tragen bewusst keine "# Kapitel:"-Ueberschrift, weil sie keine Kapitel sind, sondern der Rueckblick auf einen Teil. Der Entwurf musste sich abgrenzen: Die Teil-Einleitungen haben bereits Entscheidungsdiagramme. Eine zweite Matrix am Teil-Ende waere eine Dopplung gewesen. Die Synthesen leisten deshalb, was eine Einleitung nicht kann - den Vergleich ueber die Kapitel hinweg (Verfahren nebeneinander, mit der Spalte "wo es aufhoert"), eine Tabelle "was dieser Teil gemessen hat" (Behauptung gegen Messung gegen Fundstelle) und drei Fehler, die der Teil verhindert. Zitiert wird ausschliesslich, was im Buch tatsaechlich gerechnet wird. Drei Funde beim Einbau: * Teil III sagte "die drei Kapitel dieses Teils", hat aber fuenf. Phase 3 hatte Mehrziel und Predict-then-Optimize hinzugefuegt, die Einleitung blieb stehen. * 50_Praxis.md verwies auf die Projektwerkstatt mit "acht eigene Anwendungen" - sie hat elf. * Und der eigentliche Fund: Die Lesekette der Quelldateien fuehrte an ACHT Kapiteln vorbei. 12_Python_Oekosystem zeigte direkt auf 20_Lineare_Programmierung, 23_Graphen direkt auf 30_QP, 32_Dynamische direkt auf 40_Finanzdaten, 50_Praxis direkt auf die Projektwerkstatt. Wer der Kette folgte, uebersprang acht von 23 Kapiteln - darunter Metaheuristiken, Spaltengenerierung, Strukturbruecke, Supply-Chain und das ganze Testing-Kapitel. Zehn weitere Dateien hatten gar keine Navigationszeile. Zur Reichweite, damit sie nicht ueberschaetzt wird: Diese Zeilen stehen nur in den Quelldateien. entferne_navigation() streicht sie aus dem Gesamtdokument, und die Website baut ihre Vor/Zurueck-Knoepfe selbst aus DATEIEN. PDF und Website waren nie betroffen - wohl aber jeder, der die Markdown-Dateien im Repository liest, und das wird nach der Veroeffentlichung der Normalfall sein. Die Kette ist jetzt ueber alle 35 Uebergaenge geschlossen, und --check bewacht sie: Fehlt eine Zeile oder zeigt sie an der in DATEIEN folgenden Datei vorbei, ist der Lauf rot. Gegengetestet mit beiden Bruchformen. Stand: 36 Dateien, 296 Abschnitte, 815 Querverweise, 328 Indexmarken, 76 Programme (unveraendert), 33 pytest-Tests, PDF 758 Seiten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 12:10:02 +02:00
<p>Zwei weitere Kapitel schließen sich an, weil sie dieselbe Voraussetzung aufgeben, nur an einer anderen Stelle: <a href="mehrziel.html#kap-mehrziel">Kapitel 14</a> gibt das <strong>eine Ziel</strong> auf, <a href="prognose.html#kap-prognose">Kapitel 15</a> die Annahme, die <strong>Eingabedaten</strong> seien gegeben statt selbst geschätzt. Was dieser Teil insgesamt leistet, fasst die Synthese an seinem Ende zusammen.</p>
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>
2026-09-08 01:20:09 +02:00
<hr />
<h1 id="kap-qp-nlp">Kapitel 11: Quadratische und nichtlineare Optimierung — KKT, Lagrange, Konvexität</h1>
<div class="card card-blick">
<blockquote>
<p><strong>📌 Kapitel auf einen Blick</strong></p>
<p><strong>Worum geht es?</strong> Risiko ist quadratisch. Sobald Varianz ins Spiel kommt, verlässt man die Welt der Polyeder. Dieses Kapitel liefert das mathematische Fundament für die gesamte Portfoliooptimierung in <a href="bruecke.html#teil-anwendungen">Teil IV</a>.</p>
<p><strong>Voraussetzungen:</strong> <a href="fundament.html#kap-fundament">Kapitel 2</a> (Konvexität, Eigenwerte), <a href="lp.html#kap-lp">Kapitel 5</a> (Dualität, Schattenpreise). Gradienten werden in <a href="#sec:qp-nlp-lernziele">Abschnitt 11.2</a> wiederholt.</p>
<p><strong>Danach können Sie:</strong> Ein quadratisches Programm aufstellen, die KKT-Bedingungen anwenden, den Zusammenhang zwischen Lagrange-Multiplikator und Schattenpreis erklären, eine gültige Kovarianzmatrix konstruieren — und mit nicht-konvexen Problemen umgehen, ohne ein lokales Optimum für das Optimum zu halten.</p>
<p><strong>Zeitbedarf:</strong> ca. 6,5 Stunden.</p>
<p><strong>Programme:</strong><br />
<code>QP_Grundlagen.py</code><br />
<code>KKT_Nachweis.py</code><br />
<code>Entropie_Maximierte_Allokation.py</code><br />
<code>Lokale_Optima_Multistart.py</code></p>
Veroeffentlichung: Lizenzen, README, Colab-Entscheidung, JupyterLab im Image Sechs Dinge, alle fuer das jetzt oeffentliche Repository. LIZENZEN. LICENSE traegt die MIT-Lizenz fuer den Programmcode (alle .py, Notebooks, Dockerfile, pyproject.toml), LICENSE-TEXT.md die CC BY-SA 4.0 fuer Buchtext, PDF, Website und Diagramme. Beide Dateien nennen ausdruecklich, was sie abdecken und was nicht - ein Codeblock im Buchtext bleibt Programmcode und steht unter MIT. Vorher hatte das Repository gar keine Lizenz, womit standardmaessig "alle Rechte vorbehalten" galt und niemand die 76 Programme haette weiterverwenden duerfen. COLAB. COLAB_BASIS_URL steht jetzt auf "". Der Platzhalter zeigte auf GitHub, das Repository liegt auf einer eigenen Forgejo-Instanz - und Colab oeffnet Notebooks NUR aus GitHub, Google Drive oder einem Upload. Die URL-Form colab.research.google.com/github/... ist fest auf GitHub verdrahtet; eine selbstgehostete Adresse dort einzusetzen ergaebe keinen Link zum eigenen Server, sondern einen toten GitHub-Link. Der Kommentar im Quelltext ging von GitHub aus und war damit selbst irrefuehrend; er ist ersetzt. Verloren geht nichts: Die 25 Notebooks liegen neben der Website und bekommen einen echten Download-Link, jetzt mit dem Hinweis, was man damit tut - "herunterladen und in Jupyter oeffnen, in Colab hochladen oder mit dem Kurs-Image starten". JUPYTERLAB IM KURS-IMAGE. Neue pyproject-Gruppe [notebook] mit jupyterlab, die Notebooks werden ins Image kopiert, und ein kleiner Startbefehl macht beide Betriebsarten ohne --entrypoint moeglich: ohne Argument JupyterLab, mit Argument ein einzelnes Programm. Gebaut und geprueft - Rucksack.py laeuft, JupyterLab antwortet mit HTTP 200 und zeigt alle 25 Notebooks. Image 1,31 -> 1,46 GB. Es laeuft ohne Token, deshalb im README die Portfreigabe an 127.0.0.1 gebunden. README KOMPLETT NEU. Es war die Bau-Anleitung eines privaten Verzeichnisses und ist jetzt die Visitenkarte eines oeffentlichen Repositorys: was das Buch ist, wo man es liest, drei Wege die Beispiele auszufuehren (Container, schlanke Installation, alles auf einmal), was hier liegt, wie man baut, die Colab-Frage, die Lizenzen und wie man mitwirkt. Alle relativen Links geprueft: 0 tot. .gitattributes. Ohne die Datei entschied core.autocrlf des jeweiligen Rechners, was beim Klonen passiert - ein Windows-Leser bekam CRLF-Rauschen in jedem Diff. Jetzt: im Repository immer LF, im Arbeitsverzeichnis passend zum System, Binaerdateien ausdruecklich ausgenommen. Der Bestand war bereits sauber (git add --renormalize aendert null Dateien). Zusaetzlich sind die erzeugten Verzeichnisse als linguist-generated markiert, sonst zaehlt die Sprachstatistik das Repository als HTML-Projekt. PROGRESS.md: Remote-Repository als erledigt markiert, der Colab-Befund festgehalten. CLAUDE.md um Veroeffentlichung, Lizenzen und die neuen Dateien ergaenzt. Geprueft: --check ohne Fehler, 0 tote README-Links, 76 Programme unveraendert, 33 pytest-Tests, pyproject baut mit acht Gruppen, PDF 759 Seiten (eine weniger - die Colab-Zeile entfaellt in 25 Kapiteln). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 14:54:04 +02:00
<p><strong>Notebook:</strong> <a href="Notebooks_04/qp-nlp.ipynb">qp-nlp.ipynb</a> — herunterladen und in Jupyter öffnen, in Colab hochladen oder mit dem Kurs-Image starten</p>
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>
2026-09-08 01:20:09 +02:00
</blockquote>
</div>
<hr />
<h2 id="sec:qp-nlp-schnellstart">11.1 In 5 Minuten gelöst</h2>
<div class="card card-schnellstart">
<blockquote>
<p><strong>🚀 In 5 Minuten gelöst: Warum die riskantere Anlage das Risiko senkt</strong></p>
<p>Ein Betrieb legt Rücklagen an und hat zwei Möglichkeiten:</p>
<table>
<thead>
<tr class="header">
<th>Anlage</th>
<th style="text-align: right;">Schwankung (Standardabweichung)</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><strong>A</strong> — solide, wenig Bewegung</td>
<td style="text-align: right;">12 %</td>
</tr>
<tr class="even">
<td><strong>B</strong> — deutlich unruhiger</td>
<td style="text-align: right;">28 %</td>
</tr>
</tbody>
</table>
<p>Die beiden bewegen sich <strong>gegenläufig</strong>: Korrelation <span class="math inline">-0{,}40</span>. Wie viel soll in jede?</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> cvxpy <span class="im">as</span> cp</span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>sigma <span class="op">=</span> np.array([<span class="fl">0.12</span>, <span class="fl">0.28</span>]) <span class="co"># Schwankung je Anlage</span></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a>rho <span class="op">=</span> <span class="op">-</span><span class="fl">0.40</span> <span class="co"># Korrelation</span></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a>S <span class="op">=</span> np.array([[sigma[<span class="dv">0</span>]<span class="op">**</span><span class="dv">2</span>, rho <span class="op">*</span> sigma[<span class="dv">0</span>] <span class="op">*</span> sigma[<span class="dv">1</span>]],</span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a> [rho <span class="op">*</span> sigma[<span class="dv">0</span>] <span class="op">*</span> sigma[<span class="dv">1</span>], sigma[<span class="dv">1</span>]<span class="op">**</span><span class="dv">2</span>]])</span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a>w <span class="op">=</span> cp.Variable(<span class="dv">2</span>, nonneg<span class="op">=</span><span class="va">True</span>) <span class="co"># Anteile, keine Leerverkäufe</span></span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a>problem <span class="op">=</span> cp.Problem(cp.Minimize(cp.quad_form(w, S)), [cp.<span class="bu">sum</span>(w) <span class="op">==</span> <span class="dv">1</span>])</span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a>problem.solve()</span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="st">&quot;Anteile:&quot;</span>, np.<span class="bu">round</span>(w.value, <span class="dv">3</span>))</span>
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="ss">f&quot;Risiko der Mischung: </span><span class="sc">{</span>np<span class="sc">.</span>sqrt(problem.value) <span class="op">*</span> <span class="dv">100</span><span class="sc">:.2f}</span><span class="ss"> %&quot;</span>)</span></code></pre></div>
<p><strong>Ausgabe:</strong></p>
<pre><code>Anteile: [0.767 0.233]
Risiko der Mischung: 8.90 %</code></pre>
</blockquote>
</div>
<p><strong>Rechnen wir die naheliegenden Alternativen dagegen:</strong></p>
<table>
<thead>
<tr class="header">
<th>Aufteilung</th>
<th style="text-align: right;">Schwankung des Ganzen</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Nur A — „die sichere Anlage“</td>
<td style="text-align: right;">12,00 %</td>
</tr>
<tr class="even">
<td>50 / 50 — „breit streuen“</td>
<td style="text-align: right;"><strong>12,84 %</strong></td>
</tr>
<tr class="odd">
<td>Nur B</td>
<td style="text-align: right;">28,00 %</td>
</tr>
<tr class="even">
<td><strong>Optimum: 76,7 % A, 23,3 % B</strong></td>
<td style="text-align: right;"><strong>8,90 %</strong></td>
</tr>
</tbody>
</table>
<p>Drei Dinge stehen in dieser Tabelle, und alle drei widersprechen der Intuition:</p>
<ol type="1">
<li><strong>Die Beimischung der <em>riskanteren</em> Anlage senkt das Risiko</strong> — von 12 % auf 8,9 %, also um mehr als ein Viertel. Wer nur auf die 28 % schaut, würde B nie anfassen.</li>
<li><strong>Naives Streuen macht es schlechter.</strong> Die 50/50-Mischung liegt mit 12,84 % <em>über</em> dem Wert, den man bekäme, wenn man ausschließlich A hielte. „Diversifizieren“ ist also keine Regel, sondern eine Frage nach dem richtigen Maß.</li>
<li><strong>Das richtige Maß ist weder 0 % noch 50 %</strong>, sondern 23,3 % — eine Zahl, auf die man durch Nachdenken nicht kommt. Genau dafür gibt es dieses Kapitel.</li>
</ol>
<p><strong>Warum funktioniert das?</strong> Weil Risiko sich nicht addiert. Die Schwankung einer Mischung ist nicht der Durchschnitt der Einzelschwankungen, sondern folgt einer <strong>quadratischen</strong> Form:</p>
<p><span class="math display">
\sigma_p^2 = w_A^2\sigma_A^2 + w_B^2\sigma_B^2 + 2\,w_A w_B \rho\,\sigma_A\sigma_B
</span></p>
<p>Der letzte Term ist der entscheidende: Bei negativer Korrelation ist er <strong>negativ</strong> und zieht das Gesamtrisiko herunter. Deshalb verlässt man mit Risiko die Welt der Polyeder aus <a href="lp.html#teil-kernverfahren">Teil II</a> — die Zielfunktion ist kein Skalarprodukt mehr, sondern eine quadratische Form. Was das für die Lösbarkeit bedeutet und warum CVXPY das trotzdem mit Optimalitätsgarantie löst, klärt der Rest des Kapitels.</p>
<blockquote>
<p><strong>🎯 Merksatz</strong> <code>cp.quad_form(w, S)</code> ist die Zeile, um die es im ganzen Kapitel geht. Sie ist genau dann harmlos, wenn <span class="math inline">\mathbf{S}</span> <strong>positiv semidefinit</strong> ist — und genau dann ein Problem, wenn nicht. CVXPY prüft das für Sie und verweigert die Arbeit im Zweifelsfall. Diese Verweigerung ist ein Schutzmechanismus, keine Schikane.</p>
</blockquote>
<hr />
<h2 id="sec:qp-nlp-lernziele">11.2 Lernziele</h2>
<p>Nach diesem Kapitel können Sie …</p>
<ol type="1">
<li>… ein <strong>quadratisches Programm (QP)</strong> aufstellen und von einem allgemeinen nichtlinearen Programm abgrenzen.</li>
<li>… die <strong>KKT-Bedingungen</strong> herleiten und auf ein Optimierungsproblem anwenden.</li>
<li>… den Zusammenhang zwischen <strong>Lagrange-Multiplikator</strong> und <strong>Schattenpreis</strong> erklären.</li>
<li>… zwischen <strong>konvex</strong> und <strong>streng konvex</strong> unterscheiden und eine gültige, positiv semidefinite <strong>Kovarianzmatrix</strong> konstruieren.</li>
</ol>
<h3 id="auffrischung-gradient-in-einer-minute">Auffrischung: Gradient in einer Minute</h3>
<p>Der <strong>Gradient</strong> <span class="math inline">\nabla f(\mathbf{x})</span> ist der Vektor aller partiellen Ableitungen:</p>
<p><span class="math display">
\nabla f(\mathbf{x}) = \left(\frac{\partial f}{\partial x_1},\ \frac{\partial f}{\partial x_2},\ \dots,\ \frac{\partial f}{\partial x_n}\right)^\top
</span></p>
<blockquote>
<p><strong>📐 Formel-Lesehilfe</strong> <span class="math inline">\partial f / \partial x_1</span> bedeutet: „Wie stark ändert sich <span class="math inline">f</span>, wenn ich <strong>nur</strong> <span class="math inline">x_1</span> ein winziges Stück erhöhe und alles andere festhalte?“</p>
<p><strong>Ohne Formel gesagt:</strong> Der Gradient ist der Pfeil, der in die Richtung des steilsten <strong>Anstiegs</strong> zeigt. Seine Länge ist die Steilheit. Beim Minimieren geht man deshalb in Richtung <span class="math inline">-\nabla f</span> — bergab.</p>
<p><strong>Die eine Regel, die Sie brauchen:</strong> Im Inneren eines Gebiets ist an einem Minimum der Gradient <strong>null</strong> — es geht nirgendwohin mehr bergab. An einem Rand (also wenn eine Nebenbedingung bindet) gilt das nicht mehr, und genau dafür braucht man die KKT-Bedingungen.</p>
</blockquote>
<p>Zwei Ableitungsregeln, die im ganzen Buch gebraucht werden:</p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="header">
<th>Ausdruck</th>
<th>Gradient</th>
<th>Merkhilfe</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><span class="math inline">f(\mathbf{x}) = \mathbf{c}^\top\mathbf{x}</span></td>
<td><span class="math inline">\nabla f = \mathbf{c}</span></td>
<td>wie <span class="math inline">(cx)&#39; = c</span></td>
</tr>
<tr class="even">
<td><span class="math inline">f(\mathbf{x}) = \tfrac12\mathbf{x}^\top\mathbf{P}\mathbf{x}</span> (mit <span class="math inline">\mathbf{P}</span> symmetrisch)</td>
<td><span class="math inline">\nabla f = \mathbf{P}\mathbf{x}</span></td>
<td>wie <span class="math inline">(\tfrac12 px^2)&#39; = px</span></td>
</tr>
</tbody>
</table>
<hr />
<h2 id="sec:qp-nlp-das-quadratische-programm-qp">11.3 Das quadratische Programm (QP)</h2>
<p>Ein <strong>quadratisches Programm</strong> hat eine quadratische Zielfunktion und <strong>lineare</strong> Nebenbedingungen:</p>
<p><span class="math display">
\min_{\mathbf{x}} \quad \tfrac{1}{2}\mathbf{x}^\top\mathbf{P}\mathbf{x} + \mathbf{q}^\top\mathbf{x}
\qquad\text{u. d. N.}\qquad \mathbf{A}\mathbf{x}\le\mathbf{b},\quad \mathbf{F}\mathbf{x}=\mathbf{d}
</span></p>
<blockquote>
<p><strong>📐 Formel-Lesehilfe</strong> * <span class="math inline">\mathbf{x}^\top\mathbf{P}\mathbf{x}</span> — die <strong>quadratische Form</strong>. Ausgeschrieben: <span class="math inline">\sum_i\sum_j p_{ij}x_ix_j</span>. In der Portfoliotheorie ist das exakt die Portfoliovarianz, wenn <span class="math inline">\mathbf{P} = \boldsymbol{\Sigma}</span> die Kovarianzmatrix ist. * Der Faktor <span class="math inline">\tfrac12</span> ist reine Bequemlichkeit: Er kürzt sich beim Ableiten weg (<span class="math inline">\nabla = \mathbf{P}\mathbf{x}</span> statt <span class="math inline">2\mathbf{P}\mathbf{x}</span>). * <span class="math inline">\mathbf{q}^\top\mathbf{x}</span> — ein zusätzlicher linearer Teil, etwa die negative erwartete Rendite.</p>
<p><strong>Ohne Formel gesagt:</strong> „Minimiere das Risiko (quadratisch) abzüglich des Ertrags (linear), unter Einhaltung der Budget- und Positionsgrenzen.“</p>
</blockquote>
<h3 id="konvexität-die-präzise-aussage">Konvexität: die präzise Aussage</h3>
<p>Eine verbreitete, aber zu stark formulierte Aussage lautet: <em>„Wenn <span class="math inline">\mathbf{P}</span> positiv semi-definit ist, ist das QP streng konvex; es existiert ein eindeutiges globales Minimum.“</em> Korrekt gilt:</p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="header">
<th>Voraussetzung</th>
<th>Bedeutung</th>
<th>Folgerung</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><span class="math inline">\mathbf{P} \succeq 0</span><strong>positiv semidefinit</strong>, alle Eigenwerte <span class="math inline">\lambda_i \ge 0</span></td>
<td>Die Funktion ist eine „Schüssel“, die eine flache Rinne haben darf</td>
<td>Problem ist <strong>konvex</strong>. Jedes lokale Minimum ist global. Es kann aber <strong>mehrere Minimalstellen</strong> geben — mit demselben Zielwert</td>
</tr>
<tr class="even">
<td><span class="math inline">\mathbf{P} \succ 0</span><strong>positiv definit</strong>, alle <span class="math inline">\lambda_i &gt; 0</span></td>
<td>Echte Schüssel ohne flache Richtung</td>
<td>Problem ist <strong>streng konvex</strong>. Die Minimalstelle ist <strong>eindeutig</strong></td>
</tr>
<tr class="odd">
<td><span class="math inline">\mathbf{P}</span> hat einen negativen Eigenwert</td>
<td>Sattel oder Rinne nach unten</td>
<td>Problem ist <strong>nicht konvex</strong>. Lokale Optima möglich, keine Garantie</td>
</tr>
</tbody>
</table>
<p><strong>Warum das praktisch zählt:</strong> Eine Stichproben-Kovarianzmatrix mit mehr Titeln als Beobachtungen (<span class="math inline">N &gt; T</span>) ist <strong>singulär</strong> — also nur semidefinit. Dann gibt es unendlich viele Portfolios mit exakt demselben minimalen Risiko, und der Solver liefert eines davon, scheinbar willkürlich. Kleine Datenänderungen führen zu völlig anderen Gewichten. Genau dieses Problem behebt die Shrinkage aus <a href="finanzdaten.html#kap-finanzdaten">Kapitel 18</a>.</p>
<p>Alle drei Zeilen der Tabelle lassen sich an einem winzigen Zwei-Variablen-QP direkt beobachten — inklusive der Stelle, an der CVXPY ein nicht konvexes Problem <strong>verweigert</strong>, bevor überhaupt ein Solver aufgerufen wird:</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a><span class="co"># QP_Grundlagen.py</span></span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel QP/NLP: Die drei Faelle aus der Konvexitaets-Tabelle (Abschnitt</span></span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a><span class="co">&#39;Das quadratische Programm&#39;) an</span></span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a><span class="co">einem Mini-QP demonstriert: P positiv definit, P (singulaer) semidefinit,</span></span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a><span class="co">P mit negativem Eigenwert.</span></span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> cvxpy <span class="im">as</span> cp</span>
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> loese_qp(P, q, name):</span>
<span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a> n <span class="op">=</span> <span class="bu">len</span>(q)</span>
<span id="cb3-17"><a href="#cb3-17" aria-hidden="true" tabindex="-1"></a> w <span class="op">=</span> cp.Variable(n)</span>
<span id="cb3-18"><a href="#cb3-18" aria-hidden="true" tabindex="-1"></a> ziel <span class="op">=</span> cp.Minimize(<span class="fl">0.5</span> <span class="op">*</span> cp.quad_form(w, P) <span class="op">+</span> q <span class="op">@</span> w)</span>
<span id="cb3-19"><a href="#cb3-19" aria-hidden="true" tabindex="-1"></a> bedingungen <span class="op">=</span> [cp.<span class="bu">sum</span>(w) <span class="op">==</span> <span class="dv">1</span>, w <span class="op">&gt;=</span> <span class="dv">0</span>]</span>
<span id="cb3-20"><a href="#cb3-20" aria-hidden="true" tabindex="-1"></a> problem <span class="op">=</span> cp.Problem(ziel, bedingungen)</span>
<span id="cb3-21"><a href="#cb3-21" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-22"><a href="#cb3-22" 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">--- </span><span class="sc">{</span>name<span class="sc">}</span><span class="ss"> ---&quot;</span>)</span>
<span id="cb3-23"><a href="#cb3-23" aria-hidden="true" tabindex="-1"></a> eigenwerte <span class="op">=</span> np.linalg.eigvalsh(P)</span>
<span id="cb3-24"><a href="#cb3-24" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Eigenwerte von P: </span><span class="sc">{</span>np<span class="sc">.</span><span class="bu">round</span>(eigenwerte, <span class="dv">4</span>)<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb3-25"><a href="#cb3-25" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;DCP-konvex (CVXPY-Pruefung)? </span><span class="sc">{</span>problem<span class="sc">.</span>is_dcp()<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb3-26"><a href="#cb3-26" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-27"><a href="#cb3-27" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> problem.is_dcp():</span>
<span id="cb3-28"><a href="#cb3-28" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&gt; CVXPY lehnt das Problem ab, BEVOR ueberhaupt ein Solver laeuft.&quot;</span>)</span>
<span id="cb3-29"><a href="#cb3-29" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span></span>
<span id="cb3-30"><a href="#cb3-30" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-31"><a href="#cb3-31" aria-hidden="true" tabindex="-1"></a> problem.solve()</span>
<span id="cb3-32"><a href="#cb3-32" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Status: </span><span class="sc">{</span>problem<span class="sc">.</span>status<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb3-33"><a href="#cb3-33" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;w* = </span><span class="sc">{</span>np<span class="sc">.</span><span class="bu">round</span>(w.value, <span class="dv">4</span>)<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb3-34"><a href="#cb3-34" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Zielwert = </span><span class="sc">{</span>problem<span class="sc">.</span>value<span class="sc">:.6f}</span><span class="ss">&quot;</span>)</span>
<span id="cb3-35"><a href="#cb3-35" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-36"><a href="#cb3-36" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-37"><a href="#cb3-37" 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="cb3-38"><a href="#cb3-38" aria-hidden="true" tabindex="-1"></a> q <span class="op">=</span> np.zeros(<span class="dv">2</span>)</span>
<span id="cb3-39"><a href="#cb3-39" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-40"><a href="#cb3-40" aria-hidden="true" tabindex="-1"></a> <span class="co"># Fall 1: P positiv definit -&gt; eindeutiges Minimum</span></span>
<span id="cb3-41"><a href="#cb3-41" aria-hidden="true" tabindex="-1"></a> P_definit <span class="op">=</span> np.array([[<span class="fl">2.0</span>, <span class="fl">0.5</span>], [<span class="fl">0.5</span>, <span class="fl">1.0</span>]])</span>
<span id="cb3-42"><a href="#cb3-42" aria-hidden="true" tabindex="-1"></a> loese_qp(P_definit, q, <span class="st">&quot;P positiv definit&quot;</span>)</span>
<span id="cb3-43"><a href="#cb3-43" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-44"><a href="#cb3-44" aria-hidden="true" tabindex="-1"></a> <span class="co"># Fall 2: P singulaer/semidefinit (zwei &quot;identische&quot; Assets) -&gt; unendlich viele Minima</span></span>
<span id="cb3-45"><a href="#cb3-45" aria-hidden="true" tabindex="-1"></a> P_semidefinit <span class="op">=</span> np.array([[<span class="fl">1.0</span>, <span class="fl">1.0</span>], [<span class="fl">1.0</span>, <span class="fl">1.0</span>]])</span>
<span id="cb3-46"><a href="#cb3-46" aria-hidden="true" tabindex="-1"></a> loese_qp(P_semidefinit, q, <span class="st">&quot;P positiv semidefinit (singulaer)&quot;</span>)</span>
<span id="cb3-47"><a href="#cb3-47" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-48"><a href="#cb3-48" aria-hidden="true" tabindex="-1"></a> <span class="co"># Fall 3: P mit negativem Eigenwert -&gt; nicht konvex</span></span>
<span id="cb3-49"><a href="#cb3-49" aria-hidden="true" tabindex="-1"></a> P_indefinit <span class="op">=</span> np.array([[<span class="fl">1.0</span>, <span class="fl">2.0</span>], [<span class="fl">2.0</span>, <span class="fl">1.0</span>]])</span>
<span id="cb3-50"><a href="#cb3-50" aria-hidden="true" tabindex="-1"></a> loese_qp(P_indefinit, q, <span class="st">&quot;P indefinit (negativer Eigenwert)&quot;</span>)</span>
<span id="cb3-51"><a href="#cb3-51" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-52"><a href="#cb3-52" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">--- Nachweis: &#39;unendlich viele Minima&#39; im semidefiniten Fall ---&quot;</span>)</span>
<span id="cb3-53"><a href="#cb3-53" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> punkt <span class="kw">in</span> [np.array([<span class="fl">1.0</span>, <span class="fl">0.0</span>]), np.array([<span class="fl">0.0</span>, <span class="fl">1.0</span>]), np.array([<span class="fl">0.3</span>, <span class="fl">0.7</span>])]:</span>
<span id="cb3-54"><a href="#cb3-54" aria-hidden="true" tabindex="-1"></a> wert <span class="op">=</span> <span class="fl">0.5</span> <span class="op">*</span> punkt <span class="op">@</span> P_semidefinit <span class="op">@</span> punkt</span>
<span id="cb3-55"><a href="#cb3-55" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; w = </span><span class="sc">{</span>punkt<span class="sc">}</span><span class="ss"> -&gt; Zielwert = </span><span class="sc">{</span>wert<span class="sc">:.4f}</span><span class="ss"> (identisch, obwohl w verschieden)&quot;</span>)</span></code></pre></div>
<p><strong>Erwartete Ausgabe:</strong></p>
<pre><code>--- P positiv definit ---
Eigenwerte von P: [0.7929 2.2071]
DCP-konvex (CVXPY-Pruefung)? True
Status: optimal
w* = [0.25 0.75]
Zielwert = 0.437500
--- P positiv semidefinit (singulaer) ---
Eigenwerte von P: [0. 2.]
DCP-konvex (CVXPY-Pruefung)? True
Status: optimal
w* = [0.5 0.5]
Zielwert = 0.500000
--- P indefinit (negativer Eigenwert) ---
Eigenwerte von P: [-1. 3.]
DCP-konvex (CVXPY-Pruefung)? False
-&gt; CVXPY lehnt das Problem ab, BEVOR ueberhaupt ein Solver laeuft.
--- Nachweis: &#39;unendlich viele Minima&#39; im semidefiniten Fall ---
w = [1. 0.] -&gt; Zielwert = 0.5000 (identisch, obwohl w verschieden)
w = [0. 1.] -&gt; Zielwert = 0.5000 (identisch, obwohl w verschieden)
w = [0.3 0.7] -&gt; Zielwert = 0.5000 (identisch, obwohl w verschieden)</code></pre>
<p>Der letzte Block ist der eigentliche Beweis: Drei völlig verschiedene Portfolios <span class="math inline">(1,0)</span>, <span class="math inline">(0,1)</span> und <span class="math inline">(0{,}3;\,0{,}7)</span> liefern <strong>exakt</strong> denselben Zielwert — die „Rinne” aus der Tabelle ist keine Metapher, sie ist hier eine echte Gerade im Lösungsraum.</p>
<blockquote>
<p><strong>⚠️ Prüfen Sie jede Matrix, bevor Sie sie verwenden</strong></p>
<div class="sourceCode" id="cb5"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a>eigenwerte <span class="op">=</span> np.linalg.eigvalsh(Sigma) <span class="co"># &#39;h&#39; = fuer symmetrische Matrizen</span></span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a><span class="cf">assert</span> np.<span class="bu">all</span>(eigenwerte <span class="op">&gt;=</span> <span class="op">-</span><span class="fl">1e-10</span>), <span class="st">&quot;Sigma ist nicht positiv semidefinit!&quot;</span></span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="ss">f&quot;Konditionszahl: </span><span class="sc">{</span>eigenwerte<span class="sc">.</span><span class="bu">max</span>() <span class="op">/</span> eigenwerte<span class="sc">.</span><span class="bu">min</span>()<span class="sc">:.1f}</span><span class="ss">&quot;</span>)</span></code></pre></div>
<p>Eine einzige Zeile — und sie deckt eine ungültige Kovarianzmatrix sofort auf.</p>
</blockquote>
<hr />
<h2 id="sec:qp-nlp-die-karush-kuhn-tucker-bedingungen">11.4 Die Karush-Kuhn-Tucker-Bedingungen</h2>
<p>Die <strong>KKT-Bedingungen</strong> verallgemeinern den Lagrange-Ansatz auf Probleme mit <strong>Ungleichungen</strong>. Sie sind notwendig für ein Optimum — und bei konvexen Problemen auch hinreichend.</p>
<p>Für das Problem</p>
<p><span class="math display">
\min f(\mathbf{x}) \quad\text{u. d. N.}\quad g_i(\mathbf{x})\le0\ (i=1..m),\qquad h_j(\mathbf{x})=0\ (j=1..p)
</span></p>
<p>lautet die <strong>Lagrange-Funktion</strong>:</p>
<p><span class="math display">
\mathcal{L}(\mathbf{x},\boldsymbol{\lambda},\boldsymbol{\nu})
= f(\mathbf{x}) + \sum_{i=1}^m \lambda_i g_i(\mathbf{x}) + \sum_{j=1}^p \nu_j h_j(\mathbf{x})
</span></p>
<blockquote>
<p><strong>📐 Formel-Lesehilfe</strong> Die Lagrange-Funktion <strong>verpackt die Nebenbedingungen in die Zielfunktion</strong>. Jede Bedingung bekommt einen Preis: <span class="math inline">\lambda_i</span> bzw. <span class="math inline">\nu_j</span>. Verletzt man Bedingung <span class="math inline">i</span>, wird <span class="math inline">g_i &gt; 0</span>, und der Term <span class="math inline">\lambda_i g_i</span> erhöht <span class="math inline">\mathcal{L}</span> — es kostet also.</p>
<p><strong>Ohne Formel gesagt:</strong> Statt „du darfst nicht“ sagt man „du darfst, aber es kostet <span class="math inline">\lambda_i</span> je Einheit Überschreitung“. Bei den richtigen Preisen verhält sich der Optimierer dann von selbst regelkonform. Diese Preise sind exakt die <strong>Schattenpreise</strong> aus <a href="lp.html#kap-lp">Kapitel 5</a>.</p>
</blockquote>
<h3 id="die-vier-kkt-bedingungen">Die vier KKT-Bedingungen</h3>
<p>Am Optimum <span class="math inline">(\mathbf{x}^*, \boldsymbol{\lambda}^*, \boldsymbol{\nu}^*)</span> gilt:</p>
<p><strong>1. Stationarität</strong> — der Gradient der Lagrange-Funktion verschwindet:</p>
<p><span class="math display">
\nabla f(\mathbf{x}^*) + \sum_i \lambda_i^*\nabla g_i(\mathbf{x}^*) + \sum_j \nu_j^*\nabla h_j(\mathbf{x}^*) = \mathbf{0}
</span></p>
<p><em>Anschaulich:</em> Die Kraft, die den Punkt bergab ziehen will (<span class="math inline">-\nabla f</span>), wird exakt von den Nebenbedingungen aufgefangen. Wie ein Ball, der in einer Ecke liegen bleibt: Die Schwerkraft zieht, die Wände drücken dagegen, und die Summe ist null.</p>
<p><strong>2. Primale Zulässigkeit</strong> — die ursprünglichen Bedingungen gelten: <span class="math inline">g_i(\mathbf{x}^*)\le0</span>, <span class="math inline">h_j(\mathbf{x}^*)=0</span>.</p>
<p><strong>3. Duale Zulässigkeit</strong> — die Multiplikatoren der Ungleichungen sind nichtnegativ: <span class="math inline">\lambda_i^*\ge0</span>.</p>
<p><em>Warum?</em> Eine Wand kann nur <strong>drücken</strong>, nicht ziehen. Ein negatives <span class="math inline">\lambda</span> hieße, die Bedingung würde die Lösung von sich weg<strong>ziehen</strong> — dann wäre sie nicht bindend.</p>
<p><strong>4. Komplementärer Schlupf</strong><span class="math inline">\lambda_i^*\cdot g_i(\mathbf{x}^*)=0</span> für alle <span class="math inline">i</span>.</p>
<p><em>Bedeutung:</em> Entweder ist die Bedingung nicht bindend (<span class="math inline">g_i&lt;0</span>), <strong>dann muss</strong> <span class="math inline">\lambda_i=0</span> sein. Oder der Multiplikator ist positiv, <strong>dann muss</strong> die Bedingung mit Gleichheit binden. Das ist derselbe Satz wie in <a href="lp.html#kap-lp">Kapitel 5</a> — nur allgemeiner formuliert.</p>
<figure>
<img src="bilder_04/kap_qp_kkt.svg" alt="Abb. 11.1: Links die Handrechnung dieses Abschnitts: Das freie Minimum liegt bei x=5, die Grenze lässt nur x \le 3 zu, und \lambda^* = 4 ist genau die Steigung von f am Rand — der Preis dafür, dass es nicht weitergeht. Rechts dieselbe Aussage in zwei Dimensionen: \nabla f und \lambda^*\nabla g zeigen gegeneinander und heben sich auf. Das Skript bilder_04/erzeuge_kkt.py prüft alle vier KKT-Bedingungen numerisch, bevor es zeichnet." />
<figcaption aria-hidden="true">Abb. 11.1: Links die Handrechnung dieses Abschnitts: Das freie Minimum liegt bei <span class="math inline">x=5</span>, die Grenze lässt nur <span class="math inline">x \le 3</span> zu, und <span class="math inline">\lambda^* = 4</span> ist genau die Steigung von <span class="math inline">f</span> am Rand — der Preis dafür, dass es nicht weitergeht. Rechts dieselbe Aussage in zwei Dimensionen: <span class="math inline">\nabla f</span> und <span class="math inline">\lambda^*\nabla g</span> zeigen gegeneinander und heben sich auf. Das Skript <code>bilder_04/erzeuge_kkt.py</code> prüft alle vier KKT-Bedingungen numerisch, bevor es zeichnet.</figcaption>
</figure>
<div class="card card-formel">
<blockquote>
<p><strong>🔤 Formel-Übersetzer: die vier KKT-Bedingungen</strong></p>
<p>Vier Zeilen Mathematik, vier Sätze, die man einem Betriebsleiter sagen könnte:</p>
<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<thead>
<tr class="header">
<th>Mathematik</th>
<th>Alltagssprache</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><span class="math inline">\nabla f(\mathbf{x}^*) + \sum_i \lambda_i^*\nabla g_i(\mathbf{x}^*) = \mathbf{0}</span></td>
<td>„Der Zug ins Bessere und der Widerstand der Grenzen heben sich genau auf — sonst könnte man noch einen Schritt gehen.“</td>
</tr>
<tr class="even">
<td><span class="math inline">g_i(\mathbf{x}^*) \le 0</span></td>
<td>„Der Plan hält alle Regeln ein.“</td>
</tr>
<tr class="odd">
<td><span class="math inline">\lambda_i^* \ge 0</span></td>
<td>„Eine Grenze kann nur bremsen, nie antreiben.“ Ein negativer Preis wäre widersinnig.</td>
</tr>
<tr class="even">
<td><span class="math inline">\lambda_i^* \cdot g_i(\mathbf{x}^*) = 0</span></td>
<td>„Was Reserve hat, ist nichts wert. Was etwas wert ist, ist ausgereizt.“ Nie beides.</td>
</tr>
<tr class="odd">
<td><span class="math inline">\lambda_i^*</span> selbst</td>
<td>Der <strong>Schattenpreis</strong> von Bedingung <span class="math inline">i</span> — dieselbe Größe wie im LP (<a href="lp.html#kap-lp">Kapitel 5</a>), nur für krumme Ränder.</td>
</tr>
</tbody>
</table>
<p><strong>Alle vier zusammen in einem Satz:</strong> <em>Wir stehen an einer Stelle, an der jede Verbesserung an eine Grenze stößt — und wir wissen für jede dieser Grenzen, was ihre Lockerung wert wäre.</em></p>
<p><strong>Warum das praktisch zählt:</strong> Die KKT-Bedingungen sind nicht nur Theorie, sondern eine <strong>Prüfvorschrift</strong>. Bei einem konvexen Problem sind sie notwendig <em>und</em> hinreichend — wer sie an einer gefundenen Lösung nachrechnet, hat damit bewiesen, dass sie optimal ist. Genau das tut <code>KKT_Nachweis.py</code> weiter unten.</p>
</blockquote>
</div>
<blockquote>
<p><strong>✏️ Handrechnung 11.1: KKT an einem Minimalbeispiel</strong></p>
<p><span class="math display">\min\ f(x) = (x-5)^2 \qquad\text{u. d. N.}\qquad g(x) = x - 3 \le 0</span></p>
<p><strong>Ohne Nebenbedingung</strong> läge das Minimum bei <span class="math inline">x = 5</span>. Aber <span class="math inline">5 &gt; 3</span> — unzulässig.</p>
<p><strong>Lagrange:</strong> <span class="math inline">\mathcal{L}(x,\lambda) = (x-5)^2 + \lambda(x-3)</span>.</p>
<p><strong>KKT 1 (Stationarität):</strong> <span class="math inline">\dfrac{\partial\mathcal{L}}{\partial x} = 2(x-5) + \lambda = 0 \;\Rightarrow\; x = 5 - \lambda/2</span>.</p>
<p><strong>Fall A: <span class="math inline">\lambda = 0</span></strong> (Bedingung nicht bindend). Dann <span class="math inline">x = 5</span>. Prüfe KKT 2: <span class="math inline">g(5) = 2 &gt; 0</span><strong>unzulässig</strong> — dieser Fall entfällt.</p>
<p><strong>Fall B: <span class="math inline">\lambda &gt; 0</span></strong> (Bedingung bindend). Aus KKT 4 folgt <span class="math inline">g(x) = 0</span>, also <span class="math inline">x = 3</span>. Einsetzen in KKT 1: <span class="math inline">2(3-5) + \lambda = 0 \Rightarrow \lambda = 4</span>. Prüfe KKT 3: <span class="math inline">\lambda = 4 &gt; 0</span></p>
<p><strong>Lösung:</strong> <span class="math inline">x^* = 3</span>, <span class="math inline">\lambda^* = 4</span>, <span class="math inline">f(x^*) = 4</span>.</p>
<p><strong>Und was bedeutet <span class="math inline">\lambda^* = 4</span>?</strong> Es ist der Schattenpreis: Dürfte <span class="math inline">x</span> bis 3,1 gehen, sänke <span class="math inline">f</span> um etwa <span class="math inline">4 \times 0{,}1 = 0{,}4</span>. Probe: <span class="math inline">f(3{,}1) = (3{,}1-5)^2 = 3{,}61</span>, tatsächlicher Rückgang <span class="math inline">0{,}39</span> ✓ — die kleine Abweichung ist die Krümmung.</p>
</blockquote>
<div class="sourceCode" id="cb6"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a><span class="co"># KKT_Nachweis.py</span></span>
<span id="cb6-4"><a href="#cb6-4" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb6-5"><a href="#cb6-5" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel QP/NLP: Die KKT-Bedingungen numerisch nachpruefen.</span></span>
<span id="cb6-6"><a href="#cb6-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-7"><a href="#cb6-7" aria-hidden="true" tabindex="-1"></a><span class="co">Loest ein QP mit CVXPY, liest die Dualwerte aus und prueft alle vier</span></span>
<span id="cb6-8"><a href="#cb6-8" aria-hidden="true" tabindex="-1"></a><span class="co">KKT-Bedingungen einzeln nach. Das ist zugleich eine Vorlage fuer die</span></span>
<span id="cb6-9"><a href="#cb6-9" aria-hidden="true" tabindex="-1"></a><span class="co">Qualitaetssicherung eigener Modelle.</span></span>
<span id="cb6-10"><a href="#cb6-10" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb6-11"><a href="#cb6-11" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-12"><a href="#cb6-12" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb6-13"><a href="#cb6-13" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> cvxpy <span class="im">as</span> cp</span>
<span id="cb6-14"><a href="#cb6-14" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-15"><a href="#cb6-15" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-16"><a href="#cb6-16" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> baue_gueltige_kovarianz(vola, korrelationen, seed<span class="op">=</span><span class="dv">0</span>):</span>
<span id="cb6-17"><a href="#cb6-17" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;</span></span>
<span id="cb6-18"><a href="#cb6-18" aria-hidden="true" tabindex="-1"></a><span class="co"> Baut Sigma = D * C * D aus Volatilitaeten und einer Korrelationsmatrix.</span></span>
<span id="cb6-19"><a href="#cb6-19" aria-hidden="true" tabindex="-1"></a><span class="co"> Dieses Vorgehen ist konstruktionsbedingt positiv semidefinit - im</span></span>
<span id="cb6-20"><a href="#cb6-20" aria-hidden="true" tabindex="-1"></a><span class="co"> Gegensatz zum nachtraeglichen Ueberschreiben der Diagonalen, das die</span></span>
<span id="cb6-21"><a href="#cb6-21" aria-hidden="true" tabindex="-1"></a><span class="co"> positive Semidefinitheit zerstoeren kann.</span></span>
<span id="cb6-22"><a href="#cb6-22" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb6-23"><a href="#cb6-23" aria-hidden="true" tabindex="-1"></a> C <span class="op">=</span> np.array(korrelationen, dtype<span class="op">=</span><span class="bu">float</span>)</span>
<span id="cb6-24"><a href="#cb6-24" aria-hidden="true" tabindex="-1"></a> <span class="cf">assert</span> np.allclose(C, C.T), <span class="st">&quot;Korrelationsmatrix muss symmetrisch sein.&quot;</span></span>
<span id="cb6-25"><a href="#cb6-25" aria-hidden="true" tabindex="-1"></a> <span class="cf">assert</span> np.allclose(np.diag(C), <span class="fl">1.0</span>), <span class="st">&quot;Diagonale der Korrelationsmatrix muss 1 sein.&quot;</span></span>
<span id="cb6-26"><a href="#cb6-26" aria-hidden="true" tabindex="-1"></a> eigen <span class="op">=</span> np.linalg.eigvalsh(C)</span>
<span id="cb6-27"><a href="#cb6-27" aria-hidden="true" tabindex="-1"></a> <span class="cf">assert</span> eigen.<span class="bu">min</span>() <span class="op">&gt;</span> <span class="op">-</span><span class="fl">1e-10</span>, (</span>
<span id="cb6-28"><a href="#cb6-28" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Korrelationsmatrix ist nicht positiv semidefinit &quot;</span></span>
<span id="cb6-29"><a href="#cb6-29" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;(kleinster Eigenwert </span><span class="sc">{</span>eigen<span class="sc">.</span><span class="bu">min</span>()<span class="sc">:.4f}</span><span class="ss">). Solche Korrelationen sind unmoeglich.&quot;</span>)</span>
<span id="cb6-30"><a href="#cb6-30" aria-hidden="true" tabindex="-1"></a> D <span class="op">=</span> np.diag(vola)</span>
<span id="cb6-31"><a href="#cb6-31" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> D <span class="op">@</span> C <span class="op">@</span> D</span>
<span id="cb6-32"><a href="#cb6-32" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-33"><a href="#cb6-33" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-34"><a href="#cb6-34" 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="cb6-35"><a href="#cb6-35" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Ein kleines Portfolio-QP ----------------------------------------</span></span>
<span id="cb6-36"><a href="#cb6-36" aria-hidden="true" tabindex="-1"></a> vola <span class="op">=</span> np.array([<span class="fl">0.20</span>, <span class="fl">0.14</span>, <span class="fl">0.30</span>]) <span class="co"># Volatilitaeten</span></span>
<span id="cb6-37"><a href="#cb6-37" aria-hidden="true" tabindex="-1"></a> korr <span class="op">=</span> [[<span class="fl">1.00</span>, <span class="fl">0.30</span>, <span class="fl">0.10</span>],</span>
<span id="cb6-38"><a href="#cb6-38" aria-hidden="true" tabindex="-1"></a> [<span class="fl">0.30</span>, <span class="fl">1.00</span>, <span class="fl">0.25</span>],</span>
<span id="cb6-39"><a href="#cb6-39" aria-hidden="true" tabindex="-1"></a> [<span class="fl">0.10</span>, <span class="fl">0.25</span>, <span class="fl">1.00</span>]]</span>
<span id="cb6-40"><a href="#cb6-40" aria-hidden="true" tabindex="-1"></a> Sigma <span class="op">=</span> baue_gueltige_kovarianz(vola, korr)</span>
<span id="cb6-41"><a href="#cb6-41" aria-hidden="true" tabindex="-1"></a> mu <span class="op">=</span> np.array([<span class="fl">0.09</span>, <span class="fl">0.05</span>, <span class="fl">0.13</span>]) <span class="co"># erwartete Renditen</span></span>
<span id="cb6-42"><a href="#cb6-42" aria-hidden="true" tabindex="-1"></a> lam <span class="op">=</span> <span class="fl">3.0</span> <span class="co"># Risikoaversion</span></span>
<span id="cb6-43"><a href="#cb6-43" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-44"><a href="#cb6-44" aria-hidden="true" tabindex="-1"></a> eigenwerte <span class="op">=</span> np.linalg.eigvalsh(Sigma)</span>
<span id="cb6-45"><a href="#cb6-45" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">74</span>)</span>
<span id="cb6-46"><a href="#cb6-46" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; KKT-BEDINGUNGEN AM PORTFOLIO-QP&quot;</span>)</span>
<span id="cb6-47"><a href="#cb6-47" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">74</span>)</span>
<span id="cb6-48"><a href="#cb6-48" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Eigenwerte von Sigma: </span><span class="sc">{</span>np<span class="sc">.</span><span class="bu">round</span>(eigenwerte, <span class="dv">6</span>)<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb6-49"><a href="#cb6-49" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; -&gt; positiv definit: </span><span class="sc">{</span><span class="bu">bool</span>(eigenwerte.<span class="bu">min</span>() <span class="op">&gt;</span> <span class="dv">0</span>)<span class="sc">}</span><span class="ss"> &quot;</span></span>
<span id="cb6-50"><a href="#cb6-50" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;(Problem ist streng konvex, Loesung eindeutig)&quot;</span>)</span>
<span id="cb6-51"><a href="#cb6-51" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; -&gt; Konditionszahl: </span><span class="sc">{</span>eigenwerte<span class="sc">.</span><span class="bu">max</span>() <span class="op">/</span> eigenwerte<span class="sc">.</span><span class="bu">min</span>()<span class="sc">:.2f}</span><span class="ss">&quot;</span>)</span>
<span id="cb6-52"><a href="#cb6-52" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-53"><a href="#cb6-53" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Modell: min lam/2 * w&#39;Sigma w - mu&#39;w u.d.N. sum(w)=1, w&gt;=0 ----</span></span>
<span id="cb6-54"><a href="#cb6-54" aria-hidden="true" tabindex="-1"></a> n <span class="op">=</span> <span class="bu">len</span>(mu)</span>
<span id="cb6-55"><a href="#cb6-55" aria-hidden="true" tabindex="-1"></a> w <span class="op">=</span> cp.Variable(n)</span>
<span id="cb6-56"><a href="#cb6-56" aria-hidden="true" tabindex="-1"></a> ziel <span class="op">=</span> cp.Minimize(<span class="fl">0.5</span> <span class="op">*</span> lam <span class="op">*</span> cp.quad_form(w, Sigma) <span class="op">-</span> mu <span class="op">@</span> w)</span>
<span id="cb6-57"><a href="#cb6-57" aria-hidden="true" tabindex="-1"></a> budget <span class="op">=</span> cp.<span class="bu">sum</span>(w) <span class="op">==</span> <span class="dv">1</span></span>
<span id="cb6-58"><a href="#cb6-58" aria-hidden="true" tabindex="-1"></a> nichtnegativ <span class="op">=</span> w <span class="op">&gt;=</span> <span class="dv">0</span></span>
<span id="cb6-59"><a href="#cb6-59" aria-hidden="true" tabindex="-1"></a> problem <span class="op">=</span> cp.Problem(ziel, [budget, nichtnegativ])</span>
<span id="cb6-60"><a href="#cb6-60" aria-hidden="true" tabindex="-1"></a> problem.solve()</span>
<span id="cb6-61"><a href="#cb6-61" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-62"><a href="#cb6-62" aria-hidden="true" tabindex="-1"></a> w_opt <span class="op">=</span> w.value</span>
<span id="cb6-63"><a href="#cb6-63" aria-hidden="true" tabindex="-1"></a> nu <span class="op">=</span> budget.dual_value <span class="co"># Multiplikator der Gleichung</span></span>
<span id="cb6-64"><a href="#cb6-64" aria-hidden="true" tabindex="-1"></a> lam_i <span class="op">=</span> nichtnegativ.dual_value <span class="co"># Multiplikatoren der Ungleichungen</span></span>
<span id="cb6-65"><a href="#cb6-65" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-66"><a href="#cb6-66" 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">Status: </span><span class="sc">{</span>problem<span class="sc">.</span>status<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb6-67"><a href="#cb6-67" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Optimale Gewichte: </span><span class="sc">{</span>np<span class="sc">.</span><span class="bu">round</span>(w_opt, <span class="dv">6</span>)<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb6-68"><a href="#cb6-68" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Zielwert: </span><span class="sc">{</span>problem<span class="sc">.</span>value<span class="sc">:.6f}</span><span class="ss">&quot;</span>)</span>
<span id="cb6-69"><a href="#cb6-69" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Multiplikator der Budgetgleichung (nu): </span><span class="sc">{</span>nu<span class="sc">:.6f}</span><span class="ss">&quot;</span>)</span>
<span id="cb6-70"><a href="#cb6-70" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Multiplikatoren der w&gt;=0-Bedingungen: </span><span class="sc">{</span>np<span class="sc">.</span><span class="bu">round</span>(lam_i, <span class="dv">6</span>)<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb6-71"><a href="#cb6-71" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-72"><a href="#cb6-72" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- KKT-Bedingungen einzeln pruefen ---------------------------------</span></span>
<span id="cb6-73"><a href="#cb6-73" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">--- Pruefung der vier KKT-Bedingungen ---&quot;</span>)</span>
<span id="cb6-74"><a href="#cb6-74" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-75"><a href="#cb6-75" aria-hidden="true" tabindex="-1"></a> <span class="co"># 1. Stationaritaet: grad f - lambda + nu*1 = 0</span></span>
<span id="cb6-76"><a href="#cb6-76" aria-hidden="true" tabindex="-1"></a> <span class="co"># f(w) = lam/2 w&#39;Sigma w - mu&#39;w -&gt; grad f = lam*Sigma w - mu</span></span>
<span id="cb6-77"><a href="#cb6-77" aria-hidden="true" tabindex="-1"></a> <span class="co"># g_i(w) = -w_i &lt;= 0 -&gt; grad g_i = -e_i</span></span>
<span id="cb6-78"><a href="#cb6-78" aria-hidden="true" tabindex="-1"></a> <span class="co"># h(w) = sum(w) - 1 = 0 -&gt; grad h = 1</span></span>
<span id="cb6-79"><a href="#cb6-79" aria-hidden="true" tabindex="-1"></a> grad_f <span class="op">=</span> lam <span class="op">*</span> (Sigma <span class="op">@</span> w_opt) <span class="op">-</span> mu</span>
<span id="cb6-80"><a href="#cb6-80" aria-hidden="true" tabindex="-1"></a> stationaritaet <span class="op">=</span> grad_f <span class="op">-</span> lam_i <span class="op">+</span> nu <span class="op">*</span> np.ones(n)</span>
<span id="cb6-81"><a href="#cb6-81" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;1. Stationaritaet : max|Residuum| = </span><span class="sc">{</span>np<span class="sc">.</span><span class="bu">abs</span>(stationaritaet)<span class="sc">.</span><span class="bu">max</span>()<span class="sc">:.2e}</span><span class="ss">&quot;</span>)</span>
<span id="cb6-82"><a href="#cb6-82" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-83"><a href="#cb6-83" aria-hidden="true" tabindex="-1"></a> <span class="co"># 2. Primale Zulaessigkeit</span></span>
<span id="cb6-84"><a href="#cb6-84" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;2. Primal zulaessig : sum(w)-1 = </span><span class="sc">{</span>w_opt<span class="sc">.</span><span class="bu">sum</span>()<span class="op">-</span><span class="dv">1</span><span class="sc">:.2e}</span><span class="ss">, &quot;</span></span>
<span id="cb6-85"><a href="#cb6-85" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;min(w) = </span><span class="sc">{</span>w_opt<span class="sc">.</span><span class="bu">min</span>()<span class="sc">:.2e}</span><span class="ss">&quot;</span>)</span>
<span id="cb6-86"><a href="#cb6-86" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-87"><a href="#cb6-87" aria-hidden="true" tabindex="-1"></a> <span class="co"># 3. Duale Zulaessigkeit</span></span>
<span id="cb6-88"><a href="#cb6-88" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;3. Dual zulaessig : min(lambda) = </span><span class="sc">{</span>lam_i<span class="sc">.</span><span class="bu">min</span>()<span class="sc">:.2e}</span><span class="ss"> (muss &gt;= 0 sein)&quot;</span>)</span>
<span id="cb6-89"><a href="#cb6-89" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-90"><a href="#cb6-90" aria-hidden="true" tabindex="-1"></a> <span class="co"># 4. Komplementaerer Schlupf: lambda_i * w_i = 0</span></span>
<span id="cb6-91"><a href="#cb6-91" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;4. Kompl. Schlupf : max|lambda_i * w_i| = &quot;</span></span>
<span id="cb6-92"><a href="#cb6-92" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>np<span class="sc">.</span><span class="bu">abs</span>(lam_i <span class="op">*</span> w_opt)<span class="sc">.</span><span class="bu">max</span>()<span class="sc">:.2e}</span><span class="ss">&quot;</span>)</span>
<span id="cb6-93"><a href="#cb6-93" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-94"><a href="#cb6-94" aria-hidden="true" tabindex="-1"></a> alle_ok <span class="op">=</span> (np.<span class="bu">abs</span>(stationaritaet).<span class="bu">max</span>() <span class="op">&lt;</span> <span class="fl">1e-6</span></span>
<span id="cb6-95"><a href="#cb6-95" aria-hidden="true" tabindex="-1"></a> <span class="kw">and</span> <span class="bu">abs</span>(w_opt.<span class="bu">sum</span>() <span class="op">-</span> <span class="dv">1</span>) <span class="op">&lt;</span> <span class="fl">1e-8</span></span>
<span id="cb6-96"><a href="#cb6-96" aria-hidden="true" tabindex="-1"></a> <span class="kw">and</span> w_opt.<span class="bu">min</span>() <span class="op">&gt;</span> <span class="op">-</span><span class="fl">1e-8</span></span>
<span id="cb6-97"><a href="#cb6-97" aria-hidden="true" tabindex="-1"></a> <span class="kw">and</span> lam_i.<span class="bu">min</span>() <span class="op">&gt;</span> <span class="op">-</span><span class="fl">1e-8</span></span>
<span id="cb6-98"><a href="#cb6-98" aria-hidden="true" tabindex="-1"></a> <span class="kw">and</span> np.<span class="bu">abs</span>(lam_i <span class="op">*</span> w_opt).<span class="bu">max</span>() <span class="op">&lt;</span> <span class="fl">1e-6</span>)</span>
<span id="cb6-99"><a href="#cb6-99" 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">Alle vier KKT-Bedingungen erfuellt: </span><span class="sc">{</span>alle_ok<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb6-100"><a href="#cb6-100" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-101"><a href="#cb6-101" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Interpretation von nu -------------------------------------------</span></span>
<span id="cb6-102"><a href="#cb6-102" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">--- Was bedeutet nu? ---&quot;</span>)</span>
<span id="cb6-103"><a href="#cb6-103" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;nu ist der Schattenpreis des Budgets: Um so viel aendert sich der&quot;</span>)</span>
<span id="cb6-104"><a href="#cb6-104" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Zielwert, wenn man statt 100 % nur 99 </span><span class="sc">% i</span><span class="st">nvestieren duerfte.&quot;</span>)</span>
<span id="cb6-105"><a href="#cb6-105" aria-hidden="true" tabindex="-1"></a> problem2 <span class="op">=</span> cp.Problem(cp.Minimize(<span class="fl">0.5</span> <span class="op">*</span> lam <span class="op">*</span> cp.quad_form(w, Sigma) <span class="op">-</span> mu <span class="op">@</span> w),</span>
<span id="cb6-106"><a href="#cb6-106" aria-hidden="true" tabindex="-1"></a> [cp.<span class="bu">sum</span>(w) <span class="op">==</span> <span class="fl">1.01</span>, w <span class="op">&gt;=</span> <span class="dv">0</span>])</span>
<span id="cb6-107"><a href="#cb6-107" aria-hidden="true" tabindex="-1"></a> problem2.solve()</span>
<span id="cb6-108"><a href="#cb6-108" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Vorhergesagt (nu * 0.01): </span><span class="sc">{</span>nu <span class="op">*</span> <span class="fl">0.01</span><span class="sc">:+.6f}</span><span class="ss">&quot;</span>)</span>
<span id="cb6-109"><a href="#cb6-109" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Tatsaechlich gemessen: </span><span class="sc">{</span>problem2<span class="sc">.</span>value <span class="op">-</span> problem<span class="sc">.</span>value<span class="sc">:+.6f}</span><span class="ss">&quot;</span>)</span>
<span id="cb6-110"><a href="#cb6-110" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">74</span>)</span></code></pre></div>
<hr />
<h2 id="sec:qp-nlp-nichtlineare-optimierung-mit-scipy-optimize-minimize">11.5 Nichtlineare Optimierung mit <code>scipy.optimize.minimize</code></h2>
<p>Wenn Zielfunktion oder Nebenbedingungen weder linear noch quadratisch sind — Logarithmen, Wurzeln, Exponentialfunktionen — nutzt man gradientenbasierte Verfahren wie <strong>SLSQP</strong> (<em>Sequential Least Squares Programming</em>).</p>
<blockquote>
<p><strong>⚠️ Der entscheidende Unterschied zu CVXPY</strong> CVXPY <strong>prüft</strong> die Konvexität und garantiert bei Erfolg das globale Optimum. <code>scipy.optimize.minimize</code> prüft nichts und liefert ein <strong>lokales</strong> Optimum, das vom Startpunkt abhängt (siehe die Demonstration in <a href="fundament.html#sec:fundament-konvexitaet-die-grenze-zwischen-leicht-und">Abschnitt 2.5</a>). Wer SLSQP einsetzt, sollte <strong>immer mit mehreren Startpunkten rechnen</strong> und die Ergebnisse vergleichen.</p>
</blockquote>
<h3 id="praxisfall-entropie-maximierte-kapitalallokation">Praxisfall: Entropie-maximierte Kapitalallokation</h3>
<p>Wir optimieren ein Portfolio nicht nur nach Varianz, sondern maximieren zusätzlich die <strong>Shannon-Entropie</strong> der Gewichte, um Klumpenrisiken glatt zu bestrafen:</p>
<p><span class="math display">
\min_{\mathbf{w}}\quad \underbrace{\tfrac12\mathbf{w}^\top\boldsymbol{\Sigma}\mathbf{w}}_{\text{Risiko}}
\;-\;\underbrace{\alpha\,\boldsymbol{\mu}^\top\mathbf{w}}_{\text{Ertrag}}
\;-\;\underbrace{\beta\Big(-\textstyle\sum_i w_i\ln w_i\Big)}_{\text{Diversifikation}}
</span></p>
<blockquote>
<p><strong>📐 Formel-Lesehilfe zur Entropie</strong> <span class="math inline">H(\mathbf{w}) = -\sum_i w_i \ln w_i</span> ist ein Streuungsmaß: * Alles in einem Titel (<span class="math inline">w = (1,0,0,0)</span>): <span class="math inline">H = 0</span> — minimale Streuung. * Gleichverteilt (<span class="math inline">w_i = 1/4</span>): <span class="math inline">H = \ln 4 \approx 1{,}386</span> — maximale Streuung.</p>
<p><strong>Ohne Formel gesagt:</strong> Die Entropie misst, wie „breit verteilt“ das Portfolio ist. Weil sie mit einem Minuszeichen in die zu minimierende Zielfunktion eingeht, wird breite Streuung <strong>belohnt</strong> — und zwar mathematisch glatt, ohne harte Obergrenzen. Der Vorteil gegenüber einer Schranke wie <span class="math inline">w_i \le 0{,}2</span>: Der Übergang ist weich, das Ergebnis reagiert nicht sprunghaft auf kleine Datenänderungen.</p>
</blockquote>
<div class="sourceCode" id="cb7"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a><span class="co"># Entropie_Maximierte_Allokation.py</span></span>
<span id="cb7-4"><a href="#cb7-4" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb7-5"><a href="#cb7-5" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel QP/NLP: Nichtlineare Optimierung (NLP) mit scipy SLSQP.</span></span>
<span id="cb7-6"><a href="#cb7-6" aria-hidden="true" tabindex="-1"></a><span class="co">Modell: Mean-Variance-Portfolio mit Shannon-Entropie-Diversifikation.</span></span>
<span id="cb7-7"><a href="#cb7-7" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-8"><a href="#cb7-8" aria-hidden="true" tabindex="-1"></a><span class="co">Achtung, haeufiger Fehler: Sigma per A@A.T zu erzeugen und anschliessend die</span></span>
<span id="cb7-9"><a href="#cb7-9" aria-hidden="true" tabindex="-1"></a><span class="co">Diagonale zu ueberschreiben zerstoert die positive Semidefinitheit - die</span></span>
<span id="cb7-10"><a href="#cb7-10" aria-hidden="true" tabindex="-1"></a><span class="co">Matrix kann dadurch einen negativen Eigenwert bekommen und unmoegliche</span></span>
<span id="cb7-11"><a href="#cb7-11" aria-hidden="true" tabindex="-1"></a><span class="co">Korrelationen (&gt; 1) implizieren. Dieses Programm baut Sigma stattdessen als</span></span>
<span id="cb7-12"><a href="#cb7-12" aria-hidden="true" tabindex="-1"></a><span class="co">D * C * D aus Volatilitaeten und einer echten Korrelationsmatrix -</span></span>
<span id="cb7-13"><a href="#cb7-13" aria-hidden="true" tabindex="-1"></a><span class="co">konstruktionsbedingt immer PSD.</span></span>
<span id="cb7-14"><a href="#cb7-14" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-15"><a href="#cb7-15" aria-hidden="true" tabindex="-1"></a><span class="co">Zusaetzlich: Multistart, weil SLSQP nur lokale Optima findet.</span></span>
<span id="cb7-16"><a href="#cb7-16" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb7-17"><a href="#cb7-17" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-18"><a href="#cb7-18" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb7-19"><a href="#cb7-19" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> pandas <span class="im">as</span> pd</span>
<span id="cb7-20"><a href="#cb7-20" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> scipy.optimize <span class="im">import</span> minimize</span>
<span id="cb7-21"><a href="#cb7-21" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-22"><a href="#cb7-22" aria-hidden="true" tabindex="-1"></a>ASSETS <span class="op">=</span> [<span class="st">&quot;Tech-Aktien&quot;</span>, <span class="st">&quot;Rohstoffe&quot;</span>, <span class="st">&quot;US-Treasuries&quot;</span>, <span class="st">&quot;Krypto&quot;</span>]</span>
<span id="cb7-23"><a href="#cb7-23" aria-hidden="true" tabindex="-1"></a>MU <span class="op">=</span> np.array([<span class="fl">0.14</span>, <span class="fl">0.07</span>, <span class="fl">0.03</span>, <span class="fl">0.22</span>]) <span class="co"># erwartete Jahresrenditen</span></span>
<span id="cb7-24"><a href="#cb7-24" aria-hidden="true" tabindex="-1"></a>VOLA <span class="op">=</span> np.array([<span class="fl">0.20</span>, <span class="fl">0.14</span>, <span class="fl">0.07</span>, <span class="fl">0.40</span>]) <span class="co"># Volatilitaeten p.a.</span></span>
<span id="cb7-25"><a href="#cb7-25" aria-hidden="true" tabindex="-1"></a>KORRELATION <span class="op">=</span> np.array([</span>
<span id="cb7-26"><a href="#cb7-26" aria-hidden="true" tabindex="-1"></a> [<span class="fl">1.00</span>, <span class="fl">0.25</span>, <span class="op">-</span><span class="fl">0.10</span>, <span class="fl">0.55</span>],</span>
<span id="cb7-27"><a href="#cb7-27" aria-hidden="true" tabindex="-1"></a> [<span class="fl">0.25</span>, <span class="fl">1.00</span>, <span class="fl">0.05</span>, <span class="fl">0.20</span>],</span>
<span id="cb7-28"><a href="#cb7-28" aria-hidden="true" tabindex="-1"></a> [<span class="op">-</span><span class="fl">0.10</span>, <span class="fl">0.05</span>, <span class="fl">1.00</span>, <span class="op">-</span><span class="fl">0.15</span>],</span>
<span id="cb7-29"><a href="#cb7-29" aria-hidden="true" tabindex="-1"></a> [<span class="fl">0.55</span>, <span class="fl">0.20</span>, <span class="op">-</span><span class="fl">0.15</span>, <span class="fl">1.00</span>],</span>
<span id="cb7-30"><a href="#cb7-30" aria-hidden="true" tabindex="-1"></a>])</span>
<span id="cb7-31"><a href="#cb7-31" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-32"><a href="#cb7-32" aria-hidden="true" tabindex="-1"></a>ALPHA <span class="op">=</span> <span class="fl">1.0</span> <span class="co"># Gewicht des Ertrags</span></span>
<span id="cb7-33"><a href="#cb7-33" aria-hidden="true" tabindex="-1"></a>BETA <span class="op">=</span> <span class="fl">0.015</span> <span class="co"># Gewicht der Entropie-Diversifikation</span></span>
<span id="cb7-34"><a href="#cb7-34" aria-hidden="true" tabindex="-1"></a>UNTERGRENZE <span class="op">=</span> <span class="fl">0.001</span></span>
<span id="cb7-35"><a href="#cb7-35" aria-hidden="true" tabindex="-1"></a>OBERGRENZE <span class="op">=</span> <span class="fl">0.80</span></span>
<span id="cb7-36"><a href="#cb7-36" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-37"><a href="#cb7-37" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-38"><a href="#cb7-38" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> baue_kovarianz() <span class="op">-&gt;</span> np.ndarray:</span>
<span id="cb7-39"><a href="#cb7-39" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Sigma = D * C * D. Immer PSD, wenn C eine gueltige Korrelationsmatrix ist.&quot;&quot;&quot;</span></span>
<span id="cb7-40"><a href="#cb7-40" aria-hidden="true" tabindex="-1"></a> eig_c <span class="op">=</span> np.linalg.eigvalsh(KORRELATION)</span>
<span id="cb7-41"><a href="#cb7-41" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> eig_c.<span class="bu">min</span>() <span class="op">&lt;</span> <span class="op">-</span><span class="fl">1e-10</span>:</span>
<span id="cb7-42"><a href="#cb7-42" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="ss">f&quot;Korrelationsmatrix unmoeglich (Eigenwert </span><span class="sc">{</span>eig_c<span class="sc">.</span><span class="bu">min</span>()<span class="sc">:.4f}</span><span class="ss">).&quot;</span>)</span>
<span id="cb7-43"><a href="#cb7-43" aria-hidden="true" tabindex="-1"></a> D <span class="op">=</span> np.diag(VOLA)</span>
<span id="cb7-44"><a href="#cb7-44" aria-hidden="true" tabindex="-1"></a> Sigma <span class="op">=</span> D <span class="op">@</span> KORRELATION <span class="op">@</span> D</span>
<span id="cb7-45"><a href="#cb7-45" aria-hidden="true" tabindex="-1"></a> eig_s <span class="op">=</span> np.linalg.eigvalsh(Sigma)</span>
<span id="cb7-46"><a href="#cb7-46" aria-hidden="true" tabindex="-1"></a> <span class="cf">assert</span> eig_s.<span class="bu">min</span>() <span class="op">&gt;</span> <span class="dv">0</span>, <span class="st">&quot;Sigma nicht positiv definit!&quot;</span></span>
<span id="cb7-47"><a href="#cb7-47" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> Sigma</span>
<span id="cb7-48"><a href="#cb7-48" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-49"><a href="#cb7-49" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-50"><a href="#cb7-50" aria-hidden="true" tabindex="-1"></a>SIGMA <span class="op">=</span> baue_kovarianz()</span>
<span id="cb7-51"><a href="#cb7-51" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-52"><a href="#cb7-52" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-53"><a href="#cb7-53" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> zielfunktion(w, alpha<span class="op">=</span>ALPHA, beta<span class="op">=</span>BETA):</span>
<span id="cb7-54"><a href="#cb7-54" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Risiko - Ertrag - Entropiepraemie (wird minimiert).&quot;&quot;&quot;</span></span>
<span id="cb7-55"><a href="#cb7-55" aria-hidden="true" tabindex="-1"></a> varianz <span class="op">=</span> <span class="fl">0.5</span> <span class="op">*</span> w <span class="op">@</span> SIGMA <span class="op">@</span> w</span>
<span id="cb7-56"><a href="#cb7-56" aria-hidden="true" tabindex="-1"></a> ertrag <span class="op">=</span> MU <span class="op">@</span> w</span>
<span id="cb7-57"><a href="#cb7-57" aria-hidden="true" tabindex="-1"></a> entropie <span class="op">=</span> <span class="op">-</span>np.<span class="bu">sum</span>(w <span class="op">*</span> np.log(w <span class="op">+</span> <span class="fl">1e-12</span>))</span>
<span id="cb7-58"><a href="#cb7-58" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> varianz <span class="op">-</span> alpha <span class="op">*</span> ertrag <span class="op">-</span> beta <span class="op">*</span> entropie</span>
<span id="cb7-59"><a href="#cb7-59" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-60"><a href="#cb7-60" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-61"><a href="#cb7-61" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> gradient(w, alpha<span class="op">=</span>ALPHA, beta<span class="op">=</span>BETA):</span>
<span id="cb7-62"><a href="#cb7-62" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Exakter analytischer Gradient - beschleunigt und stabilisiert SLSQP.&quot;&quot;&quot;</span></span>
<span id="cb7-63"><a href="#cb7-63" aria-hidden="true" tabindex="-1"></a> grad_varianz <span class="op">=</span> SIGMA <span class="op">@</span> w <span class="co"># d/dw von 0.5 w&#39;Sigma w</span></span>
<span id="cb7-64"><a href="#cb7-64" aria-hidden="true" tabindex="-1"></a> grad_ertrag <span class="op">=</span> <span class="op">-</span>alpha <span class="op">*</span> MU</span>
<span id="cb7-65"><a href="#cb7-65" aria-hidden="true" tabindex="-1"></a> grad_entropie <span class="op">=</span> beta <span class="op">*</span> (np.log(w <span class="op">+</span> <span class="fl">1e-12</span>) <span class="op">+</span> <span class="fl">1.0</span>)</span>
<span id="cb7-66"><a href="#cb7-66" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> grad_varianz <span class="op">+</span> grad_ertrag <span class="op">+</span> grad_entropie</span>
<span id="cb7-67"><a href="#cb7-67" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-68"><a href="#cb7-68" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-69"><a href="#cb7-69" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> optimiere(startpunkt, alpha<span class="op">=</span>ALPHA, beta<span class="op">=</span>BETA):</span>
<span id="cb7-70"><a href="#cb7-70" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;alpha und beta werden durchgereicht - so bleibt die Funktion seiteneffektfrei.&quot;&quot;&quot;</span></span>
<span id="cb7-71"><a href="#cb7-71" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> minimize(</span>
<span id="cb7-72"><a href="#cb7-72" aria-hidden="true" tabindex="-1"></a> zielfunktion, startpunkt, args<span class="op">=</span>(alpha, beta), jac<span class="op">=</span>gradient,</span>
<span id="cb7-73"><a href="#cb7-73" aria-hidden="true" tabindex="-1"></a> method<span class="op">=</span><span class="st">&quot;SLSQP&quot;</span>, bounds<span class="op">=</span>[(UNTERGRENZE, OBERGRENZE)] <span class="op">*</span> <span class="bu">len</span>(MU),</span>
<span id="cb7-74"><a href="#cb7-74" aria-hidden="true" tabindex="-1"></a> constraints<span class="op">=</span>({<span class="st">&quot;type&quot;</span>: <span class="st">&quot;eq&quot;</span>,</span>
<span id="cb7-75"><a href="#cb7-75" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;fun&quot;</span>: <span class="kw">lambda</span> w: np.<span class="bu">sum</span>(w) <span class="op">-</span> <span class="fl">1.0</span>,</span>
<span id="cb7-76"><a href="#cb7-76" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;jac&quot;</span>: <span class="kw">lambda</span> w: np.ones(<span class="bu">len</span>(MU))}),</span>
<span id="cb7-77"><a href="#cb7-77" aria-hidden="true" tabindex="-1"></a> options<span class="op">=</span>{<span class="st">&quot;ftol&quot;</span>: <span class="fl">1e-12</span>, <span class="st">&quot;maxiter&quot;</span>: <span class="dv">300</span>})</span>
<span id="cb7-78"><a href="#cb7-78" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-79"><a href="#cb7-79" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-80"><a href="#cb7-80" 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="cb7-81"><a href="#cb7-81" aria-hidden="true" tabindex="-1"></a> n <span class="op">=</span> <span class="bu">len</span>(MU)</span>
<span id="cb7-82"><a href="#cb7-82" aria-hidden="true" tabindex="-1"></a> eig <span class="op">=</span> np.linalg.eigvalsh(SIGMA)</span>
<span id="cb7-83"><a href="#cb7-83" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-84"><a href="#cb7-84" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">74</span>)</span>
<span id="cb7-85"><a href="#cb7-85" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; NICHTLINEARE ENTROPIE-OPTIMIERTE ASSET-ALLOKATION&quot;</span>)</span>
<span id="cb7-86"><a href="#cb7-86" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">74</span>)</span>
<span id="cb7-87"><a href="#cb7-87" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Pruefung der Kovarianzmatrix:&quot;</span>)</span>
<span id="cb7-88"><a href="#cb7-88" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Eigenwerte: </span><span class="sc">{</span>np<span class="sc">.</span><span class="bu">round</span>(eig, <span class="dv">6</span>)<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb7-89"><a href="#cb7-89" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Positiv definit: </span><span class="sc">{</span><span class="bu">bool</span>(eig.<span class="bu">min</span>() <span class="op">&gt;</span> <span class="dv">0</span>)<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb7-90"><a href="#cb7-90" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Groesste Korrelation ausserhalb der Diagonale: &quot;</span></span>
<span id="cb7-91"><a href="#cb7-91" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>np<span class="sc">.</span><span class="bu">abs</span>(KORRELATION <span class="op">-</span> np.eye(n))<span class="sc">.</span><span class="bu">max</span>()<span class="sc">:.2f}</span><span class="ss"> (muss &lt;= 1 sein)&quot;</span>)</span>
<span id="cb7-92"><a href="#cb7-92" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-93"><a href="#cb7-93" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Multistart: SLSQP findet nur lokale Optima -----------------------</span></span>
<span id="cb7-94"><a href="#cb7-94" aria-hidden="true" tabindex="-1"></a> rng <span class="op">=</span> np.random.default_rng(<span class="dv">7</span>)</span>
<span id="cb7-95"><a href="#cb7-95" aria-hidden="true" tabindex="-1"></a> startpunkte <span class="op">=</span> [np.ones(n) <span class="op">/</span> n] <span class="co"># Gleichgewichtung</span></span>
<span id="cb7-96"><a href="#cb7-96" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> _ <span class="kw">in</span> <span class="bu">range</span>(<span class="dv">9</span>):</span>
<span id="cb7-97"><a href="#cb7-97" aria-hidden="true" tabindex="-1"></a> z <span class="op">=</span> rng.random(n) <span class="op">+</span> <span class="fl">0.05</span></span>
<span id="cb7-98"><a href="#cb7-98" aria-hidden="true" tabindex="-1"></a> startpunkte.append(z <span class="op">/</span> z.<span class="bu">sum</span>())</span>
<span id="cb7-99"><a href="#cb7-99" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-100"><a href="#cb7-100" aria-hidden="true" tabindex="-1"></a> ergebnisse <span class="op">=</span> [optimiere(s) <span class="cf">for</span> s <span class="kw">in</span> startpunkte]</span>
<span id="cb7-101"><a href="#cb7-101" aria-hidden="true" tabindex="-1"></a> erfolgreich <span class="op">=</span> [r <span class="cf">for</span> r <span class="kw">in</span> ergebnisse <span class="cf">if</span> r.success]</span>
<span id="cb7-102"><a href="#cb7-102" aria-hidden="true" tabindex="-1"></a> bestes <span class="op">=</span> <span class="bu">min</span>(erfolgreich, key<span class="op">=</span><span class="kw">lambda</span> r: r.fun)</span>
<span id="cb7-103"><a href="#cb7-103" aria-hidden="true" tabindex="-1"></a> zielwerte <span class="op">=</span> np.array([r.fun <span class="cf">for</span> r <span class="kw">in</span> erfolgreich])</span>
<span id="cb7-104"><a href="#cb7-104" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-105"><a href="#cb7-105" 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">Multistart mit </span><span class="sc">{</span><span class="bu">len</span>(startpunkte)<span class="sc">}</span><span class="ss"> Startpunkten:&quot;</span>)</span>
<span id="cb7-106"><a href="#cb7-106" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Erfolgreich konvergiert: </span><span class="sc">{</span><span class="bu">len</span>(erfolgreich)<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb7-107"><a href="#cb7-107" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Spannweite der Zielwerte: </span><span class="sc">{</span>zielwerte<span class="sc">.</span><span class="bu">max</span>() <span class="op">-</span> zielwerte<span class="sc">.</span><span class="bu">min</span>()<span class="sc">:.2e}</span><span class="ss">&quot;</span>)</span>
<span id="cb7-108"><a href="#cb7-108" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; -&gt; &quot;</span> <span class="op">+</span> (<span class="st">&quot;alle Startpunkte fuehren zum selben Optimum (Indiz fuer Konvexitaet)&quot;</span></span>
<span id="cb7-109"><a href="#cb7-109" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> zielwerte.<span class="bu">max</span>() <span class="op">-</span> zielwerte.<span class="bu">min</span>() <span class="op">&lt;</span> <span class="fl">1e-6</span></span>
<span id="cb7-110"><a href="#cb7-110" aria-hidden="true" tabindex="-1"></a> <span class="cf">else</span> <span class="st">&quot;ACHTUNG: verschiedene lokale Optima gefunden!&quot;</span>))</span>
<span id="cb7-111"><a href="#cb7-111" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-112"><a href="#cb7-112" aria-hidden="true" tabindex="-1"></a> w_opt <span class="op">=</span> bestes.x</span>
<span id="cb7-113"><a href="#cb7-113" aria-hidden="true" tabindex="-1"></a> rendite <span class="op">=</span> MU <span class="op">@</span> w_opt</span>
<span id="cb7-114"><a href="#cb7-114" aria-hidden="true" tabindex="-1"></a> volatilitaet <span class="op">=</span> np.sqrt(w_opt <span class="op">@</span> SIGMA <span class="op">@</span> w_opt)</span>
<span id="cb7-115"><a href="#cb7-115" aria-hidden="true" tabindex="-1"></a> entropie <span class="op">=</span> <span class="op">-</span>np.<span class="bu">sum</span>(w_opt <span class="op">*</span> np.log(w_opt))</span>
<span id="cb7-116"><a href="#cb7-116" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-117"><a href="#cb7-117" 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">Konvergenz: </span><span class="sc">{</span>bestes<span class="sc">.</span>message<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb7-118"><a href="#cb7-118" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Iterationen: </span><span class="sc">{</span>bestes<span class="sc">.</span>nit<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb7-119"><a href="#cb7-119" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Erwartete Jahresrendite: </span><span class="sc">{</span>rendite <span class="op">*</span> <span class="dv">100</span><span class="sc">:6.2f}</span><span class="ss"> %&quot;</span>)</span>
<span id="cb7-120"><a href="#cb7-120" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Erwartete Volatilitaet: </span><span class="sc">{</span>volatilitaet <span class="op">*</span> <span class="dv">100</span><span class="sc">:6.2f}</span><span class="ss"> %&quot;</span>)</span>
<span id="cb7-121"><a href="#cb7-121" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Diversifikations-Entropie: </span><span class="sc">{</span>entropie<span class="sc">:.4f}</span><span class="ss"> &quot;</span></span>
<span id="cb7-122"><a href="#cb7-122" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;(Maximum bei Gleichgewichtung: </span><span class="sc">{</span>np<span class="sc">.</span>log(n)<span class="sc">:.4f}</span><span class="ss">)&quot;</span>)</span>
<span id="cb7-123"><a href="#cb7-123" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-124"><a href="#cb7-124" 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> pd.DataFrame({</span>
<span id="cb7-125"><a href="#cb7-125" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Asset&quot;</span>: ASSETS,</span>
<span id="cb7-126"><a href="#cb7-126" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Erw. Rendite&quot;</span>: [<span class="ss">f&quot;</span><span class="sc">{</span>r<span class="op">*</span><span class="dv">100</span><span class="sc">:.1f}</span><span class="ss"> %&quot;</span> <span class="cf">for</span> r <span class="kw">in</span> MU],</span>
<span id="cb7-127"><a href="#cb7-127" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Volatilitaet&quot;</span>: [<span class="ss">f&quot;</span><span class="sc">{</span>v<span class="op">*</span><span class="dv">100</span><span class="sc">:.1f}</span><span class="ss"> %&quot;</span> <span class="cf">for</span> v <span class="kw">in</span> VOLA],</span>
<span id="cb7-128"><a href="#cb7-128" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Gewicht&quot;</span>: [<span class="ss">f&quot;</span><span class="sc">{</span>w<span class="op">*</span><span class="dv">100</span><span class="sc">:6.2f}</span><span class="ss"> %&quot;</span> <span class="cf">for</span> w <span class="kw">in</span> w_opt],</span>
<span id="cb7-129"><a href="#cb7-129" aria-hidden="true" tabindex="-1"></a> }).to_string(index<span class="op">=</span><span class="va">False</span>))</span>
<span id="cb7-130"><a href="#cb7-130" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-131"><a href="#cb7-131" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Vergleich: was passiert ohne Entropieterm? ----------------------</span></span>
<span id="cb7-132"><a href="#cb7-132" aria-hidden="true" tabindex="-1"></a> ohne <span class="op">=</span> optimiere(np.ones(n) <span class="op">/</span> n, beta<span class="op">=</span><span class="fl">0.0</span>)</span>
<span id="cb7-133"><a href="#cb7-133" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">--- Wirkung des Entropieterms ---&quot;</span>)</span>
<span id="cb7-134"><a href="#cb7-134" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Mit Entropie (beta=</span><span class="sc">{</span>BETA<span class="sc">}</span><span class="ss">): Gewichte </span><span class="sc">{</span>np<span class="sc">.</span><span class="bu">round</span>(w_opt <span class="op">*</span> <span class="dv">100</span>, <span class="dv">1</span>)<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb7-135"><a href="#cb7-135" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Ohne Entropie (beta=0): Gewichte </span><span class="sc">{</span>np<span class="sc">.</span><span class="bu">round</span>(ohne.x <span class="op">*</span> <span class="dv">100</span>, <span class="dv">1</span>)<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb7-136"><a href="#cb7-136" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; Der Entropieterm zieht Kapital aus der Spitzenposition heraus,&quot;</span>)</span>
<span id="cb7-137"><a href="#cb7-137" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; ohne dass eine harte Obergrenze noetig waere.&quot;</span>)</span>
<span id="cb7-138"><a href="#cb7-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">74</span>)</span></code></pre></div>
<p><strong>Erwartete Ausgabe:</strong></p>
<pre><code>==========================================================================
NICHTLINEARE ENTROPIE-OPTIMIERTE ASSET-ALLOKATION
==========================================================================
Pruefung der Kovarianzmatrix:
Eigenwerte: [0.004733 0.017389 0.026806 0.175572]
Positiv definit: True
Groesste Korrelation ausserhalb der Diagonale: 0.55 (muss &lt;= 1 sein)
Multistart mit 10 Startpunkten:
Erfolgreich konvergiert: 10
Spannweite der Zielwerte: 4.25e-13
-&gt; alle Startpunkte fuehren zum selben Optimum (Indiz fuer Konvexitaet)
Konvergenz: Optimization terminated successfully
Iterationen: 13
Erwartete Jahresrendite: 18.62 %
Erwartete Volatilitaet: 29.01 %
Diversifikations-Entropie: 0.7935 (Maximum bei Gleichgewichtung: 1.3863)
Asset Erw. Rendite Volatilitaet Gewicht
Tech-Aktien 14.0 % 20.0 % 35.96 %
Rohstoffe 7.0 % 14.0 % 2.75 %
US-Treasuries 3.0 % 7.0 % 0.45 %
Krypto 22.0 % 40.0 % 60.84 %
--- Wirkung des Entropieterms ---
Mit Entropie (beta=0.015): Gewichte [36. 2.8 0.5 60.8]
Ohne Entropie (beta=0): Gewichte [31.9 0.1 0.1 67.9]
Der Entropieterm zieht Kapital aus der Spitzenposition heraus,
ohne dass eine harte Obergrenze noetig waere.
==========================================================================</code></pre>
<p><strong>Vergleichen Sie diese Ausgabe mit der eingangs beschriebenen fehlerhaften Konstruktion.</strong> Die Eigenwerte sind jetzt <strong>alle positiv</strong> (kleinster: 0,0047 statt 0,084), die größte Korrelation liegt bei 0,55 statt bei unmöglichen 6,8 — und der Multistart bestätigt mit einer Spannweite von <span class="math inline">4{,}3\times10^{-13}</span>, dass alle zehn Startpunkte im selben Optimum landen. Das ist der empirische Beleg für Konvexität — mit einer indefiniten Matrix wäre das nicht möglich gewesen.</p>
<p>Interessant ist auch die Wirkung des Entropieterms: Er verschiebt nur rund 7 Prozentpunkte von Krypto weg — aber er hebt die Kleinstpositionen von 0,1 % auf 2,8 % bzw. 0,5 %. Eine harte Obergrenze hätte stattdessen genau bei der Schranke abgeschnitten und alles andere unverändert gelassen. <strong>Der weiche Term verteilt, die harte Schranke kappt.</strong></p>
<blockquote>
<p><strong>💻 Code-Durchgang: die drei Lehren</strong></p>
<ol type="1">
<li><strong><code>baue_kovarianz()</code> statt Diagonal-Überschreiben.</strong> Der Weg <span class="math inline">\boldsymbol{\Sigma} = \mathbf{D}\mathbf{C}\mathbf{D}</span> (Volatilitäten mal Korrelationsmatrix) ist der einzige, der die Zulässigkeit garantiert — und er ist zugleich <strong>interpretierbar</strong>: Man gibt Volatilitäten und Korrelationen vor, also genau die Größen, über die man fachlich diskutiert.</li>
<li><strong>Multistart.</strong> Zehn Startpunkte, und die Spannweite der Ergebnisse wird gemessen. Sind alle gleich, ist das ein starkes Indiz für Konvexität. Weichen sie ab, wissen Sie sofort, dass Sie einem lokalen Optimum aufsitzen. Diese fünf Zeilen sollten in jedem SLSQP-Projekt stehen.</li>
<li><strong>Analytischer Gradient.</strong> <code>jac=gradient</code> spart nicht nur Zeit — die numerische Approximation von Ableitungen ist bei Termen wie <span class="math inline">\ln w</span> nahe null numerisch heikel.</li>
</ol>
Achter Fund (Uebung-Verweise) und Phase 8.3: Dockerfile Zwei Dinge in einem Commit, weil beide den Plan abschliessen. ACHTER FUND: sieben Saetze der Bauart "Uebung 8.5 laesst Sie diese Balance untersuchen" - und sechs davon standen in alter Zaehlung. "8" war in Version 03 das QP/NLP-Kapitel, heute ist es Kapitel 11. Dieselbe Familie wie die Denkfehler-Verweise aus 6.1a, nur mit einem Wort, das keine der bestehenden Pruefungen kannte. Aufgaben haben kein eigenes Label, ein {ref:} auf eine einzelne Aufgabe ist also nicht moeglich. Verwiesen wird stattdessen auf Abschnitt plus Aufgabentitel - und der ist stabil. Jedes Ziel wurde einzeln ueber die alte Zaehlung bestimmt und am Zusammenhang geprueft. Ein Fall war knifflig: "Uebung 6.7 (Wochendienstplan)" meinte die siebte CP-SAT-Aufgabe der alten Zaehlung, also "Eigener Dienstplan" - die heute an achter Stelle steht, weil in Phase 6.3 eine Aufgabe davor eingefuegt wurde. Wer nur die Kapitelnummer angepasst haette, waere bei der falschen Aufgabe gelandet. --check kennt jetzt auch "Uebung"/"Übung". Gegengetestet. PHASE 8.3: Dockerfile, zweistufig. Die erste Stufe uebersetzt die Abhaengigkeiten in eine virtuelle Umgebung und braucht dafuer einen Compiler, die zweite kopiert nur /opt/venv. Installiert werden die Gruppen finance, large-scale, api und dev aus pyproject.toml; figures fehlt bewusst, weil es zusaetzlich Graphviz verlangt. Es wurde nicht behauptet, sondern gebaut. Ergebnis: 1,31 GB, und darin der Installationstest mit allen drei Solver-Funktionstests bestanden, die 33 pytest-Tests bestanden und alle 69 netzfreien Programme fehlerfrei - einschliesslich der drei aus 8.2, deren spawn-Isolation im Container ebenso traegt wie ausserhalb. Zwei Dinge, die der Bau gelehrt hat: libgomp1 fehlt im python:3.12-slim-Image und wird von OR-Tools und HiGHS zur Laufzeit gebraucht (sonst "libgomp.so.1: cannot open shared object file"). Und ein eigener Fehler: USER kurs stand vor dem mkdir /buch/output, /buch gehoert root, der Bau brach in der letzten Zeile ab. Beides steht jetzt als Kommentar im Dockerfile. Das Image fuehrt die Programme aus und baut das Buch nicht. Ein .dockerignore haelt Website, PDF und Notebooks aus dem Build-Kontext. Und es enthaelt ortools UND highspy, obwohl sie sich nicht gemeinsam importieren lassen - der Konflikt wird zur Laufzeit durch getrennte Prozesse geloest, nicht durch Weglassen. Damit ist Phase 8 abgeschlossen und der Plan abgearbeitet. Stand: 825 Querverweise, 76 Programme, 33 pytest-Tests, PDF 760 Seiten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 13:18:05 +02:00
<p><strong>Zur Skalierung:</strong> Mit <span class="math inline">\alpha = 1</span> ist der Ertragsterm (<span class="math inline">\approx 0{,}1</span>) zehnmal größer als der Varianzterm (<span class="math inline">\approx 0{,}01</span>). Das Modell ist also stark renditegetrieben. Die Aufgabe <em>Effekt der Gewichtung untersuchen</em> (<a href="#sec:qp-nlp-uebungsaufgaben">Abschnitt 11.7</a>) lässt Sie diese Balance untersuchen — sie ist eine fachliche Entscheidung, keine technische.</p>
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>
2026-09-08 01:20:09 +02:00
</blockquote>
<hr />
<h2 id="sec:qp-nlp-nichtkonvex">11.6 Jenseits der Konvexität: lokale Optima, Multistart und MINLP</h2>
<p>Alles bisher in diesem Kapitel stand unter einer Bedingung: <strong>Konvexität</strong>. Sie ist der Grund, warum CVXPY eine Optimalitätsgarantie geben kann und warum die KKT-Bedingungen nicht nur notwendig, sondern auch hinreichend sind.</p>
<p>Dieser Abschnitt handelt davon, was passiert, wenn diese Bedingung fehlt — und das ist in der Praxis oft genug der Fall.</p>
<h3 id="woher-nicht-konvexität-im-alltag-kommt">Woher Nicht-Konvexität im Alltag kommt</h3>
<p>Nicht aus exotischer Mathematik, sondern aus ganz gewöhnlichen betriebswirtschaftlichen Effekten:</p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="header">
<th>Ursache</th>
<th>Beispiel</th>
<th>Warum nicht konvex</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><strong>Mengenrabatte</strong></td>
<td>Stückpreis fällt mit der Bestellmenge</td>
<td>Kostenfunktion wird konkav — Sprünge nach unten belohnen große Lose</td>
</tr>
<tr class="even">
<td><strong>Skaleneffekte</strong></td>
<td>Stückkosten sinken mit der Losgröße</td>
<td>dasselbe Muster in der Produktion</td>
</tr>
<tr class="odd">
<td><strong>Produkte von Variablen</strong></td>
<td>„Menge mal Preis“, wenn beide entschieden werden</td>
<td><span class="math inline">x \cdot y</span> ist weder konvex noch konkav</td>
</tr>
<tr class="even">
<td><strong>Ja/Nein mal Menge</strong></td>
<td>Anlage läuft <em>und</em> wie stark</td>
<td>ganzzahlig + nichtlinear = MINLP</td>
</tr>
<tr class="odd">
<td><strong>Verhältnisse</strong></td>
<td>Auslastungsgrad, Rendite je eingesetztem Euro</td>
<td>Quotienten sind selten konvex</td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong>🎯 Merksatz</strong> Sobald ein <strong>Rabatt</strong>, ein <strong>Skaleneffekt</strong> oder ein <strong>Produkt zweier Entscheidungen</strong> im Modell steht, ist die Konvexität in Gefahr — und damit die Optimalitätsgarantie. Das ist kein Grund zur Panik, aber ein Grund, das Ergebnis anders zu behandeln.</p>
</blockquote>
<h3 id="was-ein-lokales-optimum-praktisch-bedeutet">Was ein lokales Optimum praktisch bedeutet</h3>
<p><code>scipy.optimize.minimize</code> verweigert nichts. Es rechnet, meldet <code>success: True</code> und liefert eine Zahl. Diese Meldung heißt aber <strong>nicht</strong> „das ist das Optimum“, sondern nur:</p>
<blockquote>
<p><em>„Ich bin an einer Stelle angekommen, an der es in keine Richtung mehr bergab geht.“</em></p>
</blockquote>
<p>Bei einem konvexen Problem ist das dasselbe. Bei einem nicht-konvexen sind es zwei völlig verschiedene Aussagen. Das folgende Programm macht den Unterschied sichtbar.</p>
<div class="sourceCode" id="cb9"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-3"><a href="#cb9-3" aria-hidden="true" tabindex="-1"></a><span class="co"># Lokale_Optima_Multistart.py</span></span>
<span id="cb9-4"><a href="#cb9-4" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb9-5"><a href="#cb9-5" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel QP/NLP: Was passiert, wenn die Konvexitaet fehlt.</span></span>
<span id="cb9-6"><a href="#cb9-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-7"><a href="#cb9-7" aria-hidden="true" tabindex="-1"></a><span class="co">CVXPY verweigert nicht-konvexe Probleme - das ist sein Schutzmechanismus.</span></span>
<span id="cb9-8"><a href="#cb9-8" aria-hidden="true" tabindex="-1"></a><span class="co">scipy.optimize.minimize verweigert nichts. Es rechnet, meldet &#39;success: True&#39;</span></span>
<span id="cb9-9"><a href="#cb9-9" aria-hidden="true" tabindex="-1"></a><span class="co">und liefert ein Ergebnis. Nur ist das Ergebnis dann kein Optimum, sondern</span></span>
<span id="cb9-10"><a href="#cb9-10" aria-hidden="true" tabindex="-1"></a><span class="co">irgendein lokales Minimum, das vom Startpunkt abhaengt.</span></span>
<span id="cb9-11"><a href="#cb9-11" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-12"><a href="#cb9-12" aria-hidden="true" tabindex="-1"></a><span class="co">Beispiel aus dem Einkauf: 700 Tonnen Rohstoff werden auf vier Lieferanten</span></span>
<span id="cb9-13"><a href="#cb9-13" aria-hidden="true" tabindex="-1"></a><span class="co">verteilt. Jeder gewaehrt einen MENGENRABATT - der Stueckpreis faellt, je mehr</span></span>
<span id="cb9-14"><a href="#cb9-14" aria-hidden="true" tabindex="-1"></a><span class="co">man bei ihm bestellt:</span></span>
<span id="cb9-15"><a href="#cb9-15" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-16"><a href="#cb9-16" aria-hidden="true" tabindex="-1"></a><span class="co"> preis_i(q) = basis_i * (1 - rabatt_i * (1 - exp(-q / skala_i)))</span></span>
<span id="cb9-17"><a href="#cb9-17" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-18"><a href="#cb9-18" aria-hidden="true" tabindex="-1"></a><span class="co">Genau das macht die Zielfunktion nicht-konvex: Grosse Bestellungen lohnen</span></span>
<span id="cb9-19"><a href="#cb9-19" aria-hidden="true" tabindex="-1"></a><span class="co">sich ueberproportional, es gibt also mehrere sinnvolle &quot;Cluster&quot;-Loesungen -</span></span>
<span id="cb9-20"><a href="#cb9-20" aria-hidden="true" tabindex="-1"></a><span class="co">und dazwischen schlechtere Taeler.</span></span>
<span id="cb9-21"><a href="#cb9-21" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-22"><a href="#cb9-22" aria-hidden="true" tabindex="-1"></a><span class="co">Das Programm zeigt drei Dinge:</span></span>
<span id="cb9-23"><a href="#cb9-23" aria-hidden="true" tabindex="-1"></a><span class="co"> 1. Ein einzelner Lauf liefert ein plausibles Ergebnis - ohne jede Warnung.</span></span>
<span id="cb9-24"><a href="#cb9-24" aria-hidden="true" tabindex="-1"></a><span class="co"> 2. 200 Startpunkte foerdern mehrere verschiedene lokale Optima zutage.</span></span>
<span id="cb9-25"><a href="#cb9-25" aria-hidden="true" tabindex="-1"></a><span class="co"> 3. Der Unterschied zwischen bestem und schlechtestem betraegt hier 8 %.</span></span>
<span id="cb9-26"><a href="#cb9-26" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-27"><a href="#cb9-27" aria-hidden="true" tabindex="-1"></a><span class="co">Benoetigt: numpy, scipy</span></span>
<span id="cb9-28"><a href="#cb9-28" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb9-29"><a href="#cb9-29" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-30"><a href="#cb9-30" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> __future__ <span class="im">import</span> annotations</span>
<span id="cb9-31"><a href="#cb9-31" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-32"><a href="#cb9-32" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb9-33"><a href="#cb9-33" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> scipy.optimize <span class="im">import</span> minimize</span>
<span id="cb9-34"><a href="#cb9-34" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-35"><a href="#cb9-35" aria-hidden="true" tabindex="-1"></a><span class="co"># Vier Lieferanten</span></span>
<span id="cb9-36"><a href="#cb9-36" aria-hidden="true" tabindex="-1"></a>NAMEN <span class="op">=</span> [<span class="st">&quot;Nord AG&quot;</span>, <span class="st">&quot;Ost GmbH&quot;</span>, <span class="st">&quot;Sued KG&quot;</span>, <span class="st">&quot;West SE&quot;</span>]</span>
<span id="cb9-37"><a href="#cb9-37" aria-hidden="true" tabindex="-1"></a>BASISPREIS <span class="op">=</span> np.array([<span class="fl">50.0</span>, <span class="fl">47.0</span>, <span class="fl">53.0</span>, <span class="fl">45.0</span>]) <span class="co"># EUR je Tonne ohne Rabatt</span></span>
<span id="cb9-38"><a href="#cb9-38" aria-hidden="true" tabindex="-1"></a>MAX_RABATT <span class="op">=</span> np.array([<span class="fl">0.30</span>, <span class="fl">0.18</span>, <span class="fl">0.35</span>, <span class="fl">0.12</span>]) <span class="co"># hoechstmoeglicher Rabatt</span></span>
<span id="cb9-39"><a href="#cb9-39" aria-hidden="true" tabindex="-1"></a>RABATT_SKALA <span class="op">=</span> np.array([<span class="fl">120.0</span>, <span class="fl">260.0</span>, <span class="fl">90.0</span>, <span class="fl">300.0</span>]) <span class="co"># wie schnell er greift</span></span>
<span id="cb9-40"><a href="#cb9-40" aria-hidden="true" tabindex="-1"></a>KAPAZITAET <span class="op">=</span> np.array([<span class="fl">400.0</span>, <span class="fl">400.0</span>, <span class="fl">400.0</span>, <span class="fl">400.0</span>])</span>
<span id="cb9-41"><a href="#cb9-41" aria-hidden="true" tabindex="-1"></a>BEDARF <span class="op">=</span> <span class="fl">700.0</span></span>
<span id="cb9-42"><a href="#cb9-42" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-43"><a href="#cb9-43" aria-hidden="true" tabindex="-1"></a>RNG <span class="op">=</span> np.random.default_rng(<span class="dv">0</span>)</span>
<span id="cb9-44"><a href="#cb9-44" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-45"><a href="#cb9-45" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-46"><a href="#cb9-46" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> gesamtkosten(menge: np.ndarray) <span class="op">-&gt;</span> <span class="bu">float</span>:</span>
<span id="cb9-47"><a href="#cb9-47" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Einkaufskosten bei mengenabhaengigem Stueckpreis.</span></span>
<span id="cb9-48"><a href="#cb9-48" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-49"><a href="#cb9-49" aria-hidden="true" tabindex="-1"></a><span class="co"> Der Rabatt waechst mit der Bestellmenge und laeuft gegen MAX_RABATT.</span></span>
<span id="cb9-50"><a href="#cb9-50" aria-hidden="true" tabindex="-1"></a><span class="co"> Dadurch ist der Stueckpreis fallend - und die Gesamtkostenfunktion</span></span>
<span id="cb9-51"><a href="#cb9-51" aria-hidden="true" tabindex="-1"></a><span class="co"> nicht mehr konvex.</span></span>
<span id="cb9-52"><a href="#cb9-52" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb9-53"><a href="#cb9-53" aria-hidden="true" tabindex="-1"></a> stueckpreis <span class="op">=</span> BASISPREIS <span class="op">*</span> (<span class="dv">1</span> <span class="op">-</span> MAX_RABATT <span class="op">*</span> (<span class="dv">1</span> <span class="op">-</span> np.exp(<span class="op">-</span>menge <span class="op">/</span> RABATT_SKALA)))</span>
<span id="cb9-54"><a href="#cb9-54" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="bu">float</span>(stueckpreis <span class="op">@</span> menge)</span>
<span id="cb9-55"><a href="#cb9-55" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-56"><a href="#cb9-56" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-57"><a href="#cb9-57" aria-hidden="true" tabindex="-1"></a>NEBENBEDINGUNGEN <span class="op">=</span> [{<span class="st">&quot;type&quot;</span>: <span class="st">&quot;eq&quot;</span>, <span class="st">&quot;fun&quot;</span>: <span class="kw">lambda</span> q: q.<span class="bu">sum</span>() <span class="op">-</span> BEDARF}]</span>
<span id="cb9-58"><a href="#cb9-58" aria-hidden="true" tabindex="-1"></a>GRENZEN <span class="op">=</span> [(<span class="fl">0.0</span>, k) <span class="cf">for</span> k <span class="kw">in</span> KAPAZITAET]</span>
<span id="cb9-59"><a href="#cb9-59" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-60"><a href="#cb9-60" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-61"><a href="#cb9-61" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> optimiere_von(startpunkt: np.ndarray):</span>
<span id="cb9-62"><a href="#cb9-62" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Ein Lauf von einem gegebenen Startpunkt aus.&quot;&quot;&quot;</span></span>
<span id="cb9-63"><a href="#cb9-63" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> minimize(gesamtkosten, startpunkt, method<span class="op">=</span><span class="st">&quot;SLSQP&quot;</span>,</span>
<span id="cb9-64"><a href="#cb9-64" aria-hidden="true" tabindex="-1"></a> bounds<span class="op">=</span>GRENZEN, constraints<span class="op">=</span>NEBENBEDINGUNGEN)</span>
<span id="cb9-65"><a href="#cb9-65" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-66"><a href="#cb9-66" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-67"><a href="#cb9-67" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> zufaelliger_start() <span class="op">-&gt;</span> np.ndarray:</span>
<span id="cb9-68"><a href="#cb9-68" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Zufaellige Aufteilung, die den Bedarf bereits erfuellt.&quot;&quot;&quot;</span></span>
<span id="cb9-69"><a href="#cb9-69" aria-hidden="true" tabindex="-1"></a> anteil <span class="op">=</span> RNG.random(<span class="bu">len</span>(NAMEN))</span>
<span id="cb9-70"><a href="#cb9-70" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> anteil <span class="op">/</span> anteil.<span class="bu">sum</span>() <span class="op">*</span> BEDARF</span>
<span id="cb9-71"><a href="#cb9-71" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-72"><a href="#cb9-72" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-73"><a href="#cb9-73" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> zeige_plan(titel: <span class="bu">str</span>, menge: np.ndarray, kosten: <span class="bu">float</span>) <span class="op">-&gt;</span> <span class="va">None</span>:</span>
<span id="cb9-74"><a href="#cb9-74" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="ch">\n</span><span class="sc">{</span>titel<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb9-75"><a href="#cb9-75" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> name, m <span class="kw">in</span> <span class="bu">zip</span>(NAMEN, menge):</span>
<span id="cb9-76"><a href="#cb9-76" aria-hidden="true" tabindex="-1"></a> anteil <span class="op">=</span> m <span class="op">/</span> BEDARF <span class="op">*</span> <span class="dv">100</span></span>
<span id="cb9-77"><a href="#cb9-77" 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;10}</span><span class="ss"> </span><span class="sc">{</span>m<span class="sc">:7.1f}</span><span class="ss"> t (</span><span class="sc">{</span>anteil<span class="sc">:4.1f}</span><span class="ss"> %)&quot;</span>)</span>
<span id="cb9-78"><a href="#cb9-78" 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;Gesamtkosten&#39;</span><span class="sc">:&lt;10}</span><span class="ss"> </span><span class="sc">{</span>kosten<span class="sc">:9,.2f}</span><span class="ss"> EUR&quot;</span>)</span>
<span id="cb9-79"><a href="#cb9-79" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-80"><a href="#cb9-80" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-81"><a href="#cb9-81" 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="cb9-82"><a href="#cb9-82" 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="cb9-83"><a href="#cb9-83" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; NICHT-KONVEX: DERSELBE CODE, VERSCHIEDENE ERGEBNISSE&quot;</span>)</span>
<span id="cb9-84"><a href="#cb9-84" 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="cb9-85"><a href="#cb9-85" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>BEDARF<span class="sc">:.0f}</span><span class="ss"> t Rohstoff auf </span><span class="sc">{</span><span class="bu">len</span>(NAMEN)<span class="sc">}</span><span class="ss"> Lieferanten mit Mengenrabatt.&quot;</span>)</span>
<span id="cb9-86"><a href="#cb9-86" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-87"><a href="#cb9-87" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- 1. Ein einziger Lauf, so wie man es zuerst schreibt --------------</span></span>
<span id="cb9-88"><a href="#cb9-88" aria-hidden="true" tabindex="-1"></a> erster <span class="op">=</span> optimiere_von(np.full(<span class="bu">len</span>(NAMEN), BEDARF <span class="op">/</span> <span class="bu">len</span>(NAMEN)))</span>
<span id="cb9-89"><a href="#cb9-89" 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">[1] EIN Lauf, Startpunkt &#39;gleichmaessig verteilt&#39;&quot;</span>)</span>
<span id="cb9-90"><a href="#cb9-90" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; scipy meldet: success=</span><span class="sc">{</span>erster<span class="sc">.</span>success<span class="sc">}</span><span class="ss">, &quot;</span></span>
<span id="cb9-91"><a href="#cb9-91" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;&#39;</span><span class="sc">{</span>erster<span class="sc">.</span>message<span class="sc">}</span><span class="ss">&#39;&quot;</span>)</span>
<span id="cb9-92"><a href="#cb9-92" aria-hidden="true" tabindex="-1"></a> zeige_plan(<span class="st">&quot; Ergebnis:&quot;</span>, erster.x, erster.fun)</span>
<span id="cb9-93"><a href="#cb9-93" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st"> Nichts an dieser Ausgabe deutet darauf hin, dass etwas fehlt.&quot;</span>)</span>
<span id="cb9-94"><a href="#cb9-94" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-95"><a href="#cb9-95" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- 1b. Der kaufmaennisch naheliegende Startpunkt --------------------</span></span>
<span id="cb9-96"><a href="#cb9-96" aria-hidden="true" tabindex="-1"></a> <span class="co"># &quot;Kaufe bei den beiden Lieferanten mit dem guenstigsten Basispreis&quot; -</span></span>
<span id="cb9-97"><a href="#cb9-97" aria-hidden="true" tabindex="-1"></a> <span class="co"># West SE (45) und Ost GmbH (47), jeweils bis zur Kapazitaetsgrenze.</span></span>
<span id="cb9-98"><a href="#cb9-98" aria-hidden="true" tabindex="-1"></a> guenstigste <span class="op">=</span> np.argsort(BASISPREIS)[:<span class="dv">2</span>]</span>
<span id="cb9-99"><a href="#cb9-99" aria-hidden="true" tabindex="-1"></a> kaufmaennisch <span class="op">=</span> np.zeros(<span class="bu">len</span>(NAMEN))</span>
<span id="cb9-100"><a href="#cb9-100" aria-hidden="true" tabindex="-1"></a> rest <span class="op">=</span> BEDARF</span>
<span id="cb9-101"><a href="#cb9-101" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> guenstigste:</span>
<span id="cb9-102"><a href="#cb9-102" aria-hidden="true" tabindex="-1"></a> kaufmaennisch[i] <span class="op">=</span> <span class="bu">min</span>(KAPAZITAET[i], rest)</span>
<span id="cb9-103"><a href="#cb9-103" aria-hidden="true" tabindex="-1"></a> rest <span class="op">-=</span> kaufmaennisch[i]</span>
<span id="cb9-104"><a href="#cb9-104" aria-hidden="true" tabindex="-1"></a> zweiter <span class="op">=</span> optimiere_von(kaufmaennisch)</span>
<span id="cb9-105"><a href="#cb9-105" 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">[1b] EIN Lauf, Startpunkt &#39;die zwei mit dem guenstigsten Basispreis&#39;&quot;</span>)</span>
<span id="cb9-106"><a href="#cb9-106" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; scipy meldet: success=</span><span class="sc">{</span>zweiter<span class="sc">.</span>success<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb9-107"><a href="#cb9-107" aria-hidden="true" tabindex="-1"></a> zeige_plan(<span class="st">&quot; Ergebnis:&quot;</span>, zweiter.x, zweiter.fun)</span>
<span id="cb9-108"><a href="#cb9-108" 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"> Dasselbe Programm, derselbe Aufruf, ein anderer Startpunkt -&quot;</span>)</span>
<span id="cb9-109"><a href="#cb9-109" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; und </span><span class="sc">{</span>zweiter<span class="sc">.</span>fun <span class="op">-</span> erster<span class="sc">.</span>fun<span class="sc">:,.2f}</span><span class="ss"> EUR Unterschied &quot;</span></span>
<span id="cb9-110"><a href="#cb9-110" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;(</span><span class="sc">{</span>(zweiter.fun <span class="op">/</span> erster.fun <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"> %).&quot;</span>)</span>
<span id="cb9-111"><a href="#cb9-111" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-112"><a href="#cb9-112" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- 2. Multistart: dasselbe Problem, viele Startpunkte ---------------</span></span>
<span id="cb9-113"><a href="#cb9-113" aria-hidden="true" tabindex="-1"></a> laeufe <span class="op">=</span> []</span>
<span id="cb9-114"><a href="#cb9-114" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> _ <span class="kw">in</span> <span class="bu">range</span>(<span class="dv">200</span>):</span>
<span id="cb9-115"><a href="#cb9-115" aria-hidden="true" tabindex="-1"></a> ergebnis <span class="op">=</span> optimiere_von(zufaelliger_start())</span>
<span id="cb9-116"><a href="#cb9-116" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> ergebnis.success:</span>
<span id="cb9-117"><a href="#cb9-117" aria-hidden="true" tabindex="-1"></a> laeufe.append((<span class="bu">float</span>(ergebnis.fun), ergebnis.x))</span>
<span id="cb9-118"><a href="#cb9-118" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-119"><a href="#cb9-119" aria-hidden="true" tabindex="-1"></a> <span class="co"># Ergebnisse, die sich um weniger als 1 Cent unterscheiden, sind dasselbe</span></span>
<span id="cb9-120"><a href="#cb9-120" aria-hidden="true" tabindex="-1"></a> <span class="co"># lokale Optimum - zusammenfassen, sonst zaehlt man Rundungsrauschen.</span></span>
<span id="cb9-121"><a href="#cb9-121" aria-hidden="true" tabindex="-1"></a> optima: <span class="bu">list</span>[<span class="bu">tuple</span>[<span class="bu">float</span>, np.ndarray]] <span class="op">=</span> []</span>
<span id="cb9-122"><a href="#cb9-122" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> wert, plan <span class="kw">in</span> <span class="bu">sorted</span>(laeufe, key<span class="op">=</span><span class="kw">lambda</span> t: t[<span class="dv">0</span>]):</span>
<span id="cb9-123"><a href="#cb9-123" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> optima <span class="kw">or</span> <span class="bu">abs</span>(wert <span class="op">-</span> optima[<span class="op">-</span><span class="dv">1</span>][<span class="dv">0</span>]) <span class="op">&gt;</span> <span class="fl">0.01</span>:</span>
<span id="cb9-124"><a href="#cb9-124" aria-hidden="true" tabindex="-1"></a> optima.append((wert, plan))</span>
<span id="cb9-125"><a href="#cb9-125" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-126"><a href="#cb9-126" 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="cb9-127"><a href="#cb9-127" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;[2] 200 zufaellige Startpunkte -&gt; </span><span class="sc">{</span><span class="bu">len</span>(laeufe)<span class="sc">}</span><span class="ss"> erfolgreiche Laeufe&quot;</span>)</span>
<span id="cb9-128"><a href="#cb9-128" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; darunter </span><span class="sc">{</span><span class="bu">len</span>(optima)<span class="sc">}</span><span class="ss"> VERSCHIEDENE lokale Optima:&quot;</span>)</span>
<span id="cb9-129"><a href="#cb9-129" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>()</span>
<span id="cb9-130"><a href="#cb9-130" 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;Rang&#39;</span><span class="sc">:&gt;5}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Kosten&#39;</span><span class="sc">:&gt;13}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Abstand zum besten&#39;</span><span class="sc">:&gt;20}</span><span class="ss"> Aufteilung (t)&quot;</span>)</span>
<span id="cb9-131"><a href="#cb9-131" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; &quot;</span> <span class="op">+</span> <span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">70</span>)</span>
<span id="cb9-132"><a href="#cb9-132" aria-hidden="true" tabindex="-1"></a> bester <span class="op">=</span> optima[<span class="dv">0</span>][<span class="dv">0</span>]</span>
<span id="cb9-133"><a href="#cb9-133" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> rang, (wert, plan) <span class="kw">in</span> <span class="bu">enumerate</span>(optima, start<span class="op">=</span><span class="dv">1</span>):</span>
<span id="cb9-134"><a href="#cb9-134" aria-hidden="true" tabindex="-1"></a> abstand <span class="op">=</span> (wert <span class="op">/</span> bester <span class="op">-</span> <span class="dv">1</span>) <span class="op">*</span> <span class="dv">100</span></span>
<span id="cb9-135"><a href="#cb9-135" aria-hidden="true" tabindex="-1"></a> aufteilung <span class="op">=</span> <span class="st">&quot; &quot;</span>.join(<span class="ss">f&quot;</span><span class="sc">{</span>m<span class="sc">:5.0f}</span><span class="ss">&quot;</span> <span class="cf">for</span> m <span class="kw">in</span> plan)</span>
<span id="cb9-136"><a href="#cb9-136" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; </span><span class="sc">{</span>rang<span class="sc">:&gt;5}</span><span class="ss"> </span><span class="sc">{</span>wert<span class="sc">:&gt;13,.2f}</span><span class="ss"> </span><span class="sc">{</span>abstand<span class="sc">:&gt;19.2f}</span><span class="ss"> % </span><span class="sc">{</span>aufteilung<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb9-137"><a href="#cb9-137" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-138"><a href="#cb9-138" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- 3. Was das kostet ------------------------------------------------</span></span>
<span id="cb9-139"><a href="#cb9-139" aria-hidden="true" tabindex="-1"></a> schlechtester <span class="op">=</span> optima[<span class="op">-</span><span class="dv">1</span>]</span>
<span id="cb9-140"><a href="#cb9-140" 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="cb9-141"><a href="#cb9-141" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; WAS AUF DEM SPIEL STEHT&quot;</span>)</span>
<span id="cb9-142"><a href="#cb9-142" 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="cb9-143"><a href="#cb9-143" aria-hidden="true" tabindex="-1"></a> zeige_plan(<span class="st">&quot;Bester gefundener Plan:&quot;</span>, optima[<span class="dv">0</span>][<span class="dv">1</span>], optima[<span class="dv">0</span>][<span class="dv">0</span>])</span>
<span id="cb9-144"><a href="#cb9-144" aria-hidden="true" tabindex="-1"></a> zeige_plan(<span class="st">&quot;Schlechtestes lokales Optimum:&quot;</span>, schlechtester[<span class="dv">1</span>], schlechtester[<span class="dv">0</span>])</span>
<span id="cb9-145"><a href="#cb9-145" aria-hidden="true" tabindex="-1"></a> unterschied <span class="op">=</span> schlechtester[<span class="dv">0</span>] <span class="op">-</span> optima[<span class="dv">0</span>][<span class="dv">0</span>]</span>
<span id="cb9-146"><a href="#cb9-146" 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"> Unterschied: </span><span class="sc">{</span>unterschied<span class="sc">:,.2f}</span><span class="ss"> EUR &quot;</span></span>
<span id="cb9-147"><a href="#cb9-147" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;(</span><span class="sc">{</span>unterschied <span class="op">/</span> optima[<span class="dv">0</span>][<span class="dv">0</span>] <span class="op">*</span> <span class="dv">100</span><span class="sc">:.1f}</span><span class="ss"> %)&quot;</span>)</span>
<span id="cb9-148"><a href="#cb9-148" 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"> Lauf [1] (gleichmaessiger Start): </span><span class="sc">{</span>erster<span class="sc">.</span>fun<span class="sc">:&gt;10,.2f}</span><span class="ss"> EUR&quot;</span>)</span>
<span id="cb9-149"><a href="#cb9-149" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Lauf [1b] (kaufmaennischer Start): </span><span class="sc">{</span>zweiter<span class="sc">.</span>fun<span class="sc">:&gt;10,.2f}</span><span class="ss"> EUR&quot;</span></span>
<span id="cb9-150"><a href="#cb9-150" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot; &lt;- </span><span class="sc">{</span>(zweiter.fun <span class="op">/</span> optima[<span class="dv">0</span>][<span class="dv">0</span>] <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"> % ueber dem besten&quot;</span>)</span>
<span id="cb9-151"><a href="#cb9-151" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>()</span>
<span id="cb9-152"><a href="#cb9-152" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; Bemerkenswert: Der kaufmaennisch NAHELIEGENDE Startpunkt fuehrt in&quot;</span>)</span>
<span id="cb9-153"><a href="#cb9-153" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; das schlechteste Ergebnis von allen - schlechter als jedes der 200&quot;</span>)</span>
<span id="cb9-154"><a href="#cb9-154" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; zufaellig gefundenen lokalen Optima. Wer beim guenstigsten&quot;</span>)</span>
<span id="cb9-155"><a href="#cb9-155" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; Basispreis anfaengt, uebersieht, dass hier der Mengenrabatt&quot;</span>)</span>
<span id="cb9-156"><a href="#cb9-156" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; entscheidet und nicht der Listenpreis.&quot;</span>)</span>
<span id="cb9-157"><a href="#cb9-157" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-158"><a href="#cb9-158" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>()</span>
<span id="cb9-159"><a href="#cb9-159" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Drei Konsequenzen fuer die Praxis:&quot;</span>)</span>
<span id="cb9-160"><a href="#cb9-160" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; 1. &#39;success: True&#39; heisst bei nicht-konvexen Problemen NICHT &#39;optimal&#39;.&quot;</span>)</span>
<span id="cb9-161"><a href="#cb9-161" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; Es heisst nur: &#39;Ich bin an einer Stelle angekommen, an der es in&quot;</span>)</span>
<span id="cb9-162"><a href="#cb9-162" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; keine Richtung mehr bergab geht.&#39;&quot;</span>)</span>
<span id="cb9-163"><a href="#cb9-163" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; 2. Ein einzelner Lauf ist wertlos. Nehmen Sie viele Startpunkte und&quot;</span>)</span>
<span id="cb9-164"><a href="#cb9-164" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; berichten Sie die STREUUNG mit - sie ist Ihre einzige Auskunft&quot;</span>)</span>
<span id="cb9-165"><a href="#cb9-165" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; darueber, wie zerklueftet die Landschaft ist.&quot;</span>)</span>
<span id="cb9-166"><a href="#cb9-166" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; 3. Auch Multistart liefert KEINE Garantie. Dass hier nichts unter&quot;</span>)</span>
<span id="cb9-167"><a href="#cb9-167" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; </span><span class="sc">{</span>bester<span class="sc">:,.2f}</span><span class="ss"> EUR gefunden wurde, beweist nicht, dass es nichts gibt.&quot;</span>)</span>
<span id="cb9-168"><a href="#cb9-168" 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>Erwartete Ausgabe:</strong></p>
<pre><code>==============================================================================
NICHT-KONVEX: DERSELBE CODE, VERSCHIEDENE ERGEBNISSE
==============================================================================
700 t Rohstoff auf 4 Lieferanten mit Mengenrabatt.
[1] EIN Lauf, Startpunkt &#39;gleichmaessig verteilt&#39;
scipy meldet: success=True, &#39;Optimization terminated successfully&#39;
Ergebnis:
Nord AG 343.5 t (49.1 %)
Ost GmbH 0.0 t ( 0.0 %)
Sued KG 356.5 t (50.9 %)
West SE 0.0 t ( 0.0 %)
Gesamtkosten 24,724.19 EUR
Nichts an dieser Ausgabe deutet darauf hin, dass etwas fehlt.
[1b] EIN Lauf, Startpunkt &#39;die zwei mit dem guenstigsten Basispreis&#39;
scipy meldet: success=True
Ergebnis:
Nord AG 0.0 t ( 0.0 %)
Ost GmbH 400.0 t (57.1 %)
Sued KG 0.0 t ( 0.0 %)
West SE 300.0 t (42.9 %)
Gesamtkosten 28,618.55 EUR
Dasselbe Programm, derselbe Aufruf, ein anderer Startpunkt -
und 3,894.36 EUR Unterschied (15.8 %).
==============================================================================
[2] 200 zufaellige Startpunkte -&gt; 200 erfolgreiche Laeufe
darunter 5 VERSCHIEDENE lokale Optima:
Rang Kosten Abstand zum besten Aufteilung (t)
----------------------------------------------------------------------
1 24,724.19 0.00 % 344 0 356 0
2 26,229.68 6.09 % 0 300 400 0
3 26,343.10 6.55 % 0 0 400 300
4 26,576.58 7.49 % 400 300 0 0
5 26,690.01 7.95 % 400 0 0 300
==============================================================================
WAS AUF DEM SPIEL STEHT
==============================================================================
Bester gefundener Plan:
Nord AG 343.5 t (49.1 %)
Ost GmbH 0.0 t ( 0.0 %)
Sued KG 356.5 t (50.9 %)
West SE 0.0 t ( 0.0 %)
Gesamtkosten 24,724.19 EUR
Schlechtestes lokales Optimum:
Nord AG 400.0 t (57.1 %)
Ost GmbH 0.0 t ( 0.0 %)
Sued KG 0.0 t ( 0.0 %)
West SE 300.0 t (42.9 %)
Gesamtkosten 26,690.01 EUR
Unterschied: 1,965.82 EUR (8.0 %)
Lauf [1] (gleichmaessiger Start): 24,724.19 EUR
Lauf [1b] (kaufmaennischer Start): 28,618.55 EUR &lt;- 15.8 % ueber dem besten
Bemerkenswert: Der kaufmaennisch NAHELIEGENDE Startpunkt fuehrt in
das schlechteste Ergebnis von allen - schlechter als jedes der 200
zufaellig gefundenen lokalen Optima. Wer beim guenstigsten
Basispreis anfaengt, uebersieht, dass hier der Mengenrabatt
entscheidet und nicht der Listenpreis.
Drei Konsequenzen fuer die Praxis:
1. &#39;success: True&#39; heisst bei nicht-konvexen Problemen NICHT &#39;optimal&#39;.
Es heisst nur: &#39;Ich bin an einer Stelle angekommen, an der es in
keine Richtung mehr bergab geht.&#39;
2. Ein einzelner Lauf ist wertlos. Nehmen Sie viele Startpunkte und
berichten Sie die STREUUNG mit - sie ist Ihre einzige Auskunft
darueber, wie zerklueftet die Landschaft ist.
3. Auch Multistart liefert KEINE Garantie. Dass hier nichts unter
24,724.19 EUR gefunden wurde, beweist nicht, dass es nichts gibt.
==============================================================================</code></pre>
<blockquote>
<p><strong>💻 Code-Durchgang</strong></p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="header">
<th>Stelle</th>
<th>Was passiert</th>
<th>Warum es zählt</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>1 - MAX_RABATT * (1 - exp(-menge / SKALA))</code></td>
<td>Stückpreis fällt mit der Menge</td>
<td>Ein völlig normaler Staffelrabatt — und genau er zerstört die Konvexität. Man braucht keine exotische Mathematik dafür.</td>
</tr>
<tr class="even">
<td>Zusammenfassen mit <code>&gt; 0.01</code></td>
<td>Rundungsrauschen von echten Optima trennen</td>
<td>Ohne diesen Schritt zählt man 200 „verschiedene“ Optima, die sich in der zehnten Nachkommastelle unterscheiden.</td>
</tr>
<tr class="odd">
<td>200 zufällige Startpunkte</td>
<td>Multistart</td>
<td>Die einzige praktikable Auskunft darüber, wie zerklüftet die Landschaft ist — und trotzdem <strong>kein Beweis</strong>.</td>
</tr>
<tr class="even">
<td>Lauf <code>[1b]</code></td>
<td>der kaufmännisch naheliegende Startpunkt</td>
<td>Die eigentliche Pointe: Er ist der <strong>schlechteste von allen</strong>.</td>
</tr>
</tbody>
</table>
</blockquote>
<h3 id="die-zahlen-im-klartext">Die Zahlen im Klartext</h3>
<ul>
<li>Der gleichmäßige Startpunkt findet <strong>24 724 €</strong> — zufällig das beste gefundene Optimum.</li>
<li>Der kaufmännisch naheliegende Startpunkt („die zwei mit dem günstigsten Basispreis“) findet <strong>28 619 €</strong> — 15,8 % schlechter, und schlechter als <em>jedes</em> der 200 zufällig gefundenen lokalen Optima.</li>
<li>Beide Läufe melden <code>success: True</code>. Nichts an der Ausgabe unterscheidet sie.</li>
</ul>
<p>Der Grund für das schlechte Abschneiden des „vernünftigen“ Starts ist lehrreich: Wer beim günstigsten <strong>Listenpreis</strong> beginnt, folgt genau dem Kriterium, das hier nicht entscheidet. Ausschlaggebend ist der Rabatt — und Süd KG hat mit 35 % den höchsten, obwohl sein Basispreis der teuerste ist. Eine plausible Heuristik führt den Optimierer damit zielsicher ins falsche Tal.</p>
<blockquote>
<p><strong>⚠️ Typische Fehler bei nicht-konvexen Problemen</strong></p>
<ul>
<li><strong><code>success: True</code> als „optimal“ lesen.</strong> Es heißt nur „konvergiert“.</li>
<li><strong>Einen einzigen Lauf berichten.</strong> Ohne Streuung über mehrere Startpunkte ist die Zahl nicht einordbar.</li>
<li><strong>Den „vernünftigen“ Startpunkt für den besten halten.</strong> Er ist oft der schlechteste, weil er einer Heuristik folgt, die genau das ignoriert, was das Problem schwer macht.</li>
<li><strong>Multistart für einen Beweis halten.</strong> Dass nichts Besseres gefunden wurde, beweist nicht, dass es nichts Besseres gibt.</li>
</ul>
</blockquote>
<h3 id="ausblick-minlp-und-globale-solver">Ausblick: MINLP und globale Solver</h3>
<p>Wird zusätzlich noch ganzzahlig entschieden — „welche Anlage läuft überhaupt“ <em>und</em> „wie stark“ —, entsteht ein <strong>MINLP</strong> (<em>Mixed-Integer Nonlinear Program</em>), die schwierigste der in diesem Buch behandelten Klassen. Dafür gibt es eigene Werkzeuge:</p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="header">
<th>Werkzeug</th>
<th>Art</th>
<th>Was es leistet</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><strong>Ipopt</strong> (über <code>cyipopt</code> oder Pyomo)</td>
<td>lokaler NLP-Solver</td>
<td>Innere-Punkte-Verfahren für große, glatte NLPs mit tausenden Variablen — deutlich leistungsfähiger als <code>scipy</code>, aber ebenfalls <strong>nur lokal</strong></td>
</tr>
<tr class="even">
<td><strong>Bonmin</strong>, <strong>Couenne</strong></td>
<td>MINLP</td>
<td>Bonmin lokal, Couenne mit globaler Garantie für viele Klassen</td>
</tr>
<tr class="odd">
<td><strong>SCIP</strong>, <strong>BARON</strong>, <strong>Gurobi</strong></td>
<td>global</td>
<td>Beweisbar globales Optimum, mit entsprechendem Rechenaufwand</td>
</tr>
<tr class="even">
<td><strong>Pyomo</strong> (<a href="oekosystem.html#sec:oekosystem-pyomo-linopy">Abschnitt 3.7</a>)</td>
<td>Modellierungsschicht</td>
<td>Bindet alle oben genannten an, ohne dass das Modell umgeschrieben werden muss</td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong>⚠️ Zu diesem Abschnitt gehört kein lauffähiges Programm.</strong> Ipopt, Bonmin und Couenne sind C++-Pakete, die über die Python-Installation hinaus systemweit eingerichtet werden müssen (<code>conda install -c conda-forge ipopt cyipopt</code> oder eine Distributionspaketquelle). Anders als bei allen übrigen Programmen dieses Buches konnten wir den Code deshalb nicht auf jedem Zielsystem ausführen — und drucken hier bewusst <strong>keine</strong> Ausgabe ab, die wir nicht selbst erzeugt haben. Der folgende Ausschnitt zeigt die Anbindung; prüfen Sie das Ergebnis auf Ihrem System selbst nach.</p>
</blockquote>
<div class="sourceCode" id="cb11"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb11-1"><a href="#cb11-1" aria-hidden="true" tabindex="-1"></a><span class="co"># Ipopt ueber Pyomo - Anbindungsmuster, NICHT ausgefuehrt.</span></span>
<span id="cb11-2"><a href="#cb11-2" aria-hidden="true" tabindex="-1"></a><span class="co"># Voraussetzung: conda install -c conda-forge ipopt</span></span>
<span id="cb11-3"><a href="#cb11-3" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> pyomo.environ <span class="im">as</span> pyo</span>
<span id="cb11-4"><a href="#cb11-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb11-5"><a href="#cb11-5" aria-hidden="true" tabindex="-1"></a>modell <span class="op">=</span> pyo.ConcreteModel()</span>
<span id="cb11-6"><a href="#cb11-6" aria-hidden="true" tabindex="-1"></a>modell.q <span class="op">=</span> pyo.Var(<span class="bu">range</span>(<span class="dv">4</span>), domain<span class="op">=</span>pyo.NonNegativeReals, bounds<span class="op">=</span>(<span class="dv">0</span>, <span class="dv">400</span>))</span>
<span id="cb11-7"><a href="#cb11-7" aria-hidden="true" tabindex="-1"></a>modell.bedarf <span class="op">=</span> pyo.Constraint(expr<span class="op">=</span><span class="bu">sum</span>(modell.q[i] <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(<span class="dv">4</span>)) <span class="op">==</span> <span class="dv">700</span>)</span>
<span id="cb11-8"><a href="#cb11-8" aria-hidden="true" tabindex="-1"></a>modell.ziel <span class="op">=</span> pyo.Objective(</span>
<span id="cb11-9"><a href="#cb11-9" aria-hidden="true" tabindex="-1"></a> expr<span class="op">=</span><span class="bu">sum</span>(BASISPREIS[i] <span class="op">*</span> (<span class="dv">1</span> <span class="op">-</span> MAX_RABATT[i]</span>
<span id="cb11-10"><a href="#cb11-10" aria-hidden="true" tabindex="-1"></a> <span class="op">*</span> (<span class="dv">1</span> <span class="op">-</span> pyo.exp(<span class="op">-</span>modell.q[i] <span class="op">/</span> RABATT_SKALA[i])))</span>
<span id="cb11-11"><a href="#cb11-11" aria-hidden="true" tabindex="-1"></a> <span class="op">*</span> modell.q[i] <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(<span class="dv">4</span>)),</span>
<span id="cb11-12"><a href="#cb11-12" aria-hidden="true" tabindex="-1"></a> sense<span class="op">=</span>pyo.minimize)</span>
<span id="cb11-13"><a href="#cb11-13" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb11-14"><a href="#cb11-14" aria-hidden="true" tabindex="-1"></a>loeser <span class="op">=</span> pyo.SolverFactory(<span class="st">&quot;ipopt&quot;</span>)</span>
<span id="cb11-15"><a href="#cb11-15" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> loeser.available(exception_flag<span class="op">=</span><span class="va">False</span>):</span>
<span id="cb11-16"><a href="#cb11-16" aria-hidden="true" tabindex="-1"></a> ergebnis <span class="op">=</span> loeser.solve(modell)</span>
<span id="cb11-17"><a href="#cb11-17" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(ergebnis.solver.termination_condition)</span>
<span id="cb11-18"><a href="#cb11-18" aria-hidden="true" tabindex="-1"></a><span class="cf">else</span>:</span>
<span id="cb11-19"><a href="#cb11-19" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Ipopt ist nicht installiert - siehe Hinweis oben.&quot;</span>)</span></code></pre></div>
<p><strong>Die Strategie bleibt in jedem Fall dieselbe:</strong> Ein lokaler Solver wie Ipopt löst <em>schneller</em> und <em>größer</em> als <code>scipy</code> — er löst aber nicht <em>globaler</em>. Multistart bleibt nötig. Erst ein globaler Solver (SCIP, Couenne, BARON) ersetzt ihn, und den bezahlt man mit Rechenzeit, die um Größenordnungen höher liegt.</p>
<blockquote>
<p><strong>🎯 Merksatz</strong> Bei nicht-konvexen Problemen ist die ehrliche Berichterstattung wichtiger als die letzte Nachkommastelle: <em>„Bester gefundener Wert 24 724 € aus 200 Startpunkten; die Streuung reicht bis 26 690 €.“</em> Das ist eine belastbare Aussage. <em>„Das Optimum liegt bei 24 724,19 €“</em> ist es nicht.</p>
</blockquote>
<hr />
<h2 id="sec:qp-nlp-uebungsaufgaben">11.7 Übungsaufgaben</h2>
<blockquote>
<p>Lösungen: <a href="anhang-loesungen.html#sec:loesungen-qp-nlp">Abschnitt A.11</a>.</p>
</blockquote>
<p><strong>Aufgabe 11.1 ⭐ — Konvexität einordnen.</strong> <span class="math inline">\mathbf{P}</span> hat die Eigenwerte <span class="math inline">(2{,}5;\ 0{,}0;\ 1{,}3)</span>. Ist das QP konvex? Streng konvex? Ist die Lösung eindeutig? Was bedeutet der Eigenwert 0 anschaulich?</p>
<p><strong>Aufgabe 11.2 ⭐ — Komplementärer Schlupf.</strong> Ein QP liefert <span class="math inline">w = (0{,}4;\ 0{,}0;\ 0{,}6)</span> mit Bedingung <span class="math inline">w \ge 0</span> und Multiplikatoren <span class="math inline">\lambda = (0{,}0;\ 0{,}03;\ 0{,}0)</span>. Ist das mit KKT verträglich? Was sagt <span class="math inline">\lambda_2 = 0{,}03</span> wirtschaftlich?</p>
<p><strong>Aufgabe 11.3 ⭐⭐ — KKT von Hand.</strong> Lösen Sie mit KKT vollständig von Hand: <span class="math display">\min\ x_1^2 + x_2^2 \qquad\text{u. d. N.}\qquad x_1 + x_2 \ge 4</span> Geben Sie <span class="math inline">x^*</span>, <span class="math inline">\lambda^*</span> und die Interpretation von <span class="math inline">\lambda^*</span> an. Prüfen Sie mit CVXPY.</p>
<p><strong>Aufgabe 11.4 ⭐⭐ — Ungültige Kovarianzmatrix erkennen.</strong> Prüfen Sie, ob folgende Korrelationsmatrix möglich ist, und begründen Sie: <span class="math display">\mathbf{C} = \begin{pmatrix}1{,}0&amp;0{,}9&amp;-0{,}9\\0{,}9&amp;1{,}0&amp;0{,}9\\-0{,}9&amp;0{,}9&amp;1{,}0\end{pmatrix}</span> (Tipp: Wenn A stark mit B korreliert und B stark mit C, kann A dann stark <strong>negativ</strong> mit C korrelieren?)</p>
<p><strong>Aufgabe 11.5 ⭐⭐⭐ — Effekt der Gewichtung untersuchen.</strong> Variieren Sie in <code>Entropie_Maximierte_Allokation.py</code> systematisch <span class="math inline">\alpha \in \{0{,}1;\ 0{,}5;\ 1;\ 5\}</span> und <span class="math inline">\beta \in \{0;\ 0{,}005;\ 0{,}015;\ 0{,}05\}</span>. Stellen Sie für jede Kombination Rendite, Volatilität und Entropie in einer Tabelle dar. (a) Wie verändert <span class="math inline">\beta</span> die Konzentration im Krypto-Titel? (b) Ab welchem <span class="math inline">\beta</span> nähert sich die Lösung der Gleichgewichtung? (c) Was ist Ihre Empfehlung — und mit welcher Begründung würden Sie sie einem Anlageausschuss vorlegen?</p>
<p><strong>Aufgabe 11.6 ⭐⭐⭐ — Nicht-Konvexität demonstrieren.</strong> Bauen Sie bewusst eine ungültige „Kovarianzmatrix“ (per <a href="mailto:A@A.T" class="email">A@A.T</a> mit anschließend überschriebener Diagonale) und lösen Sie das Modell mit 20 Startpunkten. Dokumentieren Sie: Wie viele verschiedene Optima entstehen? Wie groß ist der Unterschied zwischen bestem und schlechtestem Ergebnis? Kann die „Portfoliovarianz“ negativ werden?</p>
<hr />
<h2 id="sec:qp-nlp-denkfehler">11.8 Finde den Denkfehler</h2>
<div class="card card-denkfehler">
<blockquote>
Sechster und siebter Fund: die letzten handgeschriebenen Nummern Der Loesungsanhang trug 98 Marken der Bauart "**9.2 — Ansatz waehlen.**", saemtlich aus Version 03, wo Unsicherheit Kapitel 9 war. Heute ist es Kapitel 12, und die Aufgabe wird korrekt als "Aufgabe 12.2" gesetzt. Das war keine Schoenheitsfrage: "Aufgabe 9.2" existiert wirklich - sie gehoert zum Kapitel Metaheuristiken ("Zuggroesse und Temperatur"). Wer die Loesung zu 9.2 nachschlug, landete beim falschen Thema. Beide bisherigen Pruefungen liefen vorbei: Vor der Zahl steht kein Schluesselwort, und es ist keine Tabellenzelle. Zwei Messungen machten die Reparatur billig. In allen 15 nummerierten Abschnitten gab es exakt so viele Loesungen wie Aufgaben, lueckenlos 1..n - nur der Kapitelteil war falsch. Und die acht in Phase 3 ergaenzten Kapitel machten es laengst richtig (**Titel.** ohne Nummer); der Anhang wurde also nicht auf etwas Neues umgestellt, sondern auf das, was seine neueren Teile schon taten. nummeriere_marken() vergibt die Marken jetzt selbst. Der Praefix kommt NICHT aus "# Anhang A:" - der Anhang ist eine Ueberschrift, seine Abschnitte gehoeren aber zu 23 verschiedenen Kapiteln. Er kommt aus {#sec:loesungen-X} -> {#kap:X}; diese Zuordnung gilt geprueft fuer alle 23. Im Quelltext steht "**{loesung} — Titel.**". Siebter Fund unterwegs: neun Verweise auf Denkfehler-Nummern. Saetze wie "siehe Denkfehler 8.1" standen in alter Zaehlung - "8" war in Version 03 das QP/NLP-Kapitel, heute ist es Graphen. Eine blosse Umnummerierung haette sie stillschweigend woanders hin zeigen lassen; das Ziel wurde deshalb fuer jeden einzelnen aus dem Zusammenhang bestimmt und auf {ref:sec:<kapitel>-denkfehler} umgestellt. Einer steckte in einem Programm-Docstring und bekam nach Regel 12 den Kapitelnamen statt eines Verweises. Eine bewusste Abweichung vom Plan: Der Denkfehler bekommt GAR KEINE Nummer. Es gibt je Kapitel genau einen, die zweite Stelle waere immer .1, und er steht ohnehin unter einer nummerierten Ueberschrift. Eine Nummer, die nie variiert, holte nur die Fragilitaet zurueck, die hier beseitigt wird. Auch das folgt den acht neuen Kapiteln, die ihren Denkfehler schon vorher nur mit dem Titel des Raetsels ankuendigen. --check bewacht jetzt alle Familien (Denkfehler N.M, Micro-Quiz N, **N.M — im Anhang) und zaehlt zusaetzlich ab, dass jeder Loesungsabschnitt so viele {loesung}-Marken hat wie sein Kapitel Aufgaben - das faengt eine vergessene oder doppelte Loesung, was keine Textsuche leisten kann. Gegengetestet mit vier kuenstlichen Fehlern auf einmal: alle vier gemeldet, nach dem Rueckbau null. Gegenprobe der Umstellung: Gesamtdokument vorher gesichert, nachher verglichen. 168 geaenderte Zeilen, restlos einer Familie zuzuordnen - 118 Loesungsmarken (138 gesamt minus 20, die in den Kapiteln 1 bis 3 zufaellig schon stimmten), 15 Anhang-Ueberschriften, 15 Kapitel-Callouts, 12 Micro-Quiz, 7 Zeilen mit Verweisen, 1 Docstring. Keine verschobene oder inhaltlich veraenderte Zeile. Ergebnis: 138 Aufgaben, 138 Loesungen, keine ohne Gegenstueck. Micro-Quiz tragen die echten Kapitelnummern. 293 Abschnitte, 720 Querverweise, 327 Indexmarken, PDF unveraendert 725 Seiten, 33 pytest-Tests, 67 netzfreie Programme fehlerfrei (die vier yfinance-Programme scheiterten am Rate-Limit des Anbieters, nicht an dieser Aenderung). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 09:52:41 +02:00
<p><strong>🐛 Finde den Denkfehler: Die Kovarianzmatrix aus dem Controlling</strong></p>
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>
2026-09-08 01:20:09 +02:00
<p>Ein Analyst soll das Risiko eines Portfolios aus drei Anlagen minimieren. Die Korrelationen hat er aus drei verschiedenen Quartalsberichten zusammengetragen — jede für sich plausibel:</p>
<ul>
<li>A und B laufen stark gleich: <span class="math inline">\rho_{AB} = 0{,}9</span></li>
<li>B und C laufen stark gleich: <span class="math inline">\rho_{BC} = 0{,}9</span></li>
<li>A und C laufen <strong>gegen</strong>einander: <span class="math inline">\rho_{AC} = -0{,}9</span></li>
</ul>
<p>Alle drei schwanken mit 20 %. Leerverkäufe sind in seinem Mandat erlaubt, die Gewichte dürfen also negativ werden; sie müssen sich nur zu 100 % summieren.</p>
<p>Sein erster Versuch mit CVXPY bricht ab:</p>
<pre><code>DCPError: Problem does not follow DCP rules.
The objective is not DCP. Its following subexpressions are not: QuadForm(...)</code></pre>
<p>Er hält das für eine Einschränkung der Bibliothek und weicht auf <code>scipy</code> aus:</p>
<div class="sourceCode" id="cb13"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb13-1"><a href="#cb13-1" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb13-2"><a href="#cb13-2" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> scipy.optimize <span class="im">import</span> minimize</span>
<span id="cb13-3"><a href="#cb13-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb13-4"><a href="#cb13-4" aria-hidden="true" tabindex="-1"></a>R <span class="op">=</span> np.array([[ <span class="fl">1.0</span>, <span class="fl">0.9</span>, <span class="op">-</span><span class="fl">0.9</span>],</span>
<span id="cb13-5"><a href="#cb13-5" aria-hidden="true" tabindex="-1"></a> [ <span class="fl">0.9</span>, <span class="fl">1.0</span>, <span class="fl">0.9</span>],</span>
<span id="cb13-6"><a href="#cb13-6" aria-hidden="true" tabindex="-1"></a> [<span class="op">-</span><span class="fl">0.9</span>, <span class="fl">0.9</span>, <span class="fl">1.0</span>]])</span>
<span id="cb13-7"><a href="#cb13-7" aria-hidden="true" tabindex="-1"></a>S <span class="op">=</span> <span class="fl">0.20</span><span class="op">**</span><span class="dv">2</span> <span class="op">*</span> R</span>
<span id="cb13-8"><a href="#cb13-8" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb13-9"><a href="#cb13-9" aria-hidden="true" tabindex="-1"></a>varianz <span class="op">=</span> <span class="kw">lambda</span> w: <span class="bu">float</span>(w <span class="op">@</span> S <span class="op">@</span> w)</span>
<span id="cb13-10"><a href="#cb13-10" aria-hidden="true" tabindex="-1"></a>ergebnis <span class="op">=</span> minimize(varianz, np.array([<span class="fl">0.4</span>, <span class="fl">0.3</span>, <span class="fl">0.3</span>]),</span>
<span id="cb13-11"><a href="#cb13-11" aria-hidden="true" tabindex="-1"></a> constraints<span class="op">=</span>[{<span class="st">&quot;type&quot;</span>: <span class="st">&quot;eq&quot;</span>, <span class="st">&quot;fun&quot;</span>: <span class="kw">lambda</span> w: w.<span class="bu">sum</span>() <span class="op">-</span> <span class="dv">1</span>}],</span>
<span id="cb13-12"><a href="#cb13-12" aria-hidden="true" tabindex="-1"></a> bounds<span class="op">=</span>[(<span class="op">-</span><span class="dv">2</span>, <span class="dv">3</span>)] <span class="op">*</span> <span class="dv">3</span>)</span>
<span id="cb13-13"><a href="#cb13-13" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(np.<span class="bu">round</span>(ergebnis.x, <span class="dv">3</span>), <span class="bu">round</span>(ergebnis.fun, <span class="dv">4</span>), ergebnis.success)</span></code></pre></div>
<p><strong>Ausgabe:</strong></p>
<pre><code>[ 1.5 -2. 1.5] -0.254 True</code></pre>
<p>Ein Portfolio mit einer <strong>Varianz von <span class="math inline">-0{,}254</span></strong>. Der Analyst notiert erfreut ein „risikofreies Portfolio mit negativer Schwankung“.</p>
<p><strong>Ihre Aufgabe:</strong> (a) Können A und B gleichlaufen, B und C gleichlaufen — und A und C zugleich gegenläufig sein? Prüfen Sie es, ohne zu rechnen, an einem anschaulichen Beispiel. (b) Berechnen Sie die Eigenwerte von <span class="math inline">\mathbf{R}</span> mit <code>np.linalg.eigvalsh(R)</code>. Was sagt das Vorzeichen? (c) Warum ist die negative Varianz kein Fehler von <code>scipy</code>, sondern die logische Folge der Eingabe — und warum meldet es trotzdem <code>success: True</code>? (d) Warum war die Fehlermeldung von CVXPY die <strong>hilfreichste</strong> Zeile des ganzen Vorgangs, und was tut man stattdessen?</p>
<p><em>Auflösung: <a href="anhang-loesungen.html#sec:loesungen-qp-nlp">Abschnitt A.11</a>.</em></p>
</blockquote>
</div>
<blockquote>
<p><strong>🎯 Merksatz</strong> Eine Kovarianzmatrix ist kein Behälter für einzeln geschätzte Zahlen. Sie ist ein <strong>geometrisches Objekt</strong>: Ihre Einträge hängen voneinander ab, und nicht jede Kombination von Korrelationen existiert überhaupt. Zusammengetragene Korrelationen aus verschiedenen Quellen sind fast nie widerspruchsfrei — das ist einer der häufigsten Fehler in der Risikomodellierung überhaupt.</p>
</blockquote>
<hr />
<h2 id="sec:qp-nlp-quiz">11.9 Micro-Quiz</h2>
<div class="card card-quiz">
<blockquote>
Sechster und siebter Fund: die letzten handgeschriebenen Nummern Der Loesungsanhang trug 98 Marken der Bauart "**9.2 — Ansatz waehlen.**", saemtlich aus Version 03, wo Unsicherheit Kapitel 9 war. Heute ist es Kapitel 12, und die Aufgabe wird korrekt als "Aufgabe 12.2" gesetzt. Das war keine Schoenheitsfrage: "Aufgabe 9.2" existiert wirklich - sie gehoert zum Kapitel Metaheuristiken ("Zuggroesse und Temperatur"). Wer die Loesung zu 9.2 nachschlug, landete beim falschen Thema. Beide bisherigen Pruefungen liefen vorbei: Vor der Zahl steht kein Schluesselwort, und es ist keine Tabellenzelle. Zwei Messungen machten die Reparatur billig. In allen 15 nummerierten Abschnitten gab es exakt so viele Loesungen wie Aufgaben, lueckenlos 1..n - nur der Kapitelteil war falsch. Und die acht in Phase 3 ergaenzten Kapitel machten es laengst richtig (**Titel.** ohne Nummer); der Anhang wurde also nicht auf etwas Neues umgestellt, sondern auf das, was seine neueren Teile schon taten. nummeriere_marken() vergibt die Marken jetzt selbst. Der Praefix kommt NICHT aus "# Anhang A:" - der Anhang ist eine Ueberschrift, seine Abschnitte gehoeren aber zu 23 verschiedenen Kapiteln. Er kommt aus {#sec:loesungen-X} -> {#kap:X}; diese Zuordnung gilt geprueft fuer alle 23. Im Quelltext steht "**{loesung} — Titel.**". Siebter Fund unterwegs: neun Verweise auf Denkfehler-Nummern. Saetze wie "siehe Denkfehler 8.1" standen in alter Zaehlung - "8" war in Version 03 das QP/NLP-Kapitel, heute ist es Graphen. Eine blosse Umnummerierung haette sie stillschweigend woanders hin zeigen lassen; das Ziel wurde deshalb fuer jeden einzelnen aus dem Zusammenhang bestimmt und auf {ref:sec:<kapitel>-denkfehler} umgestellt. Einer steckte in einem Programm-Docstring und bekam nach Regel 12 den Kapitelnamen statt eines Verweises. Eine bewusste Abweichung vom Plan: Der Denkfehler bekommt GAR KEINE Nummer. Es gibt je Kapitel genau einen, die zweite Stelle waere immer .1, und er steht ohnehin unter einer nummerierten Ueberschrift. Eine Nummer, die nie variiert, holte nur die Fragilitaet zurueck, die hier beseitigt wird. Auch das folgt den acht neuen Kapiteln, die ihren Denkfehler schon vorher nur mit dem Titel des Raetsels ankuendigen. --check bewacht jetzt alle Familien (Denkfehler N.M, Micro-Quiz N, **N.M — im Anhang) und zaehlt zusaetzlich ab, dass jeder Loesungsabschnitt so viele {loesung}-Marken hat wie sein Kapitel Aufgaben - das faengt eine vergessene oder doppelte Loesung, was keine Textsuche leisten kann. Gegengetestet mit vier kuenstlichen Fehlern auf einmal: alle vier gemeldet, nach dem Rueckbau null. Gegenprobe der Umstellung: Gesamtdokument vorher gesichert, nachher verglichen. 168 geaenderte Zeilen, restlos einer Familie zuzuordnen - 118 Loesungsmarken (138 gesamt minus 20, die in den Kapiteln 1 bis 3 zufaellig schon stimmten), 15 Anhang-Ueberschriften, 15 Kapitel-Callouts, 12 Micro-Quiz, 7 Zeilen mit Verweisen, 1 Docstring. Keine verschobene oder inhaltlich veraenderte Zeile. Ergebnis: 138 Aufgaben, 138 Loesungen, keine ohne Gegenstueck. Micro-Quiz tragen die echten Kapitelnummern. 293 Abschnitte, 720 Querverweise, 327 Indexmarken, PDF unveraendert 725 Seiten, 33 pytest-Tests, 67 netzfreie Programme fehlerfrei (die vier yfinance-Programme scheiterten am Rate-Limit des Anbieters, nicht an dieser Aenderung). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 09:52:41 +02:00
<p><strong>❓ Micro-Quiz 11: Drei Fragen zum Selbstcheck</strong></p>
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>
2026-09-08 01:20:09 +02:00
<p>Genau eine Antwort ist jeweils richtig. Auflösung in <a href="anhang-loesungen.html#quiz-loesung-qp-nlp">Anhang A</a>.</p>
<p><strong>1. CVXPY lehnt Ihr Modell mit <code>DCPError: Problem does not follow DCP rules</code> ab. Was ist die richtige Reaktion?</strong> (a) Auf <code>scipy.optimize.minimize</code> ausweichen, das die Formulierung akzeptiert. (b) Die Meldung ernst nehmen: CVXPY sagt, dass es für diese Formulierung <strong>keine Optimalitätsgarantie</strong> geben kann. Entweder das Problem konvex umformulieren — oder bewusst auf ein lokales Verfahren mit Multistart wechseln. (c) Die Toleranzen lockern.</p>
<p><strong>2. <code>scipy.optimize.minimize</code> meldet für ein Modell mit Mengenrabatten <code>success: True</code>. Was wissen Sie damit über die gefundene Lösung?</strong> (a) Sie ist optimal. (b) Sie ist ein lokales Minimum — es geht von dort in keine Richtung mehr bergab. Ob es anderswo ein besseres gibt, ist damit offen. (c) Sie ist zulässig, aber möglicherweise nicht einmal ein lokales Minimum.</p>
<p><strong>3. Der Lagrange-Multiplikator einer bindenden Nebenbedingung beträgt <span class="math inline">\lambda^* = 4</span>. Was bedeutet das?</strong> (a) Die Nebenbedingung wird viermal verletzt. (b) Vier Einheiten der Ressource sind übrig. (c) Lockert man die Bedingung um eine kleine Einheit, verbessert sich der Zielwert um etwa 4 — es ist der Schattenpreis, genau wie im LP.</p>
</blockquote>
</div>
<hr />
<h2 id="sec:qp-nlp-selbsttest">11.10 Selbsttest</h2>
<blockquote>
<p>Antworten: <a href="anhang-loesungen.html#selbsttest-loesung-qp-nlp">Anhang A</a>.</p>
</blockquote>
<ol type="1">
<li>Worin unterscheiden sich „positiv semidefinit“ und „positiv definit“ in ihren Folgen für die Lösung?</li>
<li>Was besagt die Stationaritätsbedingung anschaulich?</li>
<li>Warum müssen die Multiplikatoren von Ungleichungen nichtnegativ sein?</li>
<li>Wie konstruiert man garantiert eine gültige Kovarianzmatrix?</li>
<li>Warum sollte man SLSQP immer mit mehreren Startpunkten laufen lassen?</li>
</ol>
<hr />
<h2 id="sec:qp-nlp-zusammenfassung">11.11 Zusammenfassung</h2>
<ul>
<li><strong>Quadratische Programme</strong> sind die Brücke zur Portfoliotheorie: Risiko ist eine quadratische Form <span class="math inline">\mathbf{w}^\top\boldsymbol{\Sigma}\mathbf{w}</span>.</li>
<li><strong>Semidefinit ⟹ konvex</strong> (lokales = globales Optimum), <strong>definit ⟹ streng konvex</strong> (Optimum eindeutig). Der Unterschied ist praktisch relevant, sobald <span class="math inline">N &gt; T</span>.</li>
<li><strong>KKT</strong> verallgemeinert Lagrange auf Ungleichungen. Die Multiplikatoren sind die Schattenpreise aus <a href="lp.html#kap-lp">Kapitel 5</a> — nur allgemeiner.</li>
<li><strong>Komplementärer Schlupf</strong> ist der beste Selbsttest für jede Optimierung.</li>
<li><strong>Kovarianzmatrizen immer prüfen</strong> und aus Volatilitäten plus Korrelationsmatrix konstruieren, nie durch Manipulation einzelner Einträge.</li>
<li><strong>SLSQP liefert nur lokale Optima.</strong> Multistart ist Pflicht, nicht Kür — und <code>success: True</code> heißt „konvergiert“, nicht „optimal“. Im Kapitelbeispiel liegen fünf lokale Optima 8 % auseinander, und ausgerechnet der kaufmännisch naheliegende Startpunkt ist der schlechteste.</li>
<li><strong>Nicht-Konvexität kommt aus dem Alltag</strong>, nicht aus der Theorie: Mengenrabatte, Skaleneffekte, Produkte zweier Entscheidungen. Wo eines davon im Modell steht, ist die Optimalitätsgarantie weg.</li>
<li><strong>Berichten Sie bei nicht-konvexen Problemen die Streuung mit</strong>, nicht die letzte Nachkommastelle. „Bester Wert aus 200 Startpunkten, Spanne bis …“ ist belastbar, ein einzelner Wert ist es nicht.</li>
</ul>
<p><strong>Ausblick.</strong> <a href="unsicherheit.html#kap-unsicherheit">Kapitel 12</a> gibt die Annahme auf, dass die Parameter überhaupt bekannt sind. Wir lernen, mit Szenarien, Erwartungswerten und Worst Cases umzugehen.</p>
</article>
<button type="button" class="fortschritt-knopf" data-kapitel="qp-nlp.html"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg> <span>Als gelesen markieren</span></button>
Phase 8.1: Synthese-Seiten je Teil - und die gebrochene Lesekette Setzt Paket 5 aus Verbesserungen_02.md um (den Teil, der nicht zurueckgestellt wurde). Fuenf neue Dateien 19_/29_/39_/49_/52_Synthese_*.md, je eine am Ende eines Teils, mit eigener Website-Seite ueber SONDERSEITEN - sie tragen bewusst keine "# Kapitel:"-Ueberschrift, weil sie keine Kapitel sind, sondern der Rueckblick auf einen Teil. Der Entwurf musste sich abgrenzen: Die Teil-Einleitungen haben bereits Entscheidungsdiagramme. Eine zweite Matrix am Teil-Ende waere eine Dopplung gewesen. Die Synthesen leisten deshalb, was eine Einleitung nicht kann - den Vergleich ueber die Kapitel hinweg (Verfahren nebeneinander, mit der Spalte "wo es aufhoert"), eine Tabelle "was dieser Teil gemessen hat" (Behauptung gegen Messung gegen Fundstelle) und drei Fehler, die der Teil verhindert. Zitiert wird ausschliesslich, was im Buch tatsaechlich gerechnet wird. Drei Funde beim Einbau: * Teil III sagte "die drei Kapitel dieses Teils", hat aber fuenf. Phase 3 hatte Mehrziel und Predict-then-Optimize hinzugefuegt, die Einleitung blieb stehen. * 50_Praxis.md verwies auf die Projektwerkstatt mit "acht eigene Anwendungen" - sie hat elf. * Und der eigentliche Fund: Die Lesekette der Quelldateien fuehrte an ACHT Kapiteln vorbei. 12_Python_Oekosystem zeigte direkt auf 20_Lineare_Programmierung, 23_Graphen direkt auf 30_QP, 32_Dynamische direkt auf 40_Finanzdaten, 50_Praxis direkt auf die Projektwerkstatt. Wer der Kette folgte, uebersprang acht von 23 Kapiteln - darunter Metaheuristiken, Spaltengenerierung, Strukturbruecke, Supply-Chain und das ganze Testing-Kapitel. Zehn weitere Dateien hatten gar keine Navigationszeile. Zur Reichweite, damit sie nicht ueberschaetzt wird: Diese Zeilen stehen nur in den Quelldateien. entferne_navigation() streicht sie aus dem Gesamtdokument, und die Website baut ihre Vor/Zurueck-Knoepfe selbst aus DATEIEN. PDF und Website waren nie betroffen - wohl aber jeder, der die Markdown-Dateien im Repository liest, und das wird nach der Veroeffentlichung der Normalfall sein. Die Kette ist jetzt ueber alle 35 Uebergaenge geschlossen, und --check bewacht sie: Fehlt eine Zeile oder zeigt sie an der in DATEIEN folgenden Datei vorbei, ist der Lauf rot. Gegengetestet mit beiden Bruchformen. Stand: 36 Dateien, 296 Abschnitte, 815 Querverweise, 328 Indexmarken, 76 Programme (unveraendert), 33 pytest-Tests, PDF 758 Seiten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 12:10:02 +02:00
<nav class="prev-next"><a class="prev-next-knopf prev-next-prev" href="synthese-kernverfahren.html"><svg class="icon" aria-hidden="true"><use href="#icon-chevron-left"></use></svg><span><small>Zurück</small>Synthese Teil II</span></a><a class="prev-next-knopf prev-next-next" href="unsicherheit.html"><span><small>Weiter</small>Kapitel 12: Optimierung unter Unsicherheit — Monte-Carlo, Stochastik, Robustheit</span><svg class="icon" aria-hidden="true"><use href="#icon-chevron-right"></use></svg></a></nav>
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>
2026-09-08 01:20:09 +02:00
</main>
</div>
<footer class="site-footer">
<p>© Dieter Schlüter · <a href="gesamtdokument.html">Gesamtdokument</a> ·
<a href="programme.html">Beispielprogramme</a></p>
</footer>
<script defer src="assets/search-index.js"></script>
<script defer src="assets/programme.js"></script>
<script defer src="assets/site.js"></script>
</body>
</html>