Grafik-Upgrade Stufe 1: elf neue Diagramme
Grafik_Upgrade.md verlangt "37 didaktisch wertvolle Visualisierungen". Es sind
19: Die Abschnitte "Modul 1-4" wiederholen die Grafiken 1-18 unter den Nummern
19-36, nur Grafik 37 ist zusaetzlich. Belegt an den Skriptnamen - 38
Nennungen, 19 verschiedene Dateien, erzeuge_kombinatorik_wand.py steht dreimal
darin. Von den 19 betreffen neun bereits vorhandene Diagramme.
Umgesetzt sind die zehn wirklich neuen plus eine elfte, die aus einem
Sachfehler des Auftrags entstand: Grafik 14 sollte in Abschnitt 12.3 stehen,
nannte aber Zahlen aus Kapitel 17. Beide Stellen haben jetzt ihre eigene
Grafik mit ihren eigenen Zahlen.
Vier Vorgaben des Auftrags wurden bewusst nicht befolgt: PNG bei 300 dpi (das
Buch kennt nur SVG - die 33 verwaisten PNGs waren gerade erst geloescht
worden), Matplotlib-Standardfarben (die Buchpalette steht in 33 Diagrammen),
harte Kapitelnummern im Fliesstext ({ref:sec:...} stattdessen) und die
fehlende Byte-Reproduzierbarkeit.
Der eigentliche Ertrag waren die Selbsttests. Jeder Generator prueft seine
Zahlen gegen das, was im Kapitel steht, und bricht bei Abweichung ab. Vier
haben angeschlagen:
* Kombinatorik-Wand: 25! ergibt 491,5 Mio. Jahre, die Tabelle nennt 490 Mio. -
Rundung auf zwei signifikante Stellen, nicht Fehler des Buchs.
* Laufzeit-Diagramm: Die abgedruckte Prozentspalte laesst sich aus den
abgedruckten Zeiten nicht exakt nachrechnen (0,001/0,008 ergibt 12 %,
gedruckt sind 14 %) - die Zeiten sind gerundet, die Prozente nicht.
* Predict-then-Optimize: geratene Datenaufteilung ergab 197,35 statt 200,95
MSE. Mit TRAINING aus dem Buchprogramm stimmen alle vier Kostenwerte.
* Almgren-Chriss: ein echter Fehler im Buchprogramm.
DER FUND: analytische_loesung() minimiert die eigene Kostenfunktion nicht. Ihr
Pfad kostet 10.860,40 EUR, der DP-Pfad 10.266,24 EUR - ein Gitterverfahren
kann das kontinuierliche Optimum aber nicht unterbieten. periodenkosten()
rechnet das Risiko mit lambda/2, die geschlossene Formel setzt
kappa~^2 = lambda*sigma^2*P0^2/eta ohne dieses Halbe. Mit lambda/2 liefert die
Formel 10.264,85 EUR und denselben Pfad wie die DP, knapp UNTER der
Gitterloesung. Die Grafik zeigt bis zur Entscheidung darueber den DP-Pfad; er
ist der, den das Kapitel abdruckt.
Nachweise: Alle elf Diagramme sind ueber zwei Laeufe byte-identisch. Kein
vorhandenes Diagramm wurde veraendert. Die Lastabwurf-Grafik reproduziert die
Befundtabelle exakt (874.870/2.504.154/28 von 40 gegen 351.356/415.924/0).
Zwei Abbildungen landeten beim automatischen Einfuegen im Codeblock (ihre
Anker standen in abgedruckten Ausgaben) und wurden dahinter verschoben; eine
Pruefung ueber alle elf zeigt null Fehlplatzierungen.
Neu: bilder_04/stil_04.py buendelt Palette, rcParams und speichere().
networkx wurde entgegen der Planung nicht gebraucht - pyproject.toml bleibt
unveraendert. PDF von 781 auf 788 Seiten, 44 Diagramme, 27 Generatoren.
Veroeffentlicht: 25 Dateien.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
@ -170,8 +170,10 @@ gehört wie die Arbeit, die sie beschreibt.
|
|||
|
||||
## Diagramme
|
||||
|
||||
`bilder_04/erzeuge_*.py` erzeugen 18 der 32 Kapiteldiagramme, jeweils **aus derselben
|
||||
Instanz wie das zugehörige Buchprogramm**. (Das 33. SVG im Verzeichnis ist `titelseite.svg`,
|
||||
`bilder_04/erzeuge_*.py` erzeugen 29 der 42 Kapiteldiagramme, jeweils **aus derselben
|
||||
Instanz wie das zugehörige Buchprogramm**. Die elf neueren benutzen `bilder_04/stil_04.py`
|
||||
— Palette, `rcParams` und `speichere()` an einer Stelle statt elfmal; die älteren setzen
|
||||
`svg.hashsalt` und `metadata={"Date": None}` weiterhin selbst. (Das 33. SVG im Verzeichnis ist `titelseite.svg`,
|
||||
die der Build bei jedem Lauf selbst schreibt — deshalb nennt `README.md` 33 und diese Datei
|
||||
32.) Das ist kein Selbstzweck: Von zehn nachgebauten Bildern förderten
|
||||
sieben einen Fehler zutage — dreimal ein Modell, das im Buch gar nicht vorkommt, einmal
|
||||
|
|
|
|||
BIN
OR_HTML_04/Operations_Research_mit_Python_Version_04.pdf
generated
2
OR_HTML_04/assets/search-index.js
generated
2284
OR_HTML_04/bilder_04/kap_dekomposition_regelkreis.svg
generated
Normal file
|
After Width: | Height: | Size: 66 KiB |
1909
OR_HTML_04/bilder_04/kap_dp_almgren_chriss.svg
generated
Normal file
|
After Width: | Height: | Size: 60 KiB |
2440
OR_HTML_04/bilder_04/kap_einfuehrung_kombinatorik.svg
generated
Normal file
|
After Width: | Height: | Size: 74 KiB |
2026
OR_HTML_04/bilder_04/kap_graphen_subtouren.svg
generated
Normal file
|
After Width: | Height: | Size: 62 KiB |
3309
OR_HTML_04/bilder_04/kap_lp_entartung.svg
generated
Normal file
|
After Width: | Height: | Size: 95 KiB |
3483
OR_HTML_04/bilder_04/kap_milp_big_m.svg
generated
Normal file
|
After Width: | Height: | Size: 106 KiB |
1556
OR_HTML_04/bilder_04/kap_modellierung_zielgroessen.svg
generated
Normal file
|
After Width: | Height: | Size: 53 KiB |
1820
OR_HTML_04/bilder_04/kap_oekosystem_laufzeit.svg
generated
Normal file
|
After Width: | Height: | Size: 55 KiB |
1888
OR_HTML_04/bilder_04/kap_prognose_entscheidungsguete.svg
generated
Normal file
|
After Width: | Height: | Size: 55 KiB |
1972
OR_HTML_04/bilder_04/kap_supplychain_lastabwurf.svg
generated
Normal file
|
After Width: | Height: | Size: 66 KiB |
2542
OR_HTML_04/bilder_04/kap_unsicherheit_durchschnitt.svg
generated
Normal file
|
After Width: | Height: | Size: 74 KiB |
5
OR_HTML_04/dekomposition.html
generated
|
|
@ -230,6 +230,11 @@
|
|||
<p>Fange mit wenigen Mustern an. Frage nach jeder Lösung, ob es ein <strong>noch nicht bekanntes</strong> Muster gibt, das sich lohnen würde — und wenn ja, nimm nur dieses eine dazu.</p>
|
||||
</blockquote>
|
||||
<p>Die Frage „lohnt sich noch ein Muster?” beantworten die <strong>Dualwerte</strong> des Master-LPs. Der Dualwert <span class="math inline">\pi_i</span> zur Bedarfszeile <span class="math inline">i</span> ist genau der Schattenpreis aus <a href="lp.html#sec:lp-dualitaet-und-schattenpreise">Abschnitt 5.6</a>: Er sagt, wie viele Rollen ein zusätzliches Stück der Breite <span class="math inline">i</span> kostet.</p>
|
||||
<figure>
|
||||
<img src="bilder_04/kap_dekomposition_regelkreis.svg" alt="Abb. 10.1: Der Kreislauf zwischen Master-LP und Pricing-Problem. Erzeugt von bilder_04/erzeuge_spaltengenerierung_flow.py." />
|
||||
<figcaption aria-hidden="true">Abb. 10.1: Der Kreislauf zwischen Master-LP und Pricing-Problem. Erzeugt von <code>bilder_04/erzeuge_spaltengenerierung_flow.py</code>.</figcaption>
|
||||
</figure>
|
||||
<p><strong>Was Sie in der Abbildung sehen.</strong> Die Information fließt in beide Richtungen: Preise nach rechts, Muster nach links. Der Ausgang unten wird erst geöffnet, wenn das Pricing nichts mehr findet — und genau das ist der Optimalitätsbeweis.</p>
|
||||
<p>Ein neues Muster <span class="math inline">a</span> lohnt sich, wenn seine <strong>reduzierten Kosten</strong> negativ sind:</p>
|
||||
<p><span class="math display">
|
||||
1 - \sum_i \pi_i\, a_i < 0
|
||||
|
|
|
|||
5
OR_HTML_04/dynamische-programmierung.html
generated
|
|
@ -776,6 +776,11 @@ C(n_t, X_t) = \underbrace{\eta\,n_t^2}_{\text{Marktauswirkung}} \;+\; \underbrac
|
|||
<span id="cb5-209"><a href="#cb5-209" aria-hidden="true" tabindex="-1"></a> plt.savefig(ziel, dpi<span class="op">=</span><span class="dv">150</span>)</span>
|
||||
<span id="cb5-210"><a href="#cb5-210" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f"</span><span class="ch">\n</span><span class="ss">Diagramm gespeichert unter '</span><span class="sc">{</span>ziel<span class="sc">}</span><span class="ss">'"</span>)</span>
|
||||
<span id="cb5-211"><a href="#cb5-211" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"="</span> <span class="op">*</span> <span class="dv">84</span>)</span></code></pre></div>
|
||||
<figure>
|
||||
<img src="bilder_04/kap_dp_almgren_chriss.svg" alt="Abb. 13.2: Drei Wege, dieselbe Position abzubauen, mit den Kosten aus periodenkosten(). Erzeugt von bilder_04/erzeuge_almgren_chriss.py." />
|
||||
<figcaption aria-hidden="true">Abb. 13.2: Drei Wege, dieselbe Position abzubauen, mit den Kosten aus <code>periodenkosten()</code>. Erzeugt von <code>bilder_04/erzeuge_almgren_chriss.py</code>.</figcaption>
|
||||
</figure>
|
||||
<p><strong>Was Sie in der Abbildung sehen.</strong> Die optimale Kurve liegt nicht in der Mitte zwischen den Extremen, sondern ist gekrümmt: vorne mehr, hinten weniger. Diese Krümmung ist die Antwort auf die Frage, wie teuer Eile gegen Warten steht.</p>
|
||||
<p><strong>Erwartete Ausgabe:</strong></p>
|
||||
<pre><code>====================================================================================
|
||||
OPTIMALE MEHRPERIODIGE ORDER-EXECUTION (BELLMAN DP)
|
||||
|
|
|
|||
9
OR_HTML_04/einfuehrung.html
generated
|
|
@ -327,6 +327,11 @@ Deckungsbeitrag: 10800 EUR</code></pre>
|
|||
</table>
|
||||
<p><strong>Der springende Punkt:</strong> Zwischen <span class="math inline">n = 15</span> (22 Minuten, machbar) und <span class="math inline">n = 20</span> (77 Jahre, hoffnungslos) liegen nur <strong>fünf zusätzliche Aufträge</strong>. Es gibt keine sanfte Verschlechterung — es gibt eine Wand.</p>
|
||||
</blockquote>
|
||||
<figure>
|
||||
<img src="bilder_04/kap_einfuehrung_kombinatorik.svg" alt="Abb. 1.2: Rechenzeit bei einer Milliarde geprüften Reihenfolgen je Sekunde. Alle drei Kurven stehen auf derselben Achse — deshalb kleben n^2 und n^3 am unteren Rand, während n! das Bild verlässt. Erzeugt von bilder_04/erzeuge_kombinatorik_wand.py." />
|
||||
<figcaption aria-hidden="true">Abb. 1.2: Rechenzeit bei einer Milliarde geprüften Reihenfolgen je Sekunde. Alle drei Kurven stehen auf derselben Achse — deshalb kleben <span class="math inline">n^2</span> und <span class="math inline">n^3</span> am unteren Rand, während <span class="math inline">n!</span> das Bild verlässt. Erzeugt von <code>bilder_04/erzeuge_kombinatorik_wand.py</code>.</figcaption>
|
||||
</figure>
|
||||
<p><strong>Was Sie in der Abbildung sehen.</strong> Der markierte Streifen zwischen <span class="math inline">n = 15</span> und <span class="math inline">n = 20</span> ist der eigentliche Befund: Dort wird aus einer Kaffeepause ein Menschenleben. Wer eine Maschine kauft, die tausendmal schneller rechnet, verschiebt diese Wand um genau zwei Aufträge.</p>
|
||||
<p>Und die Rechnung ist noch geschönt: Ein schnellerer Rechner hilft praktisch nicht. Selbst eine Beschleunigung um den Faktor <strong>eine Million</strong> verschiebt die Grenze von <span class="math inline">n=20</span> nur auf etwa <span class="math inline">n=24</span>. Gegen kombinatorisches Wachstum ist Hardware machtlos.</p>
|
||||
<p>Das folgende Programm macht diesen Effekt erfahrbar. Es löst dasselbe Zuordnungsproblem zweimal — einmal durch vollständiges Durchprobieren, einmal mit einem OR-Solver — und vergleicht die Laufzeiten.</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>
|
||||
|
|
@ -522,8 +527,8 @@ Der Solver löst dieselbe Instanz in Sekundenbruchteilen.
|
|||
<h2 id="sec:einfuehrung-die-vier-universellen-bausteine-jedes-or">1.6 Die vier universellen Bausteine jedes OR-Problems</h2>
|
||||
<p>Dies ist der wichtigste Abschnitt des Kapitels. Jedes Optimierungsproblem der Welt lässt sich — unabhängig von der Branche — auf vier Elemente reduzieren. Wenn Sie später vor einem neuen Problem sitzen, ist dies Ihre Checkliste.</p>
|
||||
<figure>
|
||||
<img src="bilder_04/kap01_vier_bausteine.svg" alt="Abb. 1.2: Die vier universellen Bausteine eines OR-Modells" />
|
||||
<figcaption aria-hidden="true">Abb. 1.2: Die vier universellen Bausteine eines OR-Modells</figcaption>
|
||||
<img src="bilder_04/kap01_vier_bausteine.svg" alt="Abb. 1.3: Die vier universellen Bausteine eines OR-Modells" />
|
||||
<figcaption aria-hidden="true">Abb. 1.3: Die vier universellen Bausteine eines OR-Modells</figcaption>
|
||||
</figure>
|
||||
<h3 id="baustein-1-entscheidungsvariablen-x">Baustein 1 — Entscheidungsvariablen (<span class="math inline">x</span>)</h3>
|
||||
<p>Die <strong>Stellschrauben</strong> des Modells: Werte, die der Algorithmus aktiv festlegen darf.</p>
|
||||
|
|
|
|||
73
OR_HTML_04/gesamtdokument.html
generated
|
|
@ -550,7 +550,6 @@
|
|||
<li><a href="#sec:modellierung-vier-fragen" id="toc-sec:modellierung-vier-fragen">4.3 Ein Satz ist noch kein Modell</a></li>
|
||||
<li><a href="#sec:modellierung-fuenf-modelle" id="toc-sec:modellierung-fuenf-modelle">4.4 Derselbe Datensatz, fünf Modelle</a>
|
||||
<ul>
|
||||
<li><a href="#teil-1-drei-ziele-drei-pläne" id="toc-teil-1-drei-ziele-drei-pläne">Teil 1: Drei Ziele, drei Pläne</a></li>
|
||||
<li><a href="#teil-2-ein-satz-drei-lesarten" id="toc-teil-2-ein-satz-drei-lesarten">Teil 2: Ein Satz, drei Lesarten</a></li>
|
||||
<li><a href="#teil-3-hart-oder-weich" id="toc-teil-3-hart-oder-weich">Teil 3: Hart oder weich</a></li>
|
||||
<li><a href="#teil-4-ein-quartal-später" id="toc-teil-4-ein-quartal-später">Teil 4: Ein Quartal später</a></li>
|
||||
|
|
@ -835,6 +834,7 @@
|
|||
<li><a href="#sec:prognose-lernziele" id="toc-sec:prognose-lernziele">15.2 Lernziele</a></li>
|
||||
<li><a href="#sec:prognose-naht" id="toc-sec:prognose-naht">15.3 Die Naht zwischen zwei Abteilungen</a></li>
|
||||
<li><a href="#sec:prognose-programm" id="toc-sec:prognose-programm">15.4 Das Programm</a></li>
|
||||
<li><a href="#was-sie-in-der-abbildung-sehen.-links-und-rechts-gewinnt-nicht-dasselbe-verfahren.-die-punktprognose-hat-den-besten-prognosefehler-und-die-höchsten-kosten-beide-maßstäbe-messen-etwas-anderes-und-nur-einer-davon-steht-am-ende-in-der-bilanz." id="toc-was-sie-in-der-abbildung-sehen.-links-und-rechts-gewinnt-nicht-dasselbe-verfahren.-die-punktprognose-hat-den-besten-prognosefehler-und-die-höchsten-kosten-beide-maßstäbe-messen-etwas-anderes-und-nur-einer-davon-steht-am-ende-in-der-bilanz."><strong>Was Sie in der Abbildung sehen.</strong> Links und rechts gewinnt nicht dasselbe Verfahren. Die Punktprognose hat den besten Prognosefehler und die höchsten Kosten — beide Maßstäbe messen etwas anderes, und nur einer davon steht am Ende in der Bilanz.</a></li>
|
||||
<li><a href="#sec:prognose-befund" id="toc-sec:prognose-befund">15.5 Der Befund</a>
|
||||
<ul>
|
||||
<li><a href="#warum-ein-pauschaler-zuschlag-zu-kurz-greift" id="toc-warum-ein-pauschaler-zuschlag-zu-kurz-greift">Warum ein pauschaler Zuschlag zu kurz greift</a></li>
|
||||
|
|
@ -3366,6 +3366,11 @@ Deckungsbeitrag: 10800 EUR</code></pre>
|
|||
</table>
|
||||
<p><strong>Der springende Punkt:</strong> Zwischen <span class="math inline">n = 15</span> (22 Minuten, machbar) und <span class="math inline">n = 20</span> (77 Jahre, hoffnungslos) liegen nur <strong>fünf zusätzliche Aufträge</strong>. Es gibt keine sanfte Verschlechterung — es gibt eine Wand.</p>
|
||||
</blockquote>
|
||||
<figure>
|
||||
<img src="bilder_04/kap_einfuehrung_kombinatorik.svg" alt="Abb. 1.2: Rechenzeit bei einer Milliarde geprüften Reihenfolgen je Sekunde. Alle drei Kurven stehen auf derselben Achse — deshalb kleben n^2 und n^3 am unteren Rand, während n! das Bild verlässt. Erzeugt von bilder_04/erzeuge_kombinatorik_wand.py." />
|
||||
<figcaption aria-hidden="true">Abb. 1.2: Rechenzeit bei einer Milliarde geprüften Reihenfolgen je Sekunde. Alle drei Kurven stehen auf derselben Achse — deshalb kleben <span class="math inline">n^2</span> und <span class="math inline">n^3</span> am unteren Rand, während <span class="math inline">n!</span> das Bild verlässt. Erzeugt von <code>bilder_04/erzeuge_kombinatorik_wand.py</code>.</figcaption>
|
||||
</figure>
|
||||
<p><strong>Was Sie in der Abbildung sehen.</strong> Der markierte Streifen zwischen <span class="math inline">n = 15</span> und <span class="math inline">n = 20</span> ist der eigentliche Befund: Dort wird aus einer Kaffeepause ein Menschenleben. Wer eine Maschine kauft, die tausendmal schneller rechnet, verschiebt diese Wand um genau zwei Aufträge.</p>
|
||||
<p>Und die Rechnung ist noch geschönt: Ein schnellerer Rechner hilft praktisch nicht. Selbst eine Beschleunigung um den Faktor <strong>eine Million</strong> verschiebt die Grenze von <span class="math inline">n=20</span> nur auf etwa <span class="math inline">n=24</span>. Gegen kombinatorisches Wachstum ist Hardware machtlos.</p>
|
||||
<p>Das folgende Programm macht diesen Effekt erfahrbar. Es löst dasselbe Zuordnungsproblem zweimal — einmal durch vollständiges Durchprobieren, einmal mit einem OR-Solver — und vergleicht die Laufzeiten.</p>
|
||||
<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>
|
||||
|
|
@ -3561,8 +3566,8 @@ Der Solver löst dieselbe Instanz in Sekundenbruchteilen.
|
|||
<h2 id="sec:einfuehrung-die-vier-universellen-bausteine-jedes-or">1.6 Die vier universellen Bausteine jedes OR-Problems</h2>
|
||||
<p>Dies ist der wichtigste Abschnitt des Kapitels. Jedes Optimierungsproblem der Welt lässt sich — unabhängig von der Branche — auf vier Elemente reduzieren. Wenn Sie später vor einem neuen Problem sitzen, ist dies Ihre Checkliste.</p>
|
||||
<figure>
|
||||
<img src="bilder_04/kap01_vier_bausteine.svg" alt="Abb. 1.2: Die vier universellen Bausteine eines OR-Modells" />
|
||||
<figcaption aria-hidden="true">Abb. 1.2: Die vier universellen Bausteine eines OR-Modells</figcaption>
|
||||
<img src="bilder_04/kap01_vier_bausteine.svg" alt="Abb. 1.3: Die vier universellen Bausteine eines OR-Modells" />
|
||||
<figcaption aria-hidden="true">Abb. 1.3: Die vier universellen Bausteine eines OR-Modells</figcaption>
|
||||
</figure>
|
||||
<h3 id="baustein-1-entscheidungsvariablen-x">Baustein 1 — Entscheidungsvariablen (<span class="math inline">x</span>)</h3>
|
||||
<p>Die <strong>Stellschrauben</strong> des Modells: Werte, die der Algorithmus aktiv festlegen darf.</p>
|
||||
|
|
@ -6849,6 +6854,11 @@ Groesse Variante Aufbau Loesen Aufba
|
|||
-> Zielwert (alle gleich) 8316.24 Spanne 1.8e-12
|
||||
----------------------------------------------------------------------------------------</code></pre>
|
||||
<p><strong>Lesen Sie die letzte Spalte.</strong> Bei 62 500 Variablen verbringt Variante A <strong>69 % der Gesamtzeit damit, das Modell überhaupt aufzuschreiben</strong> — und nur 31 % mit Rechnen. Die vektorisierten Varianten kehren das Verhältnis um: 1 % Aufbau, 99 % Solver. Erst dort ist ein schnellerer Solver überhaupt die richtige Stellschraube.</p>
|
||||
<figure>
|
||||
<img src="bilder_04/kap_oekosystem_laufzeit.svg" alt="Abb. 3.2: Dieselbe Aufgabe, zwei Arten sie aufzuschreiben — aufgeteilt in Aufbauzeit (Python) und Lösezeit (C++). Werte aus der Messung oben. Erzeugt von bilder_04/erzeuge_laufzeit_aufbau.py." />
|
||||
<figcaption aria-hidden="true">Abb. 3.2: Dieselbe Aufgabe, zwei Arten sie aufzuschreiben — aufgeteilt in Aufbauzeit (Python) und Lösezeit (C++). Werte aus der Messung oben. Erzeugt von <code>bilder_04/erzeuge_laufzeit_aufbau.py</code>.</figcaption>
|
||||
</figure>
|
||||
<p><strong>Was Sie in der Abbildung sehen.</strong> Der rote Anteil ist die Zeit, in der der Solver noch gar nicht rechnet. Bei Variante A wächst er mit dem Modell, bei Variante C verschwindet er — dieselbe Mathematik, nur anders aufgeschrieben.</p>
|
||||
<blockquote>
|
||||
<p><strong>💻 Code-Durchgang</strong></p>
|
||||
<table>
|
||||
|
|
@ -7381,6 +7391,7 @@ Es muss also ausgewaehlt werden. 3 der Kunden sind Stammkunden:
|
|||
"Maschinen besser auslasten" 205,000 54,000 100% 4 1 5
|
||||
"Deckungsbeitrag maximieren" 222,000 78,500 100% 2 1 8
|
||||
|
||||
|
||||
Der Umsatzplan bringt 5,000 EUR mehr Umsatz und 12,500 EUR WENIGER
|
||||
Deckungsbeitrag. Fuer 5,000 EUR Umsatz werden also 12,500 EUR Ergebnis
|
||||
aufgegeben - und 5 von 6 Stammkundenauftraegen dazu.
|
||||
|
|
@ -7454,7 +7465,11 @@ Drei Dinge, die deshalb vor dem ersten Modell geklaert gehoeren:
|
|||
zwar genau dann, wenn ein besonders grosser Auftrag hereinkommt.
|
||||
Weiche Regeln antworten auch dann noch.
|
||||
====================================================================================</code></pre>
|
||||
<h3 id="teil-1-drei-ziele-drei-pläne">Teil 1: Drei Ziele, drei Pläne</h3>
|
||||
<figure>
|
||||
<img src="bilder_04/kap_modellierung_zielgroessen.svg" alt="Abb. 4.1: Die drei Pläne aus der Tabelle, je Achse auf den besten der drei bezogen. Erzeugt von bilder_04/erzeuge_zielgroessen_radar.py." />
|
||||
<figcaption aria-hidden="true">Abb. 4.1: Die drei Pläne aus der Tabelle, je Achse auf den besten der drei bezogen. Erzeugt von <code>bilder_04/erzeuge_zielgroessen_radar.py</code>.</figcaption>
|
||||
</figure>
|
||||
<p><strong>Was Sie in der Abbildung sehen.</strong> Jeder Plan gewinnt auf seiner eigenen Achse — das ist keine Überraschung, sondern die Definition. Interessant ist, was daneben passiert: Der Auslastungsplan liegt bei der Auslastung gleichauf mit den anderen und verliert beim Deckungsbeitrag ein knappes Drittel. ### Teil 1: Drei Ziele, drei Pläne</p>
|
||||
<table>
|
||||
<colgroup>
|
||||
<col style="width: 15%" />
|
||||
|
|
@ -8923,6 +8938,11 @@ Pruefung: komplementaerer Schlupf fuer alle Ressourcen erfuellt.
|
|||
<hr />
|
||||
<h2 id="sec:lp-entartung">5.9 Wann Schattenpreise lügen: Entartung und Toleranzen</h2>
|
||||
<p>Der vorige Abschnitt hat gezeigt, wie wertvoll Schattenpreise sind. Dieser zeigt die zwei Fälle, in denen sie eine Entscheidung <strong>nicht</strong> tragen — und wie man beide erkennt, bevor jemand auf ihrer Grundlage eine Maschine kauft.</p>
|
||||
<figure>
|
||||
<img src="bilder_04/kap_lp_entartung.svg" alt="Abb. 5.3: Links laufen drei Geraden durch dieselbe Ecke — eine mehr, als zwei Dimensionen brauchen. Rechts die Folge: Der Schattenpreis ist keine Zahl mehr, sondern eine Spanne. Erzeugt von bilder_04/erzeuge_entartung_polyeder.py." />
|
||||
<figcaption aria-hidden="true">Abb. 5.3: Links laufen drei Geraden durch dieselbe Ecke — eine mehr, als zwei Dimensionen brauchen. Rechts die Folge: Der Schattenpreis ist keine Zahl mehr, sondern eine Spanne. Erzeugt von <code>bilder_04/erzeuge_entartung_polyeder.py</code>.</figcaption>
|
||||
</figure>
|
||||
<p><strong>Was Sie in der Abbildung sehen.</strong> Jeder Wert innerhalb der farbigen Balken erfüllt die Optimalitätsbedingungen. Zwei Solver dürfen also verschiedene Schattenpreise melden und haben beide recht — die Frage „was ist eine Stunde wert?” hat hier schlicht keine eindeutige Antwort.</p>
|
||||
<h3 id="fall-1-entartung">Fall 1: Entartung</h3>
|
||||
<p>Erinnern Sie sich an <a href="#sec:fundament-der-zulaessige-loesungsraum-und-das-polyeder">Abschnitt 2.4</a>: In zwei Dimensionen legen <strong>zwei</strong> sich schneidende Geraden eine Ecke fest, in drei Dimensionen drei Ebenen, allgemein <span class="math inline">n</span> Restriktionen bei <span class="math inline">n</span> Variablen. Was passiert, wenn in einer Ecke <strong>mehr</strong> Nebenbedingungen aktiv sind als das Problem Variablen hat?</p>
|
||||
<p>Geometrisch: Drei Geraden laufen zufällig durch denselben Punkt. Die Ecke ist immer noch eine Ecke — aber sie ist <strong>überbestimmt</strong>. Man nennt das <strong>Entartung</strong> (<em>Degeneriertheit</em>), und sie ist in der Praxis nicht die Ausnahme, sondern die Regel: Sie entsteht überall dort, wo Kapazitäten aus derselben Planung stammen und deshalb glatt aufeinander passen — Schichtlängen, Chargengrößen, runde Vertragsmengen.</p>
|
||||
|
|
@ -9669,6 +9689,11 @@ Loesung eine Nebenbedingung verletzt. 'Verlust' = Abstand zum exakten Op
|
|||
<hr />
|
||||
<h2 id="sec:milp-modellierungstricks-big-m-und-logische-bedingungen">6.5 Modellierungstricks: Big-M und logische Bedingungen</h2>
|
||||
<p>Hier kommt das Handwerkszeug, das MILP so mächtig macht: Man übersetzt Geschäftsregeln in lineare Ungleichungen — mithilfe binärer Hilfsvariablen <span class="math inline">y \in \{0,1\}</span> und einer hinreichend großen Konstante <span class="math inline">M</span>.</p>
|
||||
<figure>
|
||||
<img src="bilder_04/kap_milp_big_m.svg" alt="Abb. 6.3: Dieselbe Bedingung x \le M\,y, zwei Zahlen für M. Gezeigt ist die LP-Relaxation — also das, was der Solver sieht, bevor y ganzzahlig wird. Erzeugt von bilder_04/erzeuge_big_m_vergleich.py." />
|
||||
<figcaption aria-hidden="true">Abb. 6.3: Dieselbe Bedingung <span class="math inline">x \le M\,y</span>, zwei Zahlen für <span class="math inline">M</span>. Gezeigt ist die LP-Relaxation — also das, was der Solver sieht, bevor <span class="math inline">y</span> ganzzahlig wird. Erzeugt von <code>bilder_04/erzeuge_big_m_vergleich.py</code>.</figcaption>
|
||||
</figure>
|
||||
<p><strong>Was Sie in der Abbildung sehen.</strong> Für <span class="math inline">y \in \{0,1\}</span> sind beide Modelle gleichwertig; die beiden blauen Punkte stimmen überein. Verschieden ist nur, was dazwischen erlaubt ist — und rechts genügt bereits <span class="math inline">y = 0{,}08</span>, um die volle Kapazität freizugeben.</p>
|
||||
<h3 id="muster-1-fixkosten-aktivierungsschalter">Muster 1 — Fixkosten / Aktivierungsschalter</h3>
|
||||
<p><em>Regel:</em> Wird Maschine <span class="math inline">j</span> genutzt (<span class="math inline">x_j > 0</span>), fallen Rüstkosten <span class="math inline">F_j</span> an.</p>
|
||||
<p><span class="math display">
|
||||
|
|
@ -13232,6 +13257,11 @@ Umschlag -> Kunde_2 5.0 10 3.00 15.00
|
|||
<p>Die Touren sind aus demselben Grund <strong>Luftlinien und keine Straßenrouten</strong>: Das Modell kennt keine Straßen, es rechnet mit den Abständen der Distanzmatrix. Eine Linie, die einer echten Straße folgte, wäre hübscher und würde eine Genauigkeit vortäuschen, die das Modell nicht hat. Wer Straßenentfernungen braucht, ersetzt die Matrix — <a href="#sec:graphen-denkfehler">Abschnitt 8.7</a> sagt, was dabei zu beachten ist.</p>
|
||||
<h3 id="kurzzyklen-verhindern">Kurzzyklen verhindern</h3>
|
||||
<p>Ein naives Modell erlaubt <strong>Subtouren</strong>: isolierte Kreise, die das Depot nie anfahren. Die klassische Gegenmaßnahme ist die <strong>MTZ-Formulierung</strong> nach Miller, Tucker und Zemlin. Man führt Rangvariablen <span class="math inline">u_i</span> ein (die Position des Knotens in der Tour):</p>
|
||||
<figure>
|
||||
<img src="bilder_04/kap_graphen_subtouren.svg" alt="Abb. 8.3: Links eine Lösung, die jede Gradbedingung erfüllt und trotzdem unbrauchbar ist; rechts dieselbe Instanz mit Kurzzyklus-Bedingungen. Erzeugt von bilder_04/erzeuge_vrp_subtours.py." />
|
||||
<figcaption aria-hidden="true">Abb. 8.3: Links eine Lösung, die jede Gradbedingung erfüllt und trotzdem unbrauchbar ist; rechts dieselbe Instanz mit Kurzzyklus-Bedingungen. Erzeugt von <code>bilder_04/erzeuge_vrp_subtours.py</code>.</figcaption>
|
||||
</figure>
|
||||
<p><strong>Was Sie in der Abbildung sehen.</strong> Links wird jeder Kunde genau einmal angefahren — das Modell hat also gerechnet, was man ihm aufgeschrieben hat. Was fehlt, ist die Bedingung, an die man beim Aufschreiben nicht denkt: dass jede Tour am Depot beginnen muss.</p>
|
||||
<p><span class="math display">
|
||||
u_i - u_j + C \cdot x_{ij} \le C - d_j \qquad \forall i \ne j
|
||||
</span></p>
|
||||
|
|
@ -15397,6 +15427,11 @@ Abstand zum Optimum.
|
|||
<p>Fange mit wenigen Mustern an. Frage nach jeder Lösung, ob es ein <strong>noch nicht bekanntes</strong> Muster gibt, das sich lohnen würde — und wenn ja, nimm nur dieses eine dazu.</p>
|
||||
</blockquote>
|
||||
<p>Die Frage „lohnt sich noch ein Muster?” beantworten die <strong>Dualwerte</strong> des Master-LPs. Der Dualwert <span class="math inline">\pi_i</span> zur Bedarfszeile <span class="math inline">i</span> ist genau der Schattenpreis aus <a href="#sec:lp-dualitaet-und-schattenpreise">Abschnitt 5.6</a>: Er sagt, wie viele Rollen ein zusätzliches Stück der Breite <span class="math inline">i</span> kostet.</p>
|
||||
<figure>
|
||||
<img src="bilder_04/kap_dekomposition_regelkreis.svg" alt="Abb. 10.1: Der Kreislauf zwischen Master-LP und Pricing-Problem. Erzeugt von bilder_04/erzeuge_spaltengenerierung_flow.py." />
|
||||
<figcaption aria-hidden="true">Abb. 10.1: Der Kreislauf zwischen Master-LP und Pricing-Problem. Erzeugt von <code>bilder_04/erzeuge_spaltengenerierung_flow.py</code>.</figcaption>
|
||||
</figure>
|
||||
<p><strong>Was Sie in der Abbildung sehen.</strong> Die Information fließt in beide Richtungen: Preise nach rechts, Muster nach links. Der Ausgang unten wird erst geöffnet, wenn das Pricing nichts mehr findet — und genau das ist der Optimalitätsbeweis.</p>
|
||||
<p>Ein neues Muster <span class="math inline">a</span> lohnt sich, wenn seine <strong>reduzierten Kosten</strong> negativ sind:</p>
|
||||
<p><span class="math display">
|
||||
1 - \sum_i \pi_i\, a_i < 0
|
||||
|
|
@ -17503,6 +17538,11 @@ q^{*} = \frac{c_{\text{zu wenig}}}{c_{\text{zu wenig}} + c_{\text{zu viel}}}
|
|||
<span id="cb128-51"><a href="#cb128-51" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb128-52"><a href="#cb128-52" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"</span><span class="ch">\n</span><span class="st">Das Optimum liegt exakt auf einem Szenariowert (250 = 'Volatil'),"</span>)</span>
|
||||
<span id="cb128-53"><a href="#cb128-53" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"nicht beim Mittelwert 225 - typisch fuer asymmetrische Kostenfunktionen."</span>)</span></code></pre></div>
|
||||
<figure>
|
||||
<img src="bilder_04/kap_unsicherheit_durchschnitt.svg" alt="Abb. 12.1: Erwartete Gesamtkosten über allen Bestellmengen, gerechnet aus den drei Szenarien der Handrechnung. Erzeugt von bilder_04/erzeuge_fluch_durchschnitt.py." />
|
||||
<figcaption aria-hidden="true">Abb. 12.1: Erwartete Gesamtkosten über allen Bestellmengen, gerechnet aus den drei Szenarien der Handrechnung. Erzeugt von <code>bilder_04/erzeuge_fluch_durchschnitt.py</code>.</figcaption>
|
||||
</figure>
|
||||
<p><strong>Was Sie in der Abbildung sehen.</strong> Die Kurve ist nicht symmetrisch: Sie fällt links flach und steigt rechts steil, weil Fehlmenge 120 € kostet und Überschuss nur 5 €. Ihr tiefster Punkt liegt deshalb rechts vom Erwartungswert — nicht auf ihm.</p>
|
||||
<p><strong>Erwartete Ausgabe:</strong></p>
|
||||
<pre><code>======================================================================
|
||||
DER FLUCH DES DURCHSCHNITTS
|
||||
|
|
@ -17530,8 +17570,8 @@ nicht beim Mittelwert 225 - typisch fuer asymmetrische Kostenfunktionen.</code><
|
|||
</blockquote>
|
||||
<h3 id="die-vier-ansätze-im-überblick">Die vier Ansätze im Überblick</h3>
|
||||
<figure>
|
||||
<img src="bilder_04/kap09_unsicherheit_ansaetze.svg" alt="Abb. 12.1: Ansätze für Optimierung unter Unsicherheit" />
|
||||
<figcaption aria-hidden="true">Abb. 12.1: Ansätze für Optimierung unter Unsicherheit</figcaption>
|
||||
<img src="bilder_04/kap09_unsicherheit_ansaetze.svg" alt="Abb. 12.2: Ansätze für Optimierung unter Unsicherheit" />
|
||||
<figcaption aria-hidden="true">Abb. 12.2: Ansätze für Optimierung unter Unsicherheit</figcaption>
|
||||
</figure>
|
||||
<table>
|
||||
<colgroup>
|
||||
|
|
@ -19113,6 +19153,11 @@ C(n_t, X_t) = \underbrace{\eta\,n_t^2}_{\text{Marktauswirkung}} \;+\; \underbrac
|
|||
<span id="cb143-209"><a href="#cb143-209" aria-hidden="true" tabindex="-1"></a> plt.savefig(ziel, dpi<span class="op">=</span><span class="dv">150</span>)</span>
|
||||
<span id="cb143-210"><a href="#cb143-210" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f"</span><span class="ch">\n</span><span class="ss">Diagramm gespeichert unter '</span><span class="sc">{</span>ziel<span class="sc">}</span><span class="ss">'"</span>)</span>
|
||||
<span id="cb143-211"><a href="#cb143-211" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"="</span> <span class="op">*</span> <span class="dv">84</span>)</span></code></pre></div>
|
||||
<figure>
|
||||
<img src="bilder_04/kap_dp_almgren_chriss.svg" alt="Abb. 13.2: Drei Wege, dieselbe Position abzubauen, mit den Kosten aus periodenkosten(). Erzeugt von bilder_04/erzeuge_almgren_chriss.py." />
|
||||
<figcaption aria-hidden="true">Abb. 13.2: Drei Wege, dieselbe Position abzubauen, mit den Kosten aus <code>periodenkosten()</code>. Erzeugt von <code>bilder_04/erzeuge_almgren_chriss.py</code>.</figcaption>
|
||||
</figure>
|
||||
<p><strong>Was Sie in der Abbildung sehen.</strong> Die optimale Kurve liegt nicht in der Mitte zwischen den Extremen, sondern ist gekrümmt: vorne mehr, hinten weniger. Diese Krümmung ist die Antwort auf die Frage, wie teuer Eile gegen Warten steht.</p>
|
||||
<p><strong>Erwartete Ausgabe:</strong></p>
|
||||
<pre><code>====================================================================================
|
||||
OPTIMALE MEHRPERIODIGE ORDER-EXECUTION (BELLMAN DP)
|
||||
|
|
@ -20326,6 +20371,7 @@ Training: Tag 1 bis 1000. Bewertung: die restlichen 4000 Tage.
|
|||
bester MSE: bestelle die Punktprognose
|
||||
beste Kosten: Quantilregression aufs kritische Quantil
|
||||
|
||||
|
||||
Das Verfahren mit dem besten MSE hat die HOECHSTEN Kosten, und das
|
||||
Verfahren mit den besten Kosten hat einen um 13% SCHLECHTEREN MSE.
|
||||
Wer Prognosemodelle nach MSE auswaehlt, waehlt hier das falsche.
|
||||
|
|
@ -20400,7 +20446,11 @@ erfordert Bibliotheken wie cvxpylayers. Die dritte Zeile der Tabelle
|
|||
oben ist seine einfachste denkbare Form: ein einziger Parameter, auf
|
||||
Kosten statt auf Fehler trainiert.
|
||||
====================================================================================</code></pre>
|
||||
<hr />
|
||||
<figure>
|
||||
<img src="bilder_04/kap_prognose_entscheidungsguete.svg" alt="Abb. 15.1: Dieselben vier Verfahren, zwei Maßstäbe. Grün ist jeweils der beste Wert. Erzeugt von bilder_04/erzeuge_predict_vs_optimize.py." />
|
||||
<figcaption aria-hidden="true">Abb. 15.1: Dieselben vier Verfahren, zwei Maßstäbe. Grün ist jeweils der beste Wert. Erzeugt von <code>bilder_04/erzeuge_predict_vs_optimize.py</code>.</figcaption>
|
||||
</figure>
|
||||
<h2 id="was-sie-in-der-abbildung-sehen.-links-und-rechts-gewinnt-nicht-dasselbe-verfahren.-die-punktprognose-hat-den-besten-prognosefehler-und-die-höchsten-kosten-beide-maßstäbe-messen-etwas-anderes-und-nur-einer-davon-steht-am-ende-in-der-bilanz."><strong>Was Sie in der Abbildung sehen.</strong> Links und rechts gewinnt nicht dasselbe Verfahren. Die Punktprognose hat den besten Prognosefehler und die höchsten Kosten — beide Maßstäbe messen etwas anderes, und nur einer davon steht am Ende in der Bilanz.</h2>
|
||||
<h2 id="sec:prognose-befund">15.5 Der Befund</h2>
|
||||
<table>
|
||||
<colgroup>
|
||||
|
|
@ -22081,10 +22131,15 @@ Was Versorgungssicherheit kostet
|
|||
</tbody>
|
||||
</table>
|
||||
<p>Der Erwartungswert-Plan kostet in der Wirklichkeit <strong>149 % mehr</strong>, und sein schlimmster Tag ist sechsmal so teuer.</p>
|
||||
<figure>
|
||||
<img src="bilder_04/kap_supplychain_lastabwurf.svg" alt="Abb. 17.1: Die 40 Szenarien einzeln, aufsteigend nach Kosten. Punkte markieren Szenarien mit Lastabwurf. Erzeugt von bilder_04/erzeuge_lastabwurf_verteilung.py." />
|
||||
<figcaption aria-hidden="true">Abb. 17.1: Die 40 Szenarien einzeln, aufsteigend nach Kosten. Punkte markieren Szenarien mit Lastabwurf. Erzeugt von <code>bilder_04/erzeuge_lastabwurf_verteilung.py</code>.</figcaption>
|
||||
</figure>
|
||||
<p><strong>Was Sie in der Abbildung sehen.</strong> Der Erwartungswert-Plan ist in den ruhigen Szenarien tatsächlich der billigere — die rote Kurve verläuft links von der grünen. Er kippt erst rechts, und zwar so weit, dass der Mittelwert das Vorzeichen wechselt.</p>
|
||||
<p><strong>Wofür genau?</strong> Die Summendifferenz von 523 514 € sagt noch nicht, wo sie entsteht. Zerlegt man sie nach Kostenarten, wird aus dem Befund eine Aussage, die man vorlegen kann:</p>
|
||||
<figure>
|
||||
<img src="bilder_04/kap_supplychain_wirkung.svg" alt="Abb. 17.1: Wofür der Erwartungswert-Plan 149 % mehr bezahlt" />
|
||||
<figcaption aria-hidden="true">Abb. 17.1: Wofür der Erwartungswert-Plan 149 % mehr bezahlt</figcaption>
|
||||
<img src="bilder_04/kap_supplychain_wirkung.svg" alt="Abb. 17.2: Wofür der Erwartungswert-Plan 149 % mehr bezahlt" />
|
||||
<figcaption aria-hidden="true">Abb. 17.2: Wofür der Erwartungswert-Plan 149 % mehr bezahlt</figcaption>
|
||||
</figure>
|
||||
<p>Drei Dinge stehen darin, die die Tabelle nicht zeigt. Die <strong>Anfahrkosten sind in beiden Plänen gleich</strong> (58 000 €) — der Erwartungswert-Plan spart also nicht dort, wo man es vermuten würde. Er spart <strong>12 725 € Brennstoff</strong>, weil er weniger Blockstunden vorhält. Und er bezahlt dafür <strong>536 239 € Lastabwurf</strong>: das <strong>42-fache</strong> seiner Ersparnis.</p>
|
||||
<p>Genau diese Gegenüberstellung ist es, die eine Investitionsentscheidung trägt. „149 % mehr” ist eine Zahl, über die man streiten kann; „wir sparen 12 725 € und riskieren dafür 536 239 €” ist eine, über die man entscheidet.</p>
|
||||
|
|
|
|||
5
OR_HTML_04/graphen.html
generated
|
|
@ -584,6 +584,11 @@ Umschlag -> Kunde_2 5.0 10 3.00 15.00
|
|||
<p>Die Touren sind aus demselben Grund <strong>Luftlinien und keine Straßenrouten</strong>: Das Modell kennt keine Straßen, es rechnet mit den Abständen der Distanzmatrix. Eine Linie, die einer echten Straße folgte, wäre hübscher und würde eine Genauigkeit vortäuschen, die das Modell nicht hat. Wer Straßenentfernungen braucht, ersetzt die Matrix — <a href="#sec:graphen-denkfehler">Abschnitt 8.7</a> sagt, was dabei zu beachten ist.</p>
|
||||
<h3 id="kurzzyklen-verhindern">Kurzzyklen verhindern</h3>
|
||||
<p>Ein naives Modell erlaubt <strong>Subtouren</strong>: isolierte Kreise, die das Depot nie anfahren. Die klassische Gegenmaßnahme ist die <strong>MTZ-Formulierung</strong> nach Miller, Tucker und Zemlin. Man führt Rangvariablen <span class="math inline">u_i</span> ein (die Position des Knotens in der Tour):</p>
|
||||
<figure>
|
||||
<img src="bilder_04/kap_graphen_subtouren.svg" alt="Abb. 8.3: Links eine Lösung, die jede Gradbedingung erfüllt und trotzdem unbrauchbar ist; rechts dieselbe Instanz mit Kurzzyklus-Bedingungen. Erzeugt von bilder_04/erzeuge_vrp_subtours.py." />
|
||||
<figcaption aria-hidden="true">Abb. 8.3: Links eine Lösung, die jede Gradbedingung erfüllt und trotzdem unbrauchbar ist; rechts dieselbe Instanz mit Kurzzyklus-Bedingungen. Erzeugt von <code>bilder_04/erzeuge_vrp_subtours.py</code>.</figcaption>
|
||||
</figure>
|
||||
<p><strong>Was Sie in der Abbildung sehen.</strong> Links wird jeder Kunde genau einmal angefahren — das Modell hat also gerechnet, was man ihm aufgeschrieben hat. Was fehlt, ist die Bedingung, an die man beim Aufschreiben nicht denkt: dass jede Tour am Depot beginnen muss.</p>
|
||||
<p><span class="math display">
|
||||
u_i - u_j + C \cdot x_{ij} \le C - d_j \qquad \forall i \ne j
|
||||
</span></p>
|
||||
|
|
|
|||
5
OR_HTML_04/lp.html
generated
|
|
@ -1125,6 +1125,11 @@ Pruefung: komplementaerer Schlupf fuer alle Ressourcen erfuellt.
|
|||
<hr />
|
||||
<h2 id="sec:lp-entartung">5.9 Wann Schattenpreise lügen: Entartung und Toleranzen</h2>
|
||||
<p>Der vorige Abschnitt hat gezeigt, wie wertvoll Schattenpreise sind. Dieser zeigt die zwei Fälle, in denen sie eine Entscheidung <strong>nicht</strong> tragen — und wie man beide erkennt, bevor jemand auf ihrer Grundlage eine Maschine kauft.</p>
|
||||
<figure>
|
||||
<img src="bilder_04/kap_lp_entartung.svg" alt="Abb. 5.3: Links laufen drei Geraden durch dieselbe Ecke — eine mehr, als zwei Dimensionen brauchen. Rechts die Folge: Der Schattenpreis ist keine Zahl mehr, sondern eine Spanne. Erzeugt von bilder_04/erzeuge_entartung_polyeder.py." />
|
||||
<figcaption aria-hidden="true">Abb. 5.3: Links laufen drei Geraden durch dieselbe Ecke — eine mehr, als zwei Dimensionen brauchen. Rechts die Folge: Der Schattenpreis ist keine Zahl mehr, sondern eine Spanne. Erzeugt von <code>bilder_04/erzeuge_entartung_polyeder.py</code>.</figcaption>
|
||||
</figure>
|
||||
<p><strong>Was Sie in der Abbildung sehen.</strong> Jeder Wert innerhalb der farbigen Balken erfüllt die Optimalitätsbedingungen. Zwei Solver dürfen also verschiedene Schattenpreise melden und haben beide recht — die Frage „was ist eine Stunde wert?” hat hier schlicht keine eindeutige Antwort.</p>
|
||||
<h3 id="fall-1-entartung">Fall 1: Entartung</h3>
|
||||
<p>Erinnern Sie sich an <a href="fundament.html#sec:fundament-der-zulaessige-loesungsraum-und-das-polyeder">Abschnitt 2.4</a>: In zwei Dimensionen legen <strong>zwei</strong> sich schneidende Geraden eine Ecke fest, in drei Dimensionen drei Ebenen, allgemein <span class="math inline">n</span> Restriktionen bei <span class="math inline">n</span> Variablen. Was passiert, wenn in einer Ecke <strong>mehr</strong> Nebenbedingungen aktiv sind als das Problem Variablen hat?</p>
|
||||
<p>Geometrisch: Drei Geraden laufen zufällig durch denselben Punkt. Die Ecke ist immer noch eine Ecke — aber sie ist <strong>überbestimmt</strong>. Man nennt das <strong>Entartung</strong> (<em>Degeneriertheit</em>), und sie ist in der Praxis nicht die Ausnahme, sondern die Regel: Sie entsteht überall dort, wo Kapazitäten aus derselben Planung stammen und deshalb glatt aufeinander passen — Schichtlängen, Chargengrößen, runde Vertragsmengen.</p>
|
||||
|
|
|
|||
5
OR_HTML_04/milp.html
generated
|
|
@ -395,6 +395,11 @@ Loesung eine Nebenbedingung verletzt. 'Verlust' = Abstand zum exakten Op
|
|||
<hr />
|
||||
<h2 id="sec:milp-modellierungstricks-big-m-und-logische-bedingungen">6.5 Modellierungstricks: Big-M und logische Bedingungen</h2>
|
||||
<p>Hier kommt das Handwerkszeug, das MILP so mächtig macht: Man übersetzt Geschäftsregeln in lineare Ungleichungen — mithilfe binärer Hilfsvariablen <span class="math inline">y \in \{0,1\}</span> und einer hinreichend großen Konstante <span class="math inline">M</span>.</p>
|
||||
<figure>
|
||||
<img src="bilder_04/kap_milp_big_m.svg" alt="Abb. 6.3: Dieselbe Bedingung x \le M\,y, zwei Zahlen für M. Gezeigt ist die LP-Relaxation — also das, was der Solver sieht, bevor y ganzzahlig wird. Erzeugt von bilder_04/erzeuge_big_m_vergleich.py." />
|
||||
<figcaption aria-hidden="true">Abb. 6.3: Dieselbe Bedingung <span class="math inline">x \le M\,y</span>, zwei Zahlen für <span class="math inline">M</span>. Gezeigt ist die LP-Relaxation — also das, was der Solver sieht, bevor <span class="math inline">y</span> ganzzahlig wird. Erzeugt von <code>bilder_04/erzeuge_big_m_vergleich.py</code>.</figcaption>
|
||||
</figure>
|
||||
<p><strong>Was Sie in der Abbildung sehen.</strong> Für <span class="math inline">y \in \{0,1\}</span> sind beide Modelle gleichwertig; die beiden blauen Punkte stimmen überein. Verschieden ist nur, was dazwischen erlaubt ist — und rechts genügt bereits <span class="math inline">y = 0{,}08</span>, um die volle Kapazität freizugeben.</p>
|
||||
<h3 id="muster-1-fixkosten-aktivierungsschalter">Muster 1 — Fixkosten / Aktivierungsschalter</h3>
|
||||
<p><em>Regel:</em> Wird Maschine <span class="math inline">j</span> genutzt (<span class="math inline">x_j > 0</span>), fallen Rüstkosten <span class="math inline">F_j</span> an.</p>
|
||||
<p><span class="math display">
|
||||
|
|
|
|||
7
OR_HTML_04/modellierung.html
generated
|
|
@ -521,6 +521,7 @@ Es muss also ausgewaehlt werden. 3 der Kunden sind Stammkunden:
|
|||
"Maschinen besser auslasten" 205,000 54,000 100% 4 1 5
|
||||
"Deckungsbeitrag maximieren" 222,000 78,500 100% 2 1 8
|
||||
|
||||
|
||||
Der Umsatzplan bringt 5,000 EUR mehr Umsatz und 12,500 EUR WENIGER
|
||||
Deckungsbeitrag. Fuer 5,000 EUR Umsatz werden also 12,500 EUR Ergebnis
|
||||
aufgegeben - und 5 von 6 Stammkundenauftraegen dazu.
|
||||
|
|
@ -594,7 +595,11 @@ Drei Dinge, die deshalb vor dem ersten Modell geklaert gehoeren:
|
|||
zwar genau dann, wenn ein besonders grosser Auftrag hereinkommt.
|
||||
Weiche Regeln antworten auch dann noch.
|
||||
====================================================================================</code></pre>
|
||||
<h3 id="teil-1-drei-ziele-drei-pläne">Teil 1: Drei Ziele, drei Pläne</h3>
|
||||
<figure>
|
||||
<img src="bilder_04/kap_modellierung_zielgroessen.svg" alt="Abb. 4.1: Die drei Pläne aus der Tabelle, je Achse auf den besten der drei bezogen. Erzeugt von bilder_04/erzeuge_zielgroessen_radar.py." />
|
||||
<figcaption aria-hidden="true">Abb. 4.1: Die drei Pläne aus der Tabelle, je Achse auf den besten der drei bezogen. Erzeugt von <code>bilder_04/erzeuge_zielgroessen_radar.py</code>.</figcaption>
|
||||
</figure>
|
||||
<p><strong>Was Sie in der Abbildung sehen.</strong> Jeder Plan gewinnt auf seiner eigenen Achse — das ist keine Überraschung, sondern die Definition. Interessant ist, was daneben passiert: Der Auslastungsplan liegt bei der Auslastung gleichauf mit den anderen und verliert beim Deckungsbeitrag ein knappes Drittel. ### Teil 1: Drei Ziele, drei Pläne</p>
|
||||
<table>
|
||||
<colgroup>
|
||||
<col style="width: 15%" />
|
||||
|
|
|
|||
5
OR_HTML_04/oekosystem.html
generated
|
|
@ -1332,6 +1332,11 @@ Groesse Variante Aufbau Loesen Aufba
|
|||
-> Zielwert (alle gleich) 8316.24 Spanne 1.8e-12
|
||||
----------------------------------------------------------------------------------------</code></pre>
|
||||
<p><strong>Lesen Sie die letzte Spalte.</strong> Bei 62 500 Variablen verbringt Variante A <strong>69 % der Gesamtzeit damit, das Modell überhaupt aufzuschreiben</strong> — und nur 31 % mit Rechnen. Die vektorisierten Varianten kehren das Verhältnis um: 1 % Aufbau, 99 % Solver. Erst dort ist ein schnellerer Solver überhaupt die richtige Stellschraube.</p>
|
||||
<figure>
|
||||
<img src="bilder_04/kap_oekosystem_laufzeit.svg" alt="Abb. 3.2: Dieselbe Aufgabe, zwei Arten sie aufzuschreiben — aufgeteilt in Aufbauzeit (Python) und Lösezeit (C++). Werte aus der Messung oben. Erzeugt von bilder_04/erzeuge_laufzeit_aufbau.py." />
|
||||
<figcaption aria-hidden="true">Abb. 3.2: Dieselbe Aufgabe, zwei Arten sie aufzuschreiben — aufgeteilt in Aufbauzeit (Python) und Lösezeit (C++). Werte aus der Messung oben. Erzeugt von <code>bilder_04/erzeuge_laufzeit_aufbau.py</code>.</figcaption>
|
||||
</figure>
|
||||
<p><strong>Was Sie in der Abbildung sehen.</strong> Der rote Anteil ist die Zeit, in der der Solver noch gar nicht rechnet. Bei Variante A wächst er mit dem Modell, bei Variante C verschwindet er — dieselbe Mathematik, nur anders aufgeschrieben.</p>
|
||||
<blockquote>
|
||||
<p><strong>💻 Code-Durchgang</strong></p>
|
||||
<table>
|
||||
|
|
|
|||
7
OR_HTML_04/prognose.html
generated
|
|
@ -430,6 +430,7 @@ Training: Tag 1 bis 1000. Bewertung: die restlichen 4000 Tage.
|
|||
bester MSE: bestelle die Punktprognose
|
||||
beste Kosten: Quantilregression aufs kritische Quantil
|
||||
|
||||
|
||||
Das Verfahren mit dem besten MSE hat die HOECHSTEN Kosten, und das
|
||||
Verfahren mit den besten Kosten hat einen um 13% SCHLECHTEREN MSE.
|
||||
Wer Prognosemodelle nach MSE auswaehlt, waehlt hier das falsche.
|
||||
|
|
@ -504,7 +505,11 @@ erfordert Bibliotheken wie cvxpylayers. Die dritte Zeile der Tabelle
|
|||
oben ist seine einfachste denkbare Form: ein einziger Parameter, auf
|
||||
Kosten statt auf Fehler trainiert.
|
||||
====================================================================================</code></pre>
|
||||
<hr />
|
||||
<figure>
|
||||
<img src="bilder_04/kap_prognose_entscheidungsguete.svg" alt="Abb. 15.1: Dieselben vier Verfahren, zwei Maßstäbe. Grün ist jeweils der beste Wert. Erzeugt von bilder_04/erzeuge_predict_vs_optimize.py." />
|
||||
<figcaption aria-hidden="true">Abb. 15.1: Dieselben vier Verfahren, zwei Maßstäbe. Grün ist jeweils der beste Wert. Erzeugt von <code>bilder_04/erzeuge_predict_vs_optimize.py</code>.</figcaption>
|
||||
</figure>
|
||||
<h2 id="was-sie-in-der-abbildung-sehen.-links-und-rechts-gewinnt-nicht-dasselbe-verfahren.-die-punktprognose-hat-den-besten-prognosefehler-und-die-höchsten-kosten-beide-maßstäbe-messen-etwas-anderes-und-nur-einer-davon-steht-am-ende-in-der-bilanz."><strong>Was Sie in der Abbildung sehen.</strong> Links und rechts gewinnt nicht dasselbe Verfahren. Die Punktprognose hat den besten Prognosefehler und die höchsten Kosten — beide Maßstäbe messen etwas anderes, und nur einer davon steht am Ende in der Bilanz.</h2>
|
||||
<h2 id="sec:prognose-befund">15.5 Der Befund</h2>
|
||||
<table>
|
||||
<colgroup>
|
||||
|
|
|
|||
9
OR_HTML_04/supplychain.html
generated
|
|
@ -771,10 +771,15 @@ Was Versorgungssicherheit kostet
|
|||
</tbody>
|
||||
</table>
|
||||
<p>Der Erwartungswert-Plan kostet in der Wirklichkeit <strong>149 % mehr</strong>, und sein schlimmster Tag ist sechsmal so teuer.</p>
|
||||
<figure>
|
||||
<img src="bilder_04/kap_supplychain_lastabwurf.svg" alt="Abb. 17.1: Die 40 Szenarien einzeln, aufsteigend nach Kosten. Punkte markieren Szenarien mit Lastabwurf. Erzeugt von bilder_04/erzeuge_lastabwurf_verteilung.py." />
|
||||
<figcaption aria-hidden="true">Abb. 17.1: Die 40 Szenarien einzeln, aufsteigend nach Kosten. Punkte markieren Szenarien mit Lastabwurf. Erzeugt von <code>bilder_04/erzeuge_lastabwurf_verteilung.py</code>.</figcaption>
|
||||
</figure>
|
||||
<p><strong>Was Sie in der Abbildung sehen.</strong> Der Erwartungswert-Plan ist in den ruhigen Szenarien tatsächlich der billigere — die rote Kurve verläuft links von der grünen. Er kippt erst rechts, und zwar so weit, dass der Mittelwert das Vorzeichen wechselt.</p>
|
||||
<p><strong>Wofür genau?</strong> Die Summendifferenz von 523 514 € sagt noch nicht, wo sie entsteht. Zerlegt man sie nach Kostenarten, wird aus dem Befund eine Aussage, die man vorlegen kann:</p>
|
||||
<figure>
|
||||
<img src="bilder_04/kap_supplychain_wirkung.svg" alt="Abb. 17.1: Wofür der Erwartungswert-Plan 149 % mehr bezahlt" />
|
||||
<figcaption aria-hidden="true">Abb. 17.1: Wofür der Erwartungswert-Plan 149 % mehr bezahlt</figcaption>
|
||||
<img src="bilder_04/kap_supplychain_wirkung.svg" alt="Abb. 17.2: Wofür der Erwartungswert-Plan 149 % mehr bezahlt" />
|
||||
<figcaption aria-hidden="true">Abb. 17.2: Wofür der Erwartungswert-Plan 149 % mehr bezahlt</figcaption>
|
||||
</figure>
|
||||
<p>Drei Dinge stehen darin, die die Tabelle nicht zeigt. Die <strong>Anfahrkosten sind in beiden Plänen gleich</strong> (58 000 €) — der Erwartungswert-Plan spart also nicht dort, wo man es vermuten würde. Er spart <strong>12 725 € Brennstoff</strong>, weil er weniger Blockstunden vorhält. Und er bezahlt dafür <strong>536 239 € Lastabwurf</strong>: das <strong>42-fache</strong> seiner Ersparnis.</p>
|
||||
<p>Genau diese Gegenüberstellung ist es, die eine Investitionsentscheidung trägt. „149 % mehr” ist eine Zahl, über die man streiten kann; „wir sparen 12 725 € und riskieren dafür 536 239 €” ist eine, über die man entscheidet.</p>
|
||||
|
|
|
|||
9
OR_HTML_04/unsicherheit.html
generated
|
|
@ -317,6 +317,11 @@ q^{*} = \frac{c_{\text{zu wenig}}}{c_{\text{zu wenig}} + c_{\text{zu viel}}}
|
|||
<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">"</span><span class="ch">\n</span><span class="st">Das Optimum liegt exakt auf einem Szenariowert (250 = 'Volatil'),"</span>)</span>
|
||||
<span id="cb3-53"><a href="#cb3-53" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"nicht beim Mittelwert 225 - typisch fuer asymmetrische Kostenfunktionen."</span>)</span></code></pre></div>
|
||||
<figure>
|
||||
<img src="bilder_04/kap_unsicherheit_durchschnitt.svg" alt="Abb. 12.1: Erwartete Gesamtkosten über allen Bestellmengen, gerechnet aus den drei Szenarien der Handrechnung. Erzeugt von bilder_04/erzeuge_fluch_durchschnitt.py." />
|
||||
<figcaption aria-hidden="true">Abb. 12.1: Erwartete Gesamtkosten über allen Bestellmengen, gerechnet aus den drei Szenarien der Handrechnung. Erzeugt von <code>bilder_04/erzeuge_fluch_durchschnitt.py</code>.</figcaption>
|
||||
</figure>
|
||||
<p><strong>Was Sie in der Abbildung sehen.</strong> Die Kurve ist nicht symmetrisch: Sie fällt links flach und steigt rechts steil, weil Fehlmenge 120 € kostet und Überschuss nur 5 €. Ihr tiefster Punkt liegt deshalb rechts vom Erwartungswert — nicht auf ihm.</p>
|
||||
<p><strong>Erwartete Ausgabe:</strong></p>
|
||||
<pre><code>======================================================================
|
||||
DER FLUCH DES DURCHSCHNITTS
|
||||
|
|
@ -344,8 +349,8 @@ nicht beim Mittelwert 225 - typisch fuer asymmetrische Kostenfunktionen.</code><
|
|||
</blockquote>
|
||||
<h3 id="die-vier-ansätze-im-überblick">Die vier Ansätze im Überblick</h3>
|
||||
<figure>
|
||||
<img src="bilder_04/kap09_unsicherheit_ansaetze.svg" alt="Abb. 12.1: Ansätze für Optimierung unter Unsicherheit" />
|
||||
<figcaption aria-hidden="true">Abb. 12.1: Ansätze für Optimierung unter Unsicherheit</figcaption>
|
||||
<img src="bilder_04/kap09_unsicherheit_ansaetze.svg" alt="Abb. 12.2: Ansätze für Optimierung unter Unsicherheit" />
|
||||
<figcaption aria-hidden="true">Abb. 12.2: Ansätze für Optimierung unter Unsicherheit</figcaption>
|
||||
</figure>
|
||||
<table>
|
||||
<colgroup>
|
||||
|
|
|
|||
63
Operations_Research_mit_Python_Version_04.md
generated
|
|
@ -868,6 +868,11 @@ Die Behauptung „vollständiges Durchprobieren ist unmöglich“ wird oft aufge
|
|||
>
|
||||
> **Der springende Punkt:** Zwischen $n = 15$ (22 Minuten, machbar) und $n = 20$ (77 Jahre, hoffnungslos) liegen nur **fünf zusätzliche Aufträge**. Es gibt keine sanfte Verschlechterung — es gibt eine Wand.
|
||||
|
||||

|
||||
|
||||
**Was Sie in der Abbildung sehen.** Der markierte Streifen zwischen $n = 15$ und $n = 20$ ist der eigentliche Befund: Dort wird aus einer Kaffeepause ein Menschenleben. Wer eine Maschine kauft, die tausendmal schneller rechnet, verschiebt diese Wand um genau zwei Aufträge.
|
||||
|
||||
Und die Rechnung ist noch geschönt: Ein schnellerer Rechner hilft praktisch nicht. Selbst eine Beschleunigung um den Faktor **eine Million** verschiebt die Grenze von $n=20$ nur auf etwa $n=24$. Gegen kombinatorisches Wachstum ist Hardware machtlos.
|
||||
|
||||
Das folgende Programm macht diesen Effekt erfahrbar. Es löst dasselbe Zuordnungsproblem zweimal — einmal durch vollständiges Durchprobieren, einmal mit einem OR-Solver — und vergleicht die Laufzeiten.
|
||||
|
|
@ -1040,7 +1045,7 @@ Heute treibt OR die Kernsysteme moderner Industrien an:
|
|||
|
||||
Dies ist der wichtigste Abschnitt des Kapitels. Jedes Optimierungsproblem der Welt lässt sich — unabhängig von der Branche — auf vier Elemente reduzieren. Wenn Sie später vor einem neuen Problem sitzen, ist dies Ihre Checkliste.
|
||||
|
||||

|
||||

|
||||
|
||||
### Baustein 1 — Entscheidungsvariablen ($x$)
|
||||
|
||||
|
|
@ -3904,6 +3909,11 @@ Groesse Variante Aufbau Loesen Aufba
|
|||
|
||||
**Lesen Sie die letzte Spalte.** Bei 62 500 Variablen verbringt Variante A **69 % der Gesamtzeit damit, das Modell überhaupt aufzuschreiben** — und nur 31 % mit Rechnen. Die vektorisierten Varianten kehren das Verhältnis um: 1 % Aufbau, 99 % Solver. Erst dort ist ein schnellerer Solver überhaupt die richtige Stellschraube.
|
||||
|
||||

|
||||
|
||||
**Was Sie in der Abbildung sehen.** Der rote Anteil ist die Zeit, in der der Solver noch gar nicht rechnet. Bei Variante A wächst er mit dem Modell, bei Variante C verschwindet er — dieselbe Mathematik, nur anders aufgeschrieben.
|
||||
|
||||
> **💻 Code-Durchgang**
|
||||
>
|
||||
> | Stelle | Was passiert | Warum es zählt |
|
||||
|
|
@ -4390,6 +4400,7 @@ Es muss also ausgewaehlt werden. 3 der Kunden sind Stammkunden:
|
|||
"Maschinen besser auslasten" 205,000 54,000 100% 4 1 5
|
||||
"Deckungsbeitrag maximieren" 222,000 78,500 100% 2 1 8
|
||||
|
||||
|
||||
Der Umsatzplan bringt 5,000 EUR mehr Umsatz und 12,500 EUR WENIGER
|
||||
Deckungsbeitrag. Fuer 5,000 EUR Umsatz werden also 12,500 EUR Ergebnis
|
||||
aufgegeben - und 5 von 6 Stammkundenauftraegen dazu.
|
||||
|
|
@ -4465,6 +4476,11 @@ Drei Dinge, die deshalb vor dem ersten Modell geklaert gehoeren:
|
|||
====================================================================================
|
||||
```
|
||||
|
||||
|
||||

|
||||
|
||||
**Was Sie in der Abbildung sehen.** Jeder Plan gewinnt auf seiner eigenen Achse — das ist keine Überraschung, sondern die Definition. Interessant ist, was daneben passiert: Der Auslastungsplan liegt bei der Auslastung gleichauf mit den anderen und verliert beim Deckungsbeitrag ein knappes Drittel.
|
||||
### Teil 1: Drei Ziele, drei Pläne
|
||||
|
||||
| Zielgröße | Umsatz | Deckungsbeitrag | Auslastung | abgelehnte Stammaufträge |
|
||||
|
|
@ -5539,6 +5555,11 @@ Der Zukauf für 18 €/h ist hochprofitabel: Jede zusätzliche Prüfstunde bring
|
|||
|
||||
Der vorige Abschnitt hat gezeigt, wie wertvoll Schattenpreise sind. Dieser zeigt die zwei Fälle, in denen sie eine Entscheidung **nicht** tragen — und wie man beide erkennt, bevor jemand auf ihrer Grundlage eine Maschine kauft.
|
||||
|
||||

|
||||
|
||||
**Was Sie in der Abbildung sehen.** Jeder Wert innerhalb der farbigen Balken erfüllt die Optimalitätsbedingungen. Zwei Solver dürfen also verschiedene Schattenpreise melden und haben beide recht — die Frage „was ist eine Stunde wert?" hat hier schlicht keine eindeutige Antwort.
|
||||
|
||||
### Fall 1: Entartung
|
||||
|
||||
Erinnern Sie sich an [Abschnitt 2.4](#sec:fundament-der-zulaessige-loesungsraum-und-das-polyeder): In zwei Dimensionen legen **zwei** sich schneidende Geraden eine Ecke fest, in drei Dimensionen drei Ebenen, allgemein $n$ Restriktionen bei $n$ Variablen. Was passiert, wenn in einer Ecke **mehr** Nebenbedingungen aktiv sind als das Problem Variablen hat?
|
||||
|
|
@ -6225,6 +6246,11 @@ Zusätzlich fügen Solver **Schnittebenen**`\index{Schnittebenen}`{=latex} (*cut
|
|||
|
||||
Hier kommt das Handwerkszeug, das MILP so mächtig macht:`\index{Big-M-Methode}`{=latex} Man übersetzt Geschäftsregeln in lineare Ungleichungen — mithilfe binärer Hilfsvariablen $y \in \{0,1\}$ und einer hinreichend großen Konstante $M$.
|
||||
|
||||

|
||||
|
||||
**Was Sie in der Abbildung sehen.** Für $y \in \{0,1\}$ sind beide Modelle gleichwertig; die beiden blauen Punkte stimmen überein. Verschieden ist nur, was dazwischen erlaubt ist — und rechts genügt bereits $y = 0{,}08$, um die volle Kapazität freizugeben.
|
||||
|
||||
### Muster 1 — Fixkosten / Aktivierungsschalter
|
||||
|
||||
*Regel:* Wird Maschine $j$ genutzt ($x_j > 0$), fallen Rüstkosten $F_j$ an.
|
||||
|
|
@ -9597,6 +9623,11 @@ Die Touren sind aus demselben Grund **Luftlinien und keine Straßenrouten**: Das
|
|||
|
||||
Ein naives Modell erlaubt **Subtouren**`\index{Subtour}`{=latex}: isolierte Kreise, die das Depot nie anfahren. Die klassische Gegenmaßnahme ist die **MTZ-Formulierung**`\index{MTZ-Formulierung}`{=latex} nach Miller, Tucker und Zemlin. Man führt Rangvariablen $u_i$ ein (die Position des Knotens in der Tour):
|
||||
|
||||

|
||||
|
||||
**Was Sie in der Abbildung sehen.** Links wird jeder Kunde genau einmal angefahren — das Modell hat also gerechnet, was man ihm aufgeschrieben hat. Was fehlt, ist die Bedingung, an die man beim Aufschreiben nicht denkt: dass jede Tour am Depot beginnen muss.
|
||||
|
||||
$$
|
||||
u_i - u_j + C \cdot x_{ij} \le C - d_j \qquad \forall i \ne j
|
||||
$$
|
||||
|
|
@ -11604,6 +11635,11 @@ Die Idee ist alt (Gilmore und Gomory, 1961) und in einem Satz gesagt:
|
|||
|
||||
Die Frage „lohnt sich noch ein Muster?" beantworten die **Dualwerte** des Master-LPs. Der Dualwert $\pi_i$ zur Bedarfszeile $i$ ist genau der Schattenpreis aus [Abschnitt 5.6](#sec:lp-dualitaet-und-schattenpreise): Er sagt, wie viele Rollen ein zusätzliches Stück der Breite $i$ kostet.
|
||||
|
||||

|
||||
|
||||
**Was Sie in der Abbildung sehen.** Die Information fließt in beide Richtungen: Preise nach rechts, Muster nach links. Der Ausgang unten wird erst geöffnet, wenn das Pricing nichts mehr findet — und genau das ist der Optimalitätsbeweis.
|
||||
|
||||
Ein neues Muster $a$ lohnt sich, wenn seine **reduzierten Kosten** negativ sind:
|
||||
|
||||
$$
|
||||
|
|
@ -13507,6 +13543,11 @@ if __name__ == "__main__":
|
|||
print("nicht beim Mittelwert 225 - typisch fuer asymmetrische Kostenfunktionen.")
|
||||
```
|
||||
|
||||

|
||||
|
||||
**Was Sie in der Abbildung sehen.** Die Kurve ist nicht symmetrisch: Sie fällt links flach und steigt rechts steil, weil Fehlmenge 120 € kostet und Überschuss nur 5 €. Ihr tiefster Punkt liegt deshalb rechts vom Erwartungswert — nicht auf ihm.
|
||||
|
||||
**Erwartete Ausgabe:**
|
||||
|
||||
```
|
||||
|
|
@ -13539,7 +13580,7 @@ Kein Rechentrick, sondern brute-force über alle ganzzahligen $x$ von 0 bis 500
|
|||
|
||||
### Die vier Ansätze im Überblick
|
||||
|
||||

|
||||

|
||||
|
||||
| | **Monte-Carlo** | **Stochastische Programmierung** | **Robuste Optimierung** | **Chance Constraints** |
|
||||
| --- | --- | --- | --- | --- |
|
||||
|
|
@ -15021,6 +15062,11 @@ if __name__ == "__main__":
|
|||
print("=" * 84)
|
||||
```
|
||||
|
||||

|
||||
|
||||
**Was Sie in der Abbildung sehen.** Die optimale Kurve liegt nicht in der Mitte zwischen den Extremen, sondern ist gekrümmt: vorne mehr, hinten weniger. Diese Krümmung ist die Antwort auf die Frage, wie teuer Eile gegen Warten steht.
|
||||
|
||||
**Erwartete Ausgabe:**
|
||||
|
||||
```
|
||||
|
|
@ -16248,6 +16294,7 @@ Training: Tag 1 bis 1000. Bewertung: die restlichen 4000 Tage.
|
|||
bester MSE: bestelle die Punktprognose
|
||||
beste Kosten: Quantilregression aufs kritische Quantil
|
||||
|
||||
|
||||
Das Verfahren mit dem besten MSE hat die HOECHSTEN Kosten, und das
|
||||
Verfahren mit den besten Kosten hat einen um 13% SCHLECHTEREN MSE.
|
||||
Wer Prognosemodelle nach MSE auswaehlt, waehlt hier das falsche.
|
||||
|
|
@ -16324,6 +16371,11 @@ Kosten statt auf Fehler trainiert.
|
|||
====================================================================================
|
||||
```
|
||||
|
||||
|
||||

|
||||
|
||||
**Was Sie in der Abbildung sehen.** Links und rechts gewinnt nicht dasselbe Verfahren. Die Punktprognose hat den besten Prognosefehler und die höchsten Kosten — beide Maßstäbe messen etwas anderes, und nur einer davon steht am Ende in der Bilanz.
|
||||
---
|
||||
|
||||
## 15.5 Der Befund {#sec:prognose-befund}
|
||||
|
|
@ -17724,9 +17776,14 @@ Was Versorgungssicherheit kostet
|
|||
|
||||
Der Erwartungswert-Plan kostet in der Wirklichkeit **149 % mehr**, und sein schlimmster Tag ist sechsmal so teuer.
|
||||
|
||||

|
||||
|
||||
**Was Sie in der Abbildung sehen.** Der Erwartungswert-Plan ist in den ruhigen Szenarien tatsächlich der billigere — die rote Kurve verläuft links von der grünen. Er kippt erst rechts, und zwar so weit, dass der Mittelwert das Vorzeichen wechselt.
|
||||
|
||||
**Wofür genau?** Die Summendifferenz von 523 514 € sagt noch nicht, wo sie entsteht. Zerlegt man sie nach Kostenarten, wird aus dem Befund eine Aussage, die man vorlegen kann:
|
||||
|
||||

|
||||

|
||||
|
||||
Drei Dinge stehen darin, die die Tabelle nicht zeigt. Die **Anfahrkosten sind in beiden Plänen gleich** (58 000 €) — der Erwartungswert-Plan spart also nicht dort, wo man es vermuten würde. Er spart **12 725 € Brennstoff**, weil er weniger Blockstunden vorhält. Und er bezahlt dafür **536 239 € Lastabwurf**: das **42-fache** seiner Ersparnis.
|
||||
|
||||
|
|
|
|||
|
|
@ -188,6 +188,14 @@ Zahl im Text.
|
|||
> hoffnungslos) liegen nur **fünf zusätzliche Aufträge**. Es gibt keine sanfte
|
||||
> Verschlechterung — es gibt eine Wand.
|
||||
|
||||

|
||||
|
||||
**Was Sie in der Abbildung sehen.** Der markierte Streifen zwischen $n = 15$ und $n = 20$ ist
|
||||
der eigentliche Befund: Dort wird aus einer Kaffeepause ein Menschenleben. Wer eine Maschine
|
||||
kauft, die tausendmal schneller rechnet, verschiebt diese Wand um genau zwei Aufträge.
|
||||
|
||||
Und die Rechnung ist noch geschönt: Ein schnellerer Rechner hilft praktisch nicht. Selbst
|
||||
eine Beschleunigung um den Faktor **eine Million** verschiebt die Grenze von $n=20$ nur auf
|
||||
etwa $n=24$. Gegen kombinatorisches Wachstum ist Hardware machtlos.
|
||||
|
|
|
|||
|
|
@ -1152,6 +1152,14 @@ Gesamtzeit damit, das Modell überhaupt aufzuschreiben** — und nur 31 % mit Re
|
|||
vektorisierten Varianten kehren das Verhältnis um: 1 % Aufbau, 99 % Solver. Erst dort ist
|
||||
ein schnellerer Solver überhaupt die richtige Stellschraube.
|
||||
|
||||

|
||||
|
||||
**Was Sie in der Abbildung sehen.** Der rote Anteil ist die Zeit, in der der Solver noch gar
|
||||
nicht rechnet. Bei Variante A wächst er mit dem Modell, bei Variante C verschwindet er —
|
||||
dieselbe Mathematik, nur anders aufgeschrieben.
|
||||
|
||||
> **💻 Code-Durchgang**
|
||||
>
|
||||
> | Stelle | Was passiert | Warum es zählt |
|
||||
|
|
|
|||
|
|
@ -411,6 +411,7 @@ Es muss also ausgewaehlt werden. 3 der Kunden sind Stammkunden:
|
|||
"Maschinen besser auslasten" 205,000 54,000 100% 4 1 5
|
||||
"Deckungsbeitrag maximieren" 222,000 78,500 100% 2 1 8
|
||||
|
||||
|
||||
Der Umsatzplan bringt 5,000 EUR mehr Umsatz und 12,500 EUR WENIGER
|
||||
Deckungsbeitrag. Fuer 5,000 EUR Umsatz werden also 12,500 EUR Ergebnis
|
||||
aufgegeben - und 5 von 6 Stammkundenauftraegen dazu.
|
||||
|
|
@ -486,6 +487,14 @@ Drei Dinge, die deshalb vor dem ersten Modell geklaert gehoeren:
|
|||
====================================================================================
|
||||
```
|
||||
|
||||
|
||||

|
||||
|
||||
**Was Sie in der Abbildung sehen.** Jeder Plan gewinnt auf seiner eigenen Achse — das ist
|
||||
keine Überraschung, sondern die Definition. Interessant ist, was daneben passiert: Der
|
||||
Auslastungsplan liegt bei der Auslastung gleichauf mit den anderen und verliert beim
|
||||
Deckungsbeitrag ein knappes Drittel.
|
||||
### Teil 1: Drei Ziele, drei Pläne
|
||||
|
||||
| Zielgröße | Umsatz | Deckungsbeitrag | Auslastung | abgelehnte Stammaufträge |
|
||||
|
|
|
|||
|
|
@ -944,6 +944,15 @@ Der vorige Abschnitt hat gezeigt, wie wertvoll Schattenpreise sind. Dieser zeigt
|
|||
Fälle, in denen sie eine Entscheidung **nicht** tragen — und wie man beide erkennt, bevor
|
||||
jemand auf ihrer Grundlage eine Maschine kauft.
|
||||
|
||||

|
||||
|
||||
**Was Sie in der Abbildung sehen.** Jeder Wert innerhalb der farbigen Balken erfüllt die
|
||||
Optimalitätsbedingungen. Zwei Solver dürfen also verschiedene Schattenpreise melden und haben
|
||||
beide recht — die Frage „was ist eine Stunde wert?" hat hier schlicht keine eindeutige
|
||||
Antwort.
|
||||
|
||||
### Fall 1: Entartung
|
||||
|
||||
Erinnern Sie sich an {ref:sec:fundament-der-zulaessige-loesungsraum-und-das-polyeder}: In zwei
|
||||
|
|
|
|||
|
|
@ -366,6 +366,14 @@ Hier kommt das Handwerkszeug, das MILP so mächtig macht:{idx:Big-M-Methode} Man
|
|||
lineare Ungleichungen — mithilfe binärer Hilfsvariablen $y \in \{0,1\}$ und einer
|
||||
hinreichend großen Konstante $M$.
|
||||
|
||||

|
||||
|
||||
**Was Sie in der Abbildung sehen.** Für $y \in \{0,1\}$ sind beide Modelle gleichwertig; die
|
||||
beiden blauen Punkte stimmen überein. Verschieden ist nur, was dazwischen erlaubt ist — und
|
||||
rechts genügt bereits $y = 0{,}08$, um die volle Kapazität freizugeben.
|
||||
|
||||
### Muster 1 — Fixkosten / Aktivierungsschalter
|
||||
|
||||
*Regel:* Wird Maschine $j$ genutzt ($x_j > 0$), fallen Rüstkosten $F_j$ an.
|
||||
|
|
|
|||
|
|
@ -514,6 +514,14 @@ Ein naives Modell erlaubt **Subtouren**{idx:Subtour}: isolierte Kreise, die das
|
|||
klassische Gegenmaßnahme ist die **MTZ-Formulierung**{idx:MTZ-Formulierung} nach Miller, Tucker und Zemlin. Man
|
||||
führt Rangvariablen $u_i$ ein (die Position des Knotens in der Tour):
|
||||
|
||||

|
||||
|
||||
**Was Sie in der Abbildung sehen.** Links wird jeder Kunde genau einmal angefahren — das
|
||||
Modell hat also gerechnet, was man ihm aufgeschrieben hat. Was fehlt, ist die Bedingung, an
|
||||
die man beim Aufschreiben nicht denkt: dass jede Tour am Depot beginnen muss.
|
||||
|
||||
$$
|
||||
u_i - u_j + C \cdot x_{ij} \le C - d_j \qquad \forall i \ne j
|
||||
$$
|
||||
|
|
|
|||
|
|
@ -160,6 +160,13 @@ Dualwert $\pi_i$ zur Bedarfszeile $i$ ist genau der Schattenpreis aus
|
|||
{ref:sec:lp-dualitaet-und-schattenpreise}: Er sagt, wie viele Rollen ein zusätzliches Stück
|
||||
der Breite $i$ kostet.
|
||||
|
||||

|
||||
|
||||
**Was Sie in der Abbildung sehen.** Die Information fließt in beide Richtungen: Preise nach
|
||||
rechts, Muster nach links. Der Ausgang unten wird erst geöffnet, wenn das Pricing nichts mehr
|
||||
findet — und genau das ist der Optimalitätsbeweis.
|
||||
|
||||
Ein neues Muster $a$ lohnt sich, wenn seine **reduzierten Kosten** negativ sind:
|
||||
|
||||
$$
|
||||
|
|
|
|||
|
|
@ -225,6 +225,14 @@ if __name__ == "__main__":
|
|||
print("nicht beim Mittelwert 225 - typisch fuer asymmetrische Kostenfunktionen.")
|
||||
```
|
||||
|
||||

|
||||
|
||||
**Was Sie in der Abbildung sehen.** Die Kurve ist nicht symmetrisch: Sie fällt links flach und
|
||||
steigt rechts steil, weil Fehlmenge 120 € kostet und Überschuss nur 5 €. Ihr tiefster Punkt
|
||||
liegt deshalb rechts vom Erwartungswert — nicht auf ihm.
|
||||
|
||||
**Erwartete Ausgabe:**
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -581,6 +581,13 @@ if __name__ == "__main__":
|
|||
print("=" * 84)
|
||||
```
|
||||
|
||||

|
||||
|
||||
**Was Sie in der Abbildung sehen.** Die optimale Kurve liegt nicht in der Mitte zwischen den
|
||||
Extremen, sondern ist gekrümmt: vorne mehr, hinten weniger. Diese Krümmung ist die Antwort
|
||||
auf die Frage, wie teuer Eile gegen Warten steht.
|
||||
|
||||
**Erwartete Ausgabe:**
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -398,6 +398,7 @@ Training: Tag 1 bis 1000. Bewertung: die restlichen 4000 Tage.
|
|||
bester MSE: bestelle die Punktprognose
|
||||
beste Kosten: Quantilregression aufs kritische Quantil
|
||||
|
||||
|
||||
Das Verfahren mit dem besten MSE hat die HOECHSTEN Kosten, und das
|
||||
Verfahren mit den besten Kosten hat einen um 13% SCHLECHTEREN MSE.
|
||||
Wer Prognosemodelle nach MSE auswaehlt, waehlt hier das falsche.
|
||||
|
|
@ -474,6 +475,13 @@ Kosten statt auf Fehler trainiert.
|
|||
====================================================================================
|
||||
```
|
||||
|
||||
|
||||

|
||||
|
||||
**Was Sie in der Abbildung sehen.** Links und rechts gewinnt nicht dasselbe Verfahren. Die
|
||||
Punktprognose hat den besten Prognosefehler und die höchsten Kosten — beide Maßstäbe messen
|
||||
etwas anderes, und nur einer davon steht am Ende in der Bilanz.
|
||||
---
|
||||
|
||||
## Der Befund {#sec:prognose-befund}
|
||||
|
|
|
|||
|
|
@ -595,6 +595,14 @@ Was Versorgungssicherheit kostet
|
|||
Der Erwartungswert-Plan kostet in der Wirklichkeit **149 % mehr**, und sein schlimmster Tag
|
||||
ist sechsmal so teuer.
|
||||
|
||||

|
||||
|
||||
**Was Sie in der Abbildung sehen.** Der Erwartungswert-Plan ist in den ruhigen Szenarien
|
||||
tatsächlich der billigere — die rote Kurve verläuft links von der grünen. Er kippt erst
|
||||
rechts, und zwar so weit, dass der Mittelwert das Vorzeichen wechselt.
|
||||
|
||||
**Wofür genau?** Die Summendifferenz von 523 514 € sagt noch nicht, wo sie entsteht. Zerlegt
|
||||
man sie nach Kostenarten, wird aus dem Befund eine Aussage, die man vorlegen kann:
|
||||
|
||||
|
|
|
|||
52
PROGRESS.md
|
|
@ -2487,6 +2487,58 @@ Gegengeprüft mit einem simulierten Klon: Das Kritik-Verzeichnis fehlt dort voll
|
|||
Historie. Wer einen alten Commit auscheckt, bekommt sie weiterhin. Sie wirklich zu tilgen,
|
||||
verlangte `git filter-repo` und einen erzwungenen Push — dafür ist der Anlass zu gering.
|
||||
|
||||
### ✅ Grafik-Upgrade, Stufe 1: elf neue Diagramme
|
||||
|
||||
`Grafik_Upgrade.md` verlangt „37 didaktisch wertvolle Visualisierungen". **Es sind 19.** Die
|
||||
Abschnitte „Modul 1–4" wiederholen die Grafiken 1–18 unter den Nummern 19–36; nur Grafik 37
|
||||
ist zusätzlich. Belegt an den Skriptnamen: 38 Nennungen, 19 verschiedene Dateien —
|
||||
`erzeuge_kombinatorik_wand.py` steht dreimal darin. Von den 19 betreffen neun bereits
|
||||
vorhandene Diagramme; Grafik 37 (OR-Architektur) existiert sogar zweimal.
|
||||
|
||||
Umgesetzt sind die **zehn wirklich neuen** plus eine elfte, die aus einem Sachfehler des
|
||||
Auftrags entstand: Grafik 14 sollte in Abschnitt 12.3 stehen, nannte aber Zahlen aus
|
||||
Kapitel 17. Beide Stellen haben nun ihre eigene Grafik mit ihren eigenen Zahlen.
|
||||
|
||||
**Vier Vorgaben des Auftrags wurden bewusst nicht befolgt:** PNG bei 300 dpi (das Buch kennt
|
||||
nur SVG — die 33 verwaisten PNGs waren gerade erst gelöscht worden), Matplotlib-Standardfarben
|
||||
(die Buchpalette steht in 33 Diagrammen), harte Kapitelnummern im Fließtext (`{ref:sec:…}`
|
||||
statt dessen) und die fehlende Byte-Reproduzierbarkeit.
|
||||
|
||||
**Der eigentliche Ertrag waren die Selbsttests.** Jeder Generator prüft seine Zahlen gegen
|
||||
das, was im Kapitel steht, und bricht bei Abweichung ab. Vier davon haben angeschlagen:
|
||||
|
||||
* **Kombinatorik-Wand:** 25! ergibt 491,5 Millionen Jahre; die Tabelle nennt 490 Millionen.
|
||||
Kein Fehler des Buchs, sondern Rundung auf zwei signifikante Stellen — mein Formatierer
|
||||
rundete auf volle Millionen.
|
||||
* **Laufzeit-Diagramm:** Die abgedruckte Prozentspalte lässt sich aus den abgedruckten Zeiten
|
||||
*nicht* exakt nachrechnen (0,001 s / 0,008 s ergibt 12 %, gedruckt sind 14 %). Die Zeiten
|
||||
sind auf drei Stellen gerundet, die Prozente stammen aus den ungerundeten Werten. Das Bild
|
||||
zeigt jetzt die gedruckten Prozente.
|
||||
* **Predict-then-Optimize:** Meine geratene Datenaufteilung ergab 197,35 statt 200,95 MSE.
|
||||
Mit `TRAINING` aus dem Buchprogramm stimmen alle vier Kostenwerte auf den Cent.
|
||||
* **Almgren-Chriss — ein echter Fehler im Buchprogramm**, siehe unten.
|
||||
|
||||
**Der Fund: `analytische_loesung()` minimiert die eigene Kostenfunktion nicht.** Ihr Pfad
|
||||
kostet 10 860,40 €, der DP-Pfad 10 266,24 € — und ein Gitterverfahren kann das kontinuierliche
|
||||
Optimum nicht unterbieten. Ursache: `periodenkosten()` rechnet das Risiko mit **λ/2**, die
|
||||
geschlossene Formel setzt $\tilde\kappa^2 = \lambda\sigma^2 P_0^2/\eta$ **ohne** dieses
|
||||
Halbe. Mit λ/2 liefert die Formel 10 264,85 € und praktisch denselben Pfad wie die DP — knapp
|
||||
*unter* der Gitterlösung, wie es sein muss. Die Grafik zeigt bis zur Entscheidung darüber den
|
||||
DP-Pfad; er ist der, den das Kapitel abdruckt.
|
||||
|
||||
**Nachweise:** Alle elf Diagramme sind über zwei Läufe byte-identisch. Kein vorhandenes
|
||||
Diagramm wurde verändert. Die Zahlen decken sich mit dem Buch, teils bis auf den Cent — die
|
||||
Lastabwurf-Grafik reproduziert die Befundtabelle exakt (874 870 € / 2 504 154 € / 28 von 40
|
||||
gegen 351 356 € / 415 924 € / 0 von 40).
|
||||
|
||||
**Zwei Abbildungen landeten beim automatischen Einfügen im Codeblock** (ihre Anker standen in
|
||||
abgedruckten Ausgaben) und wurden dahinter verschoben. Eine Prüfung über alle elf zeigt jetzt
|
||||
null Fehlplatzierungen.
|
||||
|
||||
Neu: `bilder_04/stil_04.py` bündelt Palette, `rcParams` und `speichere()`. `networkx` wurde
|
||||
entgegen der Planung **nicht** gebraucht — `pyproject.toml` bleibt unverändert.
|
||||
PDF von 781 auf **788 Seiten**, 44 Diagramme, 27 Generatoren.
|
||||
|
||||
---
|
||||
|
||||
## 8. Commit-Historie des V04-Strangs
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ Technik und Finanzmärkte*
|
|||
|
||||
Ein Lehrbuch über Operations Research mit Python: **23 Kapitel in 5 Teilen, 6 Anhänge,
|
||||
141 Aufgaben mit vollständigen Lösungen, 76 lauffähige Beispielprogramme, 25 Jupyter-Notebooks,
|
||||
33 Diagramme — 759 PDF-Seiten.** Von der linearen Programmierung über Constraint Programming
|
||||
44 Diagramme — 788 PDF-Seiten.** Von der linearen Programmierung über Constraint Programming
|
||||
und Metaheuristiken bis zu Portfoliooptimierung, CVaR und dem Weg vom Prototyp in den Betrieb.
|
||||
|
||||
**Online lesen: <https://jamulix.de/OR/>** · Repository: <https://kitux.de/forgejo/dschlueter/operations_research>
|
||||
|
|
@ -114,7 +114,7 @@ pip install -r Operations_Research_mit_Python_Version_04_Programme/requirements.
|
|||
| Pfad | Rolle |
|
||||
| --- | --- |
|
||||
| `Operations_Research_mit_Python_Version_04/` | **Quelle**: 37 Kapiteldateien (inkl. 5 Teil-Synthesen), Build-Skripte und `NEUER_TITEL.md` — der Titeltext, aus dem PDF-Titelseite und Startseite entstehen |
|
||||
| `bilder_04/` | **Quelle**: 33 Diagramme (SVG) + 16 `erzeuge_*.py`-Generatoren |
|
||||
| `bilder_04/` | **Quelle**: 44 Diagramme (SVG) + 27 `erzeuge_*.py`-Generatoren |
|
||||
| `web_04/` | **Quelle**: `site.css`, `site.js`, `icons.svg`, `plotly.min.js`, `katex/` — die statischen Bestandteile der Website |
|
||||
| `pyproject.toml`, `Dockerfile`, `.dockerignore` | **Quelle**: Abhängigkeitsgruppen und Kurs-Image |
|
||||
| `pandoc-defaults-*.yaml`, `pandoc/` | **Quelle**: Konfiguration des PDF-Baus |
|
||||
|
|
|
|||
158
bilder_04/erzeuge_almgren_chriss.py
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# erzeuge_almgren_chriss.py
|
||||
"""
|
||||
Erzeugt das Ausfuehrungsdiagramm zum Kapitel Dynamische Programmierung:
|
||||
|
||||
bilder_04/kap_dp_almgren_chriss.svg
|
||||
|
||||
Drei Wege, dieselbe Position abzubauen, und was sie kosten:
|
||||
|
||||
* SOFORT - alles in der ersten Periode. Kein Kursrisiko, dafuer der volle
|
||||
Marktimpact auf einen Schlag.
|
||||
* TWAP - gleich grosse Scheiben. Wenig Impact je Scheibe, dafuer haelt man
|
||||
die Position lange und traegt ihr Kursrisiko.
|
||||
* OPTIMAL - die Loesung der Bellman-Gleichung: vorne mehr, hinten weniger.
|
||||
|
||||
Der Punkt, auf den es ankommt: Die optimale Kurve liegt nicht "in der Mitte"
|
||||
zwischen den beiden Extremen, sondern ist gekruemmt. Ihre Kruemmung ist die
|
||||
Antwort auf die Frage, wie teuer Eile gegen Warten ist - und genau diese Frage
|
||||
beantwortet die dynamische Programmierung.
|
||||
|
||||
Bestand und Kosten kommen aus Mehrperiodige_Order_Execution.py. Der Vergleich
|
||||
der Kosten steht als Zahl im Bild; sie wird gerechnet, nicht eingetragen.
|
||||
|
||||
GEZEICHNET WIRD DER DP-PFAD, NICHT DIE GESCHLOSSENE FORMEL. Beim Bau dieses
|
||||
Bildes fiel auf, dass beide auseinanderlaufen: Der DP-Pfad kostet 10 266,24 EUR
|
||||
- der Wert, den das Kapitel abdruckt -, der Pfad aus analytische_loesung()
|
||||
dagegen 10 860,40 EUR. Ein Gitterverfahren kann das kontinuierliche Optimum
|
||||
aber nicht unterbieten, also stimmt die Formel nicht mit der Kostenfunktion
|
||||
ueberein: periodenkosten() rechnet das Risiko mit lambda/2, die Formel setzt
|
||||
kappa~^2 = lambda*sigma^2*P0^2/eta ohne dieses Halbe. Mit lambda/2 ergibt die
|
||||
Formel 10 264,85 EUR und denselben Pfad wie die DP - knapp UNTER der
|
||||
Gitterloesung, wie es sein muss. Bis das im Programm entschieden ist, zeigt das
|
||||
Bild den DP-Pfad; er ist der, den das Kapitel ausweist.
|
||||
|
||||
Aufruf (aus dem Repository-Wurzelverzeichnis):
|
||||
python3 bilder_04/erzeuge_almgren_chriss.py
|
||||
|
||||
Benoetigt: numpy, matplotlib
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
HIER = os.path.dirname(os.path.abspath(__file__))
|
||||
BASIS = os.path.dirname(HIER)
|
||||
sys.path.insert(0, HIER)
|
||||
sys.path.insert(0, os.path.join(
|
||||
BASIS, "Operations_Research_mit_Python_Version_04_Programme"))
|
||||
|
||||
from stil_04 import FARBEN, neue_figur, speichere # noqa: E402
|
||||
from Mehrperiodige_Order_Execution import (ETA, GESAMTBESTAND, # noqa: E402
|
||||
KURS, PERIODEN,
|
||||
RISIKOAVERSION,
|
||||
VOLA_PERIODE,
|
||||
SCHRITTWEITE, loese_dp,
|
||||
periodenkosten)
|
||||
|
||||
# Der im Kapitel abgedruckte Wert der optimalen Strategie - nur zum
|
||||
# Gegenpruefen.
|
||||
GESAMTREIBUNG_IM_BUCH = 10_266.24
|
||||
|
||||
|
||||
def impaktkosten(bestand: np.ndarray) -> float:
|
||||
"""Temporaerer Marktimpact: eta je Stueck^2 auf jede Scheibe."""
|
||||
scheiben = -np.diff(bestand)
|
||||
return float(ETA * np.sum(scheiben ** 2))
|
||||
|
||||
|
||||
def risikokosten(bestand: np.ndarray) -> float:
|
||||
"""Risiko des jeweiligen Restbestands, mit dem Faktor des Buchprogramms."""
|
||||
return float(gesamtkosten(bestand) - impaktkosten(bestand))
|
||||
|
||||
|
||||
def gesamtkosten(bestand: np.ndarray) -> float:
|
||||
"""Summe der Periodenkosten - mit periodenkosten() des Buchprogramms.
|
||||
|
||||
Bewusst nicht selbst formuliert: Das Buch rechnet das Risiko mit dem Faktor
|
||||
lambda/2. Eine eigene Formel ohne diesen Faktor haette Zahlen ergeben, die
|
||||
zur Tabelle im Kapitel nicht passen."""
|
||||
scheiben = -np.diff(bestand)
|
||||
return float(sum(periodenkosten(float(scheiben[t]), float(bestand[t + 1]))
|
||||
for t in range(len(scheiben))))
|
||||
|
||||
|
||||
def zeichne(pfade: dict[str, np.ndarray]) -> None:
|
||||
figur, achse = neue_figur(breite=8.0, hoehe=4.6)
|
||||
t = np.arange(PERIODEN + 1)
|
||||
|
||||
stile = {
|
||||
"sofort verkaufen": (FARBEN["fehler"], "--"),
|
||||
"gleichmäßig (TWAP)": (FARBEN["zweit"], "--"),
|
||||
"optimal (Bellman)": (FARBEN["gut"], "-"),
|
||||
}
|
||||
for name, bestand in pfade.items():
|
||||
farbe, strich = stile[name]
|
||||
achse.plot(t, bestand / 1000.0, strich, color=farbe,
|
||||
linewidth=2.6 if strich == "-" else 1.9,
|
||||
marker="o", markersize=5, zorder=4,
|
||||
label=f"{name} — {gesamtkosten(bestand):,.0f} €"
|
||||
.replace(",", "."))
|
||||
|
||||
achse.set_xlabel("Handelsperiode $t$")
|
||||
achse.set_ylabel("verbleibender Bestand in Tausend Stück")
|
||||
achse.set_title(f"{GESAMTBESTAND:,} Stück abbauen — drei Wege, "
|
||||
f"drei Rechnungen".replace(",", "."),
|
||||
fontsize=10.5, color=FARBEN["text"])
|
||||
achse.set_xticks(t)
|
||||
achse.set_ylim(-4, GESAMTBESTAND / 1000.0 * 1.08)
|
||||
achse.legend(fontsize=9, loc="upper right", frameon=False)
|
||||
|
||||
achse.annotate(
|
||||
"Eile kostet Marktimpact, Warten kostet Kursrisiko.\n"
|
||||
"Die optimale Kurve ist gekrümmt, weil sie beides gegeneinander "
|
||||
"abwägt.",
|
||||
xy=(0.03, 0.06), xycoords="axes fraction", fontsize=9,
|
||||
color=FARBEN["text"], va="bottom")
|
||||
|
||||
speichere(figur, "kap_dp_almgren_chriss")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sofort = np.array([GESAMTBESTAND] + [0] * PERIODEN, dtype=float)
|
||||
twap = np.linspace(GESAMTBESTAND, 0, PERIODEN + 1)
|
||||
|
||||
# Der Pfad der Rueckwaertsinduktion - genau der, den das Kapitel abdruckt.
|
||||
_zustaende, _V, politik = loese_dp()
|
||||
bestand, verlauf = float(GESAMTBESTAND), [float(GESAMTBESTAND)]
|
||||
for t in range(PERIODEN):
|
||||
bestand -= politik[t, int(round(bestand / SCHRITTWEITE))]
|
||||
verlauf.append(bestand)
|
||||
optimal = np.array(verlauf, dtype=float)
|
||||
|
||||
pfade = {"sofort verkaufen": sofort, "gleichmäßig (TWAP)": twap,
|
||||
"optimal (Bellman)": optimal}
|
||||
for name, bestand in pfade.items():
|
||||
print(f" {name:<20} Impact {impaktkosten(bestand):>10,.0f} EUR "
|
||||
f"Risiko {risikokosten(bestand):>9,.0f} EUR "
|
||||
f"gesamt {gesamtkosten(bestand):>10,.0f} EUR")
|
||||
|
||||
if gesamtkosten(optimal) > min(gesamtkosten(sofort), gesamtkosten(twap)):
|
||||
raise SystemExit(
|
||||
"Almgren-Chriss-Bild: Die analytische Loesung ist teurer als eine "
|
||||
"der beiden Vergleichsstrategien. Dann stimmt die Kostenformel "
|
||||
"hier nicht mit der des Buchprogramms ueberein.")
|
||||
abweichung = abs(gesamtkosten(optimal) - GESAMTREIBUNG_IM_BUCH)
|
||||
if abweichung > 0.01:
|
||||
raise SystemExit(
|
||||
f"Almgren-Chriss-Bild: Der DP-Pfad kostet "
|
||||
f"{gesamtkosten(optimal):,.2f} EUR, das Kapitel druckt "
|
||||
f"{GESAMTREIBUNG_IM_BUCH:,.2f} EUR.")
|
||||
print(f" stimmt mit dem abgedruckten Wert überein: "
|
||||
f"{GESAMTREIBUNG_IM_BUCH:,.2f} EUR")
|
||||
zeichne(pfade)
|
||||
121
bilder_04/erzeuge_big_m_vergleich.py
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# erzeuge_big_m_vergleich.py
|
||||
"""
|
||||
Erzeugt das Big-M-Diagramm zum MILP-Kapitel:
|
||||
|
||||
bilder_04/kap_milp_big_m.svg
|
||||
|
||||
Zwei Bilder derselben Bedingung x <= M*y mit y in [0,1], einmal mit straffem
|
||||
und einmal mit ueberdimensioniertem M. Gezeigt wird die LP-RELAXATION - also
|
||||
das, was der Solver sieht, bevor er y auf 0 oder 1 zwingt.
|
||||
|
||||
Der Punkt: Beide Modelle sind fuer y in {0,1} GLEICHWERTIG. Sie beschreiben
|
||||
denselben zulaessigen Bereich, sobald y ganzzahlig ist. Verschieden ist nur die
|
||||
Relaxation - und die entscheidet, wie gut die Schranke ist und wie lange der
|
||||
Solver sucht. Bei zu grossem M kommt der Trickle Flow dazu: Ein y knapp ueber
|
||||
null gilt dem Solver als geschlossen und laesst trotzdem grossen Fluss durch.
|
||||
|
||||
Die Trickle-Flow-Zahlen in der Anmerkung stammen aus der Loesung im Anhang des
|
||||
Buchs (M = 7,2e9, y = 4e-8, rund 289 Einheiten Fluss). Die beiden M-Werte der
|
||||
Geometrie sind Anschauungswerte; das Skript rechnet aus, ab welchem y die
|
||||
Relaxation die volle Kapazitaet freigibt, statt eine Zahl zu behaupten. Die
|
||||
Flaeche des Relaxationsgebiets waere der falsche Massstab - sie ist wegen der
|
||||
Kapazitaetsgrenze in beiden Faellen aehnlich gross.
|
||||
|
||||
Aufruf (aus dem Repository-Wurzelverzeichnis):
|
||||
python3 bilder_04/erzeuge_big_m_vergleich.py
|
||||
|
||||
Benoetigt: numpy, matplotlib
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from stil_04 import FARBEN, speichere # noqa: E402
|
||||
import matplotlib.pyplot as plt # noqa: E402
|
||||
|
||||
KAPAZITAET = 40.0 # der tatsaechlich moegliche Hoechstwert von x
|
||||
M_STRAFF = 40.0 # so klein wie moeglich: M = Kapazitaet
|
||||
M_GROSS = 500.0 # "sicherheitshalber gross" - der haeufige Fehler
|
||||
|
||||
# Aus der Loesung im Anhang: ein verschwindend kleines y laesst grossen Fluss
|
||||
# durch, weil es mit einem riesigen M multipliziert wird.
|
||||
TRICKLE_M = 7.2e9
|
||||
TRICKLE_Y = 4e-8
|
||||
|
||||
|
||||
def schwelle(m: float) -> float:
|
||||
"""Kleinstes y, bei dem die Relaxation die volle Kapazitaet freigibt.
|
||||
|
||||
Das ist der aussagekraeftige Vergleich - nicht die Flaeche des
|
||||
Relaxationsgebiets. Die ist wegen der Kapazitaetsgrenze in beiden Faellen
|
||||
aehnlich gross; der Unterschied liegt darin, wie WENIG y genuegt, um alles
|
||||
durchzulassen."""
|
||||
return min(1.0, KAPAZITAET / m)
|
||||
|
||||
|
||||
def zeichne(faktor: float) -> None:
|
||||
figur, achsen = plt.subplots(1, 2, figsize=(9.2, 4.0), sharey=True)
|
||||
y = np.linspace(0, 1, 300)
|
||||
|
||||
for achse, m, titel, farbe in (
|
||||
(achsen[0], M_STRAFF, f"straff: $M = {M_STRAFF:.0f}$",
|
||||
FARBEN["gut"]),
|
||||
(achsen[1], M_GROSS, f"zu groß: $M = {M_GROSS:.0f}$",
|
||||
FARBEN["fehler"])):
|
||||
obergrenze = np.minimum(m * y, KAPAZITAET)
|
||||
achse.fill_between(y, 0, obergrenze, color=farbe, alpha=0.16, zorder=2)
|
||||
achse.plot(y, obergrenze, "-", color=farbe, linewidth=2.2, zorder=4,
|
||||
label=r"$x \le M\,y$ und $x \le$ Kapazität")
|
||||
achse.axhline(KAPAZITAET, color=FARBEN["linie"], linewidth=1.0,
|
||||
linestyle="--", zorder=3)
|
||||
achse.text(0.02, KAPAZITAET + 1.5, "Kapazität", fontsize=8.5,
|
||||
color=FARBEN["gedaempft"])
|
||||
|
||||
# Die beiden Punkte, die das ganzzahlige Modell zulaesst.
|
||||
achse.plot([0, 1], [0, KAPAZITAET], "o", color=FARBEN["haupt"],
|
||||
markersize=8, zorder=5)
|
||||
achse.annotate(r"$y=0$: zu", xy=(0, 0), xytext=(8, 10),
|
||||
textcoords="offset points", fontsize=8.5,
|
||||
color=FARBEN["haupt"])
|
||||
achse.annotate(r"$y=1$: offen", xy=(1, KAPAZITAET), xytext=(-64, -18),
|
||||
textcoords="offset points", fontsize=8.5,
|
||||
color=FARBEN["haupt"])
|
||||
|
||||
achse.set_xlim(0, 1.02)
|
||||
achse.set_ylim(0, KAPAZITAET * 1.25)
|
||||
achse.set_xlabel(r"Schaltvariable $y \in [0,1]$ (relaxiert)")
|
||||
achse.set_title(titel, fontsize=10.5, color=farbe)
|
||||
|
||||
achsen[0].set_ylabel(r"Menge $x$")
|
||||
achsen[1].annotate(
|
||||
f"Schon $y = {schwelle(M_GROSS):.2f}$ gibt die volle\n"
|
||||
f"Kapazität frei — links braucht es dafür $y = 1$.\n"
|
||||
f"Der Solver darf das Lager also fast\n"
|
||||
f"geschlossen lassen und trotzdem alles liefern.",
|
||||
xy=(0.16, KAPAZITAET * 0.42), fontsize=9, color=FARBEN["fehler"],
|
||||
ha="left")
|
||||
achsen[1].plot([schwelle(M_GROSS)], [KAPAZITAET], "o",
|
||||
color=FARBEN["fehler"], markersize=8, zorder=6)
|
||||
|
||||
figur.suptitle("Dieselbe Bedingung, zwei Zahlen für $M$ — gleich für "
|
||||
"$y \\in \\{0,1\\}$, verschieden für den Solver",
|
||||
fontsize=10.5, color=FARBEN["text"], y=1.02)
|
||||
speichere(figur, "kap_milp_big_m")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
faktor = schwelle(M_STRAFF) / schwelle(M_GROSS)
|
||||
print(f" volle Kapazität ab y = {schwelle(M_STRAFF):.3f} (M = "
|
||||
f"{M_STRAFF:.0f})")
|
||||
print(f" volle Kapazität ab y = {schwelle(M_GROSS):.3f} (M = "
|
||||
f"{M_GROSS:.0f}) → {faktor:.0f}-mal weniger y genügt")
|
||||
print(f" Trickle Flow im Anhang: y = {TRICKLE_Y:g} gilt als 0, "
|
||||
f"laesst aber M*y = {TRICKLE_M * TRICKLE_Y:.0f} Einheiten durch.")
|
||||
zeichne(faktor)
|
||||
130
bilder_04/erzeuge_entartung_polyeder.py
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# erzeuge_entartung_polyeder.py
|
||||
"""
|
||||
Erzeugt das Entartungs-Diagramm zum LP-Kapitel:
|
||||
|
||||
bilder_04/kap_lp_entartung.svg
|
||||
|
||||
Links die Geometrie: Drei Geraden laufen durch DIESELBE Ecke. In zwei
|
||||
Dimensionen legen schon zwei eine Ecke fest - eine ist zu viel, und genau das
|
||||
ist Entartung. Rechts die Folge davon: Der Schattenpreis jeder Nebenbedingung
|
||||
ist nicht mehr eine Zahl, sondern eine Spanne.
|
||||
|
||||
Warum beides in ein Bild gehoert: Die Geometrie allein erklaert nicht, warum
|
||||
zwei Solver verschiedene Dualwerte melden duerfen und beide recht haben. Die
|
||||
Spannen allein sehen nach einem Fehler aus. Nebeneinander ergibt es Sinn.
|
||||
|
||||
Modell und Spannen kommen aus Toleranzen_und_Entartung.py - importiert, nicht
|
||||
abgeschrieben. pruefe_ecke() rechnet zusaetzlich nach, dass wirklich alle drei
|
||||
Geraden durch den Punkt laufen; waere das nicht so, zeigte das Bild etwas
|
||||
anderes als der Text daneben behauptet.
|
||||
|
||||
Aufruf (aus dem Repository-Wurzelverzeichnis):
|
||||
python3 bilder_04/erzeuge_entartung_polyeder.py
|
||||
|
||||
Benoetigt: numpy, scipy, matplotlib
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
HIER = os.path.dirname(os.path.abspath(__file__))
|
||||
BASIS = os.path.dirname(HIER)
|
||||
sys.path.insert(0, HIER)
|
||||
sys.path.insert(0, os.path.join(
|
||||
BASIS, "Operations_Research_mit_Python_Version_04_Programme"))
|
||||
|
||||
from stil_04 import FARBEN, speichere # noqa: E402
|
||||
import matplotlib.pyplot as plt # noqa: E402
|
||||
from Toleranzen_und_Entartung import (A_UB, B_UB, NAMEN, # noqa: E402
|
||||
schattenpreis_spanne)
|
||||
|
||||
ECKE = np.array([4 / 3, 4 / 3])
|
||||
LINIENFARBEN = (FARBEN["haupt"], FARBEN["zweit"], FARBEN["akzent"])
|
||||
|
||||
|
||||
def pruefe_ecke() -> None:
|
||||
"""Alle drei Nebenbedingungen muessen in der Ecke aktiv sein."""
|
||||
schlupf = B_UB - A_UB @ ECKE
|
||||
aktiv = np.abs(schlupf) < 1e-9
|
||||
if not aktiv.all():
|
||||
raise SystemExit(
|
||||
f"Entartungsbild: In {tuple(np.round(ECKE, 3))} sind nur "
|
||||
f"{int(aktiv.sum())} von 3 Bedingungen aktiv (Schlupf "
|
||||
f"{np.round(schlupf, 6)}). Ohne drei aktive Geraden gibt es keine "
|
||||
f"Entartung zu zeigen.")
|
||||
|
||||
|
||||
def zeichne(spannen) -> None:
|
||||
figur, (links, rechts) = plt.subplots(1, 2, figsize=(9.4, 4.3),
|
||||
gridspec_kw={"width_ratios": [1, 1]})
|
||||
|
||||
# --- links: die drei Geraden ---------------------------------------
|
||||
x = np.linspace(0, 3.2, 200)
|
||||
for (a, b), grenze, name, farbe in zip(A_UB, B_UB, NAMEN, LINIENFARBEN):
|
||||
y = (grenze - a * x) / b
|
||||
links.plot(x, y, "-", color=farbe, linewidth=2.0, label=name, zorder=3)
|
||||
|
||||
# Zulaessiger Bereich: Schnitt aller Halbraeume, plus x >= 0.
|
||||
gitter = np.linspace(0, 3.2, 400)
|
||||
xx, yy = np.meshgrid(gitter, gitter)
|
||||
zulaessig = np.ones_like(xx, dtype=bool)
|
||||
for (a, b), grenze in zip(A_UB, B_UB):
|
||||
zulaessig &= (a * xx + b * yy <= grenze + 1e-12)
|
||||
links.contourf(xx, yy, zulaessig.astype(float), levels=[0.5, 1.5],
|
||||
colors=[FARBEN["gut"]], alpha=0.13, zorder=1)
|
||||
|
||||
links.plot(*ECKE, "o", color=FARBEN["fehler"], markersize=9, zorder=5)
|
||||
links.annotate(r"Optimum $(4/3,\ 4/3)$" "\n" "drei Geraden, eine Ecke",
|
||||
xy=tuple(ECKE), xytext=(18, 26),
|
||||
textcoords="offset points", fontsize=9,
|
||||
color=FARBEN["fehler"], fontweight="bold",
|
||||
arrowprops=dict(arrowstyle="->", color=FARBEN["fehler"],
|
||||
linewidth=1.2))
|
||||
|
||||
links.set_xlim(0, 3.0)
|
||||
links.set_ylim(0, 3.0)
|
||||
links.set_xlabel(r"$x_1$")
|
||||
links.set_ylabel(r"$x_2$")
|
||||
links.set_title("Eine Bedingung zu viel", fontsize=10.5,
|
||||
color=FARBEN["text"])
|
||||
links.legend(fontsize=8.5, loc="upper right", frameon=False)
|
||||
|
||||
# --- rechts: die Spanne je Schattenpreis ---------------------------
|
||||
y_pos = np.arange(len(NAMEN))[::-1]
|
||||
for stelle, ((unten, oben), name, farbe) in enumerate(
|
||||
zip(spannen, NAMEN, LINIENFARBEN)):
|
||||
y = y_pos[stelle]
|
||||
rechts.plot([unten, oben], [y, y], "-", color=farbe, linewidth=7,
|
||||
solid_capstyle="round", alpha=0.75, zorder=3)
|
||||
rechts.plot([unten, oben], [y, y], "|", color=farbe, markersize=14,
|
||||
markeredgewidth=2, zorder=4)
|
||||
rechts.text(oben + 0.012, y, f"{unten:.2f} – {oben:.2f} €",
|
||||
va="center", fontsize=9, color=FARBEN["text"])
|
||||
rechts.text(-0.012, y, name, va="center", ha="right", fontsize=9,
|
||||
color=FARBEN["text"])
|
||||
|
||||
rechts.set_yticks([])
|
||||
rechts.set_ylim(-0.7, len(NAMEN) - 0.3)
|
||||
rechts.set_xlim(-0.16, max(o for _, o in spannen) * 1.9)
|
||||
rechts.set_xlabel("Schattenpreis in € je zusätzlicher Stunde")
|
||||
rechts.set_title("Was der Solver melden darf — jeder Wert der Spanne "
|
||||
"ist richtig", fontsize=10.5, color=FARBEN["text"])
|
||||
rechts.grid(axis="y", visible=False)
|
||||
rechts.spines["left"].set_visible(False)
|
||||
|
||||
speichere(figur, "kap_lp_entartung")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pruefe_ecke()
|
||||
zielwert = float(np.ones(2) @ ECKE)
|
||||
spannen = schattenpreis_spanne(zielwert)
|
||||
for name, (unten, oben) in zip(NAMEN, spannen):
|
||||
print(f" {name:<18} {unten:>7.4f} bis {oben:>7.4f} EUR")
|
||||
zeichne(spannen)
|
||||
131
bilder_04/erzeuge_fluch_durchschnitt.py
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# erzeuge_fluch_durchschnitt.py
|
||||
"""
|
||||
Erzeugt das Diagramm zum Fluch des Durchschnitts:
|
||||
|
||||
bilder_04/kap_unsicherheit_durchschnitt.svg
|
||||
|
||||
Die Handrechnung des Abschnitts vergleicht zwei Plaene fuer dieselbe unsichere
|
||||
Nachfrage. Das Bild zeigt, warum der eine schlechter ist - und zwar nicht als
|
||||
Behauptung, sondern als Kurve: die erwarteten Kosten ueber ALLEN moeglichen
|
||||
Bestellmengen.
|
||||
|
||||
Was man daran sieht: Die Kurve hat einen Knick an jedem Szenario, faellt links
|
||||
flach und steigt rechts steil - Fehlmenge kostet 120 EUR, Ueberschuss nur 5. In
|
||||
dieser Schieflage liegt der ganze Effekt. Ihr Minimum liegt deshalb NICHT beim
|
||||
Erwartungswert der Nachfrage, sondern rechts davon.
|
||||
|
||||
Ohne Formel gesagt: Mit dem Mittelwert zu planen unterstellt, dass sich zu viel
|
||||
und zu wenig gegenseitig aufheben. Sie tun es nicht.
|
||||
|
||||
Alle Werte werden gerechnet. pruefe_gegen_handrechnung() vergleicht sie mit den
|
||||
im Kapitel abgedruckten Zwischenergebnissen - weicht eines ab, bricht das
|
||||
Skript ab.
|
||||
|
||||
Aufruf (aus dem Repository-Wurzelverzeichnis):
|
||||
python3 bilder_04/erzeuge_fluch_durchschnitt.py
|
||||
|
||||
Benoetigt: numpy, matplotlib
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from stil_04 import FARBEN, neue_figur, speichere # noqa: E402
|
||||
|
||||
# Die Instanz der Handrechnung: (Bedarf, Wahrscheinlichkeit)
|
||||
SZENARIEN = [(100, 0.50), (250, 0.30), (500, 0.20)]
|
||||
NAMEN = ["Ruhig", "Volatil", "Crash"]
|
||||
|
||||
VORAB = 40.0 # EUR je Einheit im Voraus
|
||||
FEHLT = 120.0 # EUR je fehlender Einheit, kurzfristig
|
||||
UEBRIG = 5.0 # EUR je ueberschuessiger Einheit, Verwaltung
|
||||
|
||||
# Aus dem Kapitel abgedruckt - nur zum Gegenpruefen.
|
||||
ERWARTET_IM_BUCH = {225: 16_812.50, 250: 16_375.00}
|
||||
|
||||
|
||||
def kosten(x: float) -> float:
|
||||
"""Erwartete Gesamtkosten bei Bestellmenge x."""
|
||||
return sum(p * (VORAB * x + FEHLT * max(bedarf - x, 0.0)
|
||||
+ UEBRIG * max(x - bedarf, 0.0))
|
||||
for bedarf, p in SZENARIEN)
|
||||
|
||||
|
||||
def pruefe_gegen_handrechnung() -> None:
|
||||
for x, erwartet in ERWARTET_IM_BUCH.items():
|
||||
ist = kosten(x)
|
||||
if abs(ist - erwartet) > 0.01:
|
||||
raise SystemExit(
|
||||
f"Fluch des Durchschnitts: Bei x={x} ergibt die Rechnung "
|
||||
f"{ist:,.2f} EUR, die Handrechnung im Kapitel nennt "
|
||||
f"{erwartet:,.2f} EUR.")
|
||||
|
||||
|
||||
def zeichne(mittelwert: float, bestes_x: float) -> None:
|
||||
figur, achse = neue_figur(breite=8.0, hoehe=4.6)
|
||||
|
||||
x = np.linspace(0, 560, 1200)
|
||||
y = np.array([kosten(float(k)) for k in x])
|
||||
achse.plot(x, y, "-", color=FARBEN["haupt"], linewidth=2.4, zorder=4,
|
||||
label="erwartete Gesamtkosten")
|
||||
|
||||
# Die Szenarien als senkrechte Marken - dort knickt die Kurve.
|
||||
for (bedarf, p), name in zip(SZENARIEN, NAMEN):
|
||||
achse.axvline(bedarf, color=FARBEN["linie"], linewidth=0.9,
|
||||
linestyle=":", zorder=2)
|
||||
achse.text(bedarf, achse.get_ylim()[0], f" {name}\n {p:.0%}",
|
||||
fontsize=8.5, color=FARBEN["gedaempft"], va="bottom")
|
||||
|
||||
for stelle, farbe, beschriftung in (
|
||||
(mittelwert, FARBEN["fehler"],
|
||||
f"Planung mit dem Mittelwert\n$x = {mittelwert:.0f}$ "
|
||||
f"{kosten(mittelwert):,.0f} €"),
|
||||
(bestes_x, FARBEN["gut"],
|
||||
f"stochastisch optimal\n$x = {bestes_x:.0f}$ "
|
||||
f"{kosten(bestes_x):,.0f} €")):
|
||||
achse.plot([stelle], [kosten(stelle)], "o", color=farbe, markersize=9,
|
||||
zorder=6)
|
||||
achse.annotate(beschriftung.replace(",", "."),
|
||||
xy=(stelle, kosten(stelle)),
|
||||
xytext=(14 if farbe == FARBEN["gut"] else -14, 44),
|
||||
textcoords="offset points", fontsize=9, color=farbe,
|
||||
fontweight="bold",
|
||||
ha="left" if farbe == FARBEN["gut"] else "right",
|
||||
arrowprops=dict(arrowstyle="->", color=farbe,
|
||||
linewidth=1.2))
|
||||
|
||||
mehrkosten = kosten(mittelwert) - kosten(bestes_x)
|
||||
achse.annotate(
|
||||
f"Der Mittelwertplan kostet {mehrkosten:,.0f} € mehr —\n"
|
||||
f"nicht weil er ungenau ist, sondern weil\n"
|
||||
f"Fehlmenge {FEHLT:.0f} € kostet und Überschuss nur "
|
||||
f"{UEBRIG:.0f} €.".replace(",", "."),
|
||||
xy=(0.03, 0.95), xycoords="axes fraction", fontsize=9,
|
||||
color=FARBEN["text"], va="top")
|
||||
|
||||
achse.set_xlabel("vorab gekaufte Kapazität $x$")
|
||||
achse.set_ylabel("erwartete Gesamtkosten in €")
|
||||
achse.set_title("Warum der Mittelwert der falsche Plan ist",
|
||||
fontsize=10.5, color=FARBEN["text"])
|
||||
achse.set_xlim(0, 560)
|
||||
|
||||
speichere(figur, "kap_unsicherheit_durchschnitt")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pruefe_gegen_handrechnung()
|
||||
mittelwert = sum(bedarf * p for bedarf, p in SZENARIEN)
|
||||
gitter = np.arange(0, 561, 1.0)
|
||||
bestes_x = float(gitter[int(np.argmin([kosten(float(k)) for k in gitter]))])
|
||||
print(f" Erwartungswert der Nachfrage: {mittelwert:.0f}")
|
||||
print(f" Kosten dort: {kosten(mittelwert):,.2f} EUR")
|
||||
print(f" bestes x: {bestes_x:.0f}")
|
||||
print(f" Kosten dort: {kosten(bestes_x):,.2f} EUR")
|
||||
zeichne(mittelwert, bestes_x)
|
||||
149
bilder_04/erzeuge_kombinatorik_wand.py
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# erzeuge_kombinatorik_wand.py
|
||||
"""
|
||||
Erzeugt das Wachstumsdiagramm zur Einfuehrung:
|
||||
|
||||
bilder_04/kap_einfuehrung_kombinatorik.svg
|
||||
|
||||
Die Handrechnung des Abschnitts rechnet vor, wie lange vollstaendiges
|
||||
Durchprobieren dauert, wenn ein Rechner eine Milliarde Reihenfolgen je Sekunde
|
||||
prueft. Das Bild macht daraus eine Kurve - und zeigt daneben, was polynomiales
|
||||
Wachstum im selben Massstab bedeutet: eine Linie, die am unteren Rand klebt.
|
||||
|
||||
Der didaktische Punkt ist nicht "n! waechst schnell", sondern die SCHMALHEIT
|
||||
des Uebergangs: Zwischen n=15 (22 Minuten, machbar) und n=20 (77 Jahre,
|
||||
hoffnungslos) liegen fuenf Auftraege. Deshalb der markierte Streifen.
|
||||
|
||||
ZU DEN ZAHLEN: Sie werden gerechnet, nicht eingetragen. pruefe_gegen_buch()
|
||||
vergleicht sie anschliessend mit der abgedruckten Tabelle des Kapitels - weicht
|
||||
eine ab, bricht das Skript ab, statt ein Bild zu erzeugen, das der Tabelle
|
||||
daneben widerspricht.
|
||||
|
||||
Aufruf (aus dem Repository-Wurzelverzeichnis):
|
||||
python3 bilder_04/erzeuge_kombinatorik_wand.py
|
||||
|
||||
Benoetigt: numpy, matplotlib
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from stil_04 import FARBEN, neue_figur, speichere # noqa: E402
|
||||
|
||||
PRUEFUNGEN_JE_SEKUNDE = 1e9
|
||||
SEKUNDEN_JE_JAHR = 365.25 * 24 * 3600
|
||||
|
||||
# Die vier Marken der Handrechnung. Werte NUR zum Gegenpruefen - gezeichnet
|
||||
# wird, was gerechnet wurde.
|
||||
TABELLE_IM_BUCH = {
|
||||
10: "0,004 Sekunden",
|
||||
15: "22 Minuten",
|
||||
20: "77 Jahre",
|
||||
25: "490 Millionen Jahre",
|
||||
}
|
||||
|
||||
|
||||
def sekunden(n: int) -> float:
|
||||
return math.factorial(n) / PRUEFUNGEN_JE_SEKUNDE
|
||||
|
||||
|
||||
def _signifikant(wert: float, stellen: int = 2) -> int:
|
||||
"""Rundet auf 'stellen' signifikante Ziffern.
|
||||
|
||||
Die Tabelle des Kapitels tut genau das: 491,5 Millionen Jahre stehen dort
|
||||
als '490 Millionen'. Volle Millionen zu runden ergaebe 492 - und damit ein
|
||||
Bild, das der Tabelle daneben widerspricht."""
|
||||
if wert == 0:
|
||||
return 0
|
||||
ordnung = math.floor(math.log10(abs(wert)))
|
||||
faktor = 10 ** (ordnung - stellen + 1)
|
||||
return int(round(wert / faktor) * faktor)
|
||||
|
||||
|
||||
def lesbar(n: int) -> str:
|
||||
"""Formatiert eine Dauer so, wie die Tabelle des Kapitels es tut."""
|
||||
s = sekunden(n)
|
||||
if s < 1:
|
||||
return f"{s:.3f}".replace(".", ",") + " Sekunden"
|
||||
if s < 3600:
|
||||
return f"{round(s / 60)} Minuten"
|
||||
jahre = s / SEKUNDEN_JE_JAHR
|
||||
if jahre < 1000:
|
||||
return f"{round(jahre)} Jahre"
|
||||
if jahre < 1e9:
|
||||
return f"{_signifikant(jahre / 1e6)} Millionen Jahre"
|
||||
return f"{jahre:.1e} Jahre"
|
||||
|
||||
|
||||
def pruefe_gegen_buch() -> None:
|
||||
"""Die Zahlen im Bild muessen die der abgedruckten Tabelle sein."""
|
||||
for n, erwartet in TABELLE_IM_BUCH.items():
|
||||
ist = lesbar(n)
|
||||
if ist != erwartet:
|
||||
raise SystemExit(
|
||||
f"Kombinatorik-Wand: Fuer n={n} ergibt die Rechnung {ist!r}, "
|
||||
f"die Tabelle im Kapitel nennt {erwartet!r}. Bild und Text "
|
||||
f"wuerden sich widersprechen.")
|
||||
|
||||
|
||||
def zeichne() -> None:
|
||||
figur, achse = neue_figur(breite=7.8, hoehe=5.0)
|
||||
n = np.arange(1, 31)
|
||||
|
||||
fakultaet = np.array([sekunden(int(k)) for k in n])
|
||||
quadrat = n.astype(float) ** 2 / PRUEFUNGEN_JE_SEKUNDE
|
||||
kubik = n.astype(float) ** 3 / PRUEFUNGEN_JE_SEKUNDE
|
||||
|
||||
achse.semilogy(n, fakultaet, "-", color=FARBEN["fehler"], linewidth=2.4,
|
||||
label=r"$n!$ — alle Reihenfolgen durchprobieren", zorder=4)
|
||||
achse.semilogy(n, kubik, "-", color=FARBEN["zweit"], linewidth=1.8,
|
||||
label=r"$n^3$", zorder=3)
|
||||
achse.semilogy(n, quadrat, "-", color=FARBEN["haupt"], linewidth=1.8,
|
||||
label=r"$n^2$", zorder=3)
|
||||
|
||||
# Der schmale Uebergang, um den es geht.
|
||||
achse.axvspan(15, 20, color=FARBEN["warnung"], alpha=0.12, zorder=1)
|
||||
achse.annotate("fünf Aufträge mehr —\n22 Minuten wird zu 77 Jahren",
|
||||
xy=(17.5, sekunden(17) * 0.02), fontsize=9,
|
||||
color=FARBEN["warnung"], ha="center", fontweight="bold")
|
||||
|
||||
# Waagerechte Bezugsmarken: was ein Mensch noch abwartet.
|
||||
for hoehe, text in ((1.0, "1 Sekunde"), (3600.0, "1 Stunde"),
|
||||
(SEKUNDEN_JE_JAHR, "1 Jahr")):
|
||||
achse.axhline(hoehe, color=FARBEN["linie"], linewidth=0.9,
|
||||
linestyle="--", zorder=2)
|
||||
achse.text(30.4, hoehe, text, fontsize=8, color=FARBEN["gedaempft"],
|
||||
va="center")
|
||||
|
||||
for k in sorted(TABELLE_IM_BUCH):
|
||||
achse.plot([k], [sekunden(k)], "o", color=FARBEN["fehler"],
|
||||
markersize=6, zorder=5)
|
||||
achse.annotate(f"$n={k}$: {lesbar(k)}",
|
||||
xy=(k, sekunden(k)), xytext=(-6, 12),
|
||||
textcoords="offset points", fontsize=8.5,
|
||||
color=FARBEN["text"], ha="right",
|
||||
arrowprops=dict(arrowstyle="-", color=FARBEN["linie"],
|
||||
linewidth=0.8))
|
||||
|
||||
achse.set_xlabel("Zahl der Aufträge $n$")
|
||||
achse.set_ylabel(r"Rechenzeit in Sekunden bei $10^9$ Prüfungen/s")
|
||||
achse.set_title("Warum Ausprobieren scheitert — dieselbe Achse für alle "
|
||||
"drei Kurven", fontsize=10.5, color=FARBEN["text"])
|
||||
achse.set_xlim(1, 30)
|
||||
achse.set_ylim(1e-9, 1e26)
|
||||
achse.legend(fontsize=9, loc="upper left", frameon=False)
|
||||
|
||||
speichere(figur, "kap_einfuehrung_kombinatorik")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pruefe_gegen_buch()
|
||||
print("Zahlen stimmen mit der Tabelle des Kapitels überein.")
|
||||
zeichne()
|
||||
120
bilder_04/erzeuge_lastabwurf_verteilung.py
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# erzeuge_lastabwurf_verteilung.py
|
||||
"""
|
||||
Erzeugt das Verteilungsdiagramm zum Supply-Chain-Kapitel:
|
||||
|
||||
bilder_04/kap_supplychain_lastabwurf.svg
|
||||
|
||||
Das vorhandene Bild kap_supplychain_wirkung.svg zerlegt die Kostendifferenz der
|
||||
beiden Plaene nach Kostenarten. Dieses hier zeigt etwas anderes: die 40
|
||||
Szenarien einzeln.
|
||||
|
||||
Warum das noetig ist: Ein Mittelwert von 874 870 EUR gegen 351 356 EUR sagt
|
||||
nicht, WIE die Differenz zustande kommt. Die Verteilung sagt es. Der
|
||||
Erwartungswert-Plan ist in den ruhigen Szenarien tatsaechlich billiger - und
|
||||
faellt in den windarmen Szenarien so weit durch, dass der Mittelwert kippt. Das
|
||||
ist die Gestalt, die eine einzelne Kennzahl verbirgt.
|
||||
|
||||
Die Szenarien mit Lastabwurf sind eigens markiert; ihre Zahl muss die im
|
||||
Kapitel abgedruckte sein (28 von 40 gegen 0 von 40), sonst bricht das Skript
|
||||
ab.
|
||||
|
||||
RECHENZEIT: Das Skript loest 2 x 40 Fahrplaene zuzueglich zweier
|
||||
Commitment-Probleme. Das dauert einige Minuten - anders ist die Verteilung
|
||||
nicht ehrlich zu bekommen.
|
||||
|
||||
Aufruf (aus dem Repository-Wurzelverzeichnis):
|
||||
python3 bilder_04/erzeuge_lastabwurf_verteilung.py
|
||||
|
||||
Benoetigt: numpy, matplotlib, ortools
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
HIER = os.path.dirname(os.path.abspath(__file__))
|
||||
BASIS = os.path.dirname(HIER)
|
||||
sys.path.insert(0, HIER)
|
||||
sys.path.insert(0, os.path.join(
|
||||
BASIS, "Operations_Research_mit_Python_Version_04_Programme"))
|
||||
|
||||
from stil_04 import FARBEN, neue_figur, speichere # noqa: E402
|
||||
from Kraftwerkseinsatz import (SZENARIEN, WIND_ERWARTUNG, # noqa: E402
|
||||
bewerte, erzeuge_windszenarien, plane)
|
||||
|
||||
# Aus der Befundtabelle des Kapitels - nur zum Gegenpruefen.
|
||||
ABWURF_IM_BUCH = {"Erwartungswert": 28, "alle Szenarien": 0}
|
||||
|
||||
|
||||
def pruefe_abwurf(zaehlung: dict[str, int]) -> None:
|
||||
for name, erwartet in ABWURF_IM_BUCH.items():
|
||||
if zaehlung[name] != erwartet:
|
||||
raise SystemExit(
|
||||
f"Lastabwurf-Bild: Der Plan '{name}' wirft in "
|
||||
f"{zaehlung[name]} von {SZENARIEN} Szenarien Last ab, die "
|
||||
f"Befundtabelle des Kapitels nennt {erwartet}.")
|
||||
|
||||
|
||||
def zeichne(kosten: dict, fehlt: dict) -> None:
|
||||
figur, achse = neue_figur(breite=8.2, hoehe=4.6)
|
||||
|
||||
for name, farbe in (("Erwartungswert", FARBEN["fehler"]),
|
||||
("alle Szenarien", FARBEN["gut"])):
|
||||
werte = np.sort(kosten[name]) / 1000.0
|
||||
rang = np.arange(1, len(werte) + 1)
|
||||
achse.step(werte, rang, where="post", color=farbe, linewidth=2.2,
|
||||
zorder=4,
|
||||
label=f"Plan „{name}“ — Mittel "
|
||||
f"{kosten[name].mean() / 1000:,.0f} Tsd. €"
|
||||
.replace(",", "."))
|
||||
|
||||
betroffen = int((fehlt[name] > 1e-6).sum())
|
||||
if betroffen:
|
||||
mit_abwurf = np.sort(kosten[name][fehlt[name] > 1e-6]) / 1000.0
|
||||
achse.scatter(mit_abwurf,
|
||||
np.searchsorted(werte, mit_abwurf, side="right"),
|
||||
s=26, color=farbe, zorder=5, alpha=0.85)
|
||||
|
||||
achse.set_xlabel("Kosten des Tages in Tausend €")
|
||||
achse.set_ylabel("Szenarien mit höchstens diesen Kosten")
|
||||
achse.set_title(f"Dieselben {SZENARIEN} Windszenarien, zwei Pläne — "
|
||||
f"Punkte: Szenarien mit Lastabwurf",
|
||||
fontsize=10.5, color=FARBEN["text"])
|
||||
achse.set_ylim(0, SZENARIEN + 1)
|
||||
achse.legend(fontsize=9, loc="lower right", frameon=False)
|
||||
|
||||
achse.annotate(
|
||||
f"Der Erwartungswert-Plan ist links günstiger —\n"
|
||||
f"und wirft in {int((fehlt['Erwartungswert'] > 1e-6).sum())} von "
|
||||
f"{SZENARIEN} Szenarien Last ab.\n"
|
||||
f"Der zweistufige Plan in keinem.",
|
||||
xy=(0.03, 0.95), xycoords="axes fraction", fontsize=9,
|
||||
color=FARBEN["text"], va="top")
|
||||
|
||||
speichere(figur, "kap_supplychain_lastabwurf")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# erzeuge_windszenarien() liefert (wind, ist_flaute), plane() liefert
|
||||
# (plan, dauer, optimal) - beide Rueckgaben werden hier nur teilweise
|
||||
# gebraucht.
|
||||
wind, _ist_flaute = erzeuge_windszenarien()
|
||||
gleich = np.full(SZENARIEN, 1.0 / SZENARIEN)
|
||||
|
||||
kosten, fehlt, zaehlung = {}, {}, {}
|
||||
for name, plan in (
|
||||
("Erwartungswert", plane(WIND_ERWARTUNG.reshape(1, -1), [1.0])[0]),
|
||||
("alle Szenarien", plane(wind, gleich)[0])):
|
||||
kosten[name], fehlt[name] = bewerte(plan, wind)
|
||||
zaehlung[name] = int((fehlt[name] > 1e-6).sum())
|
||||
print(f" Plan „{name}“: Mittel {kosten[name].mean():>12,.0f} EUR, "
|
||||
f"schlimmster Tag {kosten[name].max():>12,.0f} EUR, "
|
||||
f"Lastabwurf in {zaehlung[name]:>2} von {SZENARIEN}")
|
||||
|
||||
pruefe_abwurf(zaehlung)
|
||||
zeichne(kosten, fehlt)
|
||||
126
bilder_04/erzeuge_laufzeit_aufbau.py
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# erzeuge_laufzeit_aufbau.py
|
||||
"""
|
||||
Erzeugt das Laufzeit-Diagramm zum Oekosystem-Kapitel:
|
||||
|
||||
bilder_04/kap_oekosystem_laufzeit.svg
|
||||
|
||||
Gestapelte Balken: Wie viel Zeit geht in den AUFBAU des Modells (Python) und
|
||||
wie viel ins LOESEN (C++)? Verglichen werden Variante A (ein Add() je
|
||||
Nebenbedingung) und Variante C (vektorisiert ueber NumPy) bei drei
|
||||
Problemgroessen.
|
||||
|
||||
WARUM HIER NICHT GEMESSEN WIRD: Die Zeiten stehen bereits als abgedruckte
|
||||
Ausgabe im Kapitel. Ein Neumessen auf einer anderen Maschine ergaebe andere
|
||||
Werte - das Bild widerspraeche dann der Tabelle unmittelbar daneben. Die
|
||||
Konstanten unten sind aus genau dieser Ausgabe uebernommen; pruefe_anteile()
|
||||
rechnet die Prozentspalte grob nach und schlaegt an, wenn eine Abweichung
|
||||
groesser ist als die Rundung der abgedruckten Zeiten erklaeren kann. So ist das
|
||||
Uebertragen abgesichert, ohne die Messung zu wiederholen.
|
||||
|
||||
Aufruf (aus dem Repository-Wurzelverzeichnis):
|
||||
python3 bilder_04/erzeuge_laufzeit_aufbau.py
|
||||
|
||||
Benoetigt: numpy, matplotlib
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from stil_04 import FARBEN, neue_figur, speichere # noqa: E402
|
||||
|
||||
# (Groesse, Variablen, Aufbau A, Loesen A, Anteil A, Aufbau C, Loesen C,
|
||||
# Anteil C) - wortgleich aus der abgedruckten Ausgabe des Kapitels.
|
||||
MESSUNG = [
|
||||
("40 × 40", 1_600, 0.031, 0.005, 87, 0.001, 0.007, 14),
|
||||
("120 × 120", 14_400, 0.287, 0.070, 80, 0.001, 0.047, 3),
|
||||
("250 × 250", 62_500, 1.284, 0.569, 69, 0.003, 0.205, 1),
|
||||
]
|
||||
|
||||
|
||||
# Wie weit die aus den gerundeten Zeiten nachgerechnete Prozentzahl von der
|
||||
# abgedruckten abweichen darf. Die Zeiten stehen mit drei Nachkommastellen im
|
||||
# Buch, die Prozentspalte wurde aber aus den UNGERUNDETEN Werten berechnet:
|
||||
# 0,001s / 0,008s ergibt nachgerechnet 12 %, abgedruckt sind 14 %. Das ist
|
||||
# kein Fehler, sondern die Folge der Rundung - bei einer Aufbauzeit von einer
|
||||
# Millisekunde schlaegt schon die vierte Stelle durch. Die Schranke faengt
|
||||
# trotzdem einen echten Uebertragungsfehler ab.
|
||||
TOLERANZ_PROZENTPUNKTE = 4
|
||||
|
||||
|
||||
def pruefe_anteile() -> None:
|
||||
"""Plausibilitaetspruefung der uebertragenen Prozentspalte."""
|
||||
for name, _var, auf_a, los_a, pro_a, auf_c, los_c, pro_c in MESSUNG:
|
||||
for auf, los, pro, variante in ((auf_a, los_a, pro_a, "A"),
|
||||
(auf_c, los_c, pro_c, "C")):
|
||||
gerechnet = round(auf / (auf + los) * 100)
|
||||
if abs(gerechnet - pro) > TOLERANZ_PROZENTPUNKTE:
|
||||
raise SystemExit(
|
||||
f"Laufzeit-Diagramm: {name}, Variante {variante}: Aus "
|
||||
f"{auf}s Aufbau und {los}s Loesen folgen {gerechnet} % "
|
||||
f"Aufbauanteil, die Tabelle nennt {pro} %. Das ist mehr "
|
||||
f"als Rundung erklaert - Uebertragungsfehler?")
|
||||
|
||||
|
||||
def zeichne() -> None:
|
||||
figur, achse = neue_figur(breite=8.0, hoehe=4.4)
|
||||
|
||||
# Zwei Balken je Groesse: oben Variante A, darunter Variante C.
|
||||
beschriftung, hoehen = [], []
|
||||
aufbau, loesen = [], []
|
||||
# Gezeichnet wird die ABGEDRUCKTE Prozentzahl - so zeigt das Bild genau
|
||||
# das, was in der Tabelle daneben steht.
|
||||
for name, variablen, _aa, _la, pro_a, _ac, _lc, pro_c in MESSUNG:
|
||||
for variante, anteil in (("A Schleife", pro_a),
|
||||
("C vektorisiert", pro_c)):
|
||||
beschriftung.append(f"{name} {variante}")
|
||||
aufbau.append(float(anteil))
|
||||
loesen.append(100.0 - anteil)
|
||||
hoehen.append(variablen)
|
||||
|
||||
y = np.arange(len(beschriftung))[::-1].astype(float)
|
||||
# Luecke zwischen den Groessengruppen
|
||||
y = y + np.repeat(np.arange(len(MESSUNG))[::-1], 2) * 0.6
|
||||
|
||||
achse.barh(y, aufbau, height=0.62, color=FARBEN["fehler"],
|
||||
label="Aufbau in Python", zorder=3)
|
||||
achse.barh(y, loesen, height=0.62, left=aufbau, color=FARBEN["haupt"],
|
||||
label="Lösen in C++", zorder=3)
|
||||
|
||||
for stelle, (anteil, name) in enumerate(zip(aufbau, beschriftung)):
|
||||
# Bei 1 % und 3 % ist der Balken schmaler als die Beschriftung - dann
|
||||
# steht sie rechts daneben, in der Farbe des Balkens.
|
||||
if anteil >= 10:
|
||||
achse.text(anteil / 2, y[stelle], f"{round(anteil)} %",
|
||||
ha="center", va="center", fontsize=8.5, color="white",
|
||||
fontweight="bold", zorder=4)
|
||||
else:
|
||||
achse.text(anteil + 1.5, y[stelle], f"{round(anteil)} %",
|
||||
ha="left", va="center", fontsize=8.5,
|
||||
color=FARBEN["fehler"], fontweight="bold", zorder=4)
|
||||
achse.text(101, y[stelle], name, va="center", fontsize=8.5,
|
||||
color=FARBEN["text"])
|
||||
|
||||
achse.set_yticks([])
|
||||
achse.set_xlim(0, 100)
|
||||
achse.set_xlabel("Anteil an der Gesamtzeit in Prozent")
|
||||
achse.set_title("Wo die Zeit wirklich hingeht — dieselbe Aufgabe, zwei "
|
||||
"Arten sie aufzuschreiben", fontsize=10.5,
|
||||
color=FARBEN["text"])
|
||||
achse.grid(axis="y", visible=False)
|
||||
achse.legend(fontsize=9, loc="lower center", bbox_to_anchor=(0.5, -0.28),
|
||||
ncol=2, frameon=False)
|
||||
|
||||
speichere(figur, "kap_oekosystem_laufzeit")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pruefe_anteile()
|
||||
print("Prozentspalte stimmt mit den abgedruckten Zeiten überein.")
|
||||
zeichne()
|
||||
139
bilder_04/erzeuge_predict_vs_optimize.py
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# erzeuge_predict_vs_optimize.py
|
||||
"""
|
||||
Erzeugt das Diagramm zum Predict-then-Optimize-Kapitel:
|
||||
|
||||
bilder_04/kap_prognose_entscheidungsguete.svg
|
||||
|
||||
Vier Verfahren, zwei Massstaebe: links der Prognosefehler (MSE), rechts die
|
||||
Kosten je Tag. Die Balken sind absichtlich nebeneinander gestellt, denn der
|
||||
Befund des Kapitels ist genau ihr Auseinanderlaufen: Das Verfahren mit dem
|
||||
BESTEN MSE hat die HOECHSTEN Kosten.
|
||||
|
||||
Ohne Formel gesagt: Der Prognostiker optimiert seinen Fehler, der Planer traegt
|
||||
die Kosten - und beide messen etwas anderes. Wer die Prognose verbessert, ohne
|
||||
zu fragen, wofuer sie gebraucht wird, verbessert die falsche Zahl.
|
||||
|
||||
Alle Werte kommen aus Predict_then_Optimize.py: dieselben Daten, dieselbe Saat,
|
||||
dieselben vier Verfahren. pruefe_befund() stellt sicher, dass die Aussage des
|
||||
Kapitels im Bild auch wirklich zu sehen ist - sonst waere das Bild ein
|
||||
Widerspruch zum Text daneben.
|
||||
|
||||
Aufruf (aus dem Repository-Wurzelverzeichnis):
|
||||
python3 bilder_04/erzeuge_predict_vs_optimize.py
|
||||
|
||||
Benoetigt: numpy, scipy, scikit-learn, matplotlib
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
HIER = os.path.dirname(os.path.abspath(__file__))
|
||||
BASIS = os.path.dirname(HIER)
|
||||
sys.path.insert(0, HIER)
|
||||
sys.path.insert(0, os.path.join(
|
||||
BASIS, "Operations_Research_mit_Python_Version_04_Programme"))
|
||||
|
||||
from stil_04 import FARBEN, speichere # noqa: E402
|
||||
import matplotlib.pyplot as plt # noqa: E402
|
||||
from scipy.stats import norm # noqa: E402
|
||||
from sklearn.linear_model import LinearRegression, QuantileRegressor # noqa: E402
|
||||
from Predict_then_Optimize import (KRITISCHES_VERHAELTNIS, TAGE, # noqa: E402
|
||||
TRAINING, erzeuge_daten, tageskosten)
|
||||
|
||||
KURZ = ["Punkt-\nprognose", "+ Zuschlag aus der\nNormalverteilung",
|
||||
"+ Zuschlag auf\nKosten trainiert", "Quantil-\nregression"]
|
||||
|
||||
|
||||
def rechne() -> list[tuple[str, float, float]]:
|
||||
"""Dieselben vier Verfahren wie im Buchprogramm."""
|
||||
merkmale, nachfrage = erzeuge_daten()[:2]
|
||||
# Dieselbe Aufteilung wie im Buchprogramm - eine selbst gewaehlte Grenze
|
||||
# ergaebe andere Zahlen als die abgedruckte Tabelle.
|
||||
lernen = slice(0, TRAINING)
|
||||
pruefen = slice(TRAINING, TAGE)
|
||||
|
||||
kq = LinearRegression().fit(merkmale[lernen], nachfrage[lernen])
|
||||
punkt = kq.predict(merkmale[pruefen])
|
||||
rest = nachfrage[lernen] - kq.predict(merkmale[lernen])
|
||||
pauschal = norm.ppf(KRITISCHES_VERHAELTNIS) * rest.std()
|
||||
|
||||
kandidaten = np.linspace(-10.0, 30.0, 401)
|
||||
trainingsprognose = kq.predict(merkmale[lernen])
|
||||
kostenzuschlag = float(kandidaten[np.argmin(
|
||||
[tageskosten(trainingsprognose + z, nachfrage[lernen])
|
||||
for z in kandidaten])])
|
||||
|
||||
quantil = QuantileRegressor(quantile=KRITISCHES_VERHAELTNIS, alpha=0.0,
|
||||
solver="highs").fit(merkmale[lernen],
|
||||
nachfrage[lernen])
|
||||
quantilprognose = quantil.predict(merkmale[pruefen])
|
||||
|
||||
verfahren = [
|
||||
(KURZ[0], punkt, punkt),
|
||||
(KURZ[1], punkt, punkt + pauschal),
|
||||
(KURZ[2], punkt, punkt + kostenzuschlag),
|
||||
(KURZ[3], quantilprognose, quantilprognose),
|
||||
]
|
||||
aus = []
|
||||
for name, prognose, bestellung in verfahren:
|
||||
mse = float(((prognose - nachfrage[pruefen]) ** 2).mean())
|
||||
aus.append((name, mse, tageskosten(bestellung, nachfrage[pruefen])))
|
||||
return aus
|
||||
|
||||
|
||||
def pruefe_befund(werte) -> None:
|
||||
"""Die Aussage des Kapitels muss im Bild sichtbar sein."""
|
||||
bester_mse = min(werte, key=lambda z: z[1])
|
||||
teuerster = max(werte, key=lambda z: z[2])
|
||||
if bester_mse[0] != teuerster[0]:
|
||||
raise SystemExit(
|
||||
f"Predict-then-Optimize-Bild: Das Verfahren mit dem besten MSE "
|
||||
f"({bester_mse[0].replace(chr(10), ' ')}) ist nicht das teuerste "
|
||||
f"({teuerster[0].replace(chr(10), ' ')}). Das Kapitel behauptet "
|
||||
f"genau das - Bild und Text wuerden sich widersprechen.")
|
||||
|
||||
|
||||
def zeichne(werte) -> None:
|
||||
figur, achsen = plt.subplots(1, 2, figsize=(9.4, 4.4))
|
||||
namen = [n for n, _, _ in werte]
|
||||
y = np.arange(len(namen))[::-1]
|
||||
|
||||
for achse, index, titel, einheit, farbe in (
|
||||
(achsen[0], 1, "Prognosefehler (MSE)", "", FARBEN["zweit"]),
|
||||
(achsen[1], 2, "Kosten je Tag", " €", FARBEN["warnung"])):
|
||||
wert = [w[index] for w in werte]
|
||||
bester = min(wert)
|
||||
farben = [FARBEN["gut"] if abs(w - bester) < 1e-9 else farbe
|
||||
for w in wert]
|
||||
achse.barh(y, wert, height=0.62, color=farben, zorder=3)
|
||||
for stelle, w in enumerate(wert):
|
||||
achse.text(w * 1.01, y[stelle], f"{w:,.2f}{einheit}"
|
||||
.replace(",", "."), va="center", fontsize=9,
|
||||
color=FARBEN["text"])
|
||||
achse.set_yticks(y)
|
||||
achse.set_yticklabels(namen if index == 1 else [], fontsize=8.5,
|
||||
color=FARBEN["text"])
|
||||
achse.set_xlim(0, max(wert) * 1.22)
|
||||
achse.set_title(titel, fontsize=10.5, color=FARBEN["text"])
|
||||
achse.grid(axis="y", visible=False)
|
||||
|
||||
figur.suptitle("Grün ist jeweils der beste Wert — und es ist nicht "
|
||||
"dasselbe Verfahren",
|
||||
fontsize=11, color=FARBEN["text"], y=1.02)
|
||||
speichere(figur, "kap_prognose_entscheidungsguete")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
werte = rechne()
|
||||
for name, mse, kosten in werte:
|
||||
print(f" {name.replace(chr(10), ' '):<40} MSE {mse:>8.2f} "
|
||||
f"Kosten {kosten:>7.2f} EUR")
|
||||
pruefe_befund(werte)
|
||||
print(" Befund bestätigt: bester MSE = höchste Kosten.")
|
||||
zeichne(werte)
|
||||
100
bilder_04/erzeuge_spaltengenerierung_flow.py
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# erzeuge_spaltengenerierung_flow.py
|
||||
"""
|
||||
Erzeugt das Regelkreis-Diagramm zum Dekompositions-Kapitel:
|
||||
|
||||
bilder_04/kap_dekomposition_regelkreis.svg
|
||||
|
||||
Spaltengenerierung ist kein Algorithmus, den man am Stueck liest, sondern ein
|
||||
Kreislauf zwischen zwei Problemen, die einander abwechselnd Zahlen zuwerfen:
|
||||
|
||||
Master-LP -- Dualwerte pi_i --> Pricing (Rucksack)
|
||||
Master-LP <-- neue Spalte a -- Pricing
|
||||
|
||||
Das Bild macht daraus einen Kreis mit der Abbruchbedingung als Ausgang. Es ist
|
||||
bewusst schematisch: Zahlen stehen keine darin, denn die Schleife gilt fuer
|
||||
jede Instanz gleichermassen. Was sie zeigt, ist die Richtung der Information -
|
||||
und dass das Verfahren erst endet, wenn das Pricing NICHTS mehr findet.
|
||||
|
||||
Gezeichnet mit matplotlib.patches statt Graphviz: Das Bild hat fuenf Kaesten
|
||||
und vier Pfeile: dafuer eine weitere Abhaengigkeit einzufuehren, waere
|
||||
unverhaeltnismaessig - und die Positionen sollen zu den uebrigen Diagrammen des
|
||||
Buchs passen, nicht zu Graphviz' Layoutgeschmack.
|
||||
|
||||
Aufruf (aus dem Repository-Wurzelverzeichnis):
|
||||
python3 bilder_04/erzeuge_spaltengenerierung_flow.py
|
||||
|
||||
Benoetigt: matplotlib
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from stil_04 import FARBEN, speichere # noqa: E402
|
||||
import matplotlib.pyplot as plt # noqa: E402
|
||||
from matplotlib.patches import FancyArrowPatch, FancyBboxPatch # noqa: E402
|
||||
|
||||
|
||||
def kasten(achse, x, y, breite, hoehe, titel, zeilen, farbe, fuellung):
|
||||
achse.add_patch(FancyBboxPatch(
|
||||
(x, y), breite, hoehe,
|
||||
boxstyle="round,pad=0,rounding_size=0.09",
|
||||
facecolor=fuellung, edgecolor=farbe, linewidth=1.8, zorder=2))
|
||||
achse.text(x + breite / 2, y + hoehe - 0.20, titel, ha="center",
|
||||
va="center", fontsize=10, fontweight="bold", color=farbe,
|
||||
zorder=3)
|
||||
achse.text(x + breite / 2, y + hoehe / 2 - 0.16, zeilen, ha="center",
|
||||
va="center", fontsize=8.8, color=FARBEN["text"], zorder=3)
|
||||
|
||||
|
||||
def pfeil(achse, von, nach, text, farbe, bogen=0.0, versatz=(0, 0)):
|
||||
achse.add_patch(FancyArrowPatch(
|
||||
von, nach, arrowstyle="-|>", mutation_scale=16, linewidth=1.8,
|
||||
color=farbe, connectionstyle=f"arc3,rad={bogen}", zorder=4,
|
||||
shrinkA=2, shrinkB=2))
|
||||
mitte = ((von[0] + nach[0]) / 2 + versatz[0],
|
||||
(von[1] + nach[1]) / 2 + versatz[1])
|
||||
achse.text(*mitte, text, ha="center", va="center", fontsize=9,
|
||||
color=farbe, fontweight="bold", zorder=5)
|
||||
|
||||
|
||||
def zeichne() -> None:
|
||||
figur, achse = plt.subplots(figsize=(8.6, 4.3))
|
||||
achse.set_xlim(0, 10)
|
||||
achse.set_ylim(0, 5)
|
||||
achse.axis("off")
|
||||
|
||||
kasten(achse, 0.3, 2.5, 3.5, 1.9, "Master-LP (RMP)",
|
||||
"rechnet mit den bisher\nbekannten Mustern\nund liefert Dualwerte",
|
||||
FARBEN["haupt"], "#eef2ff")
|
||||
kasten(achse, 6.2, 2.5, 3.5, 1.9, "Pricing: Rucksackproblem",
|
||||
"sucht das EINE Muster mit\nden kleinsten reduzierten\nKosten",
|
||||
FARBEN["zweit"], "#ecfeff")
|
||||
kasten(achse, 3.1, 0.15, 3.8, 1.35, "Optimum bewiesen",
|
||||
"kein Muster mit negativen\nreduzierten Kosten mehr",
|
||||
FARBEN["gut"], "#ecfdf5")
|
||||
|
||||
pfeil(achse, (3.9, 3.9), (6.1, 3.9),
|
||||
r"Dualwerte $\pi_i$", FARBEN["haupt"], bogen=-0.28,
|
||||
versatz=(0, 0.62))
|
||||
pfeil(achse, (6.1, 3.0), (3.9, 3.0),
|
||||
r"neue Spalte $a$, wenn $1-\sum_i \pi_i a_i < 0$",
|
||||
FARBEN["zweit"], bogen=-0.28, versatz=(0, -0.72))
|
||||
pfeil(achse, (7.4, 2.4), (6.0, 1.5), "sonst", FARBEN["gut"], bogen=0.25,
|
||||
versatz=(0.55, 0.12))
|
||||
|
||||
achse.text(5.0, 4.85,
|
||||
"Die Schleife endet nicht, wenn sie müde wird, sondern wenn das "
|
||||
"Pricing nichts mehr findet.",
|
||||
ha="center", fontsize=9.5, color=FARBEN["gedaempft"],
|
||||
style="italic")
|
||||
|
||||
speichere(figur, "kap_dekomposition_regelkreis")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
zeichne()
|
||||
115
bilder_04/erzeuge_vrp_subtours.py
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# erzeuge_vrp_subtours.py
|
||||
"""
|
||||
Erzeugt das Kurzzyklus-Diagramm zum Graphen-Kapitel:
|
||||
|
||||
bilder_04/kap_graphen_subtouren.svg
|
||||
|
||||
Links, was ein Tourenmodell OHNE Kurzzyklus-Bedingungen zurueckgeben darf:
|
||||
Jeder Kunde hat genau einen Vorgaenger und einen Nachfolger - alle
|
||||
Gradbedingungen sind erfuellt -, und trotzdem ist die Loesung unbrauchbar, weil
|
||||
die Fahrzeuge nie am Depot vorbeikommen. Rechts dieselbe Instanz mit gueltiger
|
||||
Rundreise ab und bis Depot.
|
||||
|
||||
Der Punkt: Das linke Bild ist kein Rechenfehler. Es erfuellt jede Bedingung,
|
||||
die naiv formuliert wurde. Was fehlt, ist eine Bedingung, an die man beim
|
||||
Aufschreiben nicht denkt - und genau dafuer gibt es MTZ.
|
||||
|
||||
Die Instanz stammt aus erzeuge_vrp_touren.py, also aus demselben Datensatz wie
|
||||
das Buchprogramm. Die Kurzzyklen links sind daraus konstruiert: Es sind die
|
||||
Kundenfolgen der gueltigen Loesung, nur ohne das Depot geschlossen. So zeigt
|
||||
das Bild denselben Fall und nicht ein zweites, erfundenes Beispiel.
|
||||
|
||||
Aufruf (aus dem Repository-Wurzelverzeichnis):
|
||||
python3 bilder_04/erzeuge_vrp_subtours.py
|
||||
|
||||
Benoetigt: numpy, matplotlib, ortools
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from stil_04 import FARBEN, speichere # noqa: E402
|
||||
import matplotlib.pyplot as plt # noqa: E402
|
||||
from erzeuge_vrp_touren import (TOURFARBEN, erzeuge_daten, # noqa: E402
|
||||
loese)
|
||||
|
||||
|
||||
def kurzzyklen(touren) -> list[list[int]]:
|
||||
"""Aus jeder gueltigen Tour einen Kreis ohne Depot machen.
|
||||
|
||||
Genau das darf ein Modell liefern, das nur 'jeder Kunde einmal angefahren'
|
||||
fordert: In sich geschlossene Kreise, die das Depot nie beruehren."""
|
||||
aus = []
|
||||
for tour in touren:
|
||||
kunden = [k for k in tour["stationen"] if k != 0]
|
||||
if len(kunden) >= 2:
|
||||
aus.append(kunden + [kunden[0]])
|
||||
return aus
|
||||
|
||||
|
||||
def pruefe_grade(zyklen, anzahl_kunden: int) -> None:
|
||||
"""Die unzulaessige Loesung muss die Gradbedingungen trotzdem erfuellen."""
|
||||
besucht = [k for z in zyklen for k in z[:-1]]
|
||||
if sorted(besucht) != list(range(1, anzahl_kunden + 1)):
|
||||
raise SystemExit(
|
||||
"Subtour-Bild: Die konstruierten Kurzzyklen besuchen nicht jeden "
|
||||
"Kunden genau einmal. Dann waere das linke Bild kein gueltiges "
|
||||
"Gegenbeispiel, sondern einfach falsch.")
|
||||
|
||||
|
||||
def zeichne(koordinaten, touren, zyklen) -> None:
|
||||
figur, achsen = plt.subplots(1, 2, figsize=(9.6, 4.6), sharex=True,
|
||||
sharey=True)
|
||||
|
||||
for achse, titel in ((achsen[0], "ohne Kurzzyklus-Bedingungen"),
|
||||
(achsen[1], "mit MTZ — jede Tour über das Depot")):
|
||||
for ort in range(1, len(koordinaten)):
|
||||
achse.scatter(*koordinaten[ort], s=34, color=FARBEN["text"],
|
||||
zorder=3)
|
||||
achse.scatter(*koordinaten[0], marker="s", s=150,
|
||||
color=FARBEN["fehler"], zorder=4)
|
||||
achse.annotate("Depot", koordinaten[0], textcoords="offset points",
|
||||
xytext=(9, 7), fontsize=9, color=FARBEN["fehler"],
|
||||
fontweight="bold")
|
||||
achse.set_xlabel("km")
|
||||
achse.set_title(titel, fontsize=10.5, color=FARBEN["text"])
|
||||
|
||||
# links: die Kurzzyklen
|
||||
for nummer, (zyklus, farbe) in enumerate(zip(zyklen, TOURFARBEN), start=1):
|
||||
weg = koordinaten[zyklus]
|
||||
achsen[0].plot(weg[:, 0], weg[:, 1], "-", color=farbe, linewidth=1.8,
|
||||
alpha=0.9, zorder=2)
|
||||
achsen[0].annotate(
|
||||
"Jeder Kunde hat genau einen Vor- und\n"
|
||||
"einen Nachfolger — und kein Fahrzeug\n"
|
||||
"kommt je am Depot vorbei.",
|
||||
xy=(0.03, 0.03), xycoords="axes fraction", fontsize=9,
|
||||
color=FARBEN["fehler"], va="bottom")
|
||||
|
||||
# rechts: die gueltige Loesung
|
||||
for tour, farbe in zip(touren, TOURFARBEN):
|
||||
weg = koordinaten[tour["stationen"]]
|
||||
achsen[1].plot(weg[:, 0], weg[:, 1], "-", color=farbe, linewidth=1.8,
|
||||
alpha=0.9, zorder=2)
|
||||
|
||||
achsen[0].set_ylabel("km")
|
||||
figur.suptitle("Warum Gradbedingungen allein nicht genügen",
|
||||
fontsize=11, color=FARBEN["text"], y=1.01)
|
||||
speichere(figur, "kap_graphen_subtouren")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
koordinaten, distanz = erzeuge_daten()
|
||||
touren = loese(distanz)
|
||||
zyklen = kurzzyklen(touren)
|
||||
pruefe_grade(zyklen, len(koordinaten) - 1)
|
||||
print(f" {len(zyklen)} Kurzzyklen konstruiert, "
|
||||
f"{sum(len(z) - 1 for z in zyklen)} Kunden — jeder genau einmal.")
|
||||
zeichne(koordinaten, touren, zyklen)
|
||||
115
bilder_04/erzeuge_zielgroessen_radar.py
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# erzeuge_zielgroessen_radar.py
|
||||
"""
|
||||
Erzeugt das Netzdiagramm zum Kapitel "Vom Management-Wunsch zum Modell":
|
||||
|
||||
bilder_04/kap_modellierung_zielgroessen.svg
|
||||
|
||||
Drei Saetze aus derselben Besprechung - "Umsatz maximieren", "Maschinen
|
||||
auslasten", "Deckungsbeitrag maximieren" - ergeben drei verschiedene Plaene aus
|
||||
denselben 14 Anfragen. Das Netzdiagramm legt sie uebereinander. Der Punkt, auf
|
||||
den es ankommt: Der Auslastungsplan sieht auf seiner eigenen Achse perfekt aus
|
||||
und faellt auf der Deckungsbeitragsachse durch.
|
||||
|
||||
KEINE ZAHL IST HIER EINGETRAGEN. Das Skript importiert Vom_Wunsch_zum_Modell.py
|
||||
und loest dieselben Modelle mit demselben Solver. Weicht das Buchprogramm
|
||||
spaeter ab, aendert sich das Bild mit - nicht der Text neben einem alten Bild.
|
||||
|
||||
Aufruf (aus dem Repository-Wurzelverzeichnis):
|
||||
python3 bilder_04/erzeuge_zielgroessen_radar.py
|
||||
|
||||
Benoetigt: numpy, scipy, matplotlib
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
HIER = os.path.dirname(os.path.abspath(__file__))
|
||||
BASIS = os.path.dirname(HIER)
|
||||
sys.path.insert(0, HIER)
|
||||
# Das Buchprogramm ist die Quelle der Instanz - importiert, nicht kopiert.
|
||||
sys.path.insert(0, os.path.join(
|
||||
BASIS, "Operations_Research_mit_Python_Version_04_Programme"))
|
||||
|
||||
from stil_04 import FARBEN, speichere # noqa: E402
|
||||
import matplotlib.pyplot as plt # noqa: E402
|
||||
from Vom_Wunsch_zum_Modell import (AUFTRAEGE, DECKUNGSBEITRAG, # noqa: E402
|
||||
STUNDEN, UMSATZ, kennzahlen,
|
||||
waehle_aus)
|
||||
|
||||
# Die fuenf Achsen des Netzes und wie sie aus kennzahlen() entstehen. Jede
|
||||
# Achse wird auf ihr eigenes Maximum ueber die drei Plaene skaliert - sonst
|
||||
# erschluege der Umsatz in Euro alle uebrigen Groessen.
|
||||
ACHSEN = [
|
||||
("Deckungsbeitrag", lambda k: k["db"]),
|
||||
("Umsatz", lambda k: k["umsatz"]),
|
||||
("Maschinen-\nauslastung", lambda k: k["auslastung"]),
|
||||
("Stammkunden\nbedient", lambda k: len({a[1] for a in AUFTRAEGE if a[2]})
|
||||
- k["kunden_leer"]),
|
||||
("angenommene\nAufträge", lambda k: k["angenommen"]),
|
||||
]
|
||||
|
||||
PLAENE = [
|
||||
("„Umsatz maximieren“", UMSATZ, FARBEN["zweit"]),
|
||||
("„Maschinen auslasten“", STUNDEN, FARBEN["warnung"]),
|
||||
("„Deckungsbeitrag maximieren“", DECKUNGSBEITRAG, FARBEN["gut"]),
|
||||
]
|
||||
|
||||
|
||||
def loese_alle() -> list[tuple[str, dict, str]]:
|
||||
ergebnis = []
|
||||
for name, ziel, farbe in PLAENE:
|
||||
plan = waehle_aus(ziel)
|
||||
if plan is None:
|
||||
raise SystemExit(f"Radar: Fuer {name} liefert das Modell keinen Plan.")
|
||||
ergebnis.append((name, kennzahlen(plan), farbe))
|
||||
return ergebnis
|
||||
|
||||
|
||||
def zeichne(plaene) -> None:
|
||||
werte = {name: [f(k) for _, f in ACHSEN] for name, k, _ in plaene}
|
||||
maxima = [max(werte[name][i] for name in werte) or 1.0
|
||||
for i in range(len(ACHSEN))]
|
||||
|
||||
winkel = np.linspace(0, 2 * np.pi, len(ACHSEN), endpoint=False)
|
||||
geschlossen = np.concatenate([winkel, winkel[:1]])
|
||||
|
||||
figur, achse = plt.subplots(figsize=(6.6, 5.8),
|
||||
subplot_kw={"projection": "polar"})
|
||||
for name, _k, farbe in plaene:
|
||||
anteile = [w / m * 100 for w, m in zip(werte[name], maxima)]
|
||||
anteile += anteile[:1]
|
||||
achse.plot(geschlossen, anteile, "-", color=farbe, linewidth=2.0,
|
||||
label=name, zorder=3)
|
||||
achse.fill(geschlossen, anteile, color=farbe, alpha=0.12, zorder=2)
|
||||
|
||||
achse.set_xticks(winkel)
|
||||
achse.set_xticklabels([bez for bez, _ in ACHSEN], fontsize=9,
|
||||
color=FARBEN["text"])
|
||||
achse.set_yticks([25, 50, 75, 100])
|
||||
achse.set_yticklabels(["25 %", "50 %", "75 %", "100 %"], fontsize=8,
|
||||
color=FARBEN["gedaempft"])
|
||||
achse.set_ylim(0, 108)
|
||||
achse.grid(color=FARBEN["linie"], linewidth=0.7, alpha=0.6)
|
||||
achse.spines["polar"].set_color(FARBEN["linie"])
|
||||
achse.set_title("Drei Sätze aus derselben Besprechung, drei Pläne\n"
|
||||
"je Achse auf den besten der drei Pläne bezogen",
|
||||
fontsize=10.5, color=FARBEN["text"], pad=24)
|
||||
achse.legend(fontsize=9, loc="lower center", bbox_to_anchor=(0.5, -0.20),
|
||||
frameon=False)
|
||||
|
||||
speichere(figur, "kap_modellierung_zielgroessen")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
plaene = loese_alle()
|
||||
for name, k, _ in plaene:
|
||||
print(f" {name:<32} Umsatz {k['umsatz']:>8,.0f} "
|
||||
f"DB {k['db']:>8,.0f} Auslastung {k['auslastung']:>3.0f} % "
|
||||
f"angenommen {k['angenommen']:>2}")
|
||||
zeichne(plaene)
|
||||
2284
bilder_04/kap_dekomposition_regelkreis.svg
Normal file
|
After Width: | Height: | Size: 66 KiB |
1909
bilder_04/kap_dp_almgren_chriss.svg
Normal file
|
After Width: | Height: | Size: 60 KiB |
2440
bilder_04/kap_einfuehrung_kombinatorik.svg
Normal file
|
After Width: | Height: | Size: 74 KiB |
2026
bilder_04/kap_graphen_subtouren.svg
Normal file
|
After Width: | Height: | Size: 62 KiB |
3309
bilder_04/kap_lp_entartung.svg
Normal file
|
After Width: | Height: | Size: 95 KiB |
3483
bilder_04/kap_milp_big_m.svg
Normal file
|
After Width: | Height: | Size: 106 KiB |
1556
bilder_04/kap_modellierung_zielgroessen.svg
Normal file
|
After Width: | Height: | Size: 53 KiB |
1820
bilder_04/kap_oekosystem_laufzeit.svg
Normal file
|
After Width: | Height: | Size: 55 KiB |
1888
bilder_04/kap_prognose_entscheidungsguete.svg
Normal file
|
After Width: | Height: | Size: 55 KiB |
1972
bilder_04/kap_supplychain_lastabwurf.svg
Normal file
|
After Width: | Height: | Size: 66 KiB |
2542
bilder_04/kap_unsicherheit_durchschnitt.svg
Normal file
|
After Width: | Height: | Size: 74 KiB |
92
bilder_04/stil_04.py
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# stil_04.py
|
||||
"""
|
||||
Gemeinsamer Unterbau der Diagramm-Generatoren: Farben, Grundeinstellungen und
|
||||
das Schreiben der SVG-Datei.
|
||||
|
||||
Warum es das gibt: Elf neue Generatoren trugen sonst elfmal denselben Vorspann.
|
||||
Eine Zeile daraus ist besonders heikel -
|
||||
|
||||
plt.rcParams["svg.hashsalt"] = "or-mit-python-v04"
|
||||
|
||||
Ohne sie wuerfelt matplotlib die clip-path-IDs je Figure neu; die Datei aendert
|
||||
sich dann bei JEDEM Lauf, ohne dass sich am Bild etwas geaendert haette. Das
|
||||
faellt nicht sofort auf, sondern erst als unlesbarer git-diff - und dann sucht
|
||||
man die Ursache lange. Dasselbe gilt fuer metadata={"Date": None} beim
|
||||
Speichern: Sonst steht der Schreibzeitstempel auf die Mikrosekunde im SVG.
|
||||
|
||||
Die 33 aelteren Generatoren setzen beides jeweils selbst. Sie bleiben
|
||||
unveraendert - eine Umstellung waere ein eigener Schritt und braeuchte den
|
||||
Nachweis, dass jedes einzelne Bild byte-identisch bleibt.
|
||||
|
||||
Benutzung:
|
||||
|
||||
from stil_04 import FARBEN, neue_figur, speichere
|
||||
|
||||
figur, achse = neue_figur(breite=7.6, hoehe=4.6)
|
||||
...
|
||||
speichere(figur, "kap_einfuehrung_kombinatorik")
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
# Reproduzierbare SVG-Ausgabe - siehe Modulkommentar.
|
||||
plt.rcParams["svg.hashsalt"] = "or-mit-python-v04"
|
||||
|
||||
# Serifenlos und ueberall vorhanden. Bewusst kein Stylesheet von seaborn: Das
|
||||
# Buch hat eine eigene Palette, und zwei Stilsysteme nebeneinander sehen nach
|
||||
# zwei Buechern aus.
|
||||
plt.rcParams["font.family"] = "DejaVu Sans"
|
||||
plt.rcParams["axes.grid"] = True
|
||||
plt.rcParams["grid.linestyle"] = ":"
|
||||
plt.rcParams["grid.alpha"] = 0.4
|
||||
plt.rcParams["axes.spines.top"] = False
|
||||
plt.rcParams["axes.spines.right"] = False
|
||||
|
||||
BASIS = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
BILDER = os.path.join(BASIS, "bilder_04")
|
||||
|
||||
# Die Palette des Buchs - dieselben Werte wie in den 33 vorhandenen Diagrammen
|
||||
# und auf der Titelseite. Die Namen sagen, wofuer die Farbe steht, nicht wie
|
||||
# sie aussieht: So bleibt die Bedeutung erhalten, falls ein Ton je gewechselt
|
||||
# wird.
|
||||
FARBEN = {
|
||||
"haupt": "#4338ca", # Indigo - die primaere Groesse, Zielfunktion
|
||||
"zweit": "#0891b2", # Teal - die Vergleichsgroesse
|
||||
"warnung": "#b45309", # Amber - teuer, aber zulaessig
|
||||
"gut": "#059669", # Emerald - Optimum, zulaessige Menge
|
||||
"fehler": "#be123c", # Rose - unzulaessig, Verlust, Bruch
|
||||
"akzent": "#7c3aed", # Violett - Schranken, Niveaulinien
|
||||
"text": "#334155", # Slate - Beschriftung
|
||||
"gedaempft": "#64748b", # Slate hell - Nebeninformation
|
||||
"linie": "#94a3b8", # Hilfslinien
|
||||
"flaeche": "#f1f5f9", # Hinterlegte Flaechen
|
||||
}
|
||||
|
||||
|
||||
def neue_figur(breite: float = 7.6, hoehe: float = 4.6, **kwargs):
|
||||
"""Figure und Achse in der Standardgroesse des Buchs."""
|
||||
return plt.subplots(figsize=(breite, hoehe), **kwargs)
|
||||
|
||||
|
||||
def speichere(figur, name: str) -> str:
|
||||
"""Schreibt bilder_04/<name>.svg und meldet den Pfad.
|
||||
|
||||
Nur SVG: Das PDF bindet die Bilder ueber \\includesvg (inkscape) ein, und
|
||||
PNG-Zweitfassungen gab es in diesem Projekt schon einmal - sie wurden von
|
||||
nirgendwo referenziert und mussten wieder geloescht werden."""
|
||||
figur.tight_layout()
|
||||
os.makedirs(BILDER, exist_ok=True)
|
||||
pfad = os.path.join(BILDER, f"{name}.svg")
|
||||
figur.savefig(pfad, format="svg", dpi=160, bbox_inches="tight",
|
||||
metadata={"Date": None})
|
||||
plt.close(figur)
|
||||
print(f"geschrieben: {pfad}")
|
||||
return pfad
|
||||