Version 04 als eigenes Repository
Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python" (Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch Version_03 (eingefroren) verwaltet und Version_04/ ignoriert. Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist. Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise, 325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren, 25 Notebooks, PDF mit 715 Seiten. Zusaetzlich in diesem Commit: * pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api, figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt unveraendert daneben bestehen. ortools steht in der Grundausstattung, highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der schlanken Installation gar nicht erst auftreten. * Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo steht in requirements.txt, wird aber von keinem Programm importiert, sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in [empfehlungen]). * NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen. * PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den tatsaechlichen aus erzeuge_titelseite.py korrigiert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
commit
b7af2f1d9a
468 changed files with 262141 additions and 0 deletions
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Gilt fuer die Version 04. Alle Pfade sind relativ zu diesem Verzeichnis -
|
||||||
|
# damit funktioniert die Datei unveraendert weiter, sobald Version_04 die
|
||||||
|
# Wurzel eines eigenen Repositorys ist.
|
||||||
|
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
# LaTeX/xelatex-Nebenprodukte des PDF-Baus (build_version_04.py --pdf)
|
||||||
|
build_v04.log
|
||||||
|
build_v04.tex
|
||||||
|
build_v04.aux
|
||||||
|
build_v04.toc
|
||||||
|
build_v04.out
|
||||||
|
build_v04.idx
|
||||||
|
build_v04.ilg
|
||||||
|
build_v04.ind
|
||||||
|
svg-inkscape/
|
||||||
|
|
||||||
|
# Von den Beispielprogrammen beim Lauf erzeugte Dateien
|
||||||
|
Operations_Research_mit_Python_Version_04_Programme/output/
|
||||||
|
produktionsmix.xlsx
|
||||||
|
produktionsmix_ergebnis.xlsx
|
||||||
147
CLAUDE.md
Normal file
147
CLAUDE.md
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
Kontext zum Repository *Optimierte Entscheidungsfindung mit Python*, Version 04.
|
||||||
|
|
||||||
|
## Vor jeder Arbeit zuerst lesen
|
||||||
|
|
||||||
|
* **`PLAN.md`** — das Ziel und der Weg dorthin: alle sechs Phasen, die Zielstruktur, der
|
||||||
|
Kapitel-Baukasten, die dreizehn verbindlichen Regeln, die Verifikationsschritte. Ändert
|
||||||
|
sich nur bei einem Kurswechsel.
|
||||||
|
* **`PROGRESS.md`** — der erreichte Stand: eine Checkliste zur Wiederaufnahme, die
|
||||||
|
Referenzwerte zum Gegenprüfen, was erledigt ist, welche Funde es unterwegs gab und **was
|
||||||
|
der konkret nächste Schritt ist**. Wird nach jedem Arbeitsschritt fortgeschrieben.
|
||||||
|
|
||||||
|
Ohne diese beiden Dateien fehlt der Kontext, um sinnvoll weiterzuarbeiten — die Kapiteltexte
|
||||||
|
allein verraten weder den Stand noch die Konventionen. Alle sechs Phasen sind abgeschlossen;
|
||||||
|
`PROGRESS.md` Abschnitt 7 nennt, was bewusst offen geblieben ist und warum.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Struktur
|
||||||
|
|
||||||
|
Dieses Verzeichnis ist die Wurzel; alle Skripte leiten ihre Pfade daraus ab
|
||||||
|
(`BASIS = dirname(dirname(__file__))` bzw. `dirname(HIER)`).
|
||||||
|
|
||||||
|
```
|
||||||
|
Operations_Research_mit_Python_Version_04/ Quelle: 31 Kapiteldateien + Build-Skripte
|
||||||
|
bilder_04/ Quelle: Diagramme + erzeuge_*.py-Generatoren
|
||||||
|
Operations_Research_mit_Python_Version_04.md generiert: Gesamtdokument
|
||||||
|
Operations_Research_mit_Python_Version_04.pdf generiert: PDF (xelatex)
|
||||||
|
OR_HTML_04/ generiert: Mehrseiten-Website
|
||||||
|
Operations_Research_mit_Python_Version_04_Programme/ generiert: Beispielprogramme
|
||||||
|
Notebooks_04/ generiert: ein .ipynb je Kapitel
|
||||||
|
Kritik_und_Verbesserungsvorschlaege/ Quelle: Rezension, Verbesserungsvorschläge,
|
||||||
|
NEUER_TITEL.md (Vorlage des Titelblatts)
|
||||||
|
pyproject.toml Quelle: Abhängigkeiten in Gruppen
|
||||||
|
pandoc-defaults-basis.yaml, pandoc/, pandoc-defaults-buch.yaml PDF-Konfiguration
|
||||||
|
```
|
||||||
|
|
||||||
|
Nur die als **Quelle** markierten Verzeichnisse werden von Hand bearbeitet.
|
||||||
|
|
||||||
|
Dieses Verzeichnis ist seit dem 08.09.2026 ein **eigenes Git-Repository** (`main`). Das
|
||||||
|
übergeordnete `OR_mit_Python/` ist nur noch das Archiv der Historie bis zur Trennung und
|
||||||
|
verwaltet aktiv allein `Version_03`; es trägt `Version_04/` in seiner `.gitignore`. Commits
|
||||||
|
gehören ab jetzt hierher.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Version_04
|
||||||
|
python3 Operations_Research_mit_Python_Version_04/build_version_04.py --check # nur prüfen
|
||||||
|
python3 Operations_Research_mit_Python_Version_04/build_version_04.py --pdf --html
|
||||||
|
python3 Operations_Research_mit_Python_Version_04/extract_programme_04.py
|
||||||
|
```
|
||||||
|
|
||||||
|
`--check` prüft mehr als die Struktur: fehlende Codezäune, Links auf nicht existierende
|
||||||
|
Dateien und **harte Kapitel-/Abschnitts-/Aufgabennummern im Quelltext**. Es meldet Datei und
|
||||||
|
Zeile. Der Suchausdruck erlaubt beliebigen Zwischenraum — der letzte gefundene Fall war ein
|
||||||
|
`Handrechnung\n12.1` über zwei Zeilen, an dem jede zeilenweise Suche vorbeiläuft.
|
||||||
|
|
||||||
|
`build_version_04.py` fasst nicht notwendige harte Zeilenumbrüche innerhalb von Absätzen
|
||||||
|
zusammen (`reflow_markdown()`) — viele Markdown-Renderer stellen einen einzelnen Umbruch
|
||||||
|
sonst fälschlich als sichtbaren Umbruch dar.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Die wichtigsten Konventionen
|
||||||
|
|
||||||
|
**Keine abgeleiteten Zahlen im Quelltext.** Das ist die Regel, an der dieses Projekt am
|
||||||
|
häufigsten gescheitert ist — sie wurde vier Mal an verschiedenen Stellen verletzt gefunden
|
||||||
|
(Anhang A, 66 Programm-Docstrings, die Übersichtstabellen der Anhänge, `CLAUDE.md` selbst).
|
||||||
|
Konkret:
|
||||||
|
|
||||||
|
* Überschriften: `# Kapitel: <Titel> {#kap:<label>}`, `## Titel {#sec:<label>}` — die Nummer
|
||||||
|
vergibt der Build (`resolve_numbering()`, `nummeriere_abschnitte()`).
|
||||||
|
* Aufgaben, Handrechnungen, Abbildungen: `**Aufgabe ⭐ — Titel.**`,
|
||||||
|
`> **✏️ Handrechnung: Titel**`, `` — Nummern von
|
||||||
|
`nummeriere_marken()`.
|
||||||
|
* Im Fließtext `{ref:<label>}`, nie eine Literalzahl.
|
||||||
|
* In Code-Kommentaren und Docstrings der Kapitel**name** („Kapitel Metaheuristiken:"), nie
|
||||||
|
die Nummer. Auch nicht im Dateinamen.
|
||||||
|
|
||||||
|
**Querverweise für die Website** laufen über `baue_seiten_registry()` /
|
||||||
|
`resolve_numbering_seite()`, nicht über `resolve_numbering()` — nur dort entsteht aus einem
|
||||||
|
seitenübergreifenden Verweis `andere-seite.html#anker`.
|
||||||
|
|
||||||
|
**Stichwortregister:** `{idx:Begriff}` bzw. `{idx:Oberbegriff!Unterbegriff}`. Vor einem neuen
|
||||||
|
Begriff prüfen, ob er schon existiert:
|
||||||
|
`grep -ohE '\{idx:[^}]+\}' Operations_Research_mit_Python_Version_04/*.md | sort -u` — sonst
|
||||||
|
entstehen zwei Registereinträge für dasselbe Konzept. `texindy` sortiert mit dem deutschen
|
||||||
|
`din5007`-Modul (Umlaute wie im Telefonbuch); das generische `-L german` bricht ab.
|
||||||
|
|
||||||
|
**Programme sind ein Artefakt, keine zweite Quelle.** Änderungen gehören in den
|
||||||
|
Kapitel-Codeblock, danach `extract_programme_04.py`. Ein Codeblock gilt als vollständiges
|
||||||
|
Programm, wenn er mit `#!/usr/bin/env python3`, einer Leerzeile und `# Name.py` beginnt.
|
||||||
|
Auch das `README.md` im Programme-Verzeichnis wird erzeugt — aus der Vorlage
|
||||||
|
`README_Programme.md`; `requirements.txt` ist die einzige dort von Hand gepflegte Datei.
|
||||||
|
|
||||||
|
**`or_kern.py` ist der gemeinsame Unterbau** aller Programme (Domänenmodell, `SolverStatus`,
|
||||||
|
`Loesung`-DTO, Abnahmeprüfung). Abgedruckt im Kapitel Praxisfallen.
|
||||||
|
|
||||||
|
**`ortools` und `highspy` lassen sich nicht im selben Prozess importieren** (beide bringen
|
||||||
|
eine eigene HiGHS-Kopie mit). Deshalb lädt `or_kern.py` Solverbibliotheken erst in der
|
||||||
|
aufrufenden Funktion, und Programme, die beide brauchen, starten getrennte Prozesse (Muster:
|
||||||
|
`Ein_System_Vier_Ansaetze.py`, `Solverwechsel_CPSAT_HiGHS.py`). `cvxpy` zieht ein
|
||||||
|
installiertes `highspy` bei der Solver-Erkennung selbst mit hinein — der Konflikt entsteht
|
||||||
|
also auch indirekt.
|
||||||
|
|
||||||
|
**Neue Kapiteldatei** ⇒ in die `DATEIEN`-Liste in `build_version_04.py`.
|
||||||
|
**Neues Programm** ⇒ drei Stellen: Kapitelkopf („Programme:"), Vorwort
|
||||||
|
(„Verzeichnis der Beispielprogramme"), bei neuer Abhängigkeit `requirements.txt` **und**
|
||||||
|
`pyproject.toml` (dort in die passende Gruppe, nicht pauschal in die Grundausstattung).
|
||||||
|
|
||||||
|
Die vollständigen **dreizehn Regeln** stehen in `PLAN.md` Abschnitt 9 — darunter, dass jede
|
||||||
|
abgedruckte Ausgabe aus einem echten Lauf stammt und dass `PROGRESS.md` in denselben Commit
|
||||||
|
gehört wie die Arbeit, die sie beschreibt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Diagramme
|
||||||
|
|
||||||
|
`bilder_04/erzeuge_*.py` erzeugen 18 der 32 SVGs, jeweils **aus derselben Instanz wie das
|
||||||
|
zugehörige Buchprogramm**. Das ist kein Selbstzweck: Von zehn nachgebauten Bildern förderten
|
||||||
|
sieben einen Fehler zutage — dreimal ein Modell, das im Buch gar nicht vorkommt, einmal
|
||||||
|
widersprüchliche Zahlen zwischen Bild und Text, einmal ein gekipptes Vorzeichen. Wer ein
|
||||||
|
Diagramm anfasst, vergleicht es zuerst mit dem Modell des Kapitels.
|
||||||
|
|
||||||
|
Die übrigen 14 sind schematisch (Kästen, Pfeile, beschriftete Formeln) und bekommen bewusst
|
||||||
|
keinen Generator — dort kann nichts driften. Das Kriterium steht in `PROGRESS.md`
|
||||||
|
Abschnitt 6c.
|
||||||
|
|
||||||
|
Konventionen der Generatoren: `plt.rcParams["svg.hashsalt"] = "or-mit-python-v04"` und
|
||||||
|
`metadata={"Date": None}` für byteidentische Läufe; stammt die Instanz aus einem
|
||||||
|
Zufallsstrom, wird die Ziehungsreihenfolge des Buchprogramms nachgespielt.
|
||||||
|
|
||||||
|
## Plotly-Figuren
|
||||||
|
|
||||||
|
`{plotly:name}` bindet `bilder_04/plotly/<name>.html` in die Kapitelseite ein. Das Fragment
|
||||||
|
wird als Rohblock ` ```{=html} ` ausgegeben — **nicht** als blankes HTML: Plotlys Fragment ist
|
||||||
|
eine einzige lange Zeile mit eingebetteten Leerzeichenketten, aus der Pandoc sonst einen
|
||||||
|
Codeblock macht. Genau daran waren alle vier Figuren kaputt, bis es die Schlussabnahme fand.
|
||||||
|
Im PDF steht stattdessen ein Hinweis auf die Website.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sprache
|
||||||
|
|
||||||
|
Kommentare, Ausgaben und Fließtext sind durchgängig **Deutsch** — diesen Stil beibehalten.
|
||||||
48
Kritik_und_Verbesserungsvorschlaege/Kritik.md
Normal file
48
Kritik_und_Verbesserungsvorschlaege/Kritik.md
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
## Buchkritik: *Operations Research mit Python* (Version 03)
|
||||||
|
**Autor:** Dieter Schlüter
|
||||||
|
**Titel:** *Operations Research mit Python – Von den mathematischen Grundlagen zur quantitativen Portfolio-Optimierung an den Finanzmärkten*
|
||||||
|
**Format:** E-Book / Kompendium (PDF & Web-Handbuch auf jamulix.de)
|
||||||
|
**Eingesetztes Ökosystem:** Python, HiGHS, SciPy, Google OR-Tools (CP-SAT), CVXPY
|
||||||
|
### 1. Überblick und thematische Ausrichtung
|
||||||
|
Operations Research (OR) leidet in der Fachliteratur häufig unter einer Zweiteilung: Auf der einen Seite stehen klassisch-mathematische Universitätslehrbücher, die sich in Konvexitätsbeweisen und Simplex-Tableaus verlieren, ohne eine einzige Zeile lauffähigen Code zu liefern; auf der anderen Seite finden sich oberflächliche Programmier-Tutorials, die API-Befehle auflisten, ohne das Verständnis für Dualität, Schattenpreise oder algorithmische Komplexität zu schärfen.
|
||||||
|
Dieter Schlüters Kompendium *Operations Research mit Python* setzt genau an dieser Schnittstelle an. Das Werk spannt einen weiten Bogen von den theoretischen Grundlagen der linearen und diskreten Optimierung über Constraint Programming bis hin zur quantitativen Portfolio- und Risikomodellierung an den Finanzmärkten. Mit 15 Hauptkapiteln, einer eigenständigen Projektwerkstatt sowie umfangreichen Anhängen präsentiert sich Version 03 als anspruchsvolles, praxisorientiertes Handbuch für Praktiker, Entwickler und quantitative Analysten.
|
||||||
|
### 2. Struktur und didaktisches Konzept
|
||||||
|
Das Werk ist in fünf logische Hauptabschnitte unterteilt:
|
||||||
|
* **Teil I: Grundlagen des Operations Research (Kap. 1–3):**
|
||||||
|
Einführung in die Denkweise mathematischer Modellierung (Entscheidungsvariablen, Zielfunktion, Restriktionen), Auffrischung der Vektor- und Matrixrechnung mit Fokus auf Konvexität sowie eine systematische Einordnung der Python-Solver-Landschaft (HiGHS, SciPy, OR-Tools, CVXPY).
|
||||||
|
* **Teil II: Kernverfahren der deterministischen Optimierung (Kap. 4–7):**
|
||||||
|
Lineare Programmierung (LP) inklusive Simplex und Dualitätsinterpretation, Mixed-Integer Linear Programming (MILP) mit Branch-and-Bound, Constraint Programming via Google OR-Tools CP-SAT sowie klassische Graphen- und Tourenprobleme (Min-Cost-Flow, TSP/VRP).
|
||||||
|
* **Teil III: Nichtlinearität, Unsicherheit und Dynamik (Kap. 8–10):**
|
||||||
|
Quadratische und nichtlineare Optimierung (KKT-Bedingungen, Lagrange), stochastische Optimierung und Robustheit sowie Dynamische Programmierung mit der Bellman-Gleichung.
|
||||||
|
* **Teil IV: Operations Research an den Finanzmärkten (Kap. 11–14):**
|
||||||
|
Der Brückenschlag zur Quant-Finance: Kovarianzschätzung und Shrinkage (Ledoit-Wolf), klassische Markowitz-Portfolios, Conditional Value at Risk (CVaR) unter Berücksichtigung von Transaktionskosten sowie der Aufbau einer modularen Backtest-Handelsmaschine.
|
||||||
|
* **Teil V: Praxis, Projektwerkstatt & Anhänge (Kap. 15, Anhänge A–D):**
|
||||||
|
Konkrete Fallstricke beim Produktivgang, 7 realistische End-to-End-Projekte (von Schichtplanung bis Portfolio-Rebalancer), Musterkatalog, Debugging-Leitfaden und Übungslösungen.
|
||||||
|
Besonders hervorzuheben sind die im Einstieg definierten **Lernpfade (A bis E)**. Sie ermöglichen es Lesern, das Monumentalwerk je nach individuellem Ziel zu erschließen – beispielsweise fokussiert auf Personaleinsatz/Scheduling (~20 h), Tourenplanung (~20 h), Quantitative Finance (~30 h) oder als 80-stündiges Vollstudium.
|
||||||
|
### 3. Was das Buch besonders stark macht
|
||||||
|
#### Konsequente Fokussierung auf moderne Open-Source-Werkzeuge
|
||||||
|
Anstatt auf teure proprietäre Suiten (wie Gurobi oder CPLEX) oder veraltete Lehrbuchwerkzeuge zu setzen, nutzt der Autor moderne, produktionsreife Open-Source-Engines:
|
||||||
|
* **HiGHS** als erstklassiger linearer/MILP-Solver,
|
||||||
|
* **Google OR-Tools (CP-SAT)** für kombinatorische Scheduling- und Zuweisungsprobleme,
|
||||||
|
* **CVXPY** für konvexe Optimierungsprobleme.
|
||||||
|
Dadurch bleiben alle Beispiele ohne Lizenzhürden für jeden Leser auf Standard-Hardware nachvollziehbar.
|
||||||
|
#### Ausgeprägter Realismus und Warnung vor Modellierungs-Fallen
|
||||||
|
Das Buch meidet akademische Laborbedingungen. Dies zeigt sich exemplarisch in Kapitel 11: Anstatt die historische Kovarianzmatrix naiv in das Markowitz-Modell einzuspeisen (was in der Realität lediglich Schätzrauschen optimiert und erratische Portfoliogewichte erzeugt), zwingt der Autor den Leser zunächst durch Verfahren wie Shrinkage und Regularisierung. Ebenso widmen sich Kapitel 15 und Anhang C gezielt typischen Produktivitätskillern (numerische Instabilitäten, unbounded/infeasible states, Lookahead-Bias in Backtests).
|
||||||
|
#### Symbiose aus mathematischer Exaktheit und lauffähigem Code
|
||||||
|
Mathematische Formulierungen werden nicht als Selbstzweck präsentiert, sondern dienen unmittelbar als Blaupause für die Modellierung in Python. Die Implementierungen sind didaktisch aufbereitet: Algorithmen wie der Simplex werden in Basisschritten demonstriert, um die Mechanik zu verstehen, bevor für den Produktiveinsatz auf hochoptimierte C++-Kern-Bindings zurückgegriffen wird.
|
||||||
|
### 4. Schwachpunkte und Grenzen
|
||||||
|
* **Ungewöhnlich steiler thematischer Bruch (Logistik vs. Finance):**
|
||||||
|
Der Übergang von klassischen OR-Domänen (Schicht- und Tourenplanung, Flussnetzwerke) zur quantitativen Kapitalmarkttheorie (CVaR, Order-Execution, Markowitz) ist fachlich anspruchsvoll. Während OR-Spezialisten den Finanzteil möglicherweise als zu nischig empfinden, könnten Finanzanalysten die ersten Kapitel über klassische Tourenplanung als Ballast wahrnehmen.
|
||||||
|
* **Hohe Einstiegshürde bei Mathematik und Software-Architektur:**
|
||||||
|
Obwohl das Buch Grundlagenkapitel enthält, setzt das Tempo ein solides Verständnis von linearer Algebra, Wahrscheinlichkeitsrechnung und idiomatischem Python (OOP, NumPy, Vektorisierung) voraus. Für Quereinsteiger ohne mathematisch-technischen Hintergrund ist die Lernkurve steil.
|
||||||
|
* **Fokus auf Konvexität:**
|
||||||
|
Komplexe, hochgradig nicht-konvexe Optimierungslandschaften oder metaheuristische Verfahren (genetische Algorithmen, Simulated Annealing) werden im Vergleich zur exakten konvexen und diskreten Optimierung eher am Rande behandelt.
|
||||||
|
### 5. Fazit
|
||||||
|
*Operations Research mit Python* von Dieter Schlüter ist ein bemerkenswert tiefgründiges, anwendungsnahes deutschsprachiges Fachkompendium. Es schließt die Lücke zwischen formaler Operations-Research-Theorie und moderner Softwareentwicklung auf beispielhafte Weise.
|
||||||
|
**Zielgruppen-Empfehlung:**
|
||||||
|
* **Data Scientists und Software-Ingenieure**, die über Machine Learning hinausgehen und deterministische Entscheidungssysteme oder Zuweisungs-Engines bauen wollen.
|
||||||
|
* **Quantitative Analysten und Finanzökonomen**, die Portfolio-Optimierung jenseits vereinfachter Lehrbuchmodelle robust und fehlerresistent implementieren möchten.
|
||||||
|
* **Studierende und Dozenten** der Wirtschaftsmathematik, Wirtschaftsinformatik und des Maschinenbaus, die nach praxistauglichen Python-Vorlagen für OR-Vorlesungen suchen.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
8
Kritik_und_Verbesserungsvorschlaege/NEUER_TITEL.md
Normal file
8
Kritik_und_Verbesserungsvorschlaege/NEUER_TITEL.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Optimierte Entscheidungsfindung mit Python
|
||||||
|
|
||||||
|
### Von der mathematischen Modellierung zu praktischen intelligenten Lösungen für Betrieb, Technik und Finanzmärkte
|
||||||
|
|
||||||
|
**Ein praxisorientiertes Kompendium für Entscheider mit Constraint Programming, Vektor- und Matrixmethoden sowie HiGHS, SciPy, Google OR-Tools und CVXPY.**
|
||||||
|
|
||||||
|
Von **Personaleinsatz-, Schicht- und Vertretungsplänen, Fahrtrouten, Logistik und Maschinen- und Netzwerkauslastung** über **Energieverteilung und Ressourcenplanung** bis zur **Portfoliooptimierung und besseren Entscheidungen an den Finanzmärkten** – mit Python modellieren, optimieren und belegbar fundierte Entscheidungen treffen.
|
||||||
|
|
||||||
300
Kritik_und_Verbesserungsvorschlaege/Verbesserungen.md
Normal file
300
Kritik_und_Verbesserungsvorschlaege/Verbesserungen.md
Normal file
|
|
@ -0,0 +1,300 @@
|
||||||
|
### Rolle und Ziel
|
||||||
|
Du bist ein erfahrener Fachlektor, leitender Didaktiker für technische Weiterbildung, Senior Operations Research Scientist und Lead Python Software Architect. Überarbeite und erweitere das Kompendium **„Operations Research mit Python“ (Version 03)** von Dieter Schlüter umfassend.
|
||||||
|
|
||||||
|
|
||||||
|
### Primäres Lehr- und Kursziel
|
||||||
|
Wichtigstes Ziel: Das Buch soll nicht nur als akademisches Nachschlagewerk dienen, sondern als **begeisternder, hocheffektiver Kursbegleiter**. Die Kursteilnehmer sollen Lust bekommen, sich mit dem Thema zu beschäftigen, schnell messbare Erfolgserlebnisse erzielen und die erlernten Methoden unmittelbar am nächsten Tag in ihrer beruflichen Praxis (z. B. Logistik, Disposition, Produktion, Finanzen, Data Science) produktiv einsetzen können.
|
||||||
|
|
||||||
|
Dein Ziel ist es, bei der Überarbeitung didaktische Brüche zu glätten, methodische Lücken zu schließen, die Code-Architektur auf Enterprise-Niveau zu heben und das Werk als unangefochtenes Standardwerk im deutschsprachigen Raum zu etablieren.
|
||||||
|
|
||||||
|
|
||||||
|
### Die drei Leitprinzipien der Überarbeitung
|
||||||
|
1. **Time-to-First-Success minimieren:** Schnelle, lauffähige Aha-Erlebnisse vor formaler Theorie.
|
||||||
|
2. **Kognitive Entlastung:** Abstrakte Formeln konsequent durch visuelle Analogien und Klartext übersetzen.
|
||||||
|
3. **Robuste Praxistauglichkeit:** Solide Python-Architektur, realistische Datenpipelines (Excel/Pandas) und praxiserprobtes Debugging statt realitätsferner Laborbeispiele.
|
||||||
|
|
||||||
|
|
||||||
|
Führe die folgenden fünf Arbeitspakete präzise und vollständig aus:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Didaktik, Kurserlebnis und schneller Praxistransfer
|
||||||
|
|
||||||
|
Setze in jedem Kapitel die folgenden didaktischen Bausteine um:
|
||||||
|
|
||||||
|
### A. Das „In 5 Minuten gelöst“-Erfolgserlebnis an den Kapitelanfang
|
||||||
|
* **Struktur:** Beginne jedes Kapitel mit einer prominent hervorgehobenen Box: Ein minimales, realistisches Problem (4–6 Zeilen Daten, maximal 10–15 Zeilen Python-Code), das per Copy-Paste sofort lauffähig ist und ein klares betriebswirtschaftliches/praktisches Ergebnis liefert.
|
||||||
|
* **Didaktische Umkehrung:** Erst wenn der Code erfolgreich gelaufen ist und die Kursteilnehmer das Resultat sehen, folgt die Erklärung: *„Warum funktioniert das? Wie rechnet der Solver im Hintergrund? Was bedeuten die Variablen?“*
|
||||||
|
|
||||||
|
### B. Formel-Übersetzer: Mathe für Praktiker
|
||||||
|
* **Zweispaltige Notation:** Stelle jeder zentralen mathematischen Formel eine tabellarische Klartext-Übersetzung gegenüber:
|
||||||
|
* *Links (Mathematik):* $\sum_{i \in I} x_{i,j} \le C_j \quad \forall j \in J$
|
||||||
|
* *Rechts (Alltagssprache):* „Für jede Maschine $j$ darf die Summe aller zugewiesenen Aufträge $i$ die Maximalkapazität $C_j$ nicht überschreiten.“
|
||||||
|
* **Geometrische Intuition vor Beweis:** Führe Konzepte wie Konvexität, Simplex-Ecken, Hyperebenen und Branch-and-Bound-Bäume stets zuerst als visuelle Metaphern und 2D-/3D-Skizzen ein, bevor algebraische Definitionen folgen.
|
||||||
|
|
||||||
|
### C. Die „Excel-zu-Python“-Brücke für den Berufsalltag
|
||||||
|
* Zeige in Kapitel 1 und Kapitel 3 explizit, wie typische Modelle aus dem Standard-Excel-Solver in modernes Python überführt werden.
|
||||||
|
* Integriere wiederverwendbare Hilfsfunktionen, um Parameter direkt via `pandas.read_excel()` oder `.to_csv()` einzulesen, an den Solver zu übergeben und die Optimierungsergebnisse wieder als formatierte Tabellen (inkl. Kennzahlen) zu exportieren.
|
||||||
|
|
||||||
|
### D. Spickzettel (Cheat Sheets) & Visuelle Checklisten
|
||||||
|
* Ergänze die Anhänge und Kapitel um **1-Seiten-Syntax-Spickzettel** für die Kernbibliotheken (SciPy, HiGHS, Google OR-Tools CP-SAT, CVXPY):
|
||||||
|
* Wie definiere ich Binär-, Ganzzahl- und kontinuierliche Variablen?
|
||||||
|
* Wie formuliere ich Entweder-Oder-Bedingungen (Big-M)?
|
||||||
|
* Wie setze ich weiche Restriktionen mit Strafkosten (Slack-Variablen)?
|
||||||
|
* Wie frage ich Dualwerte, Schattenpreise und Lösungsstatus ab?
|
||||||
|
* Erstelle ein **Entscheidungs-Flussdiagramm** zu Beginn von Teil II und III: *„Welcher Solver und welches Paradigma passt zu meinem konkreten Problem?“* (Kriterien: Linearität, Ganzzahligkeit, Konvexität, Problemgröße, Laufzeitanforderungen).
|
||||||
|
|
||||||
|
### E. Praxistransfer: „Vom Management-Wunsch zum Modell“
|
||||||
|
* Füge einen Leitfaden zur **Anforderungsanalyse für OR-Projekte** ein.
|
||||||
|
* Demonstriere anhand praxisnaher Dialoge, wie man vage Vorgaben („Kosten senken, aber die Mitarbeiterzufriedenheit hochhalten“) systematisch zerlegt:
|
||||||
|
1. *Entscheidungsvariablen:* Was sind die echten Stellschrauben?
|
||||||
|
2. *Restriktionen:* Was sind harte physikalische Grenzen, was sind verhandelbare Wünsche?
|
||||||
|
3. *Zielfunktion:* Wie messen wir Erfolg mathematisch sauber, ohne Fehlanreize zu setzen?
|
||||||
|
|
||||||
|
### F. Micro-Quizzes und „Finde den Modellierungsfehler“-Übungen
|
||||||
|
* Schließe jedes Kapitel mit einem kurzen Selbsttest ab (3 Multiple-Choice-Fragen).
|
||||||
|
* Ergänze gezielte Debugging-Aufgaben: Code, der fehlerfrei kompiliert, aber betriebswirtschaftlich unsinnige Ergebnisse liefert (z. B. unendliche Produktion durch falsches Relationszeichen `>=` statt `<=`, vergessene Ganzzahligkeit oder Zirkelbezüge).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Software-Architektur und Code-Qualität
|
||||||
|
|
||||||
|
Hebe sämtliche Implementierungen im Buch auf ein sauberes, wartbares Enterprise-Niveau:
|
||||||
|
|
||||||
|
### A. Typsicherheit und Datenvalidierung
|
||||||
|
* Verwende durchgängig moderne Python-Features (Python 3.11+): strenge Type Hints (`typing`), Dataclasses oder **Pydantic v2**-Modelle für alle Input- und Output-Daten (Ressourcen, Bedarfe, Kostenmatrizen).
|
||||||
|
* Validiere Randbedingungen (z. B. strikt positive Kapazitäten, stochastische Vektoren mit Summe 1) bereits beim Einlesen der Daten.
|
||||||
|
|
||||||
|
### B. Klare Entkopplung (Clean Architecture / Service-Pattern)
|
||||||
|
* Trenne Domänenlogik strikt von der Solver-API:
|
||||||
|
$$\text{Rohdaten (Excel/JSON)} \longrightarrow \text{Pydantic-Domänenmodell} \longrightarrow \text{Model-Builder (Solver)} \longrightarrow \text{Lösungs-DTO}$$
|
||||||
|
* Kursteilnehmer sollen lernen, wie man Modelle baut, bei denen der zugrundeliegende Solver (z. B. Wechsel von HiGHS auf Gurobi oder CP-SAT) ausgetauscht werden kann, ohne die Business-Logik neu schreiben zu müssen.
|
||||||
|
|
||||||
|
### C. Explizite Status- und Fehlerbehandlung
|
||||||
|
* Beseitige alle impliziten Annahmen über optimale Lösungen.
|
||||||
|
* Implementiere für jedes Modell eine explizite Auswertung des Solver-Status (`OPTIMAL`, `FEASIBLE`, `INFEASIBLE`, `UNBOUNDED`, `TIME_LIMIT`).
|
||||||
|
* Zeige, wie Timeouts, Suboptimalitätsgrenzen (MIP-Gap) und Näherungslösungen im Code professionell abgefangen, protokolliert und weiterverarbeitet werden.
|
||||||
|
|
||||||
|
### D. Vektorisierte Modellgenerierung
|
||||||
|
* Zeige in Kapitel 3 und fortgeschrittenen Beispielen, wie man bei großen Datenmengen langsame Python-`for`-Schleifen vermeidet.
|
||||||
|
* Demonstriere den Einsatz von vektorisierten Matrizenoperationen (NumPy) bzw. Tabellen-Pipelines (**Polars** / **Pandas**), um tausende Nebenbedingungen performant aufzubauen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Fachliche, methodische und algorithmische Erweiterungen
|
||||||
|
|
||||||
|
Schließe die methodischen Lücken zwischen akademischer Lehrbuch-Theorie und realer Industrie-Komplexität:
|
||||||
|
|
||||||
|
### A. Didaktische Harmonisierung: Der Wechsel von Logistik zu Finance
|
||||||
|
* Schlage eine methodische Brücke vor Kapitel 11: Zeige auf, dass die mathematischen Strukturen der Portfoliotheorie identisch sind mit klassischen OR-Problemen (z. B. Ressourcenallokation im Lager $\leftrightarrow$ Kapitalallokation im Portfolio; Robustheit gegen Lieferkettenausfälle $\leftrightarrow$ Risikomaße gegen Kursabstürze).
|
||||||
|
* Ergänze für Nicht-Finanz-Interessierte ein paralleles, alternatives Transfer-Beispiel (z. B. **Supply-Chain-Netzwerkdesign unter Unsicherheit** oder **Energieeinsatzplanung**), das dieselben mathematischen Werkzeuge (Stochastik, CVaR, Dynamische Programmierung) nutzt.
|
||||||
|
|
||||||
|
### B. Metaheuristiken für nicht-konvexe und großskalige Probleme
|
||||||
|
* Ergänze einen praxisnahen Leitfaden für NP-schwere Probleme, bei denen exakte MILP-Solver an Skalierungsgrenzen stoßen.
|
||||||
|
* Implementiere verständliche Metaheuristiken (z. B. Large Neighborhood Search (LNS), Genetische Algorithmen via `Pymoo` oder Simulated Annealing) für komplexe Touren- und Belegungsplanungs-Probleme.
|
||||||
|
|
||||||
|
### C. Multi-Objective-Optimierung und Pareto-Effizienz
|
||||||
|
* Erkläre, warum einfache Zielgewichtungen ($\min w_1 f_1 + w_2 f_2$) bei Zielkonflikten in der Praxis oft versagen.
|
||||||
|
* Implementiere praxisnah die **$\epsilon$-Constraint-Methode** und die lexikografische Optimierung (z. B. Minimierung von Logistikkosten bei gleichzeitiger Minimierung des CO₂-Ausstoßes) inklusive Visualisierung der Pareto-Front.
|
||||||
|
|
||||||
|
### D. Schnittstelle Data Science: Predict-then-Optimize
|
||||||
|
* Zeige das Zusammenspiel von Machine Learning und Optimierung: Warum führt ein ML-Modell mit minimalem Vorhersagefehler (MSE) im nachgelagerten Optimierungsmodell oft zu suboptimalen Entscheidungen?
|
||||||
|
* Demonstriere den Unterschied an einem Beispiel (Nachfrageprognose $\rightarrow$ Lagerbestandsoptimierung) und gib einen Ausblick auf differenzierbare Optimierungsschichten (`cvxpylayers`).
|
||||||
|
|
||||||
|
### E. NLP/MINLP und Warm-Starts
|
||||||
|
* Zeige die Grundlagen nicht-konvexer Optimierung mit Open-Source-Solvern wie **Ipopt** (über CyIpopt).
|
||||||
|
* Ergänze praxisnahe Code-Beispiele für **MIP-Warm-Starts** (Einspeisen bekannter Ausgangslösungen zur massiven Beschleunigung der Lösungszeit).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Numerik, Debugging und Produktionsreife (MLOps)
|
||||||
|
|
||||||
|
Stelle sicher, dass die Modelle der Kursteilnehmer auch im harten Produktivbetrieb fehlerfrei funktionieren:
|
||||||
|
|
||||||
|
### A. Numerische Stabilität, Konditionierung und Toleranzen
|
||||||
|
* Erkläre die Matrix-Konditionszahl $\kappa(A)$ anhand eines konkreten Python-Experiments: Zeige, wie schlecht skalierte Koeffizienten (z. B. große Kostenbeträge gemischt mit winzigen Mengeneinheiten) zu scheinbar unlösbaren Modellen (`INFEASIBLE`) oder Lösungsdrift führen.
|
||||||
|
* Zeige Standard-Skalierungsverfahren (Ruiz-Equilibrierung) und schärfe das Bewusstsein für Primal-, Dual- und Integrabilitäts-Toleranzen (z. B. warum $0.99999998$ niemals leichtfertig mit `int()` gerundet werden darf).
|
||||||
|
|
||||||
|
### B. Solver-Diagnose & Unlösbarkeits-Analyse (IIS)
|
||||||
|
* Baue Anhang C zu einem vollwertigen Troubleshooting-Handbuch aus.
|
||||||
|
* Implementiere Werkzeuge zur Ermittlung eines **Irreducible Infeasible Subsystem (IIS)** bzw. Conflict Refiners: Wie isoliert man in einem Modell mit tausenden Restriktionen automatisiert genau die Teilmenge, die den mathematischen Widerspruch erzeugt?
|
||||||
|
* Erläutere, wie duale Entartung (Degeneriertheit) instabile Schattenpreise erzeugt und wie man fundierte Sensitivitätsanalysen für Entscheidungsträger erstellt.
|
||||||
|
|
||||||
|
### C. Testing, Benchmarking und API-Deployment
|
||||||
|
* **Pytest-Framework:** Entwickle standardisierte Testroutinen für Optimierungsmodelle (Invarianztests, Überprüfung von Restriktionsverletzungen, Regressionstests an Benchmark-Instanzen).
|
||||||
|
* **Solver-Benchmarks:** Zeige reproduzierbare Laufzeit- und Speicher-Benchmarks beim Vergleich von SciPy, HiGHS, OR-Tools und CVXPY über skalierende Instanzgrößen ($N \in \{10^2, 10^3, 10^4\}$).
|
||||||
|
* **FastAPI & Async:** Zeige das Deployment eines Optimierungsmodells als Microservice via **FastAPI** mit asynchroner Aufgabenwarteschlange (Celery/Redis) für rechenintensive Solver-Läufe, inklusive eines kompakten `Dockerfile`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Auszuführende Arbeitsanweisung
|
||||||
|
|
||||||
|
Setze diese Verbesserungen kapitelweise und systematisch um:
|
||||||
|
1. **Analysiere** das jeweilige Kapitel auf didaktische Verständlichkeit, mathematische Zugänglichkeit und Code-Qualität.
|
||||||
|
2. **Erstelle** für jedes Kapitel zuerst die „In 5 Minuten gelöst“-Box und die Formel-Übersetzer-Tabellen.
|
||||||
|
3. **Refaktoriere** den Python-Code gemäß den Standards für Typsicherheit, Validierung und Statusbehandlung.
|
||||||
|
4. **Erweitere** die methodischen Abschnitte und baue die praktischen Excel-/Pandas-Workflows ein.
|
||||||
|
5. **Schließe** jedes Kapitel mit den Übungen zur Fehlerdiagnose („Finde den Denkfehler“) und den interaktiven Micro-Quizzes ab.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Im Einzelnen führe unter diesen Prämissen folgendes präzise und vollständig aus:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. Architektur und Code-Qualität: Transformation zu produktionsreifem Python
|
||||||
|
Überarbeite alle Skripte und Code-Beispiele im Buch nach modernen Software-Engineering-Standards:
|
||||||
|
* **Typsicherheit & Verträge:** Führe durchgehend Type Hints (`typing`, `mypy`-kompatibel) und Laufzeitvalidierung via **Pydantic v2** für alle Modellparameter (z. B. Nachfragedaten, Kapazitäten, Renditevektoren) ein.
|
||||||
|
* **Trennung von Domäne und Solver:** Refaktoriere den Code so, dass Domänenlogik strikt von der Solver-Syntax getrennt ist. Führe ein klares Repository-/Service-Pattern ein: Rohdaten $\rightarrow$ Validiertes Domänenmodell $\rightarrow$ Solver-Builder (z. B. CVXPY/OR-Tools) $\rightarrow$ Lösungs-DTO.
|
||||||
|
* **Fehler- und Statusbehandlung:** Ersetze implizite Annahmen über Solver-Ergebnisse durch explizite Fehlerbehandlung für Randfälle: `INFEASIBLE`, `UNBOUNDED`, `NUMERICAL_ERROR` oder Timeouts. Zeige für MILP und CP-SAT, wie Suboptimalitätsgrenzen (MIP-Gap) und Abbruchkriterien sauber abgefangen und geloggt werden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Methodische Ergänzungen: Schließen der algorithmischen Lücken
|
||||||
|
Ergänze den theoretischen und praktischen Kanon um drei bisher unterrepräsentierte, in der Praxis aber essenzielle Bereiche:
|
||||||
|
* **Metaheuristiken für Nicht-Konvexes & NP-Schwere (Neues Kapitel oder Brückenteil):**
|
||||||
|
* Integriere einen praktischen Leitfaden für Probleme, bei denen exakte Solver (HiGHS, CP-SAT) skalierungsbedingt scheitern.
|
||||||
|
* Implementiere praxisnahe Metaheuristiken (z. B. Large Neighborhood Search (LNS), Simulated Annealing oder Genetische Algorithmen) mit Bibliotheken wie `Pymoo` oder nativem Python für großskalige Vehicle-Routing- oder Scheduling-Instanzen.
|
||||||
|
* **Nicht-lineare nicht-konvexe Optimierung (NLP / MINLP):**
|
||||||
|
* Zeige den praktischen Einsatz von Solvern wie **Ipopt** (über CyIpopt oder Pyomo/CasADi) für nicht-konvexe kontinuierliche Probleme. Diskutiere den Unterschied zwischen lokalen und globalen Optima sowie Strategien zur Wahl von Startwerten (Warm-Starts).
|
||||||
|
* **Warm-Starts & Dekomposition:**
|
||||||
|
* Ergänze in Teil II praxisnahe Beispiele für **MIP Warm-Starts** (Einspeisen bekannter zulässiger Lösungen) sowie eine verständliche Einführung in Dekompositionsverfahren (z. B. Spaltengenerierung / Dantzig-Wolfe für Cutting-Stock- oder Crew-Scheduling-Probleme).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Didaktische Harmonisierung: Glättung des Wechsels von Logistik zu Finance
|
||||||
|
Löse den abrupten thematischen Bruch zwischen Teil II/III (diskrete Optimierung, Logistik) und Teil IV (Finanzmärkte):
|
||||||
|
* **Konzeptionelle Brückenkapitel:** Führe vor Kapitel 11 eine explizite methodische Überleitung ein: Zeige, dass die mathematischen Strukturen identisch sind (z. B. Resource Allocation in der Produktion $\leftrightarrow$ Kapitalallokation im Portfolio; Robust Counterparts bei Lieferkettenausfällen $\leftrightarrow$ Worst-Case-Risikomaße am Markt).
|
||||||
|
* **Entflechtung der Zielgruppen:**
|
||||||
|
* Kennzeichne Teil IV klar als spezialisierte Vertiefungsdomäne.
|
||||||
|
* Ergänze für Leser, die *nicht* aus dem Finanzbereich kommen, ein paralleles, alternatives Vertiefungskapitel für industrielle Wertschöpfung (z. B. **Supply-Chain-Netzwerkdesign unter Unsicherheit** oder **Energie- und Kraftwerkseinsatzplanung**), das dieselben mathematischen Werkzeuge (Stochastik, CVaR/Robustheit, dynamische Programmierung) auf physische Systeme anwendet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Enterprise-Testing, Benchmarking & MLOps-Integration
|
||||||
|
Erweitere Kapitel 15 und die Projektwerkstatt um operative Deployment-Standards:
|
||||||
|
* **Testing-Framework für Optimierungsmodelle:**
|
||||||
|
* Entwickle ein dediziertes Testkonzept mit `pytest`: Regressionstests gegen bekannte Testinstanzen, Validierung von Restriktionsverletzungen (Infeasibility-Checks) und Invarianztests (z. B. Skalierungsinvarianz der Zielfunktion).
|
||||||
|
* **Benchmarking & Performance-Profiling:**
|
||||||
|
* Baue eine strukturierte Benchmark-Pipeline auf: Vergleich von Lösungszeit und Speicherverbrauch zwischen SciPy, HiGHS, Google OR-Tools und CVXPY bei wachsender Problemgröße ($N \in \{10^2, 10^3, 10^4, 10^5\}$).
|
||||||
|
* **Containerisierung & API-Bereitstellung:**
|
||||||
|
* Zeige die Kapselung eines Optimierungsmodells in einer **FastAPI**-Anwendung inklusive asynchroner Job-Queue (z. B. via Celery/Redis) für länger laufende Solver-Jobs.
|
||||||
|
* Liefere ein einsatzbereites `Dockerfile`, das C++-Abhängigkeiten und Solver-Bibliotheken performant und schlank baut.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Format, Interaktivität und Leseerlebnis
|
||||||
|
* **Interaktive Ausführungsumgebung:** Ergänze jedes Kapitel um Links zu validierten Jupyter Notebooks bzw. Google-Colab-Instanzen mit One-Click-Setup (`pip install highs cvxpy ortools ...`).
|
||||||
|
* **Visualisierungs-Upgrade:** Ersetze rein statische Matplotlib-Plots durch interaktive Visualisierungen (z. B. via `Plotly` für 3D-Konvexitätsflächen, KKT-Bedingungen, Pareto-Fronten bei Multi-Objective-Problemen und dynamische Gantt-Diagramme in CP-SAT).
|
||||||
|
* **Entscheidungs-Flussdiagramm:** Füge zu Beginn von Teil II und Teil III ein visuelles Ablaufdiagramm ein: *„Welcher Solver / welches Paradigma passt zu meinem Problem?“* (Entscheidungskriterien: linear vs. nichtlinear, stetig vs. diskret, konvex vs. nicht-konvex, Skalierungsanforderungen, Lösungszeitgarantien).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Numerische Stabilität, Konditionierung und Skalierung (Deep Dive)
|
||||||
|
Erweitere Kapitel 2, 4 und Anhang C um einen tiefgehenden praktischen Leitfaden zur numerischen Linearen Algebra:
|
||||||
|
* **Konditionszahlen in der Praxis:** Erkläre nicht nur formal die Matrix-Konditionszahl $\kappa(A)$, sondern demonstriere mit einem konkreten Python-Experiment, wie schlecht skalierte Koeffizienten (z. B. Kosten im Bereich $10^7$ kombiniert mit Toleranzen von $10^{-6}$) zu stillschweigendem Lösungsdrift oder falschen `INFEASIBLE`-Meldungen führen.
|
||||||
|
* **Skalierungs- und Vorverarbeitungs-Routinen:** Zeige, wie man Koeffizientenmatrizen systematisch über Methoden wie Ruiz-Equilibrierung oder geometrische Skalierung transformiert, bevor sie an den Solver übergeben werden.
|
||||||
|
* **Toleranzen verstehen:** Schärfe das Bewusstsein für Primal-, Dual- und Integrabilitäts-Toleranzen der Solver (z. B. `feasibility_tolerance` in HiGHS vs. OR-Tools). Erkläre, warum eine Variable mit dem Wert `0.99999998` in einem MILP nicht einfach mit `int()` gecastet werden darf.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. Modernes Modellierungs-Ökosystem: Pyomo, Linopy und Polars
|
||||||
|
Erweitere Kapitel 3 um moderne Modellierungs-Frameworks und Big-Data-Anbindungen:
|
||||||
|
* **Evaluierung von Linopy & Pyomo:** CVXPY und OR-Tools decken viel ab, aber für große industrielle LP/MILP-Probleme sind **Pyomo** (als klassischer Industriestandard) oder das neuere **Linopy** (optimiert für $N$-dimensionale xarray-Strukturen und extrem schnelle Matrix-Generierung) unverzichtbar. Ergänze einen kompakten Vergleich dieser Modellierungsschichten.
|
||||||
|
* **Polars & Vektorisierte Modellgenerierung:** In der Praxis verbringen OR-Ingenieure oft 80 % der Rechenzeit nicht im Solver, sondern beim Aufbau der Constraints über langsame Python-`for`-Schleifen. Zeige, wie man mithilfe von **Polars** oder Vektorisierung in NumPy Millionen von Nebenbedingungen um Größenordnungen schneller konstruiert.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. Schnittstelle zwischen Machine Learning und Operations Research (Smart Predict-then-Optimize)
|
||||||
|
Baue in Teil III oder IV eine methodische Brücke zur modernen Data Science:
|
||||||
|
* **Predict-then-Optimize:** Zeige das Zusammenspiel von Prognose und Optimierung. Klassische ML-Modelle minimieren den Mean Squared Error (MSE), nicht die Kosten im nachgelagerten Optimierungsmodell. Demonstriere anhand eines Beispiels (z. B. Nachfrageprognose $\rightarrow$ Lagerhaltung), warum ein isoliert „gutes“ ML-Modell zu teuren OR-Entscheidungen führen kann.
|
||||||
|
* **Differenzierbare Optimierung:** Führe kurz das Paradigma differenzierbarer Layer ein (z. B. über `cvxpylayers`), bei denen Optimierungsmodelle als Schicht direkt in neuronale PyTorch-Netze eingebettet werden können.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. Multi-Objective-Optimierung und Pareto-Effizienz
|
||||||
|
Ergänze in Teil II und Teil IV die Behandlung konkurrierender Zielkonflikte, die in realen Projekten die Regel sind:
|
||||||
|
* **Jenseits linearer Skalarisierung:** Erkläre, warum die simple Gewichtung von Zielen ($\min w_1 f_1 + w_2 f_2$) bei nicht-konvexen Pareto-Fronten versagt und wie man stattdessen mit der **$\epsilon$-Constraint-Methode** oder lexikografischer (hierarchischer) Optimierung arbeitet.
|
||||||
|
* **Praxisbeispiel:** Implementiere ein zweidimensionales Trade-off-Modell (z. B. Kosten vs. CO₂-Ausstoß in der Tourenplanung oder Rendite vs. Drawdown) und berechne die exakte Pareto-Front inklusive Visualisierung.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10. Professionelle Solver-Diagnose und Unlösbarkeits-Analyse (IIS)
|
||||||
|
Vertiefe Anhang C (Fehlerdiagnose) um fortgeschrittene Debugging-Techniken:
|
||||||
|
* **Irreducible Infeasible Subsystem (IIS):** Wenn ein Modell mit 50.000 Restriktionen `INFEASIBLE` meldet, ist der Entwickler ohne Werkzeuge verloren. Erkläre die mathematische Funktionsweise eines IIS/Conflict Refiners und zeige, wie man mit Python-Mitteln isoliert, welche minimale Teilmenge von Restriktionen den Konflikt verursacht.
|
||||||
|
* **Dual-Analyse bei Degeneriertheit:** Zeige auf, wie duale Entartung (Multiple Dual Solutions) dazu führt, dass Schattenpreise instabil werden, und wie man damit bei Sensitivitätsanalysen für das Management umgeht.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 11. Didaktischer Kurs-Fokus: Motivation, Zugänglichkeit & Praxistransfer
|
||||||
|
|
||||||
|
Unter der Prämisse, dass das Buch primär als **didaktischer Kursbegleiter** dient, müssen rein akademische Vertiefungen und industrielle Enterprise-Overheads zurücktreten. Im Zentrum stehen drei Faktoren:
|
||||||
|
1. **Sofortige Erfolgserlebnisse (Time-to-First-Success)**,
|
||||||
|
2. **Kognitive Entlastung bei Hürden** (Mathematik, Solver-Syntax, Debugging),
|
||||||
|
3. **Direkte Übertragbarkeit in den Arbeitsalltag**.
|
||||||
|
|
||||||
|
Führe zur Umsetzung dieser Kursziele die folgenden didaktischen und strukturellen Verbesserungen aus:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11.1. Das „5-Minuten-Erfolgserlebnis“ an den Anfang jedes Kapitels stellen
|
||||||
|
* **Didaktisches Problem:** Wer erst 15 Seiten Vektorräume oder Simplex-Tableaus liest, bevor der erste Code läuft, verliert Motivation und Schwung.
|
||||||
|
* **Maßnahme:**
|
||||||
|
* Jedes Kapitel beginnt mit einer **„In 5 Minuten gelöst“-Box**: Ein minimales, realistisches Mini-Problem (z. B. 4 Zeilen Daten, 6 Zeilen Python-Code), das der Teilnehmer per Copy-Paste sofort ausführt und dessen Ergebnis sofort einleuchtet.
|
||||||
|
* Erst **nach** dem funktionierenden Ergebnis folgt die methodische Erklärung: *„Warum funktioniert das? Wie rechnet der Solver das im Hintergrund? Was bedeuten die Variablen?“*
|
||||||
|
* **Effekt:** Neugier und Selbstwirksamkeit statt theoretischer Lähmung.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11.2. Duale Lesart: „Mathe für Anwender“ mit visuellen Analogien
|
||||||
|
* **Didaktisches Problem:** Die Formelsprache (Summenzeichen mit Doppelindizes, Matrixschreibweisen, KKT-Gradienten) wirkt auf Praktiker oft abschreckend.
|
||||||
|
* **Maßnahme:**
|
||||||
|
* **Formel-Übersetzer-Boxen:** Jede zentrale mathematische Formulierung erhält eine zweispaltige Darstellung:
|
||||||
|
* Links: Die mathematische Formel $\sum_{i \in I} x_{i,j} \le C_j$.
|
||||||
|
* Rechts: Der Klartext in Alltagssprache: *„Für jede Maschine $j$ darf die Summe aller zugewiesenen Aufträge $i$ die Gesamtkapazität $C_j$ nicht überschreiten.“*
|
||||||
|
* **Geometrische Intuition vor algebraischem Beweis:** Konvexität, Simplex-Ecken und Branch-and-Bound-Bäume zuerst als 2D-Skizzen und anschauliche Metaphern (z. B. „Eine Seifenblase, die sich in die Ecke eines Zimmers spannt“) einführen, bevor algebraische Definitionen folgen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11.3. „Excel-zu-Python“-Brücken für den Berufsalltag bauen
|
||||||
|
* **Didaktisches Problem:** Im betrieblichen Alltag liegen 90 % aller Daten in Tabellenkalkulationen, und viele Kursteilnehmer kennen Optimierung bisher nur aus dem Excel-Solver.
|
||||||
|
* **Maßnahme:**
|
||||||
|
* Baue in Kapitel 1 und Kapitel 3 explizite Brücken: *„Wie Sie Ihr Excel-Solver-Modell in sauberen Python-Code übersetzen.“*
|
||||||
|
* Zeige, wie Parameter direkt aus `pandas.read_excel()` oder CSV eingelesen, als Modell formuliert und die optimierten Zuweisungen wieder als saubere Excel-Tabelle mit Kennzahlen exportiert werden.
|
||||||
|
* **Effekt:** Die Teilnehmer können das Gelernte am nächsten Tag direkt an ihren eigenen Arbeitsdateien ausprobieren.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11.4. Spickzettel & Visuelle Checklisten („Cheat Sheets“)
|
||||||
|
* **Didaktisches Problem:** Wenn Teilnehmer ein eigenes Problem lösen wollen, wissen sie oft nicht, welcher Baustein (Variable, Constraint, Zielfunktion) wie in Python heißt.
|
||||||
|
* **Maßnahme:**
|
||||||
|
* **1-Seiten-Syntax-Cheat-Sheet:** Eine kompakte Übersichtstabelle für die drei Hauptwerkzeuge (SciPy, OR-Tools, CVXPY):
|
||||||
|
* *Wie definiere ich eine Binärvariable?*
|
||||||
|
* *Wie formuliere ich eine Entweder-Oder-Bedingung?*
|
||||||
|
* *Wie setze ich eine Obergrenze?*
|
||||||
|
* *Wie lese ich den optimalen Wert aus?*
|
||||||
|
* **Modellierungs-Baukasten:** Visuelle Rezeptkarten für Standardfragen des Arbeitsalltags: *„Wie modelliere ich Mindestabnahmemengen?“, „Wie verhindere ich Rüstzeiten?“, „Wie baue ich ein Budgetlimit ein?“*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11.5. Selbsteinschätzungs-Meilensteine & „Finde den Denkfehler“-Übungen
|
||||||
|
* **Didaktisches Problem:** Reine Programmieraufgaben frustrieren, wenn man an Syntax-Details hängenbleibt.
|
||||||
|
* **Maßnahme:**
|
||||||
|
* Ergänze jedes Kapitel um 2–3 kurze **Micro-Quizzes** (3 Multiple-Choice-Fragen) zur Selbstkontrolle vor dem nächsten Kapitel.
|
||||||
|
* Führe **„Finde den Modellierungsfehler“-Aufgaben** ein: Der Code läuft fehlerfrei durch, aber das Ergebnis ist unplausibel (z. B. Maschine produziert unendlich viel, weil eine Nebenbedingung als `>=` statt `<=` formuliert wurde; oder Ganzzahligkeit vergessen).
|
||||||
|
* **Effekt:** Das schult den kritischen Blick auf Ergebnisse, der in der Praxis wichtiger ist als das reine Schreiben von Code.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11.6. Praxistransfer-Protokoll: „Vom Management-Wunsch zum Modell“
|
||||||
|
* **Didaktisches Problem:** Im Berufsleben kommt selten jemand mit einer fertigen Zielfunktion. Chefs und Fachabteilungen formulieren vage: *„Wir wollen die Kosten senken, aber die Mitarbeiter sollen nicht meckern und die Liefertreue muss hoch bleiben.“*
|
||||||
|
* **Maßnahme:**
|
||||||
|
* Ergänze ein methodisches Kapitel oder Leitfaden: **„Anforderungsanalyse für OR-Projekte“**.
|
||||||
|
* Zeige anhand eines realistischen Dialogs, wie man vage Geschäftsanforderungen systematisch zerlegt:
|
||||||
|
1. Was sind die echten Stellschrauben? $\rightarrow$ *Entscheidungsvariablen*
|
||||||
|
2. Was sind harte Grenzen, was weiche Wünsche? $\rightarrow$ *Harte vs. Weiche Constraints (Penalties)*
|
||||||
|
3. Woran messen wir Erfolg wirklich? $\rightarrow$ *Zielfunktion*
|
||||||
|
* **Effekt:** Die Teilnehmer lernen die wichtigste Fähigkeit für den Arbeitsalltag: das Übersetzen von Business-Problemen in mathematische Logik.
|
||||||
|
|
||||||
|
|
||||||
310
Kritik_und_Verbesserungsvorschlaege/Verbesserungen_01.md
Normal file
310
Kritik_und_Verbesserungsvorschlaege/Verbesserungen_01.md
Normal file
|
|
@ -0,0 +1,310 @@
|
||||||
|
> **Historisches Dokument — nicht nachschlagen, sondern einordnen.**
|
||||||
|
> Dies ist der ursprüngliche Auftrag, mit dem der Umbau von Version 03 zu Version 04 begonnen
|
||||||
|
> wurde. Die darin genannten Pfade beziehen sich auf die damalige Verzeichnisstruktur, in der
|
||||||
|
> beide Versionen nebeneinander unter `~/Python_Programs/OR_mit_Python/` lagen. Seit der
|
||||||
|
> Trennung liegt Version 04 in `Version_04/`, Version 03 in `Version_03/`. Der Text bleibt
|
||||||
|
> unverändert, weil er festhält, was verlangt war — nicht, wo heute etwas liegt.
|
||||||
|
|
||||||
|
### Rolle und Ziel
|
||||||
|
Du bist ein erfahrener Fachlektor, leitender Didaktiker für technische Weiterbildung, Senior Operations Research Scientist und Lead Python Software Architect. Überarbeite und erweitere das Kompendium **„Operations Research mit Python“ (Version 03)** von Dieter Schlüter umfassend. Alle Veränderungen sollen sich an den MD-Dateien im Verzeichnis "Operations_Research_mit_Python_Version_03" orientieren. Sie bilden die Grundlage für alle anderen Dateien.
|
||||||
|
|
||||||
|
Lies zunächst die Buchrezension "Kritik.md" im Verzeichnis "~/Python_Programs/OR_mit_Python/Kritik_und_Verbesserungsvorschlaege". Sie wurde vom KI-Tool "Gemini" verfasst. Dann wirst Du verstehen, was es zu verbesseren gilt.
|
||||||
|
|
||||||
|
Orientiere Dich an folgenden Vorgaben und lege für die verbesserte Version eine komplett neue Fassung im neu zu erzeugenden Verzeichnis "~/Python_Programs/OR_mit_Python/Operations_Research_mit_Python_Version_04" und den entsprechenden anderen Verzeichnissen für die neue Fassung an (Operations_Research_mit_Python_Version_04_Programme, bilder, OR_HTML_04 etc.) an.
|
||||||
|
|
||||||
|
### Primäres Lehr- und Kursziel
|
||||||
|
Wichtigstes Ziel: Das Buch soll nicht nur als akademisches Nachschlagewerk dienen, sondern als **begeisternder, hocheffektiver Kursbegleiter**. Die Kursteilnehmer sollen Lust bekommen, sich mit dem Thema zu beschäftigen, schnell messbare Erfolgserlebnisse erzielen und die erlernten Methoden unmittelbar am nächsten Tag in ihrer beruflichen Praxis (z. B. Logistik, Disposition, Produktion, Finanzen, Data Science) produktiv einsetzen können.
|
||||||
|
|
||||||
|
Dein Ziel ist es, bei der Überarbeitung didaktische Brüche zu glätten, methodische Lücken zu schließen, die Code-Architektur auf Enterprise-Niveau zu heben und das Werk als unangefochtenes Standardwerk im deutschsprachigen Raum zu etablieren.
|
||||||
|
|
||||||
|
|
||||||
|
### Die drei Leitprinzipien der Überarbeitung
|
||||||
|
1. **Time-to-First-Success minimieren:** Schnelle, lauffähige Aha-Erlebnisse vor formaler Theorie.
|
||||||
|
2. **Kognitive Entlastung:** Abstrakte Formeln konsequent durch visuelle Analogien und Klartext übersetzen.
|
||||||
|
3. **Robuste Praxistauglichkeit:** Solide Python-Architektur, realistische Datenpipelines (Excel/Pandas) und praxiserprobtes Debugging statt realitätsferner Laborbeispiele.
|
||||||
|
|
||||||
|
|
||||||
|
Führe die folgenden fünf Arbeitspakete präzise und vollständig aus:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Didaktik, Kurserlebnis und schneller Praxistransfer
|
||||||
|
|
||||||
|
Setze in jedem Kapitel die folgenden didaktischen Bausteine um:
|
||||||
|
|
||||||
|
### A. Das „In 5 Minuten gelöst“-Erfolgserlebnis an den Kapitelanfang
|
||||||
|
* **Struktur:** Beginne jedes Kapitel mit einer prominent hervorgehobenen Box: Ein minimales, realistisches Problem (4–6 Zeilen Daten, maximal 10–15 Zeilen Python-Code), das per Copy-Paste sofort lauffähig ist und ein klares betriebswirtschaftliches/praktisches Ergebnis liefert.
|
||||||
|
* **Didaktische Umkehrung:** Erst wenn der Code erfolgreich gelaufen ist und die Kursteilnehmer das Resultat sehen, folgt die Erklärung: *„Warum funktioniert das? Wie rechnet der Solver im Hintergrund? Was bedeuten die Variablen?“*
|
||||||
|
|
||||||
|
### B. Formel-Übersetzer: Mathe für Praktiker
|
||||||
|
* **Zweispaltige Notation:** Stelle jeder zentralen mathematischen Formel eine tabellarische Klartext-Übersetzung gegenüber:
|
||||||
|
* *Links (Mathematik):* $\sum_{i \in I} x_{i,j} \le C_j \quad \forall j \in J$
|
||||||
|
* *Rechts (Alltagssprache):* „Für jede Maschine $j$ darf die Summe aller zugewiesenen Aufträge $i$ die Maximalkapazität $C_j$ nicht überschreiten.“
|
||||||
|
* **Geometrische Intuition vor Beweis:** Führe Konzepte wie Konvexität, Simplex-Ecken, Hyperebenen und Branch-and-Bound-Bäume stets zuerst als visuelle Metaphern und 2D-/3D-Skizzen ein, bevor algebraische Definitionen folgen.
|
||||||
|
|
||||||
|
### C. Die „Excel-zu-Python“-Brücke für den Berufsalltag
|
||||||
|
* Zeige in Kapitel 1 und Kapitel 3 explizit, wie typische Modelle aus dem Standard-Excel-Solver in modernes Python überführt werden.
|
||||||
|
* Integriere wiederverwendbare Hilfsfunktionen, um Parameter direkt via `pandas.read_excel()` oder `.to_csv()` einzulesen, an den Solver zu übergeben und die Optimierungsergebnisse wieder als formatierte Tabellen (inkl. Kennzahlen) zu exportieren.
|
||||||
|
|
||||||
|
### D. Spickzettel (Cheat Sheets) & Visuelle Checklisten
|
||||||
|
* Ergänze die Anhänge und Kapitel um **1-Seiten-Syntax-Spickzettel** für die Kernbibliotheken (SciPy, HiGHS, Google OR-Tools CP-SAT, CVXPY):
|
||||||
|
* Wie definiere ich Binär-, Ganzzahl- und kontinuierliche Variablen?
|
||||||
|
* Wie formuliere ich Entweder-Oder-Bedingungen (Big-M)?
|
||||||
|
* Wie setze ich weiche Restriktionen mit Strafkosten (Slack-Variablen)?
|
||||||
|
* Wie frage ich Dualwerte, Schattenpreise und Lösungsstatus ab?
|
||||||
|
* Erstelle ein **Entscheidungs-Flussdiagramm** zu Beginn von Teil II und III: *„Welcher Solver und welches Paradigma passt zu meinem konkreten Problem?“* (Kriterien: Linearität, Ganzzahligkeit, Konvexität, Problemgröße, Laufzeitanforderungen).
|
||||||
|
|
||||||
|
### E. Praxistransfer: „Vom Management-Wunsch zum Modell“
|
||||||
|
* Füge einen Leitfaden zur **Anforderungsanalyse für OR-Projekte** ein.
|
||||||
|
* Demonstriere anhand praxisnaher Dialoge, wie man vage Vorgaben („Kosten senken, aber die Mitarbeiterzufriedenheit hochhalten“) systematisch zerlegt:
|
||||||
|
1. *Entscheidungsvariablen:* Was sind die echten Stellschrauben?
|
||||||
|
2. *Restriktionen:* Was sind harte physikalische Grenzen, was sind verhandelbare Wünsche?
|
||||||
|
3. *Zielfunktion:* Wie messen wir Erfolg mathematisch sauber, ohne Fehlanreize zu setzen?
|
||||||
|
|
||||||
|
### F. Micro-Quizzes und „Finde den Modellierungsfehler“-Übungen
|
||||||
|
* Schließe jedes Kapitel mit einem kurzen Selbsttest ab (3 Multiple-Choice-Fragen).
|
||||||
|
* Ergänze gezielte Debugging-Aufgaben: Code, der fehlerfrei kompiliert, aber betriebswirtschaftlich unsinnige Ergebnisse liefert (z. B. unendliche Produktion durch falsches Relationszeichen `>=` statt `<=`, vergessene Ganzzahligkeit oder Zirkelbezüge).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Software-Architektur und Code-Qualität
|
||||||
|
|
||||||
|
Hebe sämtliche Implementierungen im Buch auf ein sauberes, wartbares Enterprise-Niveau:
|
||||||
|
|
||||||
|
### A. Typsicherheit und Datenvalidierung
|
||||||
|
* Verwende durchgängig moderne Python-Features (Python 3.11+): strenge Type Hints (`typing`), Dataclasses oder **Pydantic v2**-Modelle für alle Input- und Output-Daten (Ressourcen, Bedarfe, Kostenmatrizen).
|
||||||
|
* Validiere Randbedingungen (z. B. strikt positive Kapazitäten, stochastische Vektoren mit Summe 1) bereits beim Einlesen der Daten.
|
||||||
|
|
||||||
|
### B. Klare Entkopplung (Clean Architecture / Service-Pattern)
|
||||||
|
* Trenne Domänenlogik strikt von der Solver-API:
|
||||||
|
$$\text{Rohdaten (Excel/JSON)} \longrightarrow \text{Pydantic-Domänenmodell} \longrightarrow \text{Model-Builder (Solver)} \longrightarrow \text{Lösungs-DTO}$$
|
||||||
|
* Kursteilnehmer sollen lernen, wie man Modelle baut, bei denen der zugrundeliegende Solver (z. B. Wechsel von HiGHS auf Gurobi oder CP-SAT) ausgetauscht werden kann, ohne die Business-Logik neu schreiben zu müssen.
|
||||||
|
|
||||||
|
### C. Explizite Status- und Fehlerbehandlung
|
||||||
|
* Beseitige alle impliziten Annahmen über optimale Lösungen.
|
||||||
|
* Implementiere für jedes Modell eine explizite Auswertung des Solver-Status (`OPTIMAL`, `FEASIBLE`, `INFEASIBLE`, `UNBOUNDED`, `TIME_LIMIT`).
|
||||||
|
* Zeige, wie Timeouts, Suboptimalitätsgrenzen (MIP-Gap) und Näherungslösungen im Code professionell abgefangen, protokolliert und weiterverarbeitet werden.
|
||||||
|
|
||||||
|
### D. Vektorisierte Modellgenerierung
|
||||||
|
* Zeige in Kapitel 3 und fortgeschrittenen Beispielen, wie man bei großen Datenmengen langsame Python-`for`-Schleifen vermeidet.
|
||||||
|
* Demonstriere den Einsatz von vektorisierten Matrizenoperationen (NumPy) bzw. Tabellen-Pipelines (**Polars** / **Pandas**), um tausende Nebenbedingungen performant aufzubauen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Fachliche, methodische und algorithmische Erweiterungen
|
||||||
|
|
||||||
|
Schließe die methodischen Lücken zwischen akademischer Lehrbuch-Theorie und realer Industrie-Komplexität:
|
||||||
|
|
||||||
|
### A. Didaktische Harmonisierung: Der Wechsel von Logistik zu Finance
|
||||||
|
* Schlage eine methodische Brücke vor Kapitel 11: Zeige auf, dass die mathematischen Strukturen der Portfoliotheorie identisch sind mit klassischen OR-Problemen (z. B. Ressourcenallokation im Lager $\leftrightarrow$ Kapitalallokation im Portfolio; Robustheit gegen Lieferkettenausfälle $\leftrightarrow$ Risikomaße gegen Kursabstürze).
|
||||||
|
* Ergänze für Nicht-Finanz-Interessierte ein paralleles, alternatives Transfer-Beispiel (z. B. **Supply-Chain-Netzwerkdesign unter Unsicherheit** oder **Energieeinsatzplanung**), das dieselben mathematischen Werkzeuge (Stochastik, CVaR, Dynamische Programmierung) nutzt.
|
||||||
|
|
||||||
|
### B. Metaheuristiken für nicht-konvexe und großskalige Probleme
|
||||||
|
* Ergänze einen praxisnahen Leitfaden für NP-schwere Probleme, bei denen exakte MILP-Solver an Skalierungsgrenzen stoßen.
|
||||||
|
* Implementiere verständliche Metaheuristiken (z. B. Large Neighborhood Search (LNS), Genetische Algorithmen via `Pymoo` oder Simulated Annealing) für komplexe Touren- und Belegungsplanungs-Probleme.
|
||||||
|
|
||||||
|
### C. Multi-Objective-Optimierung und Pareto-Effizienz
|
||||||
|
* Erkläre, warum einfache Zielgewichtungen ($\min w_1 f_1 + w_2 f_2$) bei Zielkonflikten in der Praxis oft versagen.
|
||||||
|
* Implementiere praxisnah die **$\epsilon$-Constraint-Methode** und die lexikografische Optimierung (z. B. Minimierung von Logistikkosten bei gleichzeitiger Minimierung des CO₂-Ausstoßes) inklusive Visualisierung der Pareto-Front.
|
||||||
|
|
||||||
|
### D. Schnittstelle Data Science: Predict-then-Optimize
|
||||||
|
* Zeige das Zusammenspiel von Machine Learning und Optimierung: Warum führt ein ML-Modell mit minimalem Vorhersagefehler (MSE) im nachgelagerten Optimierungsmodell oft zu suboptimalen Entscheidungen?
|
||||||
|
* Demonstriere den Unterschied an einem Beispiel (Nachfrageprognose $\rightarrow$ Lagerbestandsoptimierung) und gib einen Ausblick auf differenzierbare Optimierungsschichten (`cvxpylayers`).
|
||||||
|
|
||||||
|
### E. NLP/MINLP und Warm-Starts
|
||||||
|
* Zeige die Grundlagen nicht-konvexer Optimierung mit Open-Source-Solvern wie **Ipopt** (über CyIpopt).
|
||||||
|
* Ergänze praxisnahe Code-Beispiele für **MIP-Warm-Starts** (Einspeisen bekannter Ausgangslösungen zur massiven Beschleunigung der Lösungszeit).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Numerik, Debugging und Produktionsreife (MLOps)
|
||||||
|
|
||||||
|
Stelle sicher, dass die Modelle der Kursteilnehmer auch im harten Produktivbetrieb fehlerfrei funktionieren:
|
||||||
|
|
||||||
|
### A. Numerische Stabilität, Konditionierung und Toleranzen
|
||||||
|
* Erkläre die Matrix-Konditionszahl $\kappa(A)$ anhand eines konkreten Python-Experiments: Zeige, wie schlecht skalierte Koeffizienten (z. B. große Kostenbeträge gemischt mit winzigen Mengeneinheiten) zu scheinbar unlösbaren Modellen (`INFEASIBLE`) oder Lösungsdrift führen.
|
||||||
|
* Zeige Standard-Skalierungsverfahren (Ruiz-Equilibrierung) und schärfe das Bewusstsein für Primal-, Dual- und Integrabilitäts-Toleranzen (z. B. warum $0.99999998$ niemals leichtfertig mit `int()` gerundet werden darf).
|
||||||
|
|
||||||
|
### B. Solver-Diagnose & Unlösbarkeits-Analyse (IIS)
|
||||||
|
* Baue Anhang C zu einem vollwertigen Troubleshooting-Handbuch aus.
|
||||||
|
* Implementiere Werkzeuge zur Ermittlung eines **Irreducible Infeasible Subsystem (IIS)** bzw. Conflict Refiners: Wie isoliert man in einem Modell mit tausenden Restriktionen automatisiert genau die Teilmenge, die den mathematischen Widerspruch erzeugt?
|
||||||
|
* Erläutere, wie duale Entartung (Degeneriertheit) instabile Schattenpreise erzeugt und wie man fundierte Sensitivitätsanalysen für Entscheidungsträger erstellt.
|
||||||
|
|
||||||
|
### C. Testing, Benchmarking und API-Deployment
|
||||||
|
* **Pytest-Framework:** Entwickle standardisierte Testroutinen für Optimierungsmodelle (Invarianztests, Überprüfung von Restriktionsverletzungen, Regressionstests an Benchmark-Instanzen).
|
||||||
|
* **Solver-Benchmarks:** Zeige reproduzierbare Laufzeit- und Speicher-Benchmarks beim Vergleich von SciPy, HiGHS, OR-Tools und CVXPY über skalierende Instanzgrößen ($N \in \{10^2, 10^3, 10^4\}$).
|
||||||
|
* **FastAPI & Async:** Zeige das Deployment eines Optimierungsmodells als Microservice via **FastAPI** mit asynchroner Aufgabenwarteschlange (Celery/Redis) für rechenintensive Solver-Läufe, inklusive eines kompakten `Dockerfile`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Auszuführende Arbeitsanweisung
|
||||||
|
|
||||||
|
Setze diese Verbesserungen kapitelweise und systematisch um:
|
||||||
|
1. **Analysiere** das jeweilige Kapitel auf didaktische Verständlichkeit, mathematische Zugänglichkeit und Code-Qualität.
|
||||||
|
2. **Erstelle** für jedes Kapitel zuerst die „In 5 Minuten gelöst“-Box und die Formel-Übersetzer-Tabellen.
|
||||||
|
3. **Refaktoriere** den Python-Code gemäß den Standards für Typsicherheit, Validierung und Statusbehandlung.
|
||||||
|
4. **Erweitere** die methodischen Abschnitte und baue die praktischen Excel-/Pandas-Workflows ein.
|
||||||
|
5. **Schließe** jedes Kapitel mit den Übungen zur Fehlerdiagnose („Finde den Denkfehler“) und den interaktiven Micro-Quizzes ab.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Im Einzelnen führe unter diesen Prämissen folgendes präzise und vollständig aus:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. Architektur und Code-Qualität: Transformation zu produktionsreifem Python
|
||||||
|
Überarbeite alle Skripte und Code-Beispiele im Buch nach modernen Software-Engineering-Standards:
|
||||||
|
* **Typsicherheit & Verträge:** Führe durchgehend Type Hints (`typing`, `mypy`-kompatibel) und Laufzeitvalidierung via **Pydantic v2** für alle Modellparameter (z. B. Nachfragedaten, Kapazitäten, Renditevektoren) ein.
|
||||||
|
* **Trennung von Domäne und Solver:** Refaktoriere den Code so, dass Domänenlogik strikt von der Solver-Syntax getrennt ist. Führe ein klares Repository-/Service-Pattern ein: Rohdaten $\rightarrow$ Validiertes Domänenmodell $\rightarrow$ Solver-Builder (z. B. CVXPY/OR-Tools) $\rightarrow$ Lösungs-DTO.
|
||||||
|
* **Fehler- und Statusbehandlung:** Ersetze implizite Annahmen über Solver-Ergebnisse durch explizite Fehlerbehandlung für Randfälle: `INFEASIBLE`, `UNBOUNDED`, `NUMERICAL_ERROR` oder Timeouts. Zeige für MILP und CP-SAT, wie Suboptimalitätsgrenzen (MIP-Gap) und Abbruchkriterien sauber abgefangen und geloggt werden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Methodische Ergänzungen: Schließen der algorithmischen Lücken
|
||||||
|
Ergänze den theoretischen und praktischen Kanon um drei bisher unterrepräsentierte, in der Praxis aber essenzielle Bereiche:
|
||||||
|
* **Metaheuristiken für Nicht-Konvexes & NP-Schwere (Neues Kapitel oder Brückenteil):**
|
||||||
|
* Integriere einen praktischen Leitfaden für Probleme, bei denen exakte Solver (HiGHS, CP-SAT) skalierungsbedingt scheitern.
|
||||||
|
* Implementiere praxisnahe Metaheuristiken (z. B. Large Neighborhood Search (LNS), Simulated Annealing oder Genetische Algorithmen) mit Bibliotheken wie `Pymoo` oder nativem Python für großskalige Vehicle-Routing- oder Scheduling-Instanzen.
|
||||||
|
* **Nicht-lineare nicht-konvexe Optimierung (NLP / MINLP):**
|
||||||
|
* Zeige den praktischen Einsatz von Solvern wie **Ipopt** (über CyIpopt oder Pyomo/CasADi) für nicht-konvexe kontinuierliche Probleme. Diskutiere den Unterschied zwischen lokalen und globalen Optima sowie Strategien zur Wahl von Startwerten (Warm-Starts).
|
||||||
|
* **Warm-Starts & Dekomposition:**
|
||||||
|
* Ergänze in Teil II praxisnahe Beispiele für **MIP Warm-Starts** (Einspeisen bekannter zulässiger Lösungen) sowie eine verständliche Einführung in Dekompositionsverfahren (z. B. Spaltengenerierung / Dantzig-Wolfe für Cutting-Stock- oder Crew-Scheduling-Probleme).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Didaktische Harmonisierung: Glättung des Wechsels von Logistik zu Finance
|
||||||
|
Löse den abrupten thematischen Bruch zwischen Teil II/III (diskrete Optimierung, Logistik) und Teil IV (Finanzmärkte):
|
||||||
|
* **Konzeptionelle Brückenkapitel:** Führe vor Kapitel 11 eine explizite methodische Überleitung ein: Zeige, dass die mathematischen Strukturen identisch sind (z. B. Resource Allocation in der Produktion $\leftrightarrow$ Kapitalallokation im Portfolio; Robust Counterparts bei Lieferkettenausfällen $\leftrightarrow$ Worst-Case-Risikomaße am Markt).
|
||||||
|
* **Entflechtung der Zielgruppen:**
|
||||||
|
* Kennzeichne Teil IV klar als spezialisierte Vertiefungsdomäne.
|
||||||
|
* Ergänze für Leser, die *nicht* aus dem Finanzbereich kommen, ein paralleles, alternatives Vertiefungskapitel für industrielle Wertschöpfung (z. B. **Supply-Chain-Netzwerkdesign unter Unsicherheit** oder **Energie- und Kraftwerkseinsatzplanung**), das dieselben mathematischen Werkzeuge (Stochastik, CVaR/Robustheit, dynamische Programmierung) auf physische Systeme anwendet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Enterprise-Testing, Benchmarking & MLOps-Integration
|
||||||
|
Erweitere Kapitel 15 und die Projektwerkstatt um operative Deployment-Standards:
|
||||||
|
* **Testing-Framework für Optimierungsmodelle:**
|
||||||
|
* Entwickle ein dediziertes Testkonzept mit `pytest`: Regressionstests gegen bekannte Testinstanzen, Validierung von Restriktionsverletzungen (Infeasibility-Checks) und Invarianztests (z. B. Skalierungsinvarianz der Zielfunktion).
|
||||||
|
* **Benchmarking & Performance-Profiling:**
|
||||||
|
* Baue eine strukturierte Benchmark-Pipeline auf: Vergleich von Lösungszeit und Speicherverbrauch zwischen SciPy, HiGHS, Google OR-Tools und CVXPY bei wachsender Problemgröße ($N \in \{10^2, 10^3, 10^4, 10^5\}$).
|
||||||
|
* **Containerisierung & API-Bereitstellung:**
|
||||||
|
* Zeige die Kapselung eines Optimierungsmodells in einer **FastAPI**-Anwendung inklusive asynchroner Job-Queue (z. B. via Celery/Redis) für länger laufende Solver-Jobs.
|
||||||
|
* Liefere ein einsatzbereites `Dockerfile`, das C++-Abhängigkeiten und Solver-Bibliotheken performant und schlank baut.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Format, Interaktivität und Leseerlebnis
|
||||||
|
* **Interaktive Ausführungsumgebung:** Ergänze jedes Kapitel um Links zu validierten Jupyter Notebooks bzw. Google-Colab-Instanzen mit One-Click-Setup (`pip install highs cvxpy ortools ...`).
|
||||||
|
* **Visualisierungs-Upgrade:** Ersetze rein statische Matplotlib-Plots durch interaktive Visualisierungen (z. B. via `Plotly` für 3D-Konvexitätsflächen, KKT-Bedingungen, Pareto-Fronten bei Multi-Objective-Problemen und dynamische Gantt-Diagramme in CP-SAT).
|
||||||
|
* **Entscheidungs-Flussdiagramm:** Füge zu Beginn von Teil II und Teil III ein visuelles Ablaufdiagramm ein: *„Welcher Solver / welches Paradigma passt zu meinem Problem?“* (Entscheidungskriterien: linear vs. nichtlinear, stetig vs. diskret, konvex vs. nicht-konvex, Skalierungsanforderungen, Lösungszeitgarantien).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Numerische Stabilität, Konditionierung und Skalierung (Deep Dive)
|
||||||
|
Erweitere Kapitel 2, 4 und Anhang C um einen tiefgehenden praktischen Leitfaden zur numerischen Linearen Algebra:
|
||||||
|
* **Konditionszahlen in der Praxis:** Erkläre nicht nur formal die Matrix-Konditionszahl $\kappa(A)$, sondern demonstriere mit einem konkreten Python-Experiment, wie schlecht skalierte Koeffizienten (z. B. Kosten im Bereich $10^7$ kombiniert mit Toleranzen von $10^{-6}$) zu stillschweigendem Lösungsdrift oder falschen `INFEASIBLE`-Meldungen führen.
|
||||||
|
* **Skalierungs- und Vorverarbeitungs-Routinen:** Zeige, wie man Koeffizientenmatrizen systematisch über Methoden wie Ruiz-Equilibrierung oder geometrische Skalierung transformiert, bevor sie an den Solver übergeben werden.
|
||||||
|
* **Toleranzen verstehen:** Schärfe das Bewusstsein für Primal-, Dual- und Integrabilitäts-Toleranzen der Solver (z. B. `feasibility_tolerance` in HiGHS vs. OR-Tools). Erkläre, warum eine Variable mit dem Wert `0.99999998` in einem MILP nicht einfach mit `int()` gecastet werden darf.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. Modernes Modellierungs-Ökosystem: Pyomo, Linopy und Polars
|
||||||
|
Erweitere Kapitel 3 um moderne Modellierungs-Frameworks und Big-Data-Anbindungen:
|
||||||
|
* **Evaluierung von Linopy & Pyomo:** CVXPY und OR-Tools decken viel ab, aber für große industrielle LP/MILP-Probleme sind **Pyomo** (als klassischer Industriestandard) oder das neuere **Linopy** (optimiert für $N$-dimensionale xarray-Strukturen und extrem schnelle Matrix-Generierung) unverzichtbar. Ergänze einen kompakten Vergleich dieser Modellierungsschichten.
|
||||||
|
* **Polars & Vektorisierte Modellgenerierung:** In der Praxis verbringen OR-Ingenieure oft 80 % der Rechenzeit nicht im Solver, sondern beim Aufbau der Constraints über langsame Python-`for`-Schleifen. Zeige, wie man mithilfe von **Polars** oder Vektorisierung in NumPy Millionen von Nebenbedingungen um Größenordnungen schneller konstruiert.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. Schnittstelle zwischen Machine Learning und Operations Research (Smart Predict-then-Optimize)
|
||||||
|
Baue in Teil III oder IV eine methodische Brücke zur modernen Data Science:
|
||||||
|
* **Predict-then-Optimize:** Zeige das Zusammenspiel von Prognose und Optimierung. Klassische ML-Modelle minimieren den Mean Squared Error (MSE), nicht die Kosten im nachgelagerten Optimierungsmodell. Demonstriere anhand eines Beispiels (z. B. Nachfrageprognose $\rightarrow$ Lagerhaltung), warum ein isoliert „gutes“ ML-Modell zu teuren OR-Entscheidungen führen kann.
|
||||||
|
* **Differenzierbare Optimierung:** Führe kurz das Paradigma differenzierbarer Layer ein (z. B. über `cvxpylayers`), bei denen Optimierungsmodelle als Schicht direkt in neuronale PyTorch-Netze eingebettet werden können.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. Multi-Objective-Optimierung und Pareto-Effizienz
|
||||||
|
Ergänze in Teil II und Teil IV die Behandlung konkurrierender Zielkonflikte, die in realen Projekten die Regel sind:
|
||||||
|
* **Jenseits linearer Skalarisierung:** Erkläre, warum die simple Gewichtung von Zielen ($\min w_1 f_1 + w_2 f_2$) bei nicht-konvexen Pareto-Fronten versagt und wie man stattdessen mit der **$\epsilon$-Constraint-Methode** oder lexikografischer (hierarchischer) Optimierung arbeitet.
|
||||||
|
* **Praxisbeispiel:** Implementiere ein zweidimensionales Trade-off-Modell (z. B. Kosten vs. CO₂-Ausstoß in der Tourenplanung oder Rendite vs. Drawdown) und berechne die exakte Pareto-Front inklusive Visualisierung.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10. Professionelle Solver-Diagnose und Unlösbarkeits-Analyse (IIS)
|
||||||
|
Vertiefe Anhang C (Fehlerdiagnose) um fortgeschrittene Debugging-Techniken:
|
||||||
|
* **Irreducible Infeasible Subsystem (IIS):** Wenn ein Modell mit 50.000 Restriktionen `INFEASIBLE` meldet, ist der Entwickler ohne Werkzeuge verloren. Erkläre die mathematische Funktionsweise eines IIS/Conflict Refiners und zeige, wie man mit Python-Mitteln isoliert, welche minimale Teilmenge von Restriktionen den Konflikt verursacht.
|
||||||
|
* **Dual-Analyse bei Degeneriertheit:** Zeige auf, wie duale Entartung (Multiple Dual Solutions) dazu führt, dass Schattenpreise instabil werden, und wie man damit bei Sensitivitätsanalysen für das Management umgeht.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 11. Didaktischer Kurs-Fokus: Motivation, Zugänglichkeit & Praxistransfer
|
||||||
|
|
||||||
|
Unter der Prämisse, dass das Buch primär als **didaktischer Kursbegleiter** dient, müssen rein akademische Vertiefungen und industrielle Enterprise-Overheads zurücktreten. Im Zentrum stehen drei Faktoren:
|
||||||
|
1. **Sofortige Erfolgserlebnisse (Time-to-First-Success)**,
|
||||||
|
2. **Kognitive Entlastung bei Hürden** (Mathematik, Solver-Syntax, Debugging),
|
||||||
|
3. **Direkte Übertragbarkeit in den Arbeitsalltag**.
|
||||||
|
|
||||||
|
Führe zur Umsetzung dieser Kursziele die folgenden didaktischen und strukturellen Verbesserungen aus:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11.1. Das „5-Minuten-Erfolgserlebnis“ an den Anfang jedes Kapitels stellen
|
||||||
|
* **Didaktisches Problem:** Wer erst 15 Seiten Vektorräume oder Simplex-Tableaus liest, bevor der erste Code läuft, verliert Motivation und Schwung.
|
||||||
|
* **Maßnahme:**
|
||||||
|
* Jedes Kapitel beginnt mit einer **„In 5 Minuten gelöst“-Box**: Ein minimales, realistisches Mini-Problem (z. B. 4 Zeilen Daten, 6 Zeilen Python-Code), das der Teilnehmer per Copy-Paste sofort ausführt und dessen Ergebnis sofort einleuchtet.
|
||||||
|
* Erst **nach** dem funktionierenden Ergebnis folgt die methodische Erklärung: *„Warum funktioniert das? Wie rechnet der Solver das im Hintergrund? Was bedeuten die Variablen?“*
|
||||||
|
* **Effekt:** Neugier und Selbstwirksamkeit statt theoretischer Lähmung.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11.2. Duale Lesart: „Mathe für Anwender“ mit visuellen Analogien
|
||||||
|
* **Didaktisches Problem:** Die Formelsprache (Summenzeichen mit Doppelindizes, Matrixschreibweisen, KKT-Gradienten) wirkt auf Praktiker oft abschreckend.
|
||||||
|
* **Maßnahme:**
|
||||||
|
* **Formel-Übersetzer-Boxen:** Jede zentrale mathematische Formulierung erhält eine zweispaltige Darstellung:
|
||||||
|
* Links: Die mathematische Formel $\sum_{i \in I} x_{i,j} \le C_j$.
|
||||||
|
* Rechts: Der Klartext in Alltagssprache: *„Für jede Maschine $j$ darf die Summe aller zugewiesenen Aufträge $i$ die Gesamtkapazität $C_j$ nicht überschreiten.“*
|
||||||
|
* **Geometrische Intuition vor algebraischem Beweis:** Konvexität, Simplex-Ecken und Branch-and-Bound-Bäume zuerst als 2D-Skizzen und anschauliche Metaphern (z. B. „Eine Seifenblase, die sich in die Ecke eines Zimmers spannt“) einführen, bevor algebraische Definitionen folgen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11.3. „Excel-zu-Python“-Brücken für den Berufsalltag bauen
|
||||||
|
* **Didaktisches Problem:** Im betrieblichen Alltag liegen 90 % aller Daten in Tabellenkalkulationen, und viele Kursteilnehmer kennen Optimierung bisher nur aus dem Excel-Solver.
|
||||||
|
* **Maßnahme:**
|
||||||
|
* Baue in Kapitel 1 und Kapitel 3 explizite Brücken: *„Wie Sie Ihr Excel-Solver-Modell in sauberen Python-Code übersetzen.“*
|
||||||
|
* Zeige, wie Parameter direkt aus `pandas.read_excel()` oder CSV eingelesen, als Modell formuliert und die optimierten Zuweisungen wieder als saubere Excel-Tabelle mit Kennzahlen exportiert werden.
|
||||||
|
* **Effekt:** Die Teilnehmer können das Gelernte am nächsten Tag direkt an ihren eigenen Arbeitsdateien ausprobieren.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11.4. Spickzettel & Visuelle Checklisten („Cheat Sheets“)
|
||||||
|
* **Didaktisches Problem:** Wenn Teilnehmer ein eigenes Problem lösen wollen, wissen sie oft nicht, welcher Baustein (Variable, Constraint, Zielfunktion) wie in Python heißt.
|
||||||
|
* **Maßnahme:**
|
||||||
|
* **1-Seiten-Syntax-Cheat-Sheet:** Eine kompakte Übersichtstabelle für die drei Hauptwerkzeuge (SciPy, OR-Tools, CVXPY):
|
||||||
|
* *Wie definiere ich eine Binärvariable?*
|
||||||
|
* *Wie formuliere ich eine Entweder-Oder-Bedingung?*
|
||||||
|
* *Wie setze ich eine Obergrenze?*
|
||||||
|
* *Wie lese ich den optimalen Wert aus?*
|
||||||
|
* **Modellierungs-Baukasten:** Visuelle Rezeptkarten für Standardfragen des Arbeitsalltags: *„Wie modelliere ich Mindestabnahmemengen?“, „Wie verhindere ich Rüstzeiten?“, „Wie baue ich ein Budgetlimit ein?“*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11.5. Selbsteinschätzungs-Meilensteine & „Finde den Denkfehler“-Übungen
|
||||||
|
* **Didaktisches Problem:** Reine Programmieraufgaben frustrieren, wenn man an Syntax-Details hängenbleibt.
|
||||||
|
* **Maßnahme:**
|
||||||
|
* Ergänze jedes Kapitel um 2–3 kurze **Micro-Quizzes** (3 Multiple-Choice-Fragen) zur Selbstkontrolle vor dem nächsten Kapitel.
|
||||||
|
* Führe **„Finde den Modellierungsfehler“-Aufgaben** ein: Der Code läuft fehlerfrei durch, aber das Ergebnis ist unplausibel (z. B. Maschine produziert unendlich viel, weil eine Nebenbedingung als `>=` statt `<=` formuliert wurde; oder Ganzzahligkeit vergessen).
|
||||||
|
* **Effekt:** Das schult den kritischen Blick auf Ergebnisse, der in der Praxis wichtiger ist als das reine Schreiben von Code.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11.6. Praxistransfer-Protokoll: „Vom Management-Wunsch zum Modell“
|
||||||
|
* **Didaktisches Problem:** Im Berufsleben kommt selten jemand mit einer fertigen Zielfunktion. Chefs und Fachabteilungen formulieren vage: *„Wir wollen die Kosten senken, aber die Mitarbeiter sollen nicht meckern und die Liefertreue muss hoch bleiben.“*
|
||||||
|
* **Maßnahme:**
|
||||||
|
* Ergänze ein methodisches Kapitel oder Leitfaden: **„Anforderungsanalyse für OR-Projekte“**.
|
||||||
|
* Zeige anhand eines realistischen Dialogs, wie man vage Geschäftsanforderungen systematisch zerlegt:
|
||||||
|
1. Was sind die echten Stellschrauben? $\rightarrow$ *Entscheidungsvariablen*
|
||||||
|
2. Was sind harte Grenzen, was weiche Wünsche? $\rightarrow$ *Harte vs. Weiche Constraints (Penalties)*
|
||||||
|
3. Woran messen wir Erfolg wirklich? $\rightarrow$ *Zielfunktion*
|
||||||
|
* **Effekt:** Die Teilnehmer lernen die wichtigste Fähigkeit für den Arbeitsalltag: das Übersetzen von Business-Problemen in mathematische Logik.
|
||||||
|
|
||||||
|
|
||||||
60
Kritik_und_Verbesserungsvorschlaege/Verbesserungen_02.md
Normal file
60
Kritik_und_Verbesserungsvorschlaege/Verbesserungen_02.md
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
### Rolle und Ziel
|
||||||
|
Du bist ein erfahrener Fachlektor, Senior Operations Research Scientist und Principal Systems Architect für High-Performance Analytics. Überarbeite und perfektioniere das Kompendium **„Optimierte Entscheidungsfindung mit Python“ (Version 04)** von Dieter Schlüter.
|
||||||
|
|
||||||
|
Das Werk hat mit Version 04 bereits ein herausragendes didaktisches Niveau erreicht. Ziel dieser nächsten Revision ist es, die letzten verbliebenen Hürden in **Software-Architektur, Packaging, Skalierung auf Multithreading/HPC, Explainable Optimization und Visualisierung** zu beseitigen.
|
||||||
|
|
||||||
|
Setze die folgenden präzisen Arbeitspakete vollständig um:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. Bereinigung des C++-Symbolkonflikts und modernes Packaging
|
||||||
|
* **Das Problem:** Derzeit crasht die gemeinsame Verwendung von `ortools` und `highspy` im selben Prozess durch doppelte HiGHS-Symbolauflösungen im dynamic linker, was im Buch über umständliche `subprocess`-Wrapper gelöst wird.
|
||||||
|
* **Maßnahme:**
|
||||||
|
* Implementiere im Setup- und Architektur-Teil ein sauberes, produktionsreifes Isolationskonzept: Zeige auf, wie ein solches Multi-Solver-Setup über **Multiprocessing mit `ProcessPoolExecutor` / IPC** oder über modulare Worker-Architekturen gelöst wird, ohne auf instabile `subprocess.run([sys.executable, "-c", ...])`-Strings zurückzugreifen.
|
||||||
|
* Baue für das Buch ein begleitendes `pyproject.toml` (unter Verwendung von `uv` oder `poetry`) mit optionalen Dependency-Gruppen (`extras = ["finance", "large-scale", "api"]`), damit Kursteilnehmer schlanke, isolierte Umgebungen ohne überflüssigen Overhead aufsetzen können.
|
||||||
|
* Erstelle ein offizielles, mehrstufiges `Dockerfile` (Multi-Stage Build), das die Solver sauber kompiliert bzw. bindet und ein reproduzierbares Kurs-Image bereitstellt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Multi-Core, Parallelisierung und Solver-Ressourcenmanagement
|
||||||
|
* **Das Problem:** In Version 04 wird Parallelität nur am Rande bei Benchmarks berührt; viele Beispiele laufen standardmäßig auf einem Core.
|
||||||
|
* **Maßnahme:**
|
||||||
|
* Ergänze in Kapitel 6 (MILP) und Kapitel 7 (CP-SAT) einen dedizierten Abschnitt über **Multi-Threading und deterministische Suche**:
|
||||||
|
* Wie nutzt man `num_workers` in CP-SAT und HiGHS optimal aus?
|
||||||
|
* Was bedeutet Nicht-Determinismus bei paralleler Branch-and-Bound-Suche (Race Conditions bei Incumbents) und wie erzwingt man reproduzierbare Runs für Tests?
|
||||||
|
* Zeige in Kapitel 23 auf, wie man parallele Modellläufe (z. B. Szenarienanalysen in Monte-Carlo oder Hyperparameter-Tuning in Metaheuristiken) speichereffizient über `joblib` oder `ray` verteilt, ohne das Python-GIL zum Flaschenhals zu machen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Explainable AI / Explainable Optimization (X-OR) für Entscheider
|
||||||
|
* **Das Problem:** Wenn das Modell eine Lösung ausgibt, fragen Fachabteilungen und Betriebsräte: *„Warum genau diese Zuweisung und nicht jene?“* Schattenpreise beantworten das nur lokal bei LPs.
|
||||||
|
* **Maßnahme:**
|
||||||
|
* Erweitere Kapitel 4 („Vom Wunsch zum Modell“) und Kapitel 22 („Praxisfallen“) um Methoden der **Erklärbaren Optimierung**:
|
||||||
|
* **Automatisierte Was-wäre-wenn-Analysen (Counterfactual Explanations):** Wie viel schlechter wird der Zielfunktionswert, wenn man eine vom Disponenten gewünschte manuelle Zuweisung fest erzwingt?
|
||||||
|
* **Constraint Attribution:** Ein System, das visualisiert, welche 3 Nebenbedingungen die größten Opportunitätskosten (Erlöseinbußen) verursachen.
|
||||||
|
* **Generierung natürlicher Sprachberichte:** Zeige, wie man aus den Dualwerten und Slack-Variablen automatisiert textuelle Zusammenfassungen für das Management formuliert (z. B.: *„Plan B kostet 4.200 € mehr, weil die gesetzliche Ruhezeit von Mitarbeiter X die günstigere Schicht blockiert.“*).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
### 4. Stochastik & Unsicherheit: Chance-Constrained Programming
|
||||||
|
* **Das Problem:** Kapitel 12 behandelt Monte-Carlo, zweistufige stochastische Programmierung und Worst-Case-Robustheit. In der Industrie verlangt das Management jedoch fast immer Wahrscheinlichkeitszusagen („Mit 95 % Sicherheit darf der Tank nicht leerlaufen“).
|
||||||
|
* **Maßnahme:**
|
||||||
|
* Ergänze in Kapitel 12 einen Abschnitt zu **Wahrscheinlichkeitsbeschränkungen (Chance Constraints / Probabilistic Constraints)**:
|
||||||
|
* Zeige, wie Bedingungen der Form $P(a^\top x \le b) \ge 1 - \alpha$ unter Normalverteilungsannahmen in deterministische Second-Order Cone Constraints (SOCP via CVXPY) umgeformt werden.
|
||||||
|
* Zeige die szenariobasierte Alternative (Big-M mit Binärvariablen für Ausfall-Szenarien) für beliebige, empirische Verteilungen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Didaktisches Redigieren: Kognitive Entlastung und modulare Leseführung
|
||||||
|
* **Das Problem:** Bei über 700 Seiten droht das Buch neue Kursteilnehmer trotz Lernpfaden zu erschlagen.
|
||||||
|
* **Maßnahme:**
|
||||||
|
* Führe ein klares visuelles Farbleitsystem (Callout-Typen) ein:
|
||||||
|
* `[Muss für alle]` (Kernkonzepte und Code).
|
||||||
|
* `[Nur für Entwickler]` (NumPy-Vektorisierung, CSR-Format, Profiling).
|
||||||
|
* `[Nur für Management/Planer]` (Kennzahleninterpretation, Zielkonflikte, Heuristiken).
|
||||||
|
* Ergänze am Ende von Teil I bis IV jeweils eine **„Synthese-Doppelseite“**, die die Kernideen auf einen Blick zusammenfasst (Mindmap + Entscheidungsmatrix), damit Kursteilnehmer vor den Übungen rekapitulieren können, ohne zurückblättern zu müssen.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
90
Kritik_und_Verbesserungsvorschlaege/Verbesserungen_03.md
Normal file
90
Kritik_und_Verbesserungsvorschlaege/Verbesserungen_03.md
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
### Rolle und Ziel
|
||||||
|
Du bist ein erfahrener Kursdesigner, Senior Operations Research Consultant und Full-Stack Python Architect. Überarbeite das Kompendium **„Optimierte Entscheidungsfindung mit Python“** mit dem klaren Fokus auf **maximale Zugänglichkeit, Begeisterung und sofortige betriebliche Überzeugungskraft** für eine heterogene Zielgruppe (Studierende, Manager, Ingenieure, Entscheider, Investoren).
|
||||||
|
|
||||||
|
Beseitige die Hürde, dass Nicht-Programmierer und Entscheider sich durch Jupyter Notebooks oder Terminal-Ausgaben kämpfen müssen. Mache Operations Research zu einem **visuellen, interaktiven und präsentierbaren Erlebnis**, das Kursteilnehmer direkt als Argumentationshilfe im Unternehmen nutzen können.
|
||||||
|
|
||||||
|
Setze dazu die folgenden sechs Module präzise um:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. Interaktive Streamlit-Apps als primäre Nutzeroberfläche (No-Code/Low-Code)
|
||||||
|
Ersetze für Einsteiger und Nicht-Programmierer die reine Notebook-Nutzung durch sofort startbare, leicht verständliche **Streamlit-Web-Apps** (unter 80 Zeilen Code je App):
|
||||||
|
* **Baukasten-Prinzip:** Kursteilnehmer müssen keinen UI-Code von Grund auf schreiben, sondern erhalten standardisierte App-Templates:
|
||||||
|
* `app_schichtplan.py` (Personal & Disposition)
|
||||||
|
* `app_touren.py` (Logistik & Routing)
|
||||||
|
* `app_portfolio.py` (Investoren & Finanzen)
|
||||||
|
* `app_produktionsmix.py` (Ingenieure & Fertigung)
|
||||||
|
* **Interaktive Eingabeelemente:**
|
||||||
|
* **Schieberegler (Sliders):** Budgets, Rohstoffpreise, Risikoaversion $\lambda$, Überstundenlimits dynamisch verändern.
|
||||||
|
* **Toggles/Checkboxen:** Einzelne Restriktionen live zu- oder abschalten (z. B. *„Mitarbeiter X darf am Wochenende arbeiten: Ja/Nein“* oder *„Maximal 3 Positionen im Depot: An/Aus“*).
|
||||||
|
* **Excel-Upload:** Drag-and-Drop für `.xlsx`-Dateien, sodass Entscheider sofort eigene Unternehmensdaten einwerfen und per Mausklick optimieren können.
|
||||||
|
* **1-Klick-Ausführung:** Bereitstellung eines zentralen Startskripts (z. B. `streamlit run dashboard.py` oder vorkonfigurierte Cloud-Demos via Streamlit Community Cloud), damit auch technisch unerfahrene Teilnehmer in unter 60 Sekunden ihre erste eigene Optimierungs-App im Browser bedienen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Interaktive Dashboards und UI-Brücke (Streamlit / Panel)
|
||||||
|
* **Das Problem:** Die Excel-Brücke in Kapitel 1.8 ist hervorragend, aber moderne Disponenten und Manager verlangen nach browserbasierten Dashboards mit Sofort-Feedback.
|
||||||
|
* **Maßnahme:**
|
||||||
|
* Ergänze die Projektwerkstatt (z. B. Projekt P1 „Schul-Vertretungsplaner“ oder P3 „Liefertouren“) um eine kompakte, einsatzbereite **Streamlit-Web-App** (unter 100 Zeilen Code):
|
||||||
|
1. Upload von Excel-Dateien per Drag-and-Drop.
|
||||||
|
2. Slider für Parameter und Gewichte (Budgetgrenzen, Überstundentoleranz).
|
||||||
|
3. Live-Visualisierung der Ergebnisse: Interaktive Gantt-Charts (via Plotly) für Maschinenbelegung und Folium-Karten für Routen.
|
||||||
|
4. 1-Klick-Download des optimierten Excel-Ergebnisberichts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 3. Präsentationsreife Management-Visualisierungen (Visual ROI)
|
||||||
|
Ersetze reine Tabellen- und Textausgaben durch Grafiken, die Kursteilnehmer ohne Nachbearbeitung direkt in eine Vorstandspräsentation oder Team-Besprechung einfügen können:
|
||||||
|
* **Interaktive Gantt-Diagramme (Plotly):** Für Schicht- und Maschinenbelegungspläne mit farbkodierter Auslastung, visualisierten Rüstzeiten und sofort erkennbaren Puffern.
|
||||||
|
* **Geografische Routenkarten (Folium / Plotly Mapbox):** Für Logistik und Außendienst: Anzeige der optimalen Touren auf echten Straßenkarten mit nummerierten Stopps, Depotstandorten und interaktiven Tooltips zu Liefermengen und Zeitfenstern.
|
||||||
|
* **Vorher-Nachher-Wasserfalldiagramme (Business Impact):**
|
||||||
|
* Gegenüberstellung: *Heuristik / Bauchgefühl des Disponenten* vs. *Optimierte Solver-Lösung*.
|
||||||
|
* Zeige in Euro und Prozent den genauen Mehrwert (z. B. Einsparung von 12 % Treibstoff, 4.500 € weniger Strafkosten, 15 % höhere Maschinenauslastung).
|
||||||
|
* **Interaktive Pareto-Fronten mit Klick-Auswahl:**
|
||||||
|
* Schieberegler für Multi-Objective-Trade-offs: Ein Klick auf die Pareto-Kurve zeigt dem Management sofort den Plan: *„Wenn wir 5 % mehr CO₂ einsparen wollen, kostet uns das exakt 3.200 € mehr Logistikkosten.“*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Zielgruppen-spezifische „One-Pager“ & Einstiegsdialoge
|
||||||
|
Differenziere den Kurseinstieg nach den vier Hauptzielgruppen, um jede Gruppe bei ihren spezifischen Schmerzpunkten abzuholen:
|
||||||
|
* **Für Manager & Entscheider („Der Business-Case“):**
|
||||||
|
* 1-Seiter vor jedem Großthema: *„Was bringt das dem Unternehmen?“*, *„Typischer ROI und Amortisationszeit“*, *„Wie verkaufe ich das dem Betriebsrat/der Geschäftsführung?“*.
|
||||||
|
* **Für Ingenieure & Produktionsplaner („Die Physik des Systems“):**
|
||||||
|
* Fokus auf Engpässe, Taktzeiten, Rüstkosten, Kapazitätsabgleiche und Toleranzen.
|
||||||
|
* **Für Investoren & Finanzanalysten („Risiko & Ertrag“):**
|
||||||
|
* Fokus auf Diversifikationseffizienz, Drawdown-Minimierung, Tail-Risk (CVaR) und Rebalancing-Transaktionskosten.
|
||||||
|
* **Für Studierende & Data Scientists („Die Methodik“):**
|
||||||
|
* Fokus auf algorithmische Eleganz, saubere Formulierung und den Brückenschlag von Machine-Learning-Prognosen zur Optimierung.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Der „Was-wäre-wenn“-Simulator (Sandbox-Modus)
|
||||||
|
Mache Optimierung greifbar, indem Kursteilnehmer erleben, wie das System auf unvorhergesehene Ereignisse reagiert:
|
||||||
|
* Baue in die Streamlit-Apps einen **Szenarien-Simulator** ein:
|
||||||
|
* *„Was passiert, wenn Rohstoff A plötzlich 30 % teurer wird?“*
|
||||||
|
* *„Was passiert, wenn Maschine 2 für 48 Stunden ausfällt?“*
|
||||||
|
* *„Was passiert, wenn ein Stammkunde seine Bestellmenge verdoppelt?“*
|
||||||
|
* **Live-Delta-Anzeige:** Die App berechnet in Sekundenbruchteilen die neue Zuweisung und blendet einen roten/grünen Differenzbericht ein: *„Ausweichplan gefunden: 800 € Mehrkosten, alle Liefertermine gehalten.“*
|
||||||
|
* **Lerneffekt:** Entscheider begreifen sofort, dass OR nicht nur starre Pläne erzeugt, sondern ein mächtiges digitales Notfall- und Simulationswerkzeug für den Ernstfall ist.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Management-taugliche 1-Klick-Exporte (PDF & Excel)
|
||||||
|
Damit der Praxistransfer im Arbeitsalltag nicht an der Weitergabe der Ergebnisse scheitert:
|
||||||
|
* Integriere in jede Streamlit-Vorlage eine **Export-Engine**:
|
||||||
|
* **Excel-Export:** Schreibt einen fertigen, optisch ansprechend formatierten Dienst- oder Produktionsplan mit Summenformeln und Kennzahlen zurück in eine `.xlsx`-Datei, die der Disponent direkt an die Mitarbeiter aushändigen kann.
|
||||||
|
* **Executive PDF-Report:** Ein 1-seitiger Management-Summary-Report (via ReportLab oder Weasyprint), der die wichtigsten Kennzahlen, Kostenvergleiche und das Hauptdiagramm für die nächste Abteilungsleiter-Runde zusammenfasst.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. Didaktisches Redigieren: Glossar für Nicht-Mathematiker („Deutsch – OR / OR – Deutsch“)
|
||||||
|
* Ergänze den Einstieg und die Anhänge um ein pragmatisches **Übersetzungslexikon für den Berufsalltag**:
|
||||||
|
* *„Wir haben nicht genug Budget/Zeit“* $\rightarrow$ Harte Kapazitätsrestriktion (`<= b`).
|
||||||
|
* *„Mitarbeiter X möchte lieber vormittags arbeiten“* $\rightarrow$ Weiche Restriktion mit Strafkosten (Penalty).
|
||||||
|
* *„Was bringt mir eine zusätzliche Arbeitsstunde?“* $\rightarrow$ Schattenpreis / Dualwert.
|
||||||
|
* *„Wir können nur ganze Kisten liefern“* $\rightarrow$ Ganzzahligkeitsbedingung (MILP).
|
||||||
|
* *„Wenn wir Anlage A bauen, brauchen wir auch Lager B“* $\rightarrow$ Logische Verknüpfung / Big-M.
|
||||||
|
* **Effekt:** Senkt die Fachsprachenbarriere radikal und befähigt jeden Teilnehmer, Fachprobleme aus Besprechungen sofort in die richtigen mathematischen Bausteine zu übersetzen.
|
||||||
|
|
||||||
280
Notebooks_04/anhang-fehlerdiagnose.ipynb
Normal file
280
Notebooks_04/anhang-fehlerdiagnose.ipynb
Normal file
|
|
@ -0,0 +1,280 @@
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"# Anhang C: Fehlerdiagnose-Handbuch\n",
|
||||||
|
"\n",
|
||||||
|
"Begleitnotebook zu *Optimierte Entscheidungsfindung mit Python*. Die Codezellen sind identisch mit den im Buch abgedruckten Programmen.\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"# Einmalig ausfuehren: installiert alle im Buch verwendeten Pakete.\n",
|
||||||
|
"# Lokal in einer virtuellen Umgebung genauso gueltig wie in Google Colab.\n",
|
||||||
|
"%pip install --quiet ortools highspy cvxpy scipy numpy pandas polars \\\n",
|
||||||
|
" scikit-learn matplotlib plotly pyomo linopy pymoo pydantic openpyxl"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Wenn die fünf Schritte nicht reichen: den Konflikt einkreisen\n",
|
||||||
|
"\n",
|
||||||
|
"`Konfliktsuche.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# Konfliktsuche.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Anhang Fehlerdiagnose: Den Konflikt finden, der INFEASIBLE verursacht.\n",
|
||||||
|
"\n",
|
||||||
|
"Ein Deletion Filter (auch: IIS, Irreducible Infeasible Subset) isoliert aus\n",
|
||||||
|
"einem unloesbaren Modell eine kleinste widerspruechliche Teilmenge von\n",
|
||||||
|
"Bedingungen: Nimmt man aus ihr auch nur eine einzige Bedingung heraus, ist\n",
|
||||||
|
"der Rest wieder loesbar.\n",
|
||||||
|
"\n",
|
||||||
|
"Das Programm zeigt sechs Dinge:\n",
|
||||||
|
" 1. Der Solver meldet INFEASIBLE - und sonst nichts.\n",
|
||||||
|
" 2. Die naive Suche (\"jede Bedingung einmal weglassen\") findet hier gar nichts.\n",
|
||||||
|
" 3. Der Deletion Filter findet einen Konflikt in n Solveraufrufen.\n",
|
||||||
|
" 4. Ein Konflikt ist selten einer: nach der Reparatur folgt der naechste.\n",
|
||||||
|
" 5. \"Den\" kleinsten Konflikt gibt es nicht - dieses Modell enthaelt neun.\n",
|
||||||
|
" 6. Welchen davon man zu sehen bekommt, steuert die Pruefreihenfolge.\n",
|
||||||
|
"\n",
|
||||||
|
"Abgrenzung zu Infeasibility_Diagnose.py (Kapitel Praxisfallen): Dort geht es\n",
|
||||||
|
"um die Relaxation - das Modell soll trotz Widerspruch eine brauchbare Antwort\n",
|
||||||
|
"liefern. Hier geht es um die Diagnose - welche Bedingungen widersprechen sich\n",
|
||||||
|
"ueberhaupt. Beides zusammen ergibt den Umgang mit INFEASIBLE in Produktion.\n",
|
||||||
|
"\n",
|
||||||
|
"Benoetigt: numpy, scipy\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"from __future__ import annotations\n",
|
||||||
|
"\n",
|
||||||
|
"import numpy as np\n",
|
||||||
|
"from scipy.optimize import linprog\n",
|
||||||
|
"\n",
|
||||||
|
"PRODUKTE = [\"Rahmen\", \"Gehaeuse\", \"Deckel\", \"Traeger\", \"Halter\"]\n",
|
||||||
|
"\n",
|
||||||
|
"# Jede Bedingung traegt einen sprechenden Namen - das ist keine Kosmetik,\n",
|
||||||
|
"# sondern die Voraussetzung dafuer, dass der Befund lesbar wird.\n",
|
||||||
|
"# (Name, Koeffizienten je Produkt, Richtung, rechte Seite)\n",
|
||||||
|
"BEDINGUNGEN: list[tuple[str, list[float], str, float]] = [\n",
|
||||||
|
" (\"Kapazitaet Montage\", [3, 2, 1, 4, 2], \"<=\", 400),\n",
|
||||||
|
" (\"Kapazitaet Lackieren\", [2, 3, 0, 1, 0], \"<=\", 150),\n",
|
||||||
|
" (\"Kapazitaet Pruefung\", [1, 1, 1, 1, 1], \"<=\", 300),\n",
|
||||||
|
" (\"Liefervertrag Rahmen\", [1, 0, 0, 0, 0], \">=\", 40),\n",
|
||||||
|
" (\"Liefervertrag Gehaeuse\", [0, 1, 0, 0, 0], \">=\", 30),\n",
|
||||||
|
" (\"Liefervertrag Deckel\", [0, 0, 1, 0, 0], \">=\", 20),\n",
|
||||||
|
" (\"Liefervertrag Traeger\", [0, 0, 0, 1, 0], \">=\", 25),\n",
|
||||||
|
" (\"Marktgrenze Rahmen\", [1, 0, 0, 0, 0], \"<=\", 120),\n",
|
||||||
|
" (\"Marktgrenze Gehaeuse\", [0, 1, 0, 0, 0], \"<=\", 90),\n",
|
||||||
|
" (\"Marktgrenze Deckel\", [0, 0, 1, 0, 0], \"<=\", 150),\n",
|
||||||
|
" (\"Marktgrenze Halter\", [0, 0, 0, 0, 1], \"<=\", 200),\n",
|
||||||
|
" (\"Mindestumsatz\", [90, 70, 30, 60, 20], \">=\", 12000),\n",
|
||||||
|
" (\"Sortimentsbreite\", [0, 0, 1, 1, 1], \">=\", 100),\n",
|
||||||
|
" (\"Lackierbudget Schicht 2\", [0, 1, 0, 1, 0], \"<=\", 55),\n",
|
||||||
|
"]\n",
|
||||||
|
"\n",
|
||||||
|
"# Welche Bedingungen koennte ein Planer im Ernstfall wirklich veraendern?\n",
|
||||||
|
"# Kapazitaeten lassen sich durch Sonderschichten dehnen, Liefervertraege und\n",
|
||||||
|
"# Marktgrenzen nicht.\n",
|
||||||
|
"VERHANDELBAR = {\"Kapazitaet Montage\", \"Kapazitaet Lackieren\",\n",
|
||||||
|
" \"Kapazitaet Pruefung\", \"Lackierbudget Schicht 2\"}\n",
|
||||||
|
"\n",
|
||||||
|
"aufrufe = 0 # zaehlt jeden Solveraufruf mit - der Preis des Verfahrens\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def ist_loesbar(auswahl: list[int]) -> bool:\n",
|
||||||
|
" \"\"\"Gibt es einen Punkt, der ALLE Bedingungen aus 'auswahl' erfuellt?\n",
|
||||||
|
"\n",
|
||||||
|
" Es wird nur Zulaessigkeit geprueft, keine Zielfunktion optimiert: die\n",
|
||||||
|
" Zielfunktion ist konstant null. INFEASIBLE haengt nie an der Zielfunktion.\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" global aufrufe\n",
|
||||||
|
" aufrufe += 1\n",
|
||||||
|
" matrix, rechte_seite = [], []\n",
|
||||||
|
" for i in auswahl:\n",
|
||||||
|
" _, koeffizienten, richtung, grenze = BEDINGUNGEN[i]\n",
|
||||||
|
" zeile = np.array(koeffizienten, dtype=float)\n",
|
||||||
|
" # linprog kennt nur \"<=\", also \">=\" durch Negation umdrehen\n",
|
||||||
|
" matrix.append(zeile if richtung == \"<=\" else -zeile)\n",
|
||||||
|
" rechte_seite.append(grenze if richtung == \"<=\" else -grenze)\n",
|
||||||
|
" ergebnis = linprog(np.zeros(len(PRODUKTE)),\n",
|
||||||
|
" A_ub=np.array(matrix), b_ub=np.array(rechte_seite),\n",
|
||||||
|
" bounds=(0, None), method=\"highs\")\n",
|
||||||
|
" return ergebnis.status == 0\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def deletion_filter(auswahl: list[int]) -> list[int]:\n",
|
||||||
|
" \"\"\"Verkleinert eine unloesbare Menge zu einer kleinsten unloesbaren Menge.\n",
|
||||||
|
"\n",
|
||||||
|
" Der Kern des Verfahrens ist eine einzige Regel: Nimm eine Bedingung\n",
|
||||||
|
" versuchsweise heraus. Bleibt der Rest unloesbar, wurde sie fuer den\n",
|
||||||
|
" Widerspruch nicht gebraucht - sie darf endgueltig weg. Wird der Rest\n",
|
||||||
|
" loesbar, war sie beteiligt und muss bleiben.\n",
|
||||||
|
"\n",
|
||||||
|
" Nach genau einem Durchlauf ueber alle Bedingungen ist das Ergebnis\n",
|
||||||
|
" unreduzierbar: n Solveraufrufe statt 2^n Teilmengen.\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" rest = list(auswahl)\n",
|
||||||
|
" for i in list(auswahl):\n",
|
||||||
|
" probe = [j for j in rest if j != i]\n",
|
||||||
|
" if not ist_loesbar(probe):\n",
|
||||||
|
" rest = probe\n",
|
||||||
|
" return rest\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def namen(auswahl: list[int]) -> list[str]:\n",
|
||||||
|
" return [BEDINGUNGEN[i][0] for i in auswahl]\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def teil_1_der_befund(alle: list[int]) -> None:\n",
|
||||||
|
" print(\"=\" * 68)\n",
|
||||||
|
" print(\"1. Was der Solver sagt\")\n",
|
||||||
|
" print(\"=\" * 68)\n",
|
||||||
|
" print(f\"Modell: {len(PRODUKTE)} Variablen, {len(BEDINGUNGEN)} Bedingungen\")\n",
|
||||||
|
" status = \"OPTIMAL\" if ist_loesbar(alle) else \"INFEASIBLE\"\n",
|
||||||
|
" print(f\"Status: {status}\")\n",
|
||||||
|
" print(\"\\nMehr ist es nicht. Der Solver nennt keine Ursache, weil es die eine\")\n",
|
||||||
|
" print(\"Ursache nicht gibt: Unloesbarkeit ist eine Eigenschaft von Mengen von\")\n",
|
||||||
|
" print(\"Bedingungen, nicht von einzelnen Bedingungen.\")\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def teil_2_die_naive_suche(alle: list[int]) -> None:\n",
|
||||||
|
" print(\"\\n\" + \"=\" * 68)\n",
|
||||||
|
" print(\"2. Die naheliegende Idee - und warum sie hier scheitert\")\n",
|
||||||
|
" print(\"=\" * 68)\n",
|
||||||
|
" print(\"Jede Bedingung einmal weglassen und schauen, ob es dann geht:\\n\")\n",
|
||||||
|
" treffer = [i for i in alle if ist_loesbar([j for j in alle if j != i])]\n",
|
||||||
|
" for i in alle:\n",
|
||||||
|
" print(f\" ohne {BEDINGUNGEN[i][0]:<26} {'loesbar' if i in treffer else 'weiter unloesbar'}\")\n",
|
||||||
|
" print(f\"\\nGefundene Schuldige: {len(treffer)}\")\n",
|
||||||
|
" print(\"Keine einzelne Bedingung ist schuld. Genau das ist der Normalfall -\")\n",
|
||||||
|
" print(\"und der Grund, warum diese Suche in der Praxis so oft im Nichts endet.\")\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def teil_3_der_filter(alle: list[int]) -> list[int]:\n",
|
||||||
|
" global aufrufe\n",
|
||||||
|
" print(\"\\n\" + \"=\" * 68)\n",
|
||||||
|
" print(\"3. Der Deletion Filter\")\n",
|
||||||
|
" print(\"=\" * 68)\n",
|
||||||
|
" vorher = aufrufe\n",
|
||||||
|
" konflikt = deletion_filter(alle)\n",
|
||||||
|
" kosten = aufrufe - vorher\n",
|
||||||
|
" print(f\"{kosten} Solveraufrufe -> Konflikt aus {len(konflikt)} von \"\n",
|
||||||
|
" f\"{len(BEDINGUNGEN)} Bedingungen:\\n\")\n",
|
||||||
|
" for name in namen(konflikt):\n",
|
||||||
|
" print(f\" * {name}\")\n",
|
||||||
|
" print(\"\\nProbe auf Unreduzierbarkeit - jede einzelne davon weglassen:\")\n",
|
||||||
|
" for i in konflikt:\n",
|
||||||
|
" rest_loesbar = ist_loesbar([j for j in konflikt if j != i])\n",
|
||||||
|
" print(f\" ohne {BEDINGUNGEN[i][0]:<26} {'loesbar' if rest_loesbar else 'UNLOESBAR - nicht minimal!'}\")\n",
|
||||||
|
" print(f\"\\nAufwand: {kosten} Aufrufe. Alle Teilmengen durchzuprobieren waeren \"\n",
|
||||||
|
" f\"2^{len(BEDINGUNGEN)} = {2 ** len(BEDINGUNGEN):,} gewesen.\".replace(\",\", \".\"))\n",
|
||||||
|
" return konflikt\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def teil_4_der_naechste_konflikt(alle: list[int], konflikt: list[int]) -> None:\n",
|
||||||
|
" print(\"\\n\" + \"=\" * 68)\n",
|
||||||
|
" print(\"4. Ein Konflikt ist selten einer\")\n",
|
||||||
|
" print(\"=\" * 68)\n",
|
||||||
|
" entfernt = konflikt[0]\n",
|
||||||
|
" rest = [i for i in alle if i != entfernt]\n",
|
||||||
|
" print(f\"Angenommen, '{BEDINGUNGEN[entfernt][0]}' laesst sich verhandeln\")\n",
|
||||||
|
" print(\"und wird aus dem Modell genommen. Dann ist das Modell ...\")\n",
|
||||||
|
" if ist_loesbar(rest):\n",
|
||||||
|
" print(\"... loesbar. Fertig.\")\n",
|
||||||
|
" return\n",
|
||||||
|
" print(\"... immer noch unloesbar. Der Filter erneut:\\n\")\n",
|
||||||
|
" zweiter = deletion_filter(rest)\n",
|
||||||
|
" for name in namen(zweiter):\n",
|
||||||
|
" print(f\" * {name}\")\n",
|
||||||
|
" gemeinsam = set(zweiter) & set(konflikt)\n",
|
||||||
|
" print(f\"\\nUeberschneidung mit dem ersten Konflikt: {len(gemeinsam)} Bedingungen\")\n",
|
||||||
|
" print(\"Ein zweiter, unabhaengiger Widerspruch, den der erste verdeckt hat.\")\n",
|
||||||
|
" print(\"Deshalb ist die Konfliktsuche eine Schleife, kein einzelner Aufruf:\")\n",
|
||||||
|
" print(\"reparieren, neu suchen, bis das Modell loesbar ist.\")\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def teil_5_die_reihenfolge(alle: list[int]) -> None:\n",
|
||||||
|
" print(\"\\n\" + \"=\" * 68)\n",
|
||||||
|
" print(\"5. Es gibt nicht DEN Konflikt\")\n",
|
||||||
|
" print(\"=\" * 68)\n",
|
||||||
|
" print(\"Derselbe Filter, nur eine andere Pruefreihenfolge - 200-mal gewuerfelt:\\n\")\n",
|
||||||
|
" zufall = np.random.default_rng(0)\n",
|
||||||
|
" haeufigkeit: dict[tuple[int, ...], int] = {}\n",
|
||||||
|
" for _ in range(200):\n",
|
||||||
|
" ordnung = [int(i) for i in zufall.permutation(alle)]\n",
|
||||||
|
" schluessel = tuple(sorted(deletion_filter(ordnung)))\n",
|
||||||
|
" haeufigkeit[schluessel] = haeufigkeit.get(schluessel, 0) + 1\n",
|
||||||
|
" for schluessel, anzahl in sorted(haeufigkeit.items(), key=lambda p: -p[1]):\n",
|
||||||
|
" print(f\" {anzahl:3d}x {len(schluessel)} Bedingungen: \"\n",
|
||||||
|
" f\"{', '.join(namen(list(schluessel)))}\")\n",
|
||||||
|
" print(f\"\\n{len(haeufigkeit)} verschiedene minimale Konflikte in EINEM Modell.\")\n",
|
||||||
|
" print(\"Der Filter liefert *einen* kleinsten Konflikt, nicht *den* kleinsten -\")\n",
|
||||||
|
" print(\"den gibt es nicht. Alle oben sind gleichermassen korrekt.\")\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def teil_6_den_befund_steuern(alle: list[int]) -> None:\n",
|
||||||
|
" print(\"\\n\" + \"=\" * 68)\n",
|
||||||
|
" print(\"6. Den Befund brauchbar machen\")\n",
|
||||||
|
" print(\"=\" * 68)\n",
|
||||||
|
" print(\"Der Filter wirft heraus, was er zuerst in die Hand bekommt. Wer die\")\n",
|
||||||
|
" print(\"unveraenderlichen Bedingungen zuerst pruefen laesst, bekommt sie eher\")\n",
|
||||||
|
" print(\"aus dem Befund heraus - und sieht dafuer mehr Stellschrauben.\\n\")\n",
|
||||||
|
" fest = [i for i in alle if BEDINGUNGEN[i][0] not in VERHANDELBAR]\n",
|
||||||
|
" frei = [i for i in alle if BEDINGUNGEN[i][0] in VERHANDELBAR]\n",
|
||||||
|
" for titel, ordnung in ((\"unveraenderliche zuerst\", fest + frei),\n",
|
||||||
|
" (\"veraenderliche zuerst \", frei + fest)):\n",
|
||||||
|
" konflikt = sorted(deletion_filter(ordnung))\n",
|
||||||
|
" stellschrauben = [i for i in konflikt if BEDINGUNGEN[i][0] in VERHANDELBAR]\n",
|
||||||
|
" print(f\" {titel} -> {len(konflikt)} Bedingungen, davon \"\n",
|
||||||
|
" f\"{len(stellschrauben)} veraenderlich\")\n",
|
||||||
|
" print(f\" {', '.join(namen(konflikt))}\")\n",
|
||||||
|
" print(\"\\nBeide Befunde sind wahr. Nur einer davon nennt dem Planer etwas,\")\n",
|
||||||
|
" print(\"das er tatsaechlich tun kann.\")\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"if __name__ == \"__main__\":\n",
|
||||||
|
" alle = list(range(len(BEDINGUNGEN)))\n",
|
||||||
|
" teil_1_der_befund(alle)\n",
|
||||||
|
" teil_2_die_naive_suche(alle)\n",
|
||||||
|
" konflikt = teil_3_der_filter(alle)\n",
|
||||||
|
" teil_4_der_naechste_konflikt(alle, konflikt)\n",
|
||||||
|
" teil_5_die_reihenfolge(alle)\n",
|
||||||
|
" teil_6_den_befund_steuern(alle)\n",
|
||||||
|
" print(\"\\n\" + \"=\" * 68)\n",
|
||||||
|
" print(f\"Insgesamt {aufrufe} Solveraufrufe fuer die gesamte Diagnose.\")\n",
|
||||||
|
" print(\"=\" * 68)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"name": "python",
|
||||||
|
"version": "3.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
330
Notebooks_04/bruecke.ipynb
Normal file
330
Notebooks_04/bruecke.ipynb
Normal file
|
|
@ -0,0 +1,330 @@
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"# Kapitel 16: Die Strukturbrücke — dieselbe Mathematik, zwei Welten\n",
|
||||||
|
"\n",
|
||||||
|
"Begleitnotebook zu *Optimierte Entscheidungsfindung mit Python*. Die Codezellen sind identisch mit den im Buch abgedruckten Programmen.\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"# Einmalig ausfuehren: installiert alle im Buch verwendeten Pakete.\n",
|
||||||
|
"# Lokal in einer virtuellen Umgebung genauso gueltig wie in Google Colab.\n",
|
||||||
|
"%pip install --quiet ortools highspy cvxpy scipy numpy pandas polars \\\n",
|
||||||
|
" scikit-learn matplotlib plotly pyomo linopy pymoo pydantic openpyxl"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Das Programm\n",
|
||||||
|
"\n",
|
||||||
|
"`Strukturbruecke.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# Strukturbruecke.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel Bruecke: Derselbe Code, zwei Welten - der Beweis statt der Behauptung.\n",
|
||||||
|
"\n",
|
||||||
|
"Der Teil-Auftakt stellt eine Tabelle auf: \"Ressourcen auf Produkte verteilen\"\n",
|
||||||
|
"entspreche \"Kapital auf Anlagen verteilen\", \"gegen den Worst Case absichern\"\n",
|
||||||
|
"entspreche \"Absicherung gegen Kursabstuerze\". Solche Tabellen stehen in vielen\n",
|
||||||
|
"Buechern. Sie sind billig - und man kann sie pruefen.\n",
|
||||||
|
"\n",
|
||||||
|
"Dieses Programm prueft sie. Es fuettert zweimal DENSELBEN Code mit Daten aus\n",
|
||||||
|
"zwei Welten und zeigt die Ergebnisse nebeneinander:\n",
|
||||||
|
"\n",
|
||||||
|
" 1. Die Allokation als LP. Das Domaenenmodell aus or_kern.py, einmal mit einer\n",
|
||||||
|
" Schreinerei (Montagestunden, Plattenmaterial) und einmal mit einem Depot\n",
|
||||||
|
" (Kapital, Risikobudget). Gleiche Klasse, gleicher Modellbauer, gleiche\n",
|
||||||
|
" Abnahmepruefung - und der Schattenpreis heisst in der einen Welt\n",
|
||||||
|
" \"Wert einer zusaetzlichen Montagestunde\" und in der anderen \"Preis des\n",
|
||||||
|
" Risikos\".\n",
|
||||||
|
" 2. Die Absicherung gegen den schlechtesten Fall als CVaR. EINE Funktion,\n",
|
||||||
|
" einmal mit Kursrenditen und einmal mit Lieferverzuegen.\n",
|
||||||
|
" 3. Was NICHT hinueberreicht. Der ehrliche Teil: Drei Unterschiede, die die\n",
|
||||||
|
" Analogie begrenzen - und die man kennen muss, bevor man Methoden aus\n",
|
||||||
|
" der einen Welt in die andere traegt.\n",
|
||||||
|
"\n",
|
||||||
|
"ZUR SOLVERWAHL: Teil 1 benutzt 'loese_mit_scipy' und nicht 'loese_mit_glop'.\n",
|
||||||
|
"Das ist kein Zufall - CVXPY laedt fuer Teil 2 highspy, und ortools vertraegt\n",
|
||||||
|
"sich damit nicht im selben Prozess (Kapitel Oekosystem). Wer hier GLOP nimmt,\n",
|
||||||
|
"bekommt beim cvxpy-Import eine Fehlermeldung ueber ein 'undefined symbol'.\n",
|
||||||
|
"Genau deshalb laedt or_kern.py seine Solver erst beim Aufruf.\n",
|
||||||
|
"\n",
|
||||||
|
"Benoetigt: numpy, cvxpy, scipy und pydantic (ueber or_kern)\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"from __future__ import annotations\n",
|
||||||
|
"\n",
|
||||||
|
"import numpy as np\n",
|
||||||
|
"\n",
|
||||||
|
"from or_kern import (Produkt, Produktionsproblem, loese_mit_scipy,\n",
|
||||||
|
" pruefe_loesung)\n",
|
||||||
|
"\n",
|
||||||
|
"SAAT = 7\n",
|
||||||
|
"SZENARIEN = 500\n",
|
||||||
|
"ALPHA = 0.95 # die schlechtesten 5 % der Faelle\n",
|
||||||
|
"MAX_ANTEIL = 0.40 # Streuungsgebot in beiden Welten\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"# --- Teil 1: Dieselbe Klasse, zwei Welten ----------------------------------\n",
|
||||||
|
"\n",
|
||||||
|
"def schreinerei() -> Produktionsproblem:\n",
|
||||||
|
" \"\"\"Montagestunden und Plattenmaterial auf Tische und Stuehle verteilen.\"\"\"\n",
|
||||||
|
" return Produktionsproblem(\n",
|
||||||
|
" produkte=[\n",
|
||||||
|
" Produkt(name=\"Tisch\", deckungsbeitrag=240.0,\n",
|
||||||
|
" verbrauch={\"Montagestunden\": 3.0, \"Plattenmaterial\": 6.0}),\n",
|
||||||
|
" Produkt(name=\"Stuhl\", deckungsbeitrag=60.0,\n",
|
||||||
|
" verbrauch={\"Montagestunden\": 1.0, \"Plattenmaterial\": 1.0}),\n",
|
||||||
|
" Produkt(name=\"Regal\", deckungsbeitrag=130.0,\n",
|
||||||
|
" verbrauch={\"Montagestunden\": 2.0, \"Plattenmaterial\": 4.0}),\n",
|
||||||
|
" ],\n",
|
||||||
|
" kapazitaeten={\"Montagestunden\": 150.0, \"Plattenmaterial\": 240.0})\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def depot() -> Produktionsproblem:\n",
|
||||||
|
" \"\"\"Kapital und Risikobudget auf Anlageklassen verteilen.\n",
|
||||||
|
"\n",
|
||||||
|
" Eine \"Einheit\" ist hier 1.000 EUR Anlagesumme. Der Deckungsbeitrag ist der\n",
|
||||||
|
" erwartete Jahresertrag dieser Einheit, der Verbrauch die beanspruchte\n",
|
||||||
|
" Kapital- und Risikomenge. Das ist keine Analogie, sondern buchstaeblich\n",
|
||||||
|
" dasselbe Modell - deshalb passt es in dieselbe Klasse.\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" return Produktionsproblem(\n",
|
||||||
|
" produkte=[\n",
|
||||||
|
" Produkt(name=\"Aktien Welt\", deckungsbeitrag=75.0,\n",
|
||||||
|
" verbrauch={\"Kapital (Tsd. EUR)\": 1.0, \"Risikobudget\": 1.00}),\n",
|
||||||
|
" Produkt(name=\"Anleihen\", deckungsbeitrag=28.0,\n",
|
||||||
|
" verbrauch={\"Kapital (Tsd. EUR)\": 1.0, \"Risikobudget\": 0.22}),\n",
|
||||||
|
" Produkt(name=\"Immobilienfonds\", deckungsbeitrag=46.0,\n",
|
||||||
|
" verbrauch={\"Kapital (Tsd. EUR)\": 1.0, \"Risikobudget\": 0.55}),\n",
|
||||||
|
" ],\n",
|
||||||
|
" kapazitaeten={\"Kapital (Tsd. EUR)\": 150.0, \"Risikobudget\": 90.0})\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def berichte_allokation(titel: str, problem: Produktionsproblem,\n",
|
||||||
|
" einheit: str, ertragsname: str) -> None:\n",
|
||||||
|
" \"\"\"Ein Bericht fuer beide Welten - nur die Beschriftung wechselt.\"\"\"\n",
|
||||||
|
" loesung = loese_mit_scipy(problem)\n",
|
||||||
|
" beanstandungen = pruefe_loesung(problem, loesung)\n",
|
||||||
|
"\n",
|
||||||
|
" print(f\" {titel}\")\n",
|
||||||
|
" print(f\" {loesung.als_bericht()}\")\n",
|
||||||
|
" for produkt in problem.produkte:\n",
|
||||||
|
" print(f\" {produkt.name:<18} {loesung.werte[produkt.name]:8.2f} {einheit}\")\n",
|
||||||
|
" print(f\" {ertragsname:<18} {loesung.zielwert:8.2f} EUR\")\n",
|
||||||
|
" for ressource, preis in loesung.schattenpreise.items():\n",
|
||||||
|
" print(f\" Schattenpreis {ressource:<24} {preis:7.2f} EUR\")\n",
|
||||||
|
" print(f\" Abnahmepruefung: \"\n",
|
||||||
|
" f\"{'bestanden' if not beanstandungen else beanstandungen}\")\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"# --- Teil 2: Eine CVaR-Funktion, zwei Welten -------------------------------\n",
|
||||||
|
"\n",
|
||||||
|
"def optimiere_cvar(verluste: np.ndarray, ertrag: np.ndarray,\n",
|
||||||
|
" mindestertrag: float, alpha: float = ALPHA,\n",
|
||||||
|
" max_anteil: float = MAX_ANTEIL):\n",
|
||||||
|
" \"\"\"Minimiert den CVaR der Verluste unter einer Mindestertragsbedingung.\n",
|
||||||
|
"\n",
|
||||||
|
" 'verluste' hat die Form (Szenarien x Optionen) und enthaelt, was in\n",
|
||||||
|
" Szenario s passiert, wenn eine Einheit in Option j steckt. Was ein\n",
|
||||||
|
" \"Verlust\" ist, entscheidet allein die Einheit der Matrix: Prozentpunkte\n",
|
||||||
|
" Kursverlust oder Tage Lieferverzug - die Formel sieht keinen Unterschied.\n",
|
||||||
|
"\n",
|
||||||
|
" Das ist die Rockafellar-Uryasev-Formulierung aus dem Kapitel CVaR, hier\n",
|
||||||
|
" ohne jede Aenderung wiederverwendet.\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" import cvxpy as cp\n",
|
||||||
|
"\n",
|
||||||
|
" anzahl_szenarien, anzahl_optionen = verluste.shape\n",
|
||||||
|
" anteil = cp.Variable(anzahl_optionen, nonneg=True)\n",
|
||||||
|
" schwelle = cp.Variable() # wird im Optimum zum VaR\n",
|
||||||
|
" ueberschuss = cp.Variable(anzahl_szenarien, nonneg=True)\n",
|
||||||
|
"\n",
|
||||||
|
" cvar = schwelle + (1.0 / (anzahl_szenarien * (1 - alpha))) * cp.sum(ueberschuss)\n",
|
||||||
|
" problem = cp.Problem(\n",
|
||||||
|
" cp.Minimize(cvar),\n",
|
||||||
|
" [ueberschuss >= verluste @ anteil - schwelle,\n",
|
||||||
|
" cp.sum(anteil) == 1,\n",
|
||||||
|
" anteil <= max_anteil,\n",
|
||||||
|
" ertrag @ anteil >= mindestertrag])\n",
|
||||||
|
" problem.solve()\n",
|
||||||
|
" if problem.status not in (\"optimal\", \"optimal_inaccurate\"):\n",
|
||||||
|
" raise SystemExit(f\"CVaR-Problem nicht loesbar: {problem.status}\")\n",
|
||||||
|
" return anteil.value, float(cvar.value), float(schwelle.value)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def kursszenarien(rng) -> tuple[np.ndarray, np.ndarray, list[str]]:\n",
|
||||||
|
" \"\"\"Taegliche Verluste (negative Renditen) von sechs Anlageklassen.\"\"\"\n",
|
||||||
|
" namen = [\"Aktien Welt\", \"Aktien EU\", \"Schwellenlaender\",\n",
|
||||||
|
" \"Staatsanleihen\", \"Unternehmensanl.\", \"Rohstoffe\"]\n",
|
||||||
|
" rendite_pa = np.array([0.080, 0.065, 0.110, 0.025, 0.045, 0.070])\n",
|
||||||
|
" schwankung = np.array([0.180, 0.160, 0.260, 0.040, 0.075, 0.210])\n",
|
||||||
|
" taeglich = rng.normal(rendite_pa / 252, schwankung / np.sqrt(252),\n",
|
||||||
|
" (SZENARIEN, len(namen)))\n",
|
||||||
|
" return -taeglich * 100.0, rendite_pa * 100.0, namen\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def lieferszenarien(rng) -> tuple[np.ndarray, np.ndarray, list[str]]:\n",
|
||||||
|
" \"\"\"Lieferverzug in Tagen bei sechs Lieferanten.\n",
|
||||||
|
"\n",
|
||||||
|
" Der Aufbau ist bewusst anders als bei den Kursen: Hier gibt es einen\n",
|
||||||
|
" normalen Verzug UND seltene Totalausfaelle, die 14 Tage kosten. Das ist\n",
|
||||||
|
" genau die Art fetter Raender, wegen der man in beiden Welten CVaR statt\n",
|
||||||
|
" Standardabweichung benutzt.\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" namen = [\"Nordwerk\", \"Sued-Metall\", \"Fernost A\", \"Lokalzulieferer\",\n",
|
||||||
|
" \"Fernost B\", \"Osteuropa\"]\n",
|
||||||
|
" zuverlaessigkeit = np.array([0.94, 0.97, 0.90, 0.99, 0.92, 0.95])\n",
|
||||||
|
" verzugsstreuung = np.array([3.5, 1.8, 5.0, 0.9, 4.2, 2.8])\n",
|
||||||
|
" normaler_verzug = np.maximum(0.0, rng.normal(0.5, 1.0, (SZENARIEN, len(namen)))\n",
|
||||||
|
" * verzugsstreuung)\n",
|
||||||
|
" totalausfall = (rng.random((SZENARIEN, len(namen))) > zuverlaessigkeit) * 14.0\n",
|
||||||
|
" return normaler_verzug + totalausfall, zuverlaessigkeit, namen\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def berichte_cvar(titel: str, verluste, ertrag, namen, mindestertrag,\n",
|
||||||
|
" einheit: str, ertragsname: str):\n",
|
||||||
|
" anteile, cvar, var = optimiere_cvar(verluste, ertrag, mindestertrag)\n",
|
||||||
|
" mittel = float((verluste @ anteile).mean())\n",
|
||||||
|
" print(f\" {titel}\")\n",
|
||||||
|
" print(f\" VaR {ALPHA:.0%}: {var:8.3f} {einheit}\")\n",
|
||||||
|
" print(f\" CVaR {ALPHA:.0%}: {cvar:8.3f} {einheit} \"\n",
|
||||||
|
" f\"(Mittelwert ueber alle Szenarien: {mittel:.3f})\")\n",
|
||||||
|
" print(f\" {ertragsname}: {float(ertrag @ anteile):.3f}\")\n",
|
||||||
|
" print(\" Aufteilung:\")\n",
|
||||||
|
" for name, anteil in zip(namen, anteile):\n",
|
||||||
|
" balken = \"#\" * int(round(anteil * 40))\n",
|
||||||
|
" grenze = \" <- an der Streuungsgrenze\" if anteil > MAX_ANTEIL - 1e-4 else \"\"\n",
|
||||||
|
" print(f\" {name:<18} {anteil:6.1%} {balken}{grenze}\")\n",
|
||||||
|
" return anteile\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"if __name__ == \"__main__\":\n",
|
||||||
|
" rng = np.random.default_rng(SAAT)\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"=\" * 80)\n",
|
||||||
|
" print(\" DIESELBE STRUKTUR, ZWEI WELTEN\")\n",
|
||||||
|
" print(\"=\" * 80)\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Teil 1 ----------------------------------------------------------\n",
|
||||||
|
" print(\"\\n1. Allokation als LP - EINE Klasse, EIN Modellbauer\\n\")\n",
|
||||||
|
" berichte_allokation(\"Werkstatt: Produktionsprogramm\", schreinerei(),\n",
|
||||||
|
" \"Stueck\", \"Deckungsbeitrag\")\n",
|
||||||
|
" print()\n",
|
||||||
|
" berichte_allokation(\"Depot: Anlageaufteilung\", depot(),\n",
|
||||||
|
" \"Tsd. \", \"Erwarteter Ertrag\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\n Beide Ausgaben stammen aus derselben Funktion \"\n",
|
||||||
|
" \"'berichte_allokation'.\")\n",
|
||||||
|
" print(\" Ausgetauscht wurden nur die Daten und die Beschriftungen -\")\n",
|
||||||
|
" print(\" keine Zeile Modellcode.\")\n",
|
||||||
|
" print()\n",
|
||||||
|
" print(\" Lesen Sie die Schattenpreise nebeneinander: In der Werkstatt sagt\")\n",
|
||||||
|
" print(\" er, was eine zusaetzliche Montagestunde wert waere. Im Depot sagt\")\n",
|
||||||
|
" print(\" dieselbe Zahl, was eine zusaetzliche Einheit Risikobudget wert\")\n",
|
||||||
|
" print(\" waere - der PREIS DES RISIKOS. Das ist kein Sprachbild, sondern\")\n",
|
||||||
|
" print(\" derselbe Dualwert derselben Nebenbedingung.\")\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Teil 2 ----------------------------------------------------------\n",
|
||||||
|
" print(\"\\n\" + \"-\" * 80)\n",
|
||||||
|
" print(\"2. Absicherung gegen den schlechtesten Fall - EINE CVaR-Funktion\\n\")\n",
|
||||||
|
"\n",
|
||||||
|
" kurse, rendite, anlagen = kursszenarien(rng)\n",
|
||||||
|
" anteile_depot = berichte_cvar(\n",
|
||||||
|
" \"Depot: die schlechtesten 5 % der Handelstage\",\n",
|
||||||
|
" kurse, rendite, anlagen, 5.5,\n",
|
||||||
|
" \"% je Tag \", \"Erwartete Jahresrendite (%)\")\n",
|
||||||
|
" print()\n",
|
||||||
|
" lieferung, zuverlaessig, lieferanten = lieferszenarien(rng)\n",
|
||||||
|
" anteile_einkauf = berichte_cvar(\"Einkauf: die schlechtesten 5 % der Bestellungen\",\n",
|
||||||
|
" lieferung, zuverlaessig, lieferanten, 0.945,\n",
|
||||||
|
" \"Tage Verzug \", \"Mittlere Zuverlaessigkeit\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\n Auch hier: eine Funktion, zwei Aufrufe. Die Zielfunktion\")\n",
|
||||||
|
" print(\" interessiert sich nicht dafuer, ob in der Matrix Prozentpunkte\")\n",
|
||||||
|
" print(\" oder Tage stehen.\")\n",
|
||||||
|
" print()\n",
|
||||||
|
" print(f\" Interessant ist, WO die Streuungsgrenze von {MAX_ANTEIL:.0%} bindet:\")\n",
|
||||||
|
" print(f\" Depot : {(anteile_depot > MAX_ANTEIL - 1e-4).sum()} von \"\n",
|
||||||
|
" f\"{len(anteile_depot)} Posten am Anschlag\")\n",
|
||||||
|
" print(f\" Einkauf : {(anteile_einkauf > MAX_ANTEIL - 1e-4).sum()} von \"\n",
|
||||||
|
" f\"{len(anteile_einkauf)} Posten am Anschlag\")\n",
|
||||||
|
" print()\n",
|
||||||
|
" print(\" Im Einkauf zieht es die Loesung an den sicheren Lokalzulieferer,\")\n",
|
||||||
|
" print(\" bis die Grenze sie stoppt. Im Depot nicht - dort verhindert die\")\n",
|
||||||
|
" print(\" Mindestrendite, dass alles in Anleihen wandert. Zwei verschiedene\")\n",
|
||||||
|
" print(\" Bremsen also, und sie stehen an verschiedenen Stellen des Modells:\")\n",
|
||||||
|
" print(\" einmal in einer Nebenbedingung ueber die Anteile, einmal in einer\")\n",
|
||||||
|
" print(\" ueber den Ertrag. Wer eine Struktur uebertraegt, uebertraegt eben\")\n",
|
||||||
|
" print(\" nicht automatisch mit, WELCHE Bedingung am Ende bindet.\")\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Teil 3: Der ehrliche Teil ---------------------------------------\n",
|
||||||
|
" print(\"\\n\" + \"=\" * 80)\n",
|
||||||
|
" print(\" WAS NICHT HINUEBERREICHT\")\n",
|
||||||
|
" print(\"=\" * 80)\n",
|
||||||
|
" print(\"Die Struktur traegt. Drei Unterschiede tragen NICHT mit, und wer sie\")\n",
|
||||||
|
" print(\"uebersieht, macht aus einer nuetzlichen Analogie einen Fehler:\")\n",
|
||||||
|
" print()\n",
|
||||||
|
" print(\"1. WOHER DIE ZAHLEN KOMMEN.\")\n",
|
||||||
|
" print(\" In der Werkstatt ist der Verbrauch je Tisch gemessen - drei\")\n",
|
||||||
|
" print(\" Montagestunden sind drei Montagestunden. Im Depot ist die\")\n",
|
||||||
|
" print(\" erwartete Rendite GESCHAETZT, und zwar mit einem Fehler, der\")\n",
|
||||||
|
" print(\" groesser sein kann als die Unterschiede zwischen den Anlagen\")\n",
|
||||||
|
" print(\" (Kapitel Markowitz, Renditeschaetzung_Falle.py). Dieselbe\")\n",
|
||||||
|
" print(\" Optimierung ist im einen Fall Planung und im anderen\")\n",
|
||||||
|
" print(\" Fehlerverstaerkung.\")\n",
|
||||||
|
" print()\n",
|
||||||
|
" print(\"2. OB DIE VERGANGENHEIT ETWAS UEBER DIE ZUKUNFT SAGT.\")\n",
|
||||||
|
" print(\" Lieferzeiten haben physikalische Ursachen: Entfernung, Zoll,\")\n",
|
||||||
|
" print(\" Kapazitaet. Sie aendern sich langsam und nachvollziehbar.\")\n",
|
||||||
|
" print(\" Kursrenditen entstehen aus dem Verhalten von Marktteilnehmern,\")\n",
|
||||||
|
" print(\" die selbst auf Modelle reagieren - dort verschwindet ein\")\n",
|
||||||
|
" print(\" erkanntes Muster oft genau deshalb, weil es erkannt wurde\")\n",
|
||||||
|
" print(\" (Kapitel Handelsmaschine, Data_Snooping.py).\")\n",
|
||||||
|
" print()\n",
|
||||||
|
" print(\"3. OB TEILBARKEIT ERLAUBT IST.\")\n",
|
||||||
|
" print(\" 37,4 % eines Aktienfonds sind ein normaler Auftrag. 37,4 % eines\")\n",
|
||||||
|
" print(\" Lieferanten sind es nicht - Vertraege, Mindestabnahmen und\")\n",
|
||||||
|
" print(\" Ruestzeiten machen Einkaufsentscheidungen ganzzahlig. Genau\")\n",
|
||||||
|
" print(\" deshalb ist Teil II voller MILP und Teil V fast frei davon.\")\n",
|
||||||
|
" print()\n",
|
||||||
|
" print(\"Die Bruecke traegt also die MODELLE, nicht die Annahmen. Wer sie\")\n",
|
||||||
|
" print(\"benutzt, spart sich das Lernen der Methoden - nicht das Nachdenken\")\n",
|
||||||
|
" print(\"ueber die Daten.\")\n",
|
||||||
|
" print(\"=\" * 80)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"name": "python",
|
||||||
|
"version": "3.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
902
Notebooks_04/cpsat.ipynb
Normal file
902
Notebooks_04/cpsat.ipynb
Normal file
|
|
@ -0,0 +1,902 @@
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"# Kapitel 7: Constraint Programming mit CP-SAT — Logik, Scheduling und Zuweisung\n",
|
||||||
|
"\n",
|
||||||
|
"Begleitnotebook zu *Optimierte Entscheidungsfindung mit Python*. Die Codezellen sind identisch mit den im Buch abgedruckten Programmen.\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"# Einmalig ausfuehren: installiert alle im Buch verwendeten Pakete.\n",
|
||||||
|
"# Lokal in einer virtuellen Umgebung genauso gueltig wie in Google Colab.\n",
|
||||||
|
"%pip install --quiet ortools highspy cvxpy scipy numpy pandas polars \\\n",
|
||||||
|
" scikit-learn matplotlib plotly pyomo linopy pymoo pydantic openpyxl"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Was Propagation leistet\n",
|
||||||
|
"\n",
|
||||||
|
"`Propagation_Demo.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# Propagation_Demo.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel CP-SAT: Propagation sichtbar machen.\n",
|
||||||
|
"Vergleicht die Zahl der geprueften Kombinationen bei roher Aufzaehlung\n",
|
||||||
|
"mit der Zahl der Verzweigungen, die CP-SAT tatsaechlich braucht.\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"import itertools\n",
|
||||||
|
"\n",
|
||||||
|
"from ortools.sat.python import cp_model\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def rohe_aufzaehlung(n: int, obergrenze: int) -> tuple[int, int]:\n",
|
||||||
|
" \"\"\"Zaehlt ALLE Kombinationen und prueft jede einzeln (Brute Force).\"\"\"\n",
|
||||||
|
" geprueft = 0\n",
|
||||||
|
" loesungen = 0\n",
|
||||||
|
" for kombination in itertools.product(range(1, obergrenze + 1), repeat=n):\n",
|
||||||
|
" geprueft += 1\n",
|
||||||
|
" if len(set(kombination)) == n and sum(kombination) == 3 * n:\n",
|
||||||
|
" loesungen += 1\n",
|
||||||
|
" return geprueft, loesungen\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def mit_cp_sat(n: int, obergrenze: int) -> tuple[int, int, float]:\n",
|
||||||
|
" \"\"\"Dasselbe Problem deklarativ: alle verschieden, Summe = 3n.\"\"\"\n",
|
||||||
|
" modell = cp_model.CpModel()\n",
|
||||||
|
" x = [modell.NewIntVar(1, obergrenze, f\"x{i}\") for i in range(n)]\n",
|
||||||
|
" modell.AddAllDifferent(x) # globales Constraint\n",
|
||||||
|
" modell.Add(sum(x) == 3 * n)\n",
|
||||||
|
"\n",
|
||||||
|
" loeser = cp_model.CpSolver()\n",
|
||||||
|
" loeser.parameters.enumerate_all_solutions = True\n",
|
||||||
|
"\n",
|
||||||
|
" class Zaehler(cp_model.CpSolverSolutionCallback):\n",
|
||||||
|
" def __init__(self):\n",
|
||||||
|
" super().__init__()\n",
|
||||||
|
" self.anzahl = 0\n",
|
||||||
|
"\n",
|
||||||
|
" def on_solution_callback(self):\n",
|
||||||
|
" self.anzahl += 1\n",
|
||||||
|
"\n",
|
||||||
|
" zaehler = Zaehler()\n",
|
||||||
|
" loeser.Solve(modell, zaehler)\n",
|
||||||
|
" return zaehler.anzahl, loeser.NumBranches(), loeser.WallTime()\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"if __name__ == \"__main__\":\n",
|
||||||
|
" print(\"=\" * 84)\n",
|
||||||
|
" print(\" PROPAGATION: WIE VIEL ARBEIT SPART SICH DER SOLVER?\")\n",
|
||||||
|
" print(\" Aufgabe: n Zahlen aus 1..G, alle verschieden, Summe = 3n\")\n",
|
||||||
|
" print(\"=\" * 84)\n",
|
||||||
|
" print(f\"{'n':>3} {'G':>4} | {'Brute Force':>14} | {'CP-SAT':>10} | \"\n",
|
||||||
|
" f\"{'Ersparnis':>11} | {'Loesungen':>10}\")\n",
|
||||||
|
" print(\"-\" * 84)\n",
|
||||||
|
"\n",
|
||||||
|
" for n, grenze in [(4, 8), (5, 10), (6, 12), (7, 14)]:\n",
|
||||||
|
" kombis, treffer_bf = rohe_aufzaehlung(n, grenze)\n",
|
||||||
|
" treffer_cp, verzweigungen, dauer = mit_cp_sat(n, grenze)\n",
|
||||||
|
" assert treffer_bf == treffer_cp, \"Beide Verfahren muessen gleich viele finden!\"\n",
|
||||||
|
" ersparnis = kombis / max(verzweigungen, 1)\n",
|
||||||
|
" print(f\"{n:>3} {grenze:>4} | {kombis:>14,} | {verzweigungen:>10,} | \"\n",
|
||||||
|
" f\"{ersparnis:>10.0f}x | {treffer_cp:>10,}\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"-\" * 84)\n",
|
||||||
|
" print(\"'Brute Force' = alle Kombinationen. 'CP-SAT' = tatsaechliche Verzweigungen.\")\n",
|
||||||
|
" print(\"Der Rest wurde durch Propagation ausgeschlossen, ohne ihn anzusehen.\")\n",
|
||||||
|
" print(\"=\" * 84)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Praxisfall: Dynamisches Vertretungssystem\n",
|
||||||
|
"\n",
|
||||||
|
"`CP_SAT_Vertretungssystem.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# CP_SAT_Vertretungssystem.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel CP-SAT: Dynamisches Vertretungs- und Einsatzplanungssystem mit CP-SAT.\n",
|
||||||
|
"\n",
|
||||||
|
"Eigenschaften:\n",
|
||||||
|
" * Solver-Status wird ueber SolverStatus aus or_kern.py ausgewertet - dieselbe\n",
|
||||||
|
" Fallunterscheidung wie bei jedem anderen Solver im Buch\n",
|
||||||
|
" * Strafkosten-Zerlegung wird ausgewiesen (Erklaerbarkeit, Kapitel Praxisfallen)\n",
|
||||||
|
" * Fairness-Kriterium ergaenzt (Spannweite der Belastung minimieren)\n",
|
||||||
|
" * Vorabdiagnose auf offensichtliche Unloesbarkeit\n",
|
||||||
|
" * Abnahmepruefung des fertigen Plans OHNE den Solver zu fragen\n",
|
||||||
|
"\n",
|
||||||
|
"Benoetigt: ortools, pandas, pydantic (ueber or_kern)\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"from __future__ import annotations\n",
|
||||||
|
"\n",
|
||||||
|
"from ortools.sat.python import cp_model\n",
|
||||||
|
"import pandas as pd\n",
|
||||||
|
"\n",
|
||||||
|
"from or_kern import Loesung, SolverStatus, status_von_cpsat\n",
|
||||||
|
"\n",
|
||||||
|
"# --- 1. Datenbasis --------------------------------------------------------\n",
|
||||||
|
"SLOTS = [1, 2, 3, 4]\n",
|
||||||
|
"FAECHER = [\"Mathematik\", \"Physik\", \"Mathematik\", \"Informatik\"]\n",
|
||||||
|
"KLASSEN = [\"Klasse 8a\", \"Klasse 10b\", \"Klasse 7a\", \"Klasse 9c\"]\n",
|
||||||
|
"\n",
|
||||||
|
"PERSONAL = [\"Frau_Mueller\", \"Herr_Schmidt\", \"Frau_Albrecht\", \"Herr_Bauer\", \"Frau_Koch\"]\n",
|
||||||
|
"\n",
|
||||||
|
"QUALIFIKATION = {\n",
|
||||||
|
" \"Frau_Mueller\": {\"Mathematik\", \"Physik\"},\n",
|
||||||
|
" \"Herr_Schmidt\": {\"Informatik\", \"Mathematik\"},\n",
|
||||||
|
" \"Frau_Albrecht\": {\"Physik\", \"Informatik\"},\n",
|
||||||
|
" \"Herr_Bauer\": {\"Mathematik\"},\n",
|
||||||
|
" \"Frau_Koch\": {\"Mathematik\", \"Physik\", \"Informatik\"},\n",
|
||||||
|
"}\n",
|
||||||
|
"\n",
|
||||||
|
"VORBELASTUNG = { # bereits geleistete Stunden heute\n",
|
||||||
|
" \"Frau_Mueller\": 1, \"Herr_Schmidt\": 0, \"Frau_Albrecht\": 2,\n",
|
||||||
|
" \"Herr_Bauer\": 0, \"Frau_Koch\": 1,\n",
|
||||||
|
"}\n",
|
||||||
|
"\n",
|
||||||
|
"MAX_VERTRETUNGEN = 2\n",
|
||||||
|
"STRAFE_VORBELASTUNG = 50 # je Stunde Vorbelastung und Zuweisung\n",
|
||||||
|
"STRAFE_LOCH = 80 # je Freistundenloch\n",
|
||||||
|
"STRAFE_UNFAIRNESS = 30 # je Einheit Spannweite der Gesamtbelastung\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def pruefe_grundsaetzliche_loesbarkeit() -> bool:\n",
|
||||||
|
" \"\"\"Vorabdiagnose: Gibt es fuer jeden Slot ueberhaupt qualifiziertes Personal?\"\"\"\n",
|
||||||
|
" ok = True\n",
|
||||||
|
" for s, fach in enumerate(FAECHER):\n",
|
||||||
|
" kandidaten = [p for p in PERSONAL if fach in QUALIFIKATION[p]]\n",
|
||||||
|
" if not kandidaten:\n",
|
||||||
|
" print(f\" UNLOESBAR: Fuer Slot {SLOTS[s]} ({fach}) gibt es niemanden.\")\n",
|
||||||
|
" ok = False\n",
|
||||||
|
" kapazitaet = len(PERSONAL) * MAX_VERTRETUNGEN\n",
|
||||||
|
" if kapazitaet < len(SLOTS):\n",
|
||||||
|
" print(f\" UNLOESBAR: Kapazitaet {kapazitaet} < {len(SLOTS)} offene Stunden.\")\n",
|
||||||
|
" ok = False\n",
|
||||||
|
" return ok\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def pruefe_plan(plan: dict[int, str]) -> list[str]:\n",
|
||||||
|
" \"\"\"Prueft den fertigen Plan gegen die harten Regeln - ohne den Solver.\n",
|
||||||
|
"\n",
|
||||||
|
" Der Solver kann nur pruefen, was ihm gesagt wurde. Diese Funktion prueft\n",
|
||||||
|
" gegen die ANFORDERUNG und benutzt dafuer bewusst keine Modellvariable\n",
|
||||||
|
" (siehe Kapitel Praxisfallen). Leere Liste heisst bestanden.\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" beanstandungen: list[str] = []\n",
|
||||||
|
" if sorted(plan) != list(range(len(SLOTS))):\n",
|
||||||
|
" beanstandungen.append(f\"nicht jede Stunde genau einmal besetzt: {sorted(plan)}\")\n",
|
||||||
|
" return beanstandungen\n",
|
||||||
|
" for s, person in plan.items():\n",
|
||||||
|
" if FAECHER[s] not in QUALIFIKATION[person]:\n",
|
||||||
|
" beanstandungen.append(f\"{person} ist nicht fuer {FAECHER[s]} qualifiziert\")\n",
|
||||||
|
" for person in PERSONAL:\n",
|
||||||
|
" anzahl = sum(1 for p in plan.values() if p == person)\n",
|
||||||
|
" if anzahl > MAX_VERTRETUNGEN:\n",
|
||||||
|
" beanstandungen.append(f\"{person} hat {anzahl} Vertretungen \"\n",
|
||||||
|
" f\"(hoechstens {MAX_VERTRETUNGEN})\")\n",
|
||||||
|
" return beanstandungen\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def plane_vertretung() -> list[dict[str, str]] | None:\n",
|
||||||
|
" if not pruefe_grundsaetzliche_loesbarkeit():\n",
|
||||||
|
" return None\n",
|
||||||
|
"\n",
|
||||||
|
" modell = cp_model.CpModel()\n",
|
||||||
|
"\n",
|
||||||
|
" # Entscheidungsvariablen: x[person, slot] = 1 <=> Person uebernimmt Slot\n",
|
||||||
|
" x = {(p, s): modell.NewBoolVar(f\"zuweisung_{p}_slot{s+1}\")\n",
|
||||||
|
" for p in PERSONAL for s in range(len(SLOTS))}\n",
|
||||||
|
"\n",
|
||||||
|
" # --- HARTE NEBENBEDINGUNGEN ------------------------------------------\n",
|
||||||
|
" # H1: Jede Stunde genau einmal besetzen\n",
|
||||||
|
" for s in range(len(SLOTS)):\n",
|
||||||
|
" modell.AddExactlyOne(x[p, s] for p in PERSONAL)\n",
|
||||||
|
"\n",
|
||||||
|
" # H2: Qualifikation - unqualifizierte Zuweisung ausschliessen\n",
|
||||||
|
" for s, fach in enumerate(FAECHER):\n",
|
||||||
|
" for p in PERSONAL:\n",
|
||||||
|
" if fach not in QUALIFIKATION[p]:\n",
|
||||||
|
" modell.Add(x[p, s] == 0)\n",
|
||||||
|
"\n",
|
||||||
|
" # H3: Hoechstens MAX_VERTRETUNGEN Stunden je Person\n",
|
||||||
|
" for p in PERSONAL:\n",
|
||||||
|
" modell.Add(sum(x[p, s] for s in range(len(SLOTS))) <= MAX_VERTRETUNGEN)\n",
|
||||||
|
"\n",
|
||||||
|
" # (H4 \"nicht an zwei Orten gleichzeitig\" ist durch H1 bereits erfuellt:\n",
|
||||||
|
" # jeder Slot hat genau eine Person, und eine Person kann pro Slot nur\n",
|
||||||
|
" # eine Variable auf 1 setzen.)\n",
|
||||||
|
"\n",
|
||||||
|
" # --- WEICHE ZIELE ----------------------------------------------------\n",
|
||||||
|
" strafterme = []\n",
|
||||||
|
"\n",
|
||||||
|
" # W1: Vorbelastete Personen schonen\n",
|
||||||
|
" strafe_last = []\n",
|
||||||
|
" for p in PERSONAL:\n",
|
||||||
|
" zuweisungen = sum(x[p, s] for s in range(len(SLOTS)))\n",
|
||||||
|
" strafe_last.append(VORBELASTUNG[p] * STRAFE_VORBELASTUNG * zuweisungen)\n",
|
||||||
|
" strafterme.extend(strafe_last)\n",
|
||||||
|
"\n",
|
||||||
|
" # W2: Freistundenloecher vermeiden (Muster: arbeitet, frei, arbeitet)\n",
|
||||||
|
" loch_variablen = []\n",
|
||||||
|
" for p in PERSONAL:\n",
|
||||||
|
" for s in range(len(SLOTS) - 2):\n",
|
||||||
|
" loch = modell.NewBoolVar(f\"loch_{p}_{s}\")\n",
|
||||||
|
" # Reifizierung: loch == 1 <=> (x_s AND NOT x_{s+1} AND x_{s+2})\n",
|
||||||
|
" modell.AddBoolAnd([x[p, s], x[p, s + 1].Not(), x[p, s + 2]]).OnlyEnforceIf(loch)\n",
|
||||||
|
" modell.AddBoolOr([x[p, s].Not(), x[p, s + 1], x[p, s + 2].Not()]) \\\n",
|
||||||
|
" .OnlyEnforceIf(loch.Not())\n",
|
||||||
|
" loch_variablen.append(loch)\n",
|
||||||
|
" strafterme.append(loch * STRAFE_LOCH)\n",
|
||||||
|
"\n",
|
||||||
|
" # W3: Fairness - Spannweite der Gesamtbelastung minimieren\n",
|
||||||
|
" gesamtlast = {}\n",
|
||||||
|
" for p in PERSONAL:\n",
|
||||||
|
" last = modell.NewIntVar(0, len(SLOTS) + max(VORBELASTUNG.values()), f\"last_{p}\")\n",
|
||||||
|
" modell.Add(last == VORBELASTUNG[p] + sum(x[p, s] for s in range(len(SLOTS))))\n",
|
||||||
|
" gesamtlast[p] = last\n",
|
||||||
|
" max_last = modell.NewIntVar(0, 10, \"max_last\")\n",
|
||||||
|
" min_last = modell.NewIntVar(0, 10, \"min_last\")\n",
|
||||||
|
" modell.AddMaxEquality(max_last, list(gesamtlast.values()))\n",
|
||||||
|
" modell.AddMinEquality(min_last, list(gesamtlast.values()))\n",
|
||||||
|
" spannweite = modell.NewIntVar(0, 10, \"spannweite\")\n",
|
||||||
|
" modell.Add(spannweite == max_last - min_last)\n",
|
||||||
|
" strafterme.append(spannweite * STRAFE_UNFAIRNESS)\n",
|
||||||
|
"\n",
|
||||||
|
" modell.Minimize(sum(strafterme))\n",
|
||||||
|
"\n",
|
||||||
|
" # --- LOESEN -----------------------------------------------------------\n",
|
||||||
|
" loeser = cp_model.CpSolver()\n",
|
||||||
|
" loeser.parameters.max_time_in_seconds = 5.0\n",
|
||||||
|
" # Reproduzierbarkeit: ein Arbeiter, fester Startwert. Ohne das kann CP-SAT\n",
|
||||||
|
" # bei mehreren gleich guten Plaenen von Lauf zu Lauf einen anderen liefern\n",
|
||||||
|
" # (siehe JobShop_Intervalle.py im naechsten Abschnitt).\n",
|
||||||
|
" loeser.parameters.num_workers = 1\n",
|
||||||
|
" loeser.parameters.random_seed = 1\n",
|
||||||
|
" rohstatus = loeser.Solve(modell)\n",
|
||||||
|
"\n",
|
||||||
|
" # Der Rueckgabewert wird in die gemeinsame Sprache uebersetzt. Ab hier\n",
|
||||||
|
" # sieht die Auswertung genauso aus wie bei HiGHS, GLOP oder CVXPY.\n",
|
||||||
|
" status = status_von_cpsat(rohstatus)\n",
|
||||||
|
" loesung = Loesung(\n",
|
||||||
|
" status=status,\n",
|
||||||
|
" zielwert=loeser.ObjectiveValue() if status.brauchbar else None,\n",
|
||||||
|
" schranke=loeser.BestObjectiveBound() if status.brauchbar else None,\n",
|
||||||
|
" laufzeit=loeser.WallTime())\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"=\" * 78)\n",
|
||||||
|
" print(\" DYNAMISCHER VERTRETUNGSPLAN (CP-SAT OPTIMIERT)\")\n",
|
||||||
|
" print(\"=\" * 78)\n",
|
||||||
|
"\n",
|
||||||
|
" if status.modellfehler:\n",
|
||||||
|
" print(f\"Kein zulaessiger Plan moeglich (Status: {status.value}).\")\n",
|
||||||
|
" print(\"Die harten Regeln widersprechen sich. Naechster Schritt: eine Regel\")\n",
|
||||||
|
" print(\"weich machen (Kapitel Praxisfallen) oder den Konflikt eingrenzen\")\n",
|
||||||
|
" print(\"(Anhang Fehlerdiagnose).\")\n",
|
||||||
|
" return None\n",
|
||||||
|
" if not status.brauchbar:\n",
|
||||||
|
" print(f\"Solver lieferte kein Ergebnis (Status: {status.value}).\")\n",
|
||||||
|
" print(\"Zeitlimit erhoehen oder das Modell vereinfachen.\")\n",
|
||||||
|
" return None\n",
|
||||||
|
"\n",
|
||||||
|
" guete = (\"beweisbar optimal\" if status is SolverStatus.OPTIMAL\n",
|
||||||
|
" else \"zulaessig, aber nicht bewiesen - Gap beachten\")\n",
|
||||||
|
" print(f\"Solver-Status: {loeser.StatusName(rohstatus)} -> {status.value} ({guete})\")\n",
|
||||||
|
" print(f\"{loesung.als_bericht()}\\n\")\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Plan ausgeben ----------------------------------------------------\n",
|
||||||
|
" zuweisung = {s: next(p for p in PERSONAL if loeser.Value(x[p, s]) == 1)\n",
|
||||||
|
" for s in range(len(SLOTS))}\n",
|
||||||
|
" plan = [{\n",
|
||||||
|
" \"Stunde\": f\"Std. {SLOTS[s]}\",\n",
|
||||||
|
" \"Klasse\": KLASSEN[s],\n",
|
||||||
|
" \"Fach\": FAECHER[s],\n",
|
||||||
|
" \"Vertretung\": zuweisung[s],\n",
|
||||||
|
" \"Vorbelastung\": f\"{VORBELASTUNG[zuweisung[s]]} Std.\",\n",
|
||||||
|
" } for s in range(len(SLOTS))]\n",
|
||||||
|
" print(pd.DataFrame(plan).to_string(index=False))\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Abnahmepruefung gegen die Anforderung ----------------------------\n",
|
||||||
|
" beanstandungen = pruefe_plan(zuweisung)\n",
|
||||||
|
" if beanstandungen:\n",
|
||||||
|
" print(\"\\nABNAHMEPRUEFUNG FEHLGESCHLAGEN:\")\n",
|
||||||
|
" for beanstandung in beanstandungen:\n",
|
||||||
|
" print(f\" - {beanstandung}\")\n",
|
||||||
|
" return None\n",
|
||||||
|
" print(\"\\nAbnahmepruefung: bestanden (Besetzung, Qualifikation, Hoechstzahl).\")\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Erklaerbarkeit: Strafkosten aufschluesseln -----------------------\n",
|
||||||
|
" kosten_last = sum(VORBELASTUNG[p] * STRAFE_VORBELASTUNG\n",
|
||||||
|
" * sum(loeser.Value(x[p, s]) for s in range(len(SLOTS)))\n",
|
||||||
|
" for p in PERSONAL)\n",
|
||||||
|
" kosten_loecher = sum(loeser.Value(v) for v in loch_variablen) * STRAFE_LOCH\n",
|
||||||
|
" kosten_fairness = loeser.Value(spannweite) * STRAFE_UNFAIRNESS\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\n--- Woraus bestehen die Strafkosten? ---\")\n",
|
||||||
|
" print(f\" Vorbelastung geschont: {kosten_last:5.0f} Punkte\")\n",
|
||||||
|
" print(f\" Freistundenloecher: {kosten_loecher:5.0f} Punkte \"\n",
|
||||||
|
" f\"({sum(loeser.Value(v) for v in loch_variablen)} Loch/Loecher)\")\n",
|
||||||
|
" print(f\" Fairness (Spannweite {loeser.Value(spannweite)}): {kosten_fairness:5.0f} Punkte\")\n",
|
||||||
|
" print(f\" {'Summe':<23} {loeser.ObjectiveValue():5.0f} Punkte\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\n--- Auslastung nach Optimierung ---\")\n",
|
||||||
|
" for p in PERSONAL:\n",
|
||||||
|
" heute = sum(loeser.Value(x[p, s]) for s in range(len(SLOTS)))\n",
|
||||||
|
" print(f\" * {p:<15}: {VORBELASTUNG[p]} vorher + {heute} Vertretung \"\n",
|
||||||
|
" f\"= {VORBELASTUNG[p] + heute} Stunden\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(f\"\\nSuchstatistik: {loeser.NumBranches()} Verzweigungen, \"\n",
|
||||||
|
" f\"{loeser.NumConflicts()} Konflikte, {loeser.WallTime():.3f} s\")\n",
|
||||||
|
" print(\"=\" * 78)\n",
|
||||||
|
" return plan\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"if __name__ == \"__main__\":\n",
|
||||||
|
" plane_vertretung()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Intervallvariablen: Job-Shop-Scheduling\n",
|
||||||
|
"\n",
|
||||||
|
"`JobShop_Intervalle.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# JobShop_Intervalle.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel CP-SAT: Job-Shop-Scheduling mit Intervallvariablen.\n",
|
||||||
|
"Zeigt NewIntervalVar, AddNoOverlap und die Minimierung des Makespan.\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"import collections\n",
|
||||||
|
"\n",
|
||||||
|
"from ortools.sat.python import cp_model\n",
|
||||||
|
"\n",
|
||||||
|
"# (Maschine, Dauer) je Arbeitsgang, in der Reihenfolge des Auftrags\n",
|
||||||
|
"AUFTRAEGE = [\n",
|
||||||
|
" [(0, 3), (1, 2), (2, 2)], # Auftrag 0\n",
|
||||||
|
" [(0, 2), (2, 1), (1, 4)], # Auftrag 1\n",
|
||||||
|
" [(1, 4), (2, 3)], # Auftrag 2\n",
|
||||||
|
"]\n",
|
||||||
|
"MASCHINENNAMEN = [\"Fraese\", \"Dreherei\", \"Lackiererei\"]\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def loese_jobshop():\n",
|
||||||
|
" anzahl_maschinen = 1 + max(m for auftrag in AUFTRAEGE for m, _ in auftrag)\n",
|
||||||
|
" horizont = sum(dauer for auftrag in AUFTRAEGE for _, dauer in auftrag)\n",
|
||||||
|
"\n",
|
||||||
|
" modell = cp_model.CpModel()\n",
|
||||||
|
" Gang = collections.namedtuple(\"Gang\", \"start ende intervall\")\n",
|
||||||
|
" plaene = {}\n",
|
||||||
|
" maschinen_intervalle = collections.defaultdict(list)\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Intervallvariablen anlegen --------------------------------------\n",
|
||||||
|
" for a, auftrag in enumerate(AUFTRAEGE):\n",
|
||||||
|
" for g, (maschine, dauer) in enumerate(auftrag):\n",
|
||||||
|
" start = modell.NewIntVar(0, horizont, f\"start_{a}_{g}\")\n",
|
||||||
|
" ende = modell.NewIntVar(0, horizont, f\"ende_{a}_{g}\")\n",
|
||||||
|
" # Ein Intervall koppelt start + dauer == ende automatisch\n",
|
||||||
|
" intervall = modell.NewIntervalVar(start, dauer, ende, f\"intervall_{a}_{g}\")\n",
|
||||||
|
" plaene[a, g] = Gang(start, ende, intervall)\n",
|
||||||
|
" maschinen_intervalle[maschine].append(intervall)\n",
|
||||||
|
"\n",
|
||||||
|
" # --- H1: eine Maschine bearbeitet nur einen Gang gleichzeitig --------\n",
|
||||||
|
" for maschine in range(anzahl_maschinen):\n",
|
||||||
|
" modell.AddNoOverlap(maschinen_intervalle[maschine])\n",
|
||||||
|
"\n",
|
||||||
|
" # --- H2: Reihenfolge innerhalb eines Auftrags einhalten -------------\n",
|
||||||
|
" for a, auftrag in enumerate(AUFTRAEGE):\n",
|
||||||
|
" for g in range(len(auftrag) - 1):\n",
|
||||||
|
" modell.Add(plaene[a, g + 1].start >= plaene[a, g].ende)\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Ziel: Makespan minimieren ---------------------------------------\n",
|
||||||
|
" makespan = modell.NewIntVar(0, horizont, \"makespan\")\n",
|
||||||
|
" modell.AddMaxEquality(makespan, [plaene[a, len(auftrag) - 1].ende\n",
|
||||||
|
" for a, auftrag in enumerate(AUFTRAEGE)])\n",
|
||||||
|
" modell.Minimize(makespan)\n",
|
||||||
|
"\n",
|
||||||
|
" loeser = cp_model.CpSolver()\n",
|
||||||
|
" loeser.parameters.max_time_in_seconds = 10.0\n",
|
||||||
|
" # Es gibt mehrere Plaene mit demselben kuerzesten Makespan. Welchen CP-SAT\n",
|
||||||
|
" # findet, haengt sonst davon ab, welcher seiner parallelen Suchstraenge\n",
|
||||||
|
" # zuerst fertig wird - dasselbe Programm liefert dann von Lauf zu Lauf\n",
|
||||||
|
" # verschiedene (gleich gute) Plaene. Fuer ein reproduzierbares Buchbeispiel\n",
|
||||||
|
" # fixieren wir beides. Im Produktivbetrieb laesst man die Standardwerte\n",
|
||||||
|
" # stehen: Mehrere Arbeiter sind dort deutlich schneller.\n",
|
||||||
|
" loeser.parameters.num_workers = 1\n",
|
||||||
|
" loeser.parameters.random_seed = 1\n",
|
||||||
|
" status = loeser.Solve(modell)\n",
|
||||||
|
"\n",
|
||||||
|
" if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE):\n",
|
||||||
|
" print(f\"Kein Plan gefunden: {loeser.StatusName(status)}\")\n",
|
||||||
|
" return\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"=\" * 74)\n",
|
||||||
|
" print(\" JOB-SHOP-SCHEDULING MIT INTERVALLVARIABLEN\")\n",
|
||||||
|
" print(\"=\" * 74)\n",
|
||||||
|
" print(f\"Status: {loeser.StatusName(status)} | \"\n",
|
||||||
|
" f\"Kuerzeste Gesamtdauer (Makespan): {loeser.Value(makespan)} Zeiteinheiten\\n\")\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Gantt-Diagramm als Text ------------------------------------------\n",
|
||||||
|
" dauer_gesamt = loeser.Value(makespan)\n",
|
||||||
|
" print(f\"{'Maschine':<14}|\" + \"\".join(f\"{t:<3}\" for t in range(dauer_gesamt)))\n",
|
||||||
|
" print(\"-\" * (15 + 3 * dauer_gesamt))\n",
|
||||||
|
" for maschine in range(anzahl_maschinen):\n",
|
||||||
|
" zeile = [\" . \"] * dauer_gesamt\n",
|
||||||
|
" for a, auftrag in enumerate(AUFTRAEGE):\n",
|
||||||
|
" for g, (m, dauer) in enumerate(auftrag):\n",
|
||||||
|
" if m == maschine:\n",
|
||||||
|
" beginn = loeser.Value(plaene[a, g].start)\n",
|
||||||
|
" for t in range(beginn, beginn + dauer):\n",
|
||||||
|
" zeile[t] = f\" A{a}\"\n",
|
||||||
|
" print(f\"{MASCHINENNAMEN[maschine]:<14}|\" + \"\".join(zeile))\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\n--- Detailplan ---\")\n",
|
||||||
|
" for a, auftrag in enumerate(AUFTRAEGE):\n",
|
||||||
|
" teile = []\n",
|
||||||
|
" for g, (maschine, dauer) in enumerate(auftrag):\n",
|
||||||
|
" beginn = loeser.Value(plaene[a, g].start)\n",
|
||||||
|
" teile.append(f\"{MASCHINENNAMEN[maschine]} {beginn}-{beginn + dauer}\")\n",
|
||||||
|
" print(f\" Auftrag {a}: \" + \" -> \".join(teile))\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Untere Schranke zum Vergleich ------------------------------------\n",
|
||||||
|
" laengster_auftrag = max(sum(d for _, d in a) for a in AUFTRAEGE)\n",
|
||||||
|
" hoechste_maschinenlast = max(\n",
|
||||||
|
" sum(d for auftrag in AUFTRAEGE for m, d in auftrag if m == maschine)\n",
|
||||||
|
" for maschine in range(anzahl_maschinen))\n",
|
||||||
|
" schranke = max(laengster_auftrag, hoechste_maschinenlast)\n",
|
||||||
|
" print(f\"\\nUntere Schranke (laengster Auftrag / hoechste Maschinenlast): {schranke}\")\n",
|
||||||
|
" print(f\"Erreichter Makespan: {loeser.Value(makespan)}\"\n",
|
||||||
|
" f\"{' -> beweisbar bestmoeglich' if loeser.Value(makespan) == schranke else ''}\")\n",
|
||||||
|
" print(\"=\" * 74)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"if __name__ == \"__main__\":\n",
|
||||||
|
" loese_jobshop()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Die fünf Antworten von CP-SAT\n",
|
||||||
|
"\n",
|
||||||
|
"`CP_SAT_Statusfaelle.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# CP_SAT_Statusfaelle.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel CP-SAT: Alle fuenf Antworten von CP-SAT - und was jede bedeutet.\n",
|
||||||
|
"\n",
|
||||||
|
"Der haeufigste Fehler beim Einsatz von CP-SAT im Betrieb ist eine Zeile wie\n",
|
||||||
|
"\n",
|
||||||
|
" if status == cp_model.OPTIMAL:\n",
|
||||||
|
" ...verwerte die Loesung...\n",
|
||||||
|
"\n",
|
||||||
|
"Sie wirft in drei von fuenf Faellen etwas weg, das man haette gebrauchen\n",
|
||||||
|
"koennen, und verschweigt in einem weiteren Fall einen Modellierungsfehler.\n",
|
||||||
|
"\n",
|
||||||
|
"Dieses Programm erzeugt jeden Statusfall absichtlich und zeigt die passende\n",
|
||||||
|
"Reaktion:\n",
|
||||||
|
"\n",
|
||||||
|
" OPTIMAL beweisbar bestmoeglich -> ausfuehren\n",
|
||||||
|
" FEASIBLE zulaessig, Zeit war um -> Gap pruefen, dann entscheiden\n",
|
||||||
|
" INFEASIBLE kein Plan existiert -> harte Regeln lockern\n",
|
||||||
|
" MODEL_INVALID das Modell ist fehlerhaft -> Programmierfehler beheben\n",
|
||||||
|
" UNKNOWN nichts gefunden, Zeit war um -> mehr Zeit oder Heuristik\n",
|
||||||
|
"\n",
|
||||||
|
"Benoetigt: numpy, ortools\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"from __future__ import annotations\n",
|
||||||
|
"\n",
|
||||||
|
"from dataclasses import dataclass\n",
|
||||||
|
"\n",
|
||||||
|
"import numpy as np\n",
|
||||||
|
"from ortools.sat.python import cp_model\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"@dataclass\n",
|
||||||
|
"class Auswertung:\n",
|
||||||
|
" \"\"\"Was nach einem CP-SAT-Lauf tatsaechlich feststeht.\"\"\"\n",
|
||||||
|
" status: str\n",
|
||||||
|
" hat_loesung: bool\n",
|
||||||
|
" beweisbar_optimal: bool\n",
|
||||||
|
" zielwert: float | None\n",
|
||||||
|
" schranke: float | None\n",
|
||||||
|
" gap: float | None\n",
|
||||||
|
" empfehlung: str\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def werte_aus(loeser: cp_model.CpSolver, status: int,\n",
|
||||||
|
" mit_zielfunktion: bool) -> Auswertung:\n",
|
||||||
|
" \"\"\"Uebersetzt einen CP-SAT-Status in eine Handlungsempfehlung.\n",
|
||||||
|
"\n",
|
||||||
|
" Genau diese Funktion fehlt in den meisten Projekten. Sie ist kurz, aber\n",
|
||||||
|
" sie ist der Unterschied zwischen einem Prototyp und einem System, das\n",
|
||||||
|
" nachts ohne Aufsicht laeuft.\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" name = loeser.StatusName(status)\n",
|
||||||
|
" hat_loesung = status in (cp_model.OPTIMAL, cp_model.FEASIBLE)\n",
|
||||||
|
"\n",
|
||||||
|
" zielwert = schranke = gap = None\n",
|
||||||
|
" if hat_loesung and mit_zielfunktion:\n",
|
||||||
|
" zielwert = loeser.ObjectiveValue()\n",
|
||||||
|
" schranke = loeser.BestObjectiveBound()\n",
|
||||||
|
" nenner = max(abs(zielwert), 1e-9)\n",
|
||||||
|
" gap = abs(zielwert - schranke) / nenner\n",
|
||||||
|
"\n",
|
||||||
|
" if status == cp_model.OPTIMAL:\n",
|
||||||
|
" empfehlung = \"Ausfuehren. Besser geht es nachweislich nicht.\"\n",
|
||||||
|
" elif status == cp_model.FEASIBLE:\n",
|
||||||
|
" empfehlung = (f\"Brauchbar. Der Plan ist hoechstens {gap * 100:.2f} % \"\n",
|
||||||
|
" f\"schlechter als das theoretisch Bestmoegliche.\"\n",
|
||||||
|
" if gap is not None else\n",
|
||||||
|
" \"Brauchbar, aber nicht als optimal bewiesen.\")\n",
|
||||||
|
" elif status == cp_model.INFEASIBLE:\n",
|
||||||
|
" empfehlung = (\"KEIN Plan existiert. Das ist eine Aussage ueber Ihre \"\n",
|
||||||
|
" \"harten Regeln, nicht ueber die Rechenzeit: Auch \"\n",
|
||||||
|
" \"unendlich viel Zeit wuerde nichts aendern. Regeln \"\n",
|
||||||
|
" \"lockern oder weich machen (Anhang C).\")\n",
|
||||||
|
" elif status == cp_model.MODEL_INVALID:\n",
|
||||||
|
" empfehlung = (\"Das MODELL ist fehlerhaft, nicht das Problem. \"\n",
|
||||||
|
" \"Typisch: eine Variable mit leerem Wertebereich. \"\n",
|
||||||
|
" \"model.Validate() nennt die Ursache.\")\n",
|
||||||
|
" else: # UNKNOWN\n",
|
||||||
|
" empfehlung = (\"Nichts gefunden - aber auch nicht widerlegt. Mehr Zeit \"\n",
|
||||||
|
" \"geben, das Modell vereinfachen oder mit einer \"\n",
|
||||||
|
" \"Heuristik vorbelegen (AddHint).\")\n",
|
||||||
|
"\n",
|
||||||
|
" return Auswertung(\n",
|
||||||
|
" status=name,\n",
|
||||||
|
" hat_loesung=hat_loesung,\n",
|
||||||
|
" beweisbar_optimal=status == cp_model.OPTIMAL,\n",
|
||||||
|
" zielwert=zielwert, schranke=schranke, gap=gap,\n",
|
||||||
|
" empfehlung=empfehlung,\n",
|
||||||
|
" )\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def loese(baue, zeitlimit: float, mit_zielfunktion: bool) -> Auswertung:\n",
|
||||||
|
" modell = cp_model.CpModel()\n",
|
||||||
|
" baue(modell)\n",
|
||||||
|
" loeser = cp_model.CpSolver()\n",
|
||||||
|
" loeser.parameters.max_time_in_seconds = zeitlimit\n",
|
||||||
|
" # Fuer reproduzierbare Buchausgaben; im Betrieb weglassen.\n",
|
||||||
|
" loeser.parameters.num_workers = 1\n",
|
||||||
|
" loeser.parameters.random_seed = 1\n",
|
||||||
|
" status = loeser.Solve(modell)\n",
|
||||||
|
" return werte_aus(loeser, status, mit_zielfunktion)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"# --- Die fuenf Faelle --------------------------------------------------------\n",
|
||||||
|
"\n",
|
||||||
|
"def dienstplan_loesbar(modell: cp_model.CpModel) -> None:\n",
|
||||||
|
" \"\"\"Vier Personen, fuenf Dienste, hoechstens zwei je Person - loesbar.\"\"\"\n",
|
||||||
|
" personen, dienste = 4, 5\n",
|
||||||
|
" x = [[modell.NewBoolVar(f\"x_{i}_{j}\") for j in range(dienste)]\n",
|
||||||
|
" for i in range(personen)]\n",
|
||||||
|
" for j in range(dienste):\n",
|
||||||
|
" modell.AddExactlyOne(x[i][j] for i in range(personen))\n",
|
||||||
|
" for i in range(personen):\n",
|
||||||
|
" modell.Add(sum(x[i]) <= 2)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def dienstplan_unloesbar(modell: cp_model.CpModel) -> None:\n",
|
||||||
|
" \"\"\"Dieselben fuenf Dienste, aber hoechstens EIN Dienst je Person.\n",
|
||||||
|
"\n",
|
||||||
|
" Vier Personen koennen zusammen hoechstens vier Dienste uebernehmen - fuer\n",
|
||||||
|
" fuenf Dienste reicht das nicht. Kein Solver der Welt findet hier etwas.\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" personen, dienste = 4, 5\n",
|
||||||
|
" x = [[modell.NewBoolVar(f\"x_{i}_{j}\") for j in range(dienste)]\n",
|
||||||
|
" for i in range(personen)]\n",
|
||||||
|
" for j in range(dienste):\n",
|
||||||
|
" modell.AddExactlyOne(x[i][j] for i in range(personen))\n",
|
||||||
|
" for i in range(personen):\n",
|
||||||
|
" modell.Add(sum(x[i]) <= 1)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def modell_fehlerhaft(modell: cp_model.CpModel) -> None:\n",
|
||||||
|
" \"\"\"Eine Variable mit leerem Wertebereich: untere Schranke > obere.\n",
|
||||||
|
"\n",
|
||||||
|
" Im Alltag entsteht das durch einen Rechenfehler in den Grenzen, etwa\n",
|
||||||
|
" NewIntVar(mindestbesetzung, kapazitaet, ...) bei falsch sortierten Daten.\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" kaputt = modell.NewIntVar(5, 2, \"leerer_bereich\")\n",
|
||||||
|
" modell.Add(kaputt >= 0)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def lastverteilung(modell: cp_model.CpModel) -> None:\n",
|
||||||
|
" \"\"\"120 Auftraege auf 11 Maschinen - gross genug, dass der Optimalitaets-\n",
|
||||||
|
" beweis laenger dauert als das Finden einer sehr guten Loesung.\"\"\"\n",
|
||||||
|
" rng = np.random.default_rng(3)\n",
|
||||||
|
" dauer = rng.integers(100, 9000, 120)\n",
|
||||||
|
" n, maschinen = len(dauer), 11\n",
|
||||||
|
" obergrenze = int(dauer.sum())\n",
|
||||||
|
"\n",
|
||||||
|
" x = [[modell.NewBoolVar(f\"x_{i}_{k}\") for k in range(maschinen)]\n",
|
||||||
|
" for i in range(n)]\n",
|
||||||
|
" for i in range(n):\n",
|
||||||
|
" modell.AddExactlyOne(x[i])\n",
|
||||||
|
" belegung = [modell.NewIntVar(0, obergrenze, f\"l_{k}\") for k in range(maschinen)]\n",
|
||||||
|
" for k in range(maschinen):\n",
|
||||||
|
" modell.Add(belegung[k] == sum(int(dauer[i]) * x[i][k] for i in range(n)))\n",
|
||||||
|
" makespan = modell.NewIntVar(0, obergrenze, \"makespan\")\n",
|
||||||
|
" modell.AddMaxEquality(makespan, belegung)\n",
|
||||||
|
" modell.Minimize(makespan)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def zahlpartition(modell: cp_model.CpModel) -> None:\n",
|
||||||
|
" \"\"\"46 grosse Zahlen exakt in zwei gleich schwere Haelften teilen.\n",
|
||||||
|
"\n",
|
||||||
|
" Ein beruehmt schweres Problem: Es gibt keine Zielfunktion, entweder es\n",
|
||||||
|
" geht auf oder nicht - und beides zu entscheiden dauert lange.\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" rng = np.random.default_rng(1)\n",
|
||||||
|
" gewichte = rng.integers(10**6, 2 * 10**6, 46)\n",
|
||||||
|
" x = [modell.NewBoolVar(f\"x_{i}\") for i in range(len(gewichte))]\n",
|
||||||
|
" modell.Add(sum(int(gewichte[i]) * x[i] for i in range(len(gewichte)))\n",
|
||||||
|
" == int(gewichte.sum() // 2))\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def zeige(titel: str, a: Auswertung) -> None:\n",
|
||||||
|
" print(f\"\\n{titel}\")\n",
|
||||||
|
" print(f\" Status {a.status}\")\n",
|
||||||
|
" print(f\" Loesung vorhanden {'ja' if a.hat_loesung else 'nein'}\")\n",
|
||||||
|
" if a.zielwert is not None:\n",
|
||||||
|
" print(f\" Zielwert / Schranke {a.zielwert:,.0f} / {a.schranke:,.0f}\"\n",
|
||||||
|
" f\" (Gap {a.gap * 100:.3f} %)\")\n",
|
||||||
|
" print(f\" -> {a.empfehlung}\")\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"if __name__ == \"__main__\":\n",
|
||||||
|
" print(\"=\" * 78)\n",
|
||||||
|
" print(\" DIE FUENF ANTWORTEN VON CP-SAT\")\n",
|
||||||
|
" print(\"=\" * 78)\n",
|
||||||
|
"\n",
|
||||||
|
" zeige(\"[1] Dienstplan, hoechstens 2 Dienste je Person\",\n",
|
||||||
|
" loese(dienstplan_loesbar, 10.0, mit_zielfunktion=False))\n",
|
||||||
|
"\n",
|
||||||
|
" zeige(\"[2] Lastverteilung 120 Auftraege / 11 Maschinen, 0,5 s Limit\",\n",
|
||||||
|
" loese(lastverteilung, 0.5, mit_zielfunktion=True))\n",
|
||||||
|
"\n",
|
||||||
|
" zeige(\"[3] Derselbe Fall mit 10 s Limit\",\n",
|
||||||
|
" loese(lastverteilung, 10.0, mit_zielfunktion=True))\n",
|
||||||
|
"\n",
|
||||||
|
" zeige(\"[4] Dienstplan, hoechstens 1 Dienst je Person\",\n",
|
||||||
|
" loese(dienstplan_unloesbar, 10.0, mit_zielfunktion=False))\n",
|
||||||
|
"\n",
|
||||||
|
" zeige(\"[5] Variable mit leerem Wertebereich\",\n",
|
||||||
|
" loese(modell_fehlerhaft, 10.0, mit_zielfunktion=False))\n",
|
||||||
|
"\n",
|
||||||
|
" zeige(\"[6] Zahlpartition mit 46 grossen Zahlen, 2 s Limit\",\n",
|
||||||
|
" loese(zahlpartition, 2.0, mit_zielfunktion=False))\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\n\" + \"=\" * 78)\n",
|
||||||
|
" print(\" DIE DREI, DIE MAN NICHT VERWECHSELN DARF\")\n",
|
||||||
|
" print(\"=\" * 78)\n",
|
||||||
|
" print(\"INFEASIBLE Es gibt keine Loesung. Eine Aussage ueber Ihr MODELL.\")\n",
|
||||||
|
" print(\" Mehr Rechenzeit aendert daran nichts.\")\n",
|
||||||
|
" print(\"UNKNOWN Es wurde keine gefunden. Eine Aussage ueber die ZEIT.\")\n",
|
||||||
|
" print(\" Ob es eine gibt, ist offen.\")\n",
|
||||||
|
" print(\"MODEL_INVALID Das Modell ist gar kein gueltiges Modell. Eine\")\n",
|
||||||
|
" print(\" Aussage ueber Ihren CODE - immer ein Programmierfehler.\")\n",
|
||||||
|
" print()\n",
|
||||||
|
" print(\"Wer diese drei in einem 'else: return None' zusammenfasst, verliert\")\n",
|
||||||
|
" print(\"genau die Information, die zur Fehlersuche noetig waere.\")\n",
|
||||||
|
" print(\"=\" * 78)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Finde den Denkfehler\n",
|
||||||
|
"\n",
|
||||||
|
"`Strafgewichte.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# Strafgewichte.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel CP-SAT: Warum ein Strafgewicht keine Wichtigkeit ausdrueckt, sondern\n",
|
||||||
|
"einen Wechselkurs.\n",
|
||||||
|
"\n",
|
||||||
|
"Weiche Regeln kommen als Strafterm in die Zielfunktion. Die Frage, die dabei\n",
|
||||||
|
"regelmaessig falsch beantwortet wird, lautet: \"Wie gross muss die Strafe sein?\"\n",
|
||||||
|
"Die uebliche Antwort - \"je wichtiger, desto groesser\" - klingt vernuenftig und\n",
|
||||||
|
"fuehrt zuverlaessig zu Plaenen, die niemand unterschreiben will.\n",
|
||||||
|
"\n",
|
||||||
|
"Der Grund: Zwei Strafgewichte legen nicht Wichtigkeiten fest, sondern einen\n",
|
||||||
|
"UMRECHNUNGSKURS. Steht die Serienregel bei 1 und der Wunschfrei-Tag bei 10,\n",
|
||||||
|
"dann hat man dem Solver woertlich gesagt: \"Zehn Verstoesse gegen die\n",
|
||||||
|
"Serienregel sind so schlimm wie ein abgelehnter Wunsch.\" Er nimmt das\n",
|
||||||
|
"ernst - und handelt danach.\n",
|
||||||
|
"\n",
|
||||||
|
"Szenario: Schichtplan, 6 Personen, 14 Tage, 4 Besetzungen pro Tag.\n",
|
||||||
|
" HART jeder Tag genau 4 Personen; hoechstens 11 Dienste je Person\n",
|
||||||
|
" WEICH keine drei Dienste in Folge (Betriebsvereinbarung)\n",
|
||||||
|
" WEICH Wunschfrei-Tage einhalten (26 Antraege)\n",
|
||||||
|
"\n",
|
||||||
|
"Benoetigt: numpy, ortools\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"from __future__ import annotations\n",
|
||||||
|
"\n",
|
||||||
|
"import numpy as np\n",
|
||||||
|
"from ortools.sat.python import cp_model\n",
|
||||||
|
"\n",
|
||||||
|
"PERSONEN, TAGE, PRO_TAG = 6, 14, 4\n",
|
||||||
|
"MAX_DIENSTE = 11\n",
|
||||||
|
"STRAFE_WUNSCH = 10 # bleibt fest - variiert wird die Serienstrafe\n",
|
||||||
|
"\n",
|
||||||
|
"RNG = np.random.default_rng(2)\n",
|
||||||
|
"WUNSCHFREI = sorted({(p, t) for p in range(PERSONEN) for t in range(TAGE)\n",
|
||||||
|
" if RNG.random() < 0.35})\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def plane(strafe_serie: int, zeitlimit: float = 25.0) -> dict:\n",
|
||||||
|
" \"\"\"Baut den Schichtplan mit dem angegebenen Gewicht fuer die Serienregel.\"\"\"\n",
|
||||||
|
" modell = cp_model.CpModel()\n",
|
||||||
|
" x = {(p, t): modell.NewBoolVar(f\"x_{p}_{t}\")\n",
|
||||||
|
" for p in range(PERSONEN) for t in range(TAGE)}\n",
|
||||||
|
"\n",
|
||||||
|
" # --- harte Regeln --------------------------------------------------\n",
|
||||||
|
" for t in range(TAGE):\n",
|
||||||
|
" modell.Add(sum(x[p, t] for p in range(PERSONEN)) == PRO_TAG)\n",
|
||||||
|
" for p in range(PERSONEN):\n",
|
||||||
|
" modell.Add(sum(x[p, t] for t in range(TAGE)) <= MAX_DIENSTE)\n",
|
||||||
|
"\n",
|
||||||
|
" # --- weiche Regel 1: keine drei Dienste in Folge --------------------\n",
|
||||||
|
" # serie[p, t] = 1 <=> Person p arbeitet an t, t+1 UND t+2.\n",
|
||||||
|
" # Die Ungleichung erzwingt das nur in eine Richtung; das genuegt, weil\n",
|
||||||
|
" # der Solver serie minimiert - er wuerde die Variable nie freiwillig\n",
|
||||||
|
" # auf 1 setzen.\n",
|
||||||
|
" strafterme = []\n",
|
||||||
|
" serien = []\n",
|
||||||
|
" for p in range(PERSONEN):\n",
|
||||||
|
" for t in range(TAGE - 2):\n",
|
||||||
|
" serie = modell.NewBoolVar(f\"serie_{p}_{t}\")\n",
|
||||||
|
" modell.Add(x[p, t] + x[p, t + 1] + x[p, t + 2] <= 2 + serie)\n",
|
||||||
|
" serien.append(serie)\n",
|
||||||
|
" strafterme.append(serie * strafe_serie)\n",
|
||||||
|
"\n",
|
||||||
|
" # --- weiche Regel 2: Wunschfrei ------------------------------------\n",
|
||||||
|
" wunschverstoesse = []\n",
|
||||||
|
" for p, t in WUNSCHFREI:\n",
|
||||||
|
" wunschverstoesse.append(x[p, t])\n",
|
||||||
|
" strafterme.append(x[p, t] * STRAFE_WUNSCH)\n",
|
||||||
|
"\n",
|
||||||
|
" modell.Minimize(sum(strafterme))\n",
|
||||||
|
"\n",
|
||||||
|
" loeser = cp_model.CpSolver()\n",
|
||||||
|
" loeser.parameters.max_time_in_seconds = zeitlimit\n",
|
||||||
|
" loeser.parameters.num_workers = 1 # reproduzierbare Buchausgabe\n",
|
||||||
|
" loeser.parameters.random_seed = 1\n",
|
||||||
|
" status = loeser.Solve(modell)\n",
|
||||||
|
" if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE):\n",
|
||||||
|
" raise RuntimeError(f\"Kein Plan: {loeser.StatusName(status)}\")\n",
|
||||||
|
"\n",
|
||||||
|
" return {\n",
|
||||||
|
" \"status\": loeser.StatusName(status),\n",
|
||||||
|
" \"zielwert\": loeser.ObjectiveValue(),\n",
|
||||||
|
" \"serien\": sum(loeser.Value(v) for v in serien),\n",
|
||||||
|
" \"wunsch_verletzt\": sum(loeser.Value(v) for v in wunschverstoesse),\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"if __name__ == \"__main__\":\n",
|
||||||
|
" print(\"=\" * 78)\n",
|
||||||
|
" print(\" STRAFGEWICHTE SIND WECHSELKURSE, KEINE WICHTIGKEITEN\")\n",
|
||||||
|
" print(\"=\" * 78)\n",
|
||||||
|
" print(f\"{PERSONEN} Personen, {TAGE} Tage, {PRO_TAG} Besetzungen pro Tag \"\n",
|
||||||
|
" f\"= {TAGE * PRO_TAG} Dienste\")\n",
|
||||||
|
" print(f\"Das sind {TAGE * PRO_TAG / PERSONEN:.1f} Dienste je Person - der Plan ist eng.\")\n",
|
||||||
|
" print(f\"{len(WUNSCHFREI)} Wunschfrei-Antraege, Strafe je abgelehntem Wunsch: \"\n",
|
||||||
|
" f\"{STRAFE_WUNSCH}\\n\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(f\"{'Strafe je 3er-Serie':>20} {'3er-Serien':>12} {'Wunsch verletzt':>17} \"\n",
|
||||||
|
" f\"{'Zielwert':>10}\")\n",
|
||||||
|
" print(\"-\" * 78)\n",
|
||||||
|
" ergebnisse = {}\n",
|
||||||
|
" for strafe in (1, 2, 5, 10, 30, 100):\n",
|
||||||
|
" e = plane(strafe)\n",
|
||||||
|
" ergebnisse[strafe] = e\n",
|
||||||
|
" print(f\"{strafe:>20} {e['serien']:>12} {e['wunsch_verletzt']:>17} \"\n",
|
||||||
|
" f\"{e['zielwert']:>10.0f}\")\n",
|
||||||
|
" print(\"-\" * 78)\n",
|
||||||
|
"\n",
|
||||||
|
" niedrig, hoch = ergebnisse[1], ergebnisse[30]\n",
|
||||||
|
" print(f\"\\nBei Strafe 1 baut der Solver {niedrig['serien']} Drei-Tage-Serien ein.\")\n",
|
||||||
|
" print(\"Die Betriebsvereinbarung steht im Modell - und wird trotzdem\")\n",
|
||||||
|
" print(\"systematisch verletzt. Das ist kein Fehler des Solvers: Bei einem\")\n",
|
||||||
|
" print(f\"Gewicht von 1 gegen {STRAFE_WUNSCH} lohnt sich jede Serie, solange sie\")\n",
|
||||||
|
" print(\"auch nur einen Zehntel-Wunsch rettet.\")\n",
|
||||||
|
" print(f\"\\nBei Strafe 30 sind es {hoch['serien']} Serien - dafuer werden\")\n",
|
||||||
|
" print(f\"{hoch['wunsch_verletzt']} statt {niedrig['wunsch_verletzt']} Wuensche abgelehnt.\")\n",
|
||||||
|
" print(\"Beide Plaene sind optimal. Sie beantworten nur verschiedene Fragen.\")\n",
|
||||||
|
" print()\n",
|
||||||
|
" print(\"Die Frage lautet also nie 'wie wichtig ist mir diese Regel?', sondern:\")\n",
|
||||||
|
" print(\" 'Wie viele abgelehnte Wuensche bin ich bereit zu akzeptieren,\")\n",
|
||||||
|
" print(\" um eine Drei-Tage-Serie zu vermeiden?'\")\n",
|
||||||
|
" print(\"Wer darauf keine Zahl nennen kann, hat das Problem noch nicht\")\n",
|
||||||
|
" print(\"verstanden - und sollte diese Tabelle dem Auftraggeber vorlegen,\")\n",
|
||||||
|
" print(\"statt ein Gewicht zu raten.\")\n",
|
||||||
|
" print()\n",
|
||||||
|
" print(\"Und die wichtigste Konsequenz: Eine Regel, die NIE gebrochen werden\")\n",
|
||||||
|
" print(\"darf, gehoert nicht in die Zielfunktion, sondern unter die harten\")\n",
|
||||||
|
" print(\"Nebenbedingungen. Alles, was einen Preis hat, wird irgendwann gekauft.\")\n",
|
||||||
|
" print(\"=\" * 78)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"name": "python",
|
||||||
|
"version": "3.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
365
Notebooks_04/cvar.ipynb
Normal file
365
Notebooks_04/cvar.ipynb
Normal file
|
|
@ -0,0 +1,365 @@
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"# Kapitel 20: Tail-Risiko, CVaR und Transaktionskosten\n",
|
||||||
|
"\n",
|
||||||
|
"Begleitnotebook zu *Optimierte Entscheidungsfindung mit Python*. Die Codezellen sind identisch mit den im Buch abgedruckten Programmen.\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"# Einmalig ausfuehren: installiert alle im Buch verwendeten Pakete.\n",
|
||||||
|
"# Lokal in einer virtuellen Umgebung genauso gueltig wie in Google Colab.\n",
|
||||||
|
"%pip install --quiet ortools highspy cvxpy scipy numpy pandas polars \\\n",
|
||||||
|
" scikit-learn matplotlib plotly pyomo linopy pymoo pydantic openpyxl"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Die zwei Schwächen des Markowitz-Modells\n",
|
||||||
|
"\n",
|
||||||
|
"`VaR_CVaR_Demo.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# VaR_CVaR_Demo.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel CVaR: Fat Tails, VaR und CVaR anschaulich.\n",
|
||||||
|
"\n",
|
||||||
|
"Teil 1: Wie oft treten \"unmoegliche\" Tage wirklich auf?\n",
|
||||||
|
"Teil 2: Warum ist der VaR nicht subadditiv - ein Gegenbeispiel zum Nachrechnen.\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"import numpy as np\n",
|
||||||
|
"from scipy import stats\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def var_quantil(verluste: np.ndarray, alpha: float = 0.95) -> float:\n",
|
||||||
|
" \"\"\"VaR = Quantil der Verlustverteilung (Verluste positiv, Gewinne negativ).\"\"\"\n",
|
||||||
|
" return float(np.quantile(verluste, alpha))\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def cvar_rockafellar(verluste: np.ndarray, alpha: float = 0.95) -> float:\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" CVaR ueber die Rockafellar-Uryasev-Formel:\n",
|
||||||
|
" CVaR = min_gamma { gamma + 1/(1-alpha) * E[max(Verlust - gamma, 0)] }\n",
|
||||||
|
"\n",
|
||||||
|
" WICHTIG: Der naheliegende Weg \"Mittelwert aller Werte >= VaR\" ist FALSCH,\n",
|
||||||
|
" sobald die Verteilung Atome hat (z. B. genau zwei moegliche Verluste).\n",
|
||||||
|
" Dann liegt der VaR selbst auf einem Atom, und der Vergleich '>=' erfasst\n",
|
||||||
|
" zu viel Wahrscheinlichkeitsmasse. Die Formel unten behandelt das korrekt -\n",
|
||||||
|
" und ist zugleich genau der Ausdruck, den wir im Abschnitt 'Value at Risk\n",
|
||||||
|
" und Conditional Value at Risk' optimieren.\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" kandidaten = np.unique(verluste) # Optimum liegt immer auf einem Datenpunkt\n",
|
||||||
|
" return float(min(g + np.mean(np.maximum(verluste - g, 0.0)) / (1.0 - alpha)\n",
|
||||||
|
" for g in kandidaten))\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"if __name__ == \"__main__\":\n",
|
||||||
|
" rng = np.random.default_rng(2026)\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Teil 1: Fat Tails (analytisch, nicht simuliert) ------------------\n",
|
||||||
|
" print(\"=\" * 88)\n",
|
||||||
|
" print(\" TEIL 1: WIE OFT TRITT DAS 'UNMOEGLICHE' EIN?\")\n",
|
||||||
|
" print(\"=\" * 88)\n",
|
||||||
|
" print(\"Vergleich: Normalverteilung gegen t-Verteilung mit 3 Freiheitsgraden\")\n",
|
||||||
|
" print(\"(beide auf Standardabweichung 1 normiert).\\n\")\n",
|
||||||
|
"\n",
|
||||||
|
" t_verteilung = stats.t(df=3)\n",
|
||||||
|
" skalierung = t_verteilung.std() # auf Varianz 1 bringen\n",
|
||||||
|
"\n",
|
||||||
|
" print(f\"{'Ereignis':<22} {'Normal':>14} {'t (df=3)':>14} {'Faktor':>11} \"\n",
|
||||||
|
" f\"{'Normal: 1 Tag in':>18}\")\n",
|
||||||
|
" print(\"-\" * 88)\n",
|
||||||
|
" for k in [3, 4, 5, 6]:\n",
|
||||||
|
" p_normal = 2 * stats.norm.sf(k) # beidseitig\n",
|
||||||
|
" p_t = 2 * t_verteilung.sf(k * skalierung)\n",
|
||||||
|
" print(f\"Abweichung > {k} Sigma {p_normal*100:>13.6f} % {p_t*100:>13.6f} % \"\n",
|
||||||
|
" f\"{p_t/p_normal:>10.1f}x {1/p_normal/252:>15,.0f} Jahre\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\nDeutung: Ein 5-Sigma-Tag ist unter Normalverteilung ein Ereignis von\")\n",
|
||||||
|
" print(\"etwa einmal in 6.900 Jahren. Reale Aktienmaerkte liefern solche Tage\")\n",
|
||||||
|
" print(\"mehrfach pro Jahrzehnt. Wer allein mit Varianz steuert, plant fuer\")\n",
|
||||||
|
" print(\"eine Welt, in der Crashs praktisch nicht vorkommen.\")\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Teil 2: VaR ist nicht subadditiv ---------------------------------\n",
|
||||||
|
" print(\"\\n\" + \"=\" * 88)\n",
|
||||||
|
" print(\" TEIL 2: WARUM DER VaR KEIN KOHAERENTES RISIKOMASS IST\")\n",
|
||||||
|
" print(\"=\" * 88)\n",
|
||||||
|
" print(\"Zwei unabhaengige Anleihen, je 100 EUR Nominal.\")\n",
|
||||||
|
" print(\"Jede faellt mit 4 % Wahrscheinlichkeit aus (Verlust 100),\")\n",
|
||||||
|
" print(\"sonst zahlt sie 2 EUR Kupon (Verlust -2).\\n\")\n",
|
||||||
|
"\n",
|
||||||
|
" ziehungen = 2_000_000\n",
|
||||||
|
" verlust_a = np.where(rng.random(ziehungen) < 0.04, 100.0, -2.0)\n",
|
||||||
|
" verlust_b = np.where(rng.random(ziehungen) < 0.04, 100.0, -2.0)\n",
|
||||||
|
" verlust_ab = verlust_a + verlust_b\n",
|
||||||
|
"\n",
|
||||||
|
" print(f\"{'':<28} {'VaR 95%':>12} {'CVaR 95%':>12}\")\n",
|
||||||
|
" print(\"-\" * 88)\n",
|
||||||
|
" werte = {}\n",
|
||||||
|
" for name, v in [(\"Anleihe A allein\", verlust_a),\n",
|
||||||
|
" (\"Anleihe B allein\", verlust_b),\n",
|
||||||
|
" (\"Portfolio A+B\", verlust_ab)]:\n",
|
||||||
|
" werte[name] = (var_quantil(v), cvar_rockafellar(v))\n",
|
||||||
|
" print(f\"{name:<28} {werte[name][0]:>12.2f} {werte[name][1]:>12.2f}\")\n",
|
||||||
|
"\n",
|
||||||
|
" var_summe = werte[\"Anleihe A allein\"][0] + werte[\"Anleihe B allein\"][0]\n",
|
||||||
|
" cvar_summe = werte[\"Anleihe A allein\"][1] + werte[\"Anleihe B allein\"][1]\n",
|
||||||
|
" print(f\"{'Summe der Einzelwerte':<28} {var_summe:>12.2f} {cvar_summe:>12.2f}\")\n",
|
||||||
|
"\n",
|
||||||
|
" var_port, cvar_port = werte[\"Portfolio A+B\"]\n",
|
||||||
|
" print(\"-\" * 88)\n",
|
||||||
|
" print(f\"VaR: Portfolio {var_port:7.2f} vs. Summe {var_summe:7.2f} -> \"\n",
|
||||||
|
" f\"{'VERLETZT die Subadditivitaet!' if var_port > var_summe else 'subadditiv'}\")\n",
|
||||||
|
" print(f\"CVaR: Portfolio {cvar_port:7.2f} vs. Summe {cvar_summe:7.2f} -> \"\n",
|
||||||
|
" f\"{'subadditiv (kohaerent)' if cvar_port <= cvar_summe + 1e-6 else 'verletzt'}\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\nDeutung: Einzeln betrachtet meldet der VaR fuer jede Anleihe einen\")\n",
|
||||||
|
" print(\"GEWINN von 2 EUR - denn mit 96 % Wahrscheinlichkeit passiert nichts,\")\n",
|
||||||
|
" print(\"und 4 % liegen unterhalb der 5-%-Schwelle. Im Portfolio steigt die\")\n",
|
||||||
|
" print(\"Wahrscheinlichkeit mindestens eines Ausfalls auf 7,8 % und damit UEBER\")\n",
|
||||||
|
" print(\"die Schwelle - der VaR springt auf 98. Er behauptet also, Streuung\")\n",
|
||||||
|
" print(\"habe das Risiko erhoeht. Das ist oekonomisch unsinnig und der Grund,\")\n",
|
||||||
|
" print(\"warum die Bankenaufsicht mit Basel III auf den Expected Shortfall\")\n",
|
||||||
|
" print(\"umgestellt hat.\")\n",
|
||||||
|
" print(\"=\" * 88)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Implementierung: CVaR-Portfolio mit Reibung\n",
|
||||||
|
"\n",
|
||||||
|
"`CVaR_Portfolio.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# CVaR_Portfolio.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel CVaR: CVaR-Optimierung mit L1-Transaktionskosten via CVXPY.\n",
|
||||||
|
"\n",
|
||||||
|
"Eigenschaften:\n",
|
||||||
|
" * Spaltenreihenfolge erzwungen\n",
|
||||||
|
" * Einheiten konsistent (alles taeglich, Annualisierung nur in der Ausgabe)\n",
|
||||||
|
" * Szenario-Nebenbedingungen VEKTORISIERT statt in einer Python-Schleife\n",
|
||||||
|
" (eine Matrixbedingung statt S einzelner Constraints - deutlich schneller)\n",
|
||||||
|
" * Vergleich CVaR- gegen Varianz-Optimierung\n",
|
||||||
|
" * Nachrechnung von VaR/CVaR aus den realisierten Szenarien\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"import time\n",
|
||||||
|
"\n",
|
||||||
|
"import cvxpy as cp\n",
|
||||||
|
"import numpy as np\n",
|
||||||
|
"import pandas as pd\n",
|
||||||
|
"import yfinance as yf\n",
|
||||||
|
"\n",
|
||||||
|
"TICKER = [\"AAPL\", \"MSFT\", \"NVDA\", \"AMZN\", \"JNJ\", \"PFE\", \"JPM\", \"GS\", \"XOM\", \"CVX\"]\n",
|
||||||
|
"ALPHA = 0.95 # Konfidenzniveau: schlechteste 5 % der Tage\n",
|
||||||
|
"MAX_GEWICHT = 0.25\n",
|
||||||
|
"GEBUEHRENSATZ = 0.002 # 0,2 % je Einheit Turnover (Spread + Brokerage)\n",
|
||||||
|
"RISIKOAVERSION = 1.5 # bezogen auf TAEGLICHE Groessen\n",
|
||||||
|
"HANDELSTAGE = 252\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def lade_renditen():\n",
|
||||||
|
" ende = pd.Timestamp.today().normalize()\n",
|
||||||
|
" start = ende - pd.DateOffset(years=2)\n",
|
||||||
|
" roh = yf.download(TICKER, start=start, end=ende, auto_adjust=True, progress=False)\n",
|
||||||
|
" if roh.empty:\n",
|
||||||
|
" raise SystemExit(\"Download fehlgeschlagen (Netz, Ticker oder Rate-Limit pruefen).\")\n",
|
||||||
|
"\n",
|
||||||
|
" if isinstance(roh.columns, pd.MultiIndex):\n",
|
||||||
|
" kurse = roh[\"Close\"][TICKER].dropna() # erzwingt eigene Spaltenreihenfolge\n",
|
||||||
|
" else:\n",
|
||||||
|
" kurse = roh[[\"Close\"]].dropna()\n",
|
||||||
|
" kurse.columns = TICKER\n",
|
||||||
|
" assert list(kurse.columns) == TICKER, \"Spaltenreihenfolge weicht ab!\"\n",
|
||||||
|
" return kurse.pct_change().dropna()\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def optimiere_cvar(R, w_alt, vektorisiert=True):\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" Maximiere: taegliche Rendite - lambda * CVaR - Transaktionskosten\n",
|
||||||
|
" Alle Groessen TAEGLICH.\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" S, N = R.shape\n",
|
||||||
|
" mu_taeglich = R.mean(axis=0)\n",
|
||||||
|
"\n",
|
||||||
|
" w = cp.Variable(N, nonneg=True)\n",
|
||||||
|
" gamma = cp.Variable() # wird im Optimum zum VaR\n",
|
||||||
|
" u = cp.Variable(S, nonneg=True) # Ueberschuss ueber die Schwelle\n",
|
||||||
|
"\n",
|
||||||
|
" cvar = gamma + (1.0 / (S * (1.0 - ALPHA))) * cp.sum(u)\n",
|
||||||
|
" turnover = cp.norm1(w - w_alt)\n",
|
||||||
|
" kosten = GEBUEHRENSATZ * turnover\n",
|
||||||
|
"\n",
|
||||||
|
" ziel = cp.Maximize(mu_taeglich @ w - RISIKOAVERSION * cvar - kosten)\n",
|
||||||
|
"\n",
|
||||||
|
" bedingungen = [cp.sum(w) == 1, w <= MAX_GEWICHT]\n",
|
||||||
|
" if vektorisiert:\n",
|
||||||
|
" # EINE Matrixbedingung statt S einzelner - deutlich schneller\n",
|
||||||
|
" bedingungen.append(u >= -(R @ w) - gamma)\n",
|
||||||
|
" else:\n",
|
||||||
|
" for s in range(S): # Alternative: S einzelne Constraints (langsamer)\n",
|
||||||
|
" bedingungen.append(u[s] >= -R[s] @ w - gamma)\n",
|
||||||
|
"\n",
|
||||||
|
" problem = cp.Problem(ziel, bedingungen)\n",
|
||||||
|
" problem.solve()\n",
|
||||||
|
" if problem.status not in (\"optimal\", \"optimal_inaccurate\"):\n",
|
||||||
|
" raise SystemExit(f\"CVaR-Problem nicht loesbar: {problem.status}\")\n",
|
||||||
|
" return w.value, float(gamma.value), float(cvar.value), float(turnover.value)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def optimiere_varianz(R, w_alt):\n",
|
||||||
|
" \"\"\"Klassisches Mean-Variance zum Vergleich - ebenfalls taeglich gerechnet.\n",
|
||||||
|
"\n",
|
||||||
|
" ACHTUNG, DCP-Falle: Die Standardabweichung ist hier NICHT als\n",
|
||||||
|
" cp.sqrt(cp.quad_form(w, sigma)) formulierbar. cp.sqrt ist konkav und\n",
|
||||||
|
" verlangt ein konkaves Argument; quad_form ist konvex - CVXPY lehnt den\n",
|
||||||
|
" Ausdruck mit einem DCPError ab, und zwar voellig zu Recht (Anhang\n",
|
||||||
|
" Fehlerdiagnose). cp.psd_wrap() hilft dagegen nicht: Es behebt eine\n",
|
||||||
|
" NUMERISCHE Beanstandung an sigma, keine Regelverletzung im Aufbau.\n",
|
||||||
|
"\n",
|
||||||
|
" Der Standardweg ist die Cholesky-Zerlegung sigma = L L^T. Damit gilt\n",
|
||||||
|
" w' sigma w = ||L^T w||^2, also ist die Standardabweichung die 2-Norm\n",
|
||||||
|
" eines AFFINEN Ausdrucks - konvex und damit regelkonform.\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" N = R.shape[1]\n",
|
||||||
|
" mu_taeglich = R.mean(axis=0)\n",
|
||||||
|
" sigma = np.cov(R, rowvar=False, ddof=1)\n",
|
||||||
|
" # Der winzige Diagonalzuschlag faengt den Fall ab, dass sigma numerisch\n",
|
||||||
|
" # nur halbdefinit ist (mehr Titel als Handelstage, doppelte Spalten).\n",
|
||||||
|
" L = np.linalg.cholesky(sigma + 1e-12 * np.eye(N))\n",
|
||||||
|
"\n",
|
||||||
|
" w = cp.Variable(N, nonneg=True)\n",
|
||||||
|
" ziel = cp.Maximize(mu_taeglich @ w\n",
|
||||||
|
" - RISIKOAVERSION * cp.norm2(L.T @ w)\n",
|
||||||
|
" - GEBUEHRENSATZ * cp.norm1(w - w_alt))\n",
|
||||||
|
" problem = cp.Problem(ziel, [cp.sum(w) == 1, w <= MAX_GEWICHT])\n",
|
||||||
|
" problem.solve()\n",
|
||||||
|
" if problem.status not in (\"optimal\", \"optimal_inaccurate\"):\n",
|
||||||
|
" raise SystemExit(f\"Varianz-Problem nicht loesbar: {problem.status}\")\n",
|
||||||
|
" return w.value\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def realisierte_kennzahlen(w, R):\n",
|
||||||
|
" \"\"\"VaR und CVaR direkt aus den Szenarien - unabhaengige Gegenprobe.\"\"\"\n",
|
||||||
|
" portfoliorenditen = R @ w\n",
|
||||||
|
" verluste = -portfoliorenditen\n",
|
||||||
|
" var = float(np.quantile(verluste, ALPHA))\n",
|
||||||
|
" cvar = float(verluste[verluste >= var].mean())\n",
|
||||||
|
" return var, cvar, float(portfoliorenditen.mean()), float(portfoliorenditen.std(ddof=1))\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"if __name__ == \"__main__\":\n",
|
||||||
|
" renditen = lade_renditen()\n",
|
||||||
|
" R = renditen.values\n",
|
||||||
|
" S, N = R.shape\n",
|
||||||
|
" w_alt = np.ones(N) / N # Ausgangslage: Gleichgewichtung\n",
|
||||||
|
"\n",
|
||||||
|
" t0 = time.perf_counter()\n",
|
||||||
|
" w_cvar, var_modell, cvar_modell, turnover = optimiere_cvar(R, w_alt, vektorisiert=True)\n",
|
||||||
|
" dauer_vektor = time.perf_counter() - t0\n",
|
||||||
|
"\n",
|
||||||
|
" w_var = optimiere_varianz(R, w_alt)\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"=\" * 90)\n",
|
||||||
|
" print(\" CVaR-PORTFOLIO-OPTIMIERUNG MIT TRANSAKTIONSKOSTEN\")\n",
|
||||||
|
" print(\"=\" * 90)\n",
|
||||||
|
" print(f\"Datenbasis: {S} Handelstage, {N} Titel | Konfidenzniveau \"\n",
|
||||||
|
" f\"{ALPHA*100:.0f} % | Loesungszeit {dauer_vektor:.2f} s\\n\")\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Gegenprobe: Modellwerte gegen realisierte Szenariowerte ---------\n",
|
||||||
|
" var_real, cvar_real, mu_real, sd_real = realisierte_kennzahlen(w_cvar, R)\n",
|
||||||
|
" print(\"--- Gegenprobe: stimmen Modell und Szenarien ueberein? ---\")\n",
|
||||||
|
" print(f\" VaR aus dem Modell (gamma): {var_modell*100:7.4f} % | \"\n",
|
||||||
|
" f\"aus den Szenarien: {var_real*100:7.4f} %\")\n",
|
||||||
|
" print(f\" CVaR aus dem Modell: {cvar_modell*100:7.4f} % | \"\n",
|
||||||
|
" f\"aus den Szenarien: {cvar_real*100:7.4f} %\")\n",
|
||||||
|
" assert abs(cvar_modell - cvar_real) < 1e-4, \"CVaR stimmt nicht mit den Szenarien!\"\n",
|
||||||
|
" print(\" -> Der Rockafellar-Uryasev-Trick liefert exakt den empirischen CVaR.\")\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Kennzahlen beider Portfolios ------------------------------------\n",
|
||||||
|
" print(f\"\\n{'Portfolio':<24} {'Rendite p.a.':>13} {'Vola p.a.':>11} \"\n",
|
||||||
|
" f\"{'VaR 95% (Tag)':>15} {'CVaR 95% (Tag)':>16} {'Turnover':>10}\")\n",
|
||||||
|
" print(\"-\" * 90)\n",
|
||||||
|
" for name, w in [(\"CVaR-optimiert\", w_cvar), (\"Varianz-optimiert\", w_var),\n",
|
||||||
|
" (\"Gleichgewichtung\", w_alt)]:\n",
|
||||||
|
" v, c, m, s = realisierte_kennzahlen(w, R)\n",
|
||||||
|
" to = float(np.abs(w - w_alt).sum())\n",
|
||||||
|
" print(f\"{name:<24} {m*HANDELSTAGE*100:>12.2f} % \"\n",
|
||||||
|
" f\"{s*np.sqrt(HANDELSTAGE)*100:>10.2f} % {v*100:>14.3f} % \"\n",
|
||||||
|
" f\"{c*100:>15.3f} % {to*100:>9.1f} %\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\nHinweis zur Annualisierung: Renditen werden mit 252 skaliert,\")\n",
|
||||||
|
" print(\"Volatilitaeten mit sqrt(252). Fuer VaR/CVaR ist eine solche Skalierung\")\n",
|
||||||
|
" print(\"nur unter starken Annahmen (Unabhaengigkeit, kein Drift) zulaessig -\")\n",
|
||||||
|
" print(\"sie werden hier deshalb bewusst als TAGESwerte ausgewiesen.\")\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Allokationstabelle ------------------------------------------------\n",
|
||||||
|
" print(\"\\n--- Allokation ---\")\n",
|
||||||
|
" tabelle = pd.DataFrame({\n",
|
||||||
|
" \"Ticker\": TICKER,\n",
|
||||||
|
" \"vorher\": [f\"{v*100:5.1f} %\" for v in w_alt],\n",
|
||||||
|
" \"CVaR-opt.\": [f\"{v*100:5.1f} %\" for v in w_cvar],\n",
|
||||||
|
" \"Handel\": [f\"{(w_cvar[i]-w_alt[i])*100:+6.1f} %\" for i in range(N)],\n",
|
||||||
|
" \"Varianz-opt.\": [f\"{v*100:5.1f} %\" for v in w_var],\n",
|
||||||
|
" })\n",
|
||||||
|
" print(tabelle.to_string(index=False))\n",
|
||||||
|
" print(f\"\\nTurnover {turnover*100:.1f} % -> Transaktionskosten \"\n",
|
||||||
|
" f\"{GEBUEHRENSATZ*turnover*100:.3f} % des Portfoliowerts\")\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Laufzeitvergleich vektorisiert vs. Schleife ----------------------\n",
|
||||||
|
" if S <= 600: # bei sehr vielen Szenarien zu langsam\n",
|
||||||
|
" t0 = time.perf_counter()\n",
|
||||||
|
" optimiere_cvar(R, w_alt, vektorisiert=False)\n",
|
||||||
|
" dauer_schleife = time.perf_counter() - t0\n",
|
||||||
|
" print(f\"\\n--- Laufzeit: {S} Nebenbedingungen aufbauen ---\")\n",
|
||||||
|
" print(f\" vektorisiert (u >= -(R @ w) - gamma): {dauer_vektor:6.2f} s\")\n",
|
||||||
|
" print(f\" Schleife ueber Szenarien (V01): {dauer_schleife:6.2f} s \"\n",
|
||||||
|
" f\"({dauer_schleife/dauer_vektor:.1f}x langsamer)\")\n",
|
||||||
|
" print(\"=\" * 90)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"name": "python",
|
||||||
|
"version": "3.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
347
Notebooks_04/dekomposition.ipynb
Normal file
347
Notebooks_04/dekomposition.ipynb
Normal file
|
|
@ -0,0 +1,347 @@
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"# Kapitel 10: Spaltengenerierung — das Modell umbauen statt die Lösung raten\n",
|
||||||
|
"\n",
|
||||||
|
"Begleitnotebook zu *Optimierte Entscheidungsfindung mit Python*. Die Codezellen sind identisch mit den im Buch abgedruckten Programmen.\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"# Einmalig ausfuehren: installiert alle im Buch verwendeten Pakete.\n",
|
||||||
|
"# Lokal in einer virtuellen Umgebung genauso gueltig wie in Google Colab.\n",
|
||||||
|
"%pip install --quiet ortools highspy cvxpy scipy numpy pandas polars \\\n",
|
||||||
|
" scikit-learn matplotlib plotly pyomo linopy pymoo pydantic openpyxl"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Das Programm\n",
|
||||||
|
"\n",
|
||||||
|
"`Spaltengenerierung.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# Spaltengenerierung.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel Dekomposition: Das Modell umbauen, statt die Loesung zu raten.\n",
|
||||||
|
"\n",
|
||||||
|
"Eine Papierfabrik liefert Rollen von 5.600 mm Breite. Die Kunden bestellen\n",
|
||||||
|
"schmalere Breiten, und aus jeder Mutterrolle werden mehrere davon geschnitten.\n",
|
||||||
|
"Gesucht ist der Schnittplan, der mit den wenigsten Mutterrollen auskommt.\n",
|
||||||
|
"\n",
|
||||||
|
"Der naheliegende Modellansatz - \"welches Stueck kommt auf welche Rolle?\" -\n",
|
||||||
|
"fuehrt in eine Sackgasse: Er braucht eine Binaervariable je Stueck-und-Rolle,\n",
|
||||||
|
"und weil alle Rollen gleich sind, ist er hochgradig symmetrisch. Der Solver\n",
|
||||||
|
"probiert dieselbe Loesung in tausend Umbenennungen durch.\n",
|
||||||
|
"\n",
|
||||||
|
"Die Spaltengenerierung dreht die Frage um:\n",
|
||||||
|
"\n",
|
||||||
|
" Nicht \"welches Stueck auf welche Rolle\",\n",
|
||||||
|
" sondern \"wie oft schneide ich welches MUSTER\".\n",
|
||||||
|
"\n",
|
||||||
|
"Damit verschwindet die Symmetrie - aber es entsteht ein neues Problem: Die Zahl\n",
|
||||||
|
"der moeglichen Muster waechst kombinatorisch. Man kann sie nicht aufschreiben.\n",
|
||||||
|
"Der Trick besteht darin, sie auch nicht aufzuschreiben, sondern nur die wenigen\n",
|
||||||
|
"zu erzeugen, die tatsaechlich gebraucht werden - und zwar mit Hilfe der\n",
|
||||||
|
"Schattenpreise aus dem Kapitel LP.\n",
|
||||||
|
"\n",
|
||||||
|
"Das Programm zeigt vier Dinge:\n",
|
||||||
|
"\n",
|
||||||
|
" 1. Die beiden Formulierungen desselben Problems und ihre Groesse.\n",
|
||||||
|
" 2. Wie schnell die Musterzahl waechst - und wie wenige davon genuegen.\n",
|
||||||
|
" 3. Die Schleife selbst: Master-LP, Dualwerte, Rucksack, neue Spalte.\n",
|
||||||
|
" 4. Wann sich der Aufwand lohnt und wann nicht. Die Antwort haengt an einer\n",
|
||||||
|
" einzigen Kennzahl der Instanz, und sie erklaert nebenbei, warum eine\n",
|
||||||
|
" Faustregel bei manchen Zuschnittproblemen schon optimal ist.\n",
|
||||||
|
"\n",
|
||||||
|
"Benoetigt: numpy, scipy\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"from __future__ import annotations\n",
|
||||||
|
"\n",
|
||||||
|
"import numpy as np\n",
|
||||||
|
"from scipy.optimize import linprog\n",
|
||||||
|
"\n",
|
||||||
|
"ROLLENBREITE = 5600\n",
|
||||||
|
"\n",
|
||||||
|
"# Zwei Instanzen mit demselben Bedarf, aber verschiedenen Breiten. Der\n",
|
||||||
|
"# Unterschied ist die eine Kennzahl, um die es in Teil 4 geht.\n",
|
||||||
|
"BEDARF = [22, 25, 12, 14, 18, 18, 20, 10, 12, 14, 16, 18, 20]\n",
|
||||||
|
"BREIT = [1380, 1520, 1560, 1710, 1820, 1880, 1930, 2000, 2050, 2100, 2140,\n",
|
||||||
|
" 2150, 2200]\n",
|
||||||
|
"SCHMAL = [380, 520, 560, 710, 820, 880, 930, 1000, 1050, 1100, 1140,\n",
|
||||||
|
" 1150, 1200]\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"# --- Die Musterzahl ---------------------------------------------------------\n",
|
||||||
|
"\n",
|
||||||
|
"def zaehle_muster(breiten, rollenbreite: int = ROLLENBREITE) -> int:\n",
|
||||||
|
" \"\"\"Wie viele zulaessige Schnittmuster gibt es ueberhaupt?\n",
|
||||||
|
"\n",
|
||||||
|
" Nur zum Zeigen - in einem produktiven Programm wuerde man das nie\n",
|
||||||
|
" ausrechnen, weil die Zahl bei realistischen Instanzen jede Vorstellung\n",
|
||||||
|
" sprengt. Genau das ist der Punkt.\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" speicher: dict[tuple[int, int], int] = {}\n",
|
||||||
|
"\n",
|
||||||
|
" def ab(i: int, rest: int) -> int:\n",
|
||||||
|
" if i == len(breiten):\n",
|
||||||
|
" return 1\n",
|
||||||
|
" if (i, rest) in speicher:\n",
|
||||||
|
" return speicher[(i, rest)]\n",
|
||||||
|
" summe = sum(ab(i + 1, rest - anzahl * breiten[i])\n",
|
||||||
|
" for anzahl in range(rest // breiten[i] + 1))\n",
|
||||||
|
" speicher[(i, rest)] = summe\n",
|
||||||
|
" return summe\n",
|
||||||
|
"\n",
|
||||||
|
" return ab(0, rollenbreite)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"# --- Die beiden Bausteine der Schleife --------------------------------------\n",
|
||||||
|
"\n",
|
||||||
|
"def loese_master(muster, bedarf, ganzzahlig: bool = False):\n",
|
||||||
|
" \"\"\"Restringiertes Master: Wie oft schneide ich jedes bekannte Muster?\n",
|
||||||
|
"\n",
|
||||||
|
" min sum_p x_p unter sum_p a_ip * x_p >= bedarf_i\n",
|
||||||
|
"\n",
|
||||||
|
" Als LP gerechnet liefert es zusaetzlich die DUALWERTE - und die sind der\n",
|
||||||
|
" eigentliche Ertrag: Der Dualwert zu Breite i sagt, was ein zusaetzliches\n",
|
||||||
|
" Stueck dieser Breite an Rollen kostet. Es ist derselbe Schattenpreis wie\n",
|
||||||
|
" im Kapitel LP, nur dass er hier nicht berichtet, sondern weiterverarbeitet\n",
|
||||||
|
" wird.\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" matrix = -np.array(muster, dtype=float).T # >= wird zu <= mit Minus\n",
|
||||||
|
" ergebnis = linprog(np.ones(len(muster)),\n",
|
||||||
|
" A_ub=matrix, b_ub=-np.array(bedarf, dtype=float),\n",
|
||||||
|
" bounds=(0, None),\n",
|
||||||
|
" integrality=(1 if ganzzahlig else 0), method=\"highs\")\n",
|
||||||
|
" if not ergebnis.success:\n",
|
||||||
|
" raise SystemExit(f\"Master nicht loesbar: {ergebnis.message}\")\n",
|
||||||
|
" dual = None if ganzzahlig else -ergebnis.ineqlin.marginals\n",
|
||||||
|
" return ergebnis.fun, ergebnis.x, dual\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def bestes_neues_muster(dual, breiten, rollenbreite: int = ROLLENBREITE):\n",
|
||||||
|
" \"\"\"Pricing: Gibt es ein Muster, das sich noch lohnt?\n",
|
||||||
|
"\n",
|
||||||
|
" Gesucht ist das Muster mit dem groessten Gesamtwert zu den aktuellen\n",
|
||||||
|
" Schattenpreisen - unter der Bedingung, dass es in die Rolle passt. Das ist\n",
|
||||||
|
" ein RUCKSACKPROBLEM (Kapitel MILP, Rucksack.py): Preise sind der Nutzen,\n",
|
||||||
|
" Breiten das Gewicht, die Rollenbreite die Kapazitaet. Hier mit dynamischer\n",
|
||||||
|
" Programmierung geloest, weil die Breiten ganzzahlig sind.\n",
|
||||||
|
"\n",
|
||||||
|
" Bei Gleichstand wird bewusst der KLEINSTE Index bevorzugt (>-Vergleich mit\n",
|
||||||
|
" Toleranz statt >=). Sonst haengt das erzeugte Muster von der Reihenfolge\n",
|
||||||
|
" der Gleitkommaoperationen ab, und das Programm liefert von Lauf zu Lauf\n",
|
||||||
|
" verschiedene Ausgaben.\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" wert = np.zeros(rollenbreite + 1)\n",
|
||||||
|
" herkunft = [-1] * (rollenbreite + 1)\n",
|
||||||
|
" for platz in range(rollenbreite + 1):\n",
|
||||||
|
" for i, breite in enumerate(breiten):\n",
|
||||||
|
" if breite <= platz and wert[platz - breite] + dual[i] > wert[platz] + 1e-9:\n",
|
||||||
|
" wert[platz] = wert[platz - breite] + dual[i]\n",
|
||||||
|
" herkunft[platz] = i\n",
|
||||||
|
"\n",
|
||||||
|
" muster = [0] * len(breiten)\n",
|
||||||
|
" platz = rollenbreite\n",
|
||||||
|
" while herkunft[platz] >= 0:\n",
|
||||||
|
" i = herkunft[platz]\n",
|
||||||
|
" muster[i] += 1\n",
|
||||||
|
" platz -= breiten[i]\n",
|
||||||
|
" return float(wert[rollenbreite]), muster\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def spaltengenerierung(breiten, bedarf, rollenbreite: int = ROLLENBREITE):\n",
|
||||||
|
" \"\"\"Die Schleife. Startbasis: je ein Muster mit nur einer Breite.\n",
|
||||||
|
"\n",
|
||||||
|
" Abbruch, wenn kein Muster mehr einen Wert ueber 1 hat: Eine zusaetzliche\n",
|
||||||
|
" Rolle kostet 1, also lohnt sich ein neues Muster nur, wenn es zu den\n",
|
||||||
|
" aktuellen Preisen mehr als 1 wert ist. Das ist das Kriterium der\n",
|
||||||
|
" reduzierten Kosten.\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" anzahl = len(breiten)\n",
|
||||||
|
" muster = [[0] * anzahl for _ in range(anzahl)]\n",
|
||||||
|
" for i in range(anzahl):\n",
|
||||||
|
" muster[i][i] = rollenbreite // breiten[i]\n",
|
||||||
|
"\n",
|
||||||
|
" runden = 0\n",
|
||||||
|
" while True:\n",
|
||||||
|
" zielwert, _, dual = loese_master(muster, bedarf)\n",
|
||||||
|
" wert, neu = bestes_neues_muster(dual, breiten, rollenbreite)\n",
|
||||||
|
" runden += 1\n",
|
||||||
|
" if wert <= 1 + 1e-6:\n",
|
||||||
|
" return muster, zielwert, runden\n",
|
||||||
|
" muster.append(neu)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def first_fit(breiten, bedarf, rollenbreite: int = ROLLENBREITE) -> int:\n",
|
||||||
|
" \"\"\"Die Faustregel: groesstes Stueck zuerst, auf die erste passende Rolle.\"\"\"\n",
|
||||||
|
" stuecke = sorted([b for b, menge in zip(breiten, bedarf)\n",
|
||||||
|
" for _ in range(menge)], reverse=True)\n",
|
||||||
|
" rollen: list[list[int]] = []\n",
|
||||||
|
" for stueck in stuecke:\n",
|
||||||
|
" for rolle in rollen:\n",
|
||||||
|
" if sum(rolle) + stueck <= rollenbreite:\n",
|
||||||
|
" rolle.append(stueck)\n",
|
||||||
|
" break\n",
|
||||||
|
" else:\n",
|
||||||
|
" rollen.append([stueck])\n",
|
||||||
|
" return len(rollen)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def auswerten(name: str, breiten, bedarf) -> dict:\n",
|
||||||
|
" muster, schranke, runden = spaltengenerierung(breiten, bedarf)\n",
|
||||||
|
" ganz, _, _ = loese_master(muster, bedarf, ganzzahlig=True)\n",
|
||||||
|
" return {\"name\": name, \"muster_gesamt\": zaehle_muster(breiten),\n",
|
||||||
|
" \"muster_erzeugt\": len(muster), \"runden\": runden,\n",
|
||||||
|
" \"schranke\": schranke, \"ganzzahlig\": int(round(ganz)),\n",
|
||||||
|
" \"first_fit\": first_fit(breiten, bedarf),\n",
|
||||||
|
" \"stuecke_je_rolle\": ROLLENBREITE / np.mean(breiten)}\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"if __name__ == \"__main__\":\n",
|
||||||
|
" print(\"=\" * 84)\n",
|
||||||
|
" print(\" SPALTENGENERIERUNG: NUR DIE MUSTER ERZEUGEN, DIE MAN BRAUCHT\")\n",
|
||||||
|
" print(\"=\" * 84)\n",
|
||||||
|
" print(f\"Mutterrolle {ROLLENBREITE:,} mm, {len(BREIT)} bestellte Breiten, \"\n",
|
||||||
|
" f\"{sum(BEDARF)} Zuschnitte.\\n\")\n",
|
||||||
|
"\n",
|
||||||
|
" # --- 1. Zwei Formulierungen ------------------------------------------\n",
|
||||||
|
" stuecke = sum(BEDARF)\n",
|
||||||
|
" # Wie viele Rollen braucht man hoechstens? Die Faustregel liefert eine\n",
|
||||||
|
" # brauchbare Obergrenze - mehr Rollen als das wird niemand benoetigen.\n",
|
||||||
|
" rollen_obergrenze = first_fit(BREIT, BEDARF)\n",
|
||||||
|
" print(\"1. Zwei Modelle fuer dieselbe Aufgabe\\n\")\n",
|
||||||
|
" print(f\" {'Formulierung':<34} {'Binaervariablen':>16} {'symmetrisch?':>16}\")\n",
|
||||||
|
" print(\" \" + \"-\" * 70)\n",
|
||||||
|
" print(f\" {'Stueck -> Rolle (naheliegend)':<34} \"\n",
|
||||||
|
" f\"{stuecke * rollen_obergrenze:>16,} {'ja':>16}\")\n",
|
||||||
|
" print(f\" {'wie oft welches Muster':<34} \"\n",
|
||||||
|
" f\"{'eine je Muster':>16} {'nein':>16}\")\n",
|
||||||
|
" print(f\"\\n Der naheliegende Ansatz braucht eine Variable je Stueck und Rolle:\")\n",
|
||||||
|
" print(f\" {stuecke} Zuschnitte x {rollen_obergrenze} Rollen (Obergrenze aus der Faustregel)\")\n",
|
||||||
|
" print(f\" = {stuecke * rollen_obergrenze:,} Binaervariablen.\")\n",
|
||||||
|
" print(\"\\n Schlimmer als die Zahl ist die Symmetrie: Alle Mutterrollen sind\")\n",
|
||||||
|
" print(\" gleich, also beschreibt jede Loesung dieselbe Schnittvorschrift in\")\n",
|
||||||
|
" print(\" unzaehligen Umbenennungen. Branch-and-Bound probiert sie einzeln\")\n",
|
||||||
|
" print(\" durch und kommt nicht voran - der Suchbaum waechst, ohne dass sich\")\n",
|
||||||
|
" print(\" die Schranke bewegt.\")\n",
|
||||||
|
" print(\"\\n Das Mustermodell hat je Muster genau eine Variable und kennt keine\")\n",
|
||||||
|
" print(\" einzelnen Rollen mehr. Damit ist die Symmetrie weg. Sein Problem ist\")\n",
|
||||||
|
" print(\" ein anderes - und zwar das folgende.\")\n",
|
||||||
|
"\n",
|
||||||
|
" # --- 2. Die Musterzahl ------------------------------------------------\n",
|
||||||
|
" print(\"\\n\" + \"-\" * 84)\n",
|
||||||
|
" print(\"2. Warum man die Muster nicht aufschreiben kann\\n\")\n",
|
||||||
|
" gross = auswerten(\"breite Zuschnitte (1.380-2.200 mm)\", BREIT, BEDARF)\n",
|
||||||
|
" schmal = auswerten(\"schmale Zuschnitte (380-1.200 mm)\", SCHMAL, BEDARF)\n",
|
||||||
|
"\n",
|
||||||
|
" print(f\" {'Instanz':<36} {'Muster':>12} {'davon erzeugt':>14} {'Anteil':>9}\")\n",
|
||||||
|
" print(\" \" + \"-\" * 76)\n",
|
||||||
|
" for fall in (gross, schmal):\n",
|
||||||
|
" anteil = fall[\"muster_erzeugt\"] / fall[\"muster_gesamt\"]\n",
|
||||||
|
" print(f\" {fall['name']:<36} {fall['muster_gesamt']:>12,} \"\n",
|
||||||
|
" f\"{fall['muster_erzeugt']:>14} {anteil:>8.1%}\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(f\"\\n Schon das Halbieren der Breiten laesst die Musterzahl von \"\n",
|
||||||
|
" f\"{gross['muster_gesamt']:,} auf\")\n",
|
||||||
|
" print(f\" {schmal['muster_gesamt']:,} springen - Faktor \"\n",
|
||||||
|
" f\"{schmal['muster_gesamt'] / gross['muster_gesamt']:.0f}. Bei einer echten \"\n",
|
||||||
|
" f\"Papierfabrik mit\")\n",
|
||||||
|
" print(\" vierzig Breiten und Millimeterschritten sind es mehr, als sich\")\n",
|
||||||
|
" print(\" speichern liesse.\")\n",
|
||||||
|
" print(f\"\\n Gebraucht werden davon {schmal['muster_erzeugt']} - \"\n",
|
||||||
|
" f\"{schmal['muster_erzeugt'] / schmal['muster_gesamt']:.2%} der Gesamtzahl.\")\n",
|
||||||
|
"\n",
|
||||||
|
" # --- 3. Die Schleife --------------------------------------------------\n",
|
||||||
|
" print(\"\\n\" + \"-\" * 84)\n",
|
||||||
|
" print(\"3. Die Schleife an der breiten Instanz\\n\")\n",
|
||||||
|
" print(f\" Startbasis: {len(BREIT)} triviale Muster (je Rolle nur eine Breite)\")\n",
|
||||||
|
" print(f\" Runden bis kein Muster mehr lohnt: {gross['runden']}\")\n",
|
||||||
|
" print(f\" Muster am Ende: {gross['muster_erzeugt']}\")\n",
|
||||||
|
" print(f\" LP-Schranke: {gross['schranke']:.4f} Rollen\")\n",
|
||||||
|
" print(f\"\\n Die LP-Schranke ist eine ZUSAGE: Weniger als \"\n",
|
||||||
|
" f\"{np.ceil(gross['schranke'] - 1e-9):.0f} Rollen sind\")\n",
|
||||||
|
" print(\" nicht moeglich - unabhaengig davon, wie clever man weiterschneidet.\")\n",
|
||||||
|
" print(\" Genau diese Aussage fehlt einer Heuristik (Kapitel Metaheuristiken).\")\n",
|
||||||
|
"\n",
|
||||||
|
" # --- 4. Wann es sich lohnt --------------------------------------------\n",
|
||||||
|
" print(\"\\n\" + \"-\" * 84)\n",
|
||||||
|
" print(\"4. Wann sich der Aufwand lohnt - und wann nicht\\n\")\n",
|
||||||
|
" print(f\" {'Instanz':<36} {'Faustregel':>11} {'exakt':>8} {'Schranke':>10} \"\n",
|
||||||
|
" f\"{'Ersparnis':>11}\")\n",
|
||||||
|
" print(\" \" + \"-\" * 80)\n",
|
||||||
|
" for fall in (gross, schmal):\n",
|
||||||
|
" ersparnis = (fall[\"first_fit\"] - fall[\"ganzzahlig\"]) / fall[\"first_fit\"]\n",
|
||||||
|
" print(f\" {fall['name']:<36} {fall['first_fit']:>11} \"\n",
|
||||||
|
" f\"{fall['ganzzahlig']:>8} {fall['schranke']:>10.2f} \"\n",
|
||||||
|
" f\"{ersparnis:>10.0%}\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(f\"\\n Bei den breiten Zuschnitten spart die Spaltengenerierung \"\n",
|
||||||
|
" f\"{gross['first_fit'] - gross['ganzzahlig']} von\")\n",
|
||||||
|
" print(f\" {gross['first_fit']} Rollen. Bei den schmalen spart sie NICHTS - dort ist die\")\n",
|
||||||
|
" print(\" Faustregel bereits optimal.\")\n",
|
||||||
|
" print(f\"\\n Der Unterschied haengt an einer einzigen Kennzahl:\\n\")\n",
|
||||||
|
" print(f\" {'Instanz':<36} {'Stuecke je Rolle (etwa)':>24}\")\n",
|
||||||
|
" print(\" \" + \"-\" * 62)\n",
|
||||||
|
" for fall in (gross, schmal):\n",
|
||||||
|
" print(f\" {fall['name']:<36} {fall['stuecke_je_rolle']:>24.1f}\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\n Passen nur zwei bis drei Stuecke auf eine Rolle, entscheidet jede\")\n",
|
||||||
|
" print(\" einzelne Zuordnung viel, und eine kurzsichtige Regel verschenkt\")\n",
|
||||||
|
" print(\" ganze Rollen. Passen sechs oder mehr darauf, gleichen sich die\")\n",
|
||||||
|
" print(\" Fehler aus - die Reste sind klein gegen die Rollenbreite, und die\")\n",
|
||||||
|
" print(\" Faustregel trifft es fast immer.\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\n\" + \"=\" * 84)\n",
|
||||||
|
" print(\" WAS MAN DARAUS MITNIMMT\")\n",
|
||||||
|
" print(\"=\" * 84)\n",
|
||||||
|
" print(\"1. Der Perspektivwechsel ist die eigentliche Arbeit: nicht 'welches\")\n",
|
||||||
|
" print(\" Stueck wohin', sondern 'wie oft welches Muster'. Damit verschwindet\")\n",
|
||||||
|
" print(\" die Symmetrie, die das naheliegende Modell unloesbar macht.\")\n",
|
||||||
|
" print(\"2. Der Preis dafuer ist eine unaufschreibbare Zahl von Variablen. Die\")\n",
|
||||||
|
" print(\" Spaltengenerierung zahlt ihn nicht, sondern erzeugt nur die wenigen\")\n",
|
||||||
|
" print(\" Spalten, die die Dualwerte als lohnend ausweisen.\")\n",
|
||||||
|
" print(\"3. Das Teilproblem ist ein Rucksack - ein Verfahren, das die Leser\")\n",
|
||||||
|
" print(\" dieses Buchs schon kennen. Neu ist allein die Schleife.\")\n",
|
||||||
|
" print(\"4. Und die unbequeme Erkenntnis: Ob sich das alles lohnt, entscheidet\")\n",
|
||||||
|
" print(\" die Instanz, nicht die Methode. Bei sechs Stuecken je Rolle ist die\")\n",
|
||||||
|
" print(\" Faustregel so gut wie das Optimum - und der Ertrag der\")\n",
|
||||||
|
" print(\" Spaltengenerierung liegt dann allein in der SCHRANKE, die beweist,\")\n",
|
||||||
|
" print(\" dass man aufhoeren kann zu suchen.\")\n",
|
||||||
|
" print(\"=\" * 84)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"name": "python",
|
||||||
|
"version": "3.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
368
Notebooks_04/dynamische-programmierung.ipynb
Normal file
368
Notebooks_04/dynamische-programmierung.ipynb
Normal file
|
|
@ -0,0 +1,368 @@
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"# Kapitel 13: Dynamische Programmierung — Die Bellman-Gleichung und Order-Execution\n",
|
||||||
|
"\n",
|
||||||
|
"Begleitnotebook zu *Optimierte Entscheidungsfindung mit Python*. Die Codezellen sind identisch mit den im Buch abgedruckten Programmen.\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"# Einmalig ausfuehren: installiert alle im Buch verwendeten Pakete.\n",
|
||||||
|
"# Lokal in einer virtuellen Umgebung genauso gueltig wie in Google Colab.\n",
|
||||||
|
"%pip install --quiet ortools highspy cvxpy scipy numpy pandas polars \\\n",
|
||||||
|
" scikit-learn matplotlib plotly pyomo linopy pymoo pydantic openpyxl"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Die Bellman-Gleichung\n",
|
||||||
|
"\n",
|
||||||
|
"`Bellman_Minimalbeispiel.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# Bellman_Minimalbeispiel.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel Dynamische Programmierung: Die Handrechnung zur Rueckwaertsinduktion als Code.\n",
|
||||||
|
"Zeigt die Wertfunktionstabelle und die optimale Politik Schritt fuer Schritt.\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"import numpy as np\n",
|
||||||
|
"\n",
|
||||||
|
"GESAMT = 3 # zu verkaufende Einheiten\n",
|
||||||
|
"PERIODEN = 2 # Anzahl Verkaufsperioden\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def kosten(menge: int) -> float:\n",
|
||||||
|
" \"\"\"Ueberproportionale Marktauswirkung: doppelte Menge kostet vierfach.\"\"\"\n",
|
||||||
|
" return float(menge ** 2)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def loese_rueckwaerts():\n",
|
||||||
|
" # V[t, x] = minimale Restkosten, wenn zu Beginn von Periode t noch x Stueck offen sind\n",
|
||||||
|
" V = np.full((PERIODEN + 1, GESAMT + 1), np.inf)\n",
|
||||||
|
" politik = np.zeros((PERIODEN, GESAMT + 1), dtype=int)\n",
|
||||||
|
"\n",
|
||||||
|
" # Endbedingung: nach der letzten Periode darf nichts mehr offen sein\n",
|
||||||
|
" V[PERIODEN, 0] = 0.0\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"=\" * 70)\n",
|
||||||
|
" print(\" RUECKWAERTSINDUKTION SCHRITT FUER SCHRITT\")\n",
|
||||||
|
" print(\"=\" * 70)\n",
|
||||||
|
"\n",
|
||||||
|
" for t in range(PERIODEN - 1, -1, -1):\n",
|
||||||
|
" letzte_periode = (t == PERIODEN - 1)\n",
|
||||||
|
" print(f\"\\nStufe t = {t}\" + (\" (letzte Periode: alles muss weg)\" if letzte_periode\n",
|
||||||
|
" else \" (freie Wahl der Menge)\"))\n",
|
||||||
|
" print(f\" {'Zustand x':>10} | {'beste Aktion':>12} | {'Sofortkosten':>13} | \"\n",
|
||||||
|
" f\"{'V[t+1]':>9} | {'V[t]':>8}\")\n",
|
||||||
|
" print(\" \" + \"-\" * 62)\n",
|
||||||
|
"\n",
|
||||||
|
" for x in range(GESAMT + 1):\n",
|
||||||
|
" aktionen = [x] if letzte_periode else range(x + 1)\n",
|
||||||
|
" bester_wert, beste_aktion, beste_teile = np.inf, 0, (0.0, 0.0)\n",
|
||||||
|
"\n",
|
||||||
|
" for n in aktionen:\n",
|
||||||
|
" rest = x - n\n",
|
||||||
|
" sofort = kosten(n)\n",
|
||||||
|
" zukunft = V[t + 1, rest]\n",
|
||||||
|
" gesamt = sofort + zukunft\n",
|
||||||
|
" if gesamt < bester_wert:\n",
|
||||||
|
" bester_wert, beste_aktion = gesamt, n\n",
|
||||||
|
" beste_teile = (sofort, zukunft)\n",
|
||||||
|
"\n",
|
||||||
|
" V[t, x] = bester_wert\n",
|
||||||
|
" politik[t, x] = beste_aktion\n",
|
||||||
|
" print(f\" {x:>10} | {beste_aktion:>12} | {beste_teile[0]:>13.1f} | \"\n",
|
||||||
|
" f\"{beste_teile[1]:>9.1f} | {bester_wert:>8.1f}\")\n",
|
||||||
|
"\n",
|
||||||
|
" return V, politik\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"if __name__ == \"__main__\":\n",
|
||||||
|
" V, politik = loese_rueckwaerts()\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Vorwaertspfad: der optimalen Politik folgen ---------------------\n",
|
||||||
|
" print(\"\\n\" + \"=\" * 70)\n",
|
||||||
|
" print(\" OPTIMALER PFAD (Vorwaertssimulation)\")\n",
|
||||||
|
" print(\"=\" * 70)\n",
|
||||||
|
" bestand = GESAMT\n",
|
||||||
|
" gesamtkosten = 0.0\n",
|
||||||
|
" for t in range(PERIODEN):\n",
|
||||||
|
" aktion = politik[t, bestand]\n",
|
||||||
|
" gesamtkosten += kosten(aktion)\n",
|
||||||
|
" print(f\" Periode {t}: Bestand {bestand} -> verkaufe {aktion} \"\n",
|
||||||
|
" f\"(Kosten {kosten(aktion):.1f}) -> Rest {bestand - aktion}\")\n",
|
||||||
|
" bestand -= aktion\n",
|
||||||
|
"\n",
|
||||||
|
" print(f\"\\n Gesamtkosten: {gesamtkosten:.1f} (V[0, {GESAMT}] = {V[0, GESAMT]:.1f})\")\n",
|
||||||
|
" assert abs(gesamtkosten - V[0, GESAMT]) < 1e-9, \"Pfadkosten != Wertfunktion!\"\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Vergleich mit naiven Strategien ---------------------------------\n",
|
||||||
|
" alles_sofort = kosten(GESAMT)\n",
|
||||||
|
" print(f\"\\n Zum Vergleich - alles in Periode 0 verkaufen: {alles_sofort:.1f}\")\n",
|
||||||
|
" print(f\" Ersparnis durch Stueckelung: {alles_sofort - gesamtkosten:.1f} \"\n",
|
||||||
|
" f\"({(1 - gesamtkosten/alles_sofort)*100:.0f} %)\")\n",
|
||||||
|
" print(\"=\" * 70)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Das Almgren-Chriss-Problem: optimale Orderausführung\n",
|
||||||
|
"\n",
|
||||||
|
"`Mehrperiodige_Order_Execution.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# Mehrperiodige_Order_Execution.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel Dynamische Programmierung: Dynamische Programmierung fuer optimale Orderausfuehrung\n",
|
||||||
|
"(Almgren-Chriss-Rahmen, geloest per Rueckwaertsinduktion).\n",
|
||||||
|
"\n",
|
||||||
|
"Eigenschaften:\n",
|
||||||
|
" * Risikoterm sauber hergeleitet ueber den Aktienkurs P0 (Einheiten: EUR),\n",
|
||||||
|
" ohne undokumentierte Skalierungsfaktoren\n",
|
||||||
|
" * Vergleich mit der analytischen Almgren-Chriss-Loesung\n",
|
||||||
|
" * Vergleich mit naiven Strategien (alles sofort / gleichmaessig)\n",
|
||||||
|
" * Sensitivitaet gegenueber der Risikoaversion\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"import os\n",
|
||||||
|
"\n",
|
||||||
|
"import numpy as np\n",
|
||||||
|
"import pandas as pd\n",
|
||||||
|
"import matplotlib\n",
|
||||||
|
"matplotlib.use(\"Agg\")\n",
|
||||||
|
"import matplotlib.pyplot as plt\n",
|
||||||
|
"\n",
|
||||||
|
"OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), \"output\")\n",
|
||||||
|
"os.makedirs(OUTPUT_DIR, exist_ok=True)\n",
|
||||||
|
"\n",
|
||||||
|
"# --- Parameter -------------------------------------------------------------\n",
|
||||||
|
"GESAMTBESTAND = 100_000 # X_0, zu verkaufende Aktien\n",
|
||||||
|
"PERIODEN = 5 # T Handelsperioden\n",
|
||||||
|
"KURS = 50.0 # P_0 in EUR, zur Skalierung des Risikoterms\n",
|
||||||
|
"ETA = 2.5e-6 # EUR je Stueck^2 (Slippage-Koeffizient)\n",
|
||||||
|
"VOLA_JAHR = 0.30 # 30 % p.a.\n",
|
||||||
|
"HANDELSSTUNDEN_JAHR = 252 * 6.5\n",
|
||||||
|
"RISIKOAVERSION = 1e-5 # 1/EUR; kalibriert, siehe Kommentar unten\n",
|
||||||
|
"SCHRITTWEITE = 1000 # Diskretisierung des Zustandsraums\n",
|
||||||
|
"\n",
|
||||||
|
"# Kalibrierungshinweis: Die dimensionslose Kennzahl des Modells ist\n",
|
||||||
|
"# kappa_tilde^2 = lambda * sigma_periode^2 * P0^2 / eta\n",
|
||||||
|
"# Sie entscheidet ueber den Charakter der Loesung:\n",
|
||||||
|
"# << 1 -> praktisch gleichmaessige Aufteilung (Risiko spielt keine Rolle)\n",
|
||||||
|
"# ~ 1 -> ausgewogener Kompromiss <- hier: 0.55\n",
|
||||||
|
"# >> 1 -> fast alles sofort verkaufen\n",
|
||||||
|
"# Genau diese Interpretierbarkeit geht mit einem undokumentierten\n",
|
||||||
|
"# Skalierungsfaktor verloren.\n",
|
||||||
|
"\n",
|
||||||
|
"VOLA_PERIODE = VOLA_JAHR / np.sqrt(HANDELSSTUNDEN_JAHR)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def periodenkosten(verkauf: float, restbestand: float) -> float:\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" Sofortkosten einer Periode in EUR:\n",
|
||||||
|
" (1) Marktauswirkung: eta * n^2\n",
|
||||||
|
" (2) Risiko des Restbestands: lambda/2 * sigma^2 * P0^2 * X^2\n",
|
||||||
|
" -> P0^2 macht aus \"Stueck^2\" einen EUR^2-Wert; lambda hat damit\n",
|
||||||
|
" die Einheit 1/EUR und ist interpretierbar.\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" marktauswirkung = ETA * verkauf ** 2\n",
|
||||||
|
" wertvarianz = (VOLA_PERIODE ** 2) * (KURS ** 2) * (restbestand ** 2)\n",
|
||||||
|
" risiko = 0.5 * RISIKOAVERSION * wertvarianz\n",
|
||||||
|
" return marktauswirkung + risiko\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def loese_dp():\n",
|
||||||
|
" \"\"\"Rueckwaertsinduktion ueber den diskretisierten Zustandsraum.\"\"\"\n",
|
||||||
|
" zustaende = np.arange(0, GESAMTBESTAND + SCHRITTWEITE, SCHRITTWEITE)\n",
|
||||||
|
" anzahl = len(zustaende)\n",
|
||||||
|
"\n",
|
||||||
|
" V = np.full((PERIODEN + 1, anzahl), np.inf)\n",
|
||||||
|
" politik = np.zeros((PERIODEN, anzahl), dtype=int)\n",
|
||||||
|
" V[PERIODEN, 0] = 0.0 # am Ende muss alles verkauft sein\n",
|
||||||
|
"\n",
|
||||||
|
" for t in range(PERIODEN - 1, -1, -1):\n",
|
||||||
|
" for idx, bestand in enumerate(zustaende):\n",
|
||||||
|
" if t == PERIODEN - 1:\n",
|
||||||
|
" moegliche = [bestand] # letzte Periode: Rest muss weg\n",
|
||||||
|
" else:\n",
|
||||||
|
" moegliche = zustaende[zustaende <= bestand]\n",
|
||||||
|
"\n",
|
||||||
|
" bester_wert, beste_aktion = np.inf, 0\n",
|
||||||
|
" for verkauf in moegliche:\n",
|
||||||
|
" rest = bestand - verkauf\n",
|
||||||
|
" rest_idx = int(round(rest / SCHRITTWEITE))\n",
|
||||||
|
" gesamt = periodenkosten(verkauf, rest) + V[t + 1, rest_idx]\n",
|
||||||
|
" if gesamt < bester_wert:\n",
|
||||||
|
" bester_wert, beste_aktion = gesamt, int(verkauf)\n",
|
||||||
|
"\n",
|
||||||
|
" V[t, idx] = bester_wert\n",
|
||||||
|
" politik[t, idx] = beste_aktion\n",
|
||||||
|
"\n",
|
||||||
|
" return zustaende, V, politik\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def analytische_loesung():\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" Geschlossene Almgren-Chriss-Loesung fuer den kontinuierlichen Fall.\n",
|
||||||
|
" Der optimale Pfad ist X_t = X_0 * sinh(kappa*(T-t)) / sinh(kappa*T)\n",
|
||||||
|
" mit kappa = arccosh(tilde_kappa^2/2 + 1), tilde_kappa^2 = lambda*sigma^2*P0^2/eta.\n",
|
||||||
|
" Dient hier als unabhaengige Kontrolle des DP-Ergebnisses.\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" kappa_tilde_quadrat = (RISIKOAVERSION * (VOLA_PERIODE ** 2) * (KURS ** 2)) / ETA\n",
|
||||||
|
" kappa = np.arccosh(kappa_tilde_quadrat / 2.0 + 1.0)\n",
|
||||||
|
" if kappa < 1e-12: # Grenzfall: risikoneutral -> linear\n",
|
||||||
|
" return np.linspace(GESAMTBESTAND, 0, PERIODEN + 1)\n",
|
||||||
|
" t = np.arange(PERIODEN + 1)\n",
|
||||||
|
" return GESAMTBESTAND * np.sinh(kappa * (PERIODEN - t)) / np.sinh(kappa * PERIODEN)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def bewerte_pfad(bestaende):\n",
|
||||||
|
" \"\"\"Gesamtkosten eines beliebigen Bestandspfades.\"\"\"\n",
|
||||||
|
" summe = 0.0\n",
|
||||||
|
" for t in range(len(bestaende) - 1):\n",
|
||||||
|
" verkauf = bestaende[t] - bestaende[t + 1]\n",
|
||||||
|
" summe += periodenkosten(verkauf, bestaende[t + 1])\n",
|
||||||
|
" return summe\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"if __name__ == \"__main__\":\n",
|
||||||
|
" zustaende, V, politik = loese_dp()\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Vorwaertspfad der optimalen Politik -----------------------------\n",
|
||||||
|
" bestand = GESAMTBESTAND\n",
|
||||||
|
" verlauf = [bestand]\n",
|
||||||
|
" verkaeufe = []\n",
|
||||||
|
" for t in range(PERIODEN):\n",
|
||||||
|
" idx = int(round(bestand / SCHRITTWEITE))\n",
|
||||||
|
" verkauf = politik[t, idx]\n",
|
||||||
|
" verkaeufe.append(verkauf)\n",
|
||||||
|
" bestand -= verkauf\n",
|
||||||
|
" verlauf.append(bestand)\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"=\" * 84)\n",
|
||||||
|
" print(\" OPTIMALE MEHRPERIODIGE ORDER-EXECUTION (BELLMAN DP)\")\n",
|
||||||
|
" print(\"=\" * 84)\n",
|
||||||
|
" print(f\"Gesamtvolumen: {GESAMTBESTAND:,} Stueck zu {KURS:.2f} EUR \"\n",
|
||||||
|
" f\"= {GESAMTBESTAND*KURS:,.0f} EUR Positionswert\")\n",
|
||||||
|
" print(f\"Zeithorizont: {PERIODEN} Handelsperioden\")\n",
|
||||||
|
" print(f\"Volatilitaet: {VOLA_JAHR*100:.0f} % p.a. \"\n",
|
||||||
|
" f\"= {VOLA_PERIODE*100:.3f} % je Periode\")\n",
|
||||||
|
" print(f\"Slippage eta: {ETA:.2e} EUR/Stueck^2\")\n",
|
||||||
|
" print(f\"Risikoaversion: {RISIKOAVERSION:.2e} 1/EUR\")\n",
|
||||||
|
" print(f\"Erwartete Gesamtreibung: {V[0, -1]:,.2f} EUR \"\n",
|
||||||
|
" f\"({V[0, -1]/(GESAMTBESTAND*KURS)*10000:.1f} Basispunkte)\\n\")\n",
|
||||||
|
"\n",
|
||||||
|
" plan = pd.DataFrame([{\n",
|
||||||
|
" \"Periode\": f\"t = {t} -> {t+1}\",\n",
|
||||||
|
" \"Startbestand\": f\"{verlauf[t]:,}\",\n",
|
||||||
|
" \"Verkauf n_t\": f\"{verkaeufe[t]:,}\",\n",
|
||||||
|
" \"Restbestand\": f\"{verlauf[t+1]:,}\",\n",
|
||||||
|
" \"Anteil\": f\"{verkaeufe[t]/GESAMTBESTAND*100:5.1f} %\",\n",
|
||||||
|
" \"Kosten (EUR)\": f\"{periodenkosten(verkaeufe[t], verlauf[t+1]):,.0f}\",\n",
|
||||||
|
" } for t in range(PERIODEN)])\n",
|
||||||
|
" print(plan.to_string(index=False))\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Vergleich mit Alternativen und der analytischen Loesung ---------\n",
|
||||||
|
" sofort = [GESAMTBESTAND] + [0] * PERIODEN\n",
|
||||||
|
" gleichmaessig = [GESAMTBESTAND * (1 - t / PERIODEN) for t in range(PERIODEN + 1)]\n",
|
||||||
|
" analytisch = analytische_loesung()\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\n\" + \"-\" * 84)\n",
|
||||||
|
" print(f\"{'Strategie':<34} {'Kosten (EUR)':>15} {'Basispunkte':>13} \"\n",
|
||||||
|
" f\"{'ggue. Optimum':>16}\")\n",
|
||||||
|
" print(\"-\" * 84)\n",
|
||||||
|
" optimum = V[0, -1]\n",
|
||||||
|
" for name, pfad in [(\"DP-Optimum\", verlauf),\n",
|
||||||
|
" (\"Analytisch (Almgren-Chriss)\", list(analytisch)),\n",
|
||||||
|
" (\"Gleichmaessig (TWAP)\", gleichmaessig),\n",
|
||||||
|
" (\"Alles sofort\", sofort)]:\n",
|
||||||
|
" kosten = bewerte_pfad(pfad)\n",
|
||||||
|
" bp = kosten / (GESAMTBESTAND * KURS) * 10000\n",
|
||||||
|
" print(f\"{name:<34} {kosten:>15,.0f} {bp:>12.1f} \"\n",
|
||||||
|
" f\"{kosten - optimum:>+15,.0f}\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"-\" * 84)\n",
|
||||||
|
" abweichung = abs(bewerte_pfad(list(analytisch)) - optimum) / optimum\n",
|
||||||
|
" print(f\"Abweichung DP zur analytischen Loesung: {abweichung*100:.3f} % \"\n",
|
||||||
|
" f\"(Diskretisierung: {SCHRITTWEITE} Stueck)\")\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Sensitivitaet gegenueber der Risikoaversion ---------------------\n",
|
||||||
|
" print(\"\\n--- Wie wirkt die Risikoaversion? ---\")\n",
|
||||||
|
" print(f\"{'lambda':>10} | {'Verkauf in Periode 0':>22} | {'Charakter':<28}\")\n",
|
||||||
|
" print(\"-\" * 70)\n",
|
||||||
|
" for lam in [1e-7, 1e-6, 1e-5, 1e-4, 1e-3]:\n",
|
||||||
|
" globals()[\"RISIKOAVERSION\"] = lam\n",
|
||||||
|
" _, V_l, pol_l = loese_dp()\n",
|
||||||
|
" erste = pol_l[0, -1]\n",
|
||||||
|
" anteil = erste / GESAMTBESTAND * 100\n",
|
||||||
|
" charakter = (\"nahezu gleichmaessig\" if anteil < 25 else\n",
|
||||||
|
" \"front-loaded\" if anteil < 60 else \"fast alles sofort\")\n",
|
||||||
|
" print(f\"{lam:>10.0e} | {erste:>13,} ({anteil:5.1f} %) | {charakter:<28}\")\n",
|
||||||
|
" globals()[\"RISIKOAVERSION\"] = 1e-5 # zuruecksetzen\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Diagramm ---------------------------------------------------------\n",
|
||||||
|
" plt.figure(figsize=(10, 5.5))\n",
|
||||||
|
" plt.plot(range(PERIODEN + 1), verlauf, \"o-\", lw=2.5, label=\"DP-Optimum\")\n",
|
||||||
|
" plt.plot(range(PERIODEN + 1), analytisch, \"s--\", lw=1.8, alpha=0.8,\n",
|
||||||
|
" label=\"Analytisch (Almgren-Chriss)\")\n",
|
||||||
|
" plt.plot(range(PERIODEN + 1), gleichmaessig, \"^:\", lw=1.8, alpha=0.8,\n",
|
||||||
|
" label=\"Gleichmaessig (TWAP)\")\n",
|
||||||
|
" plt.bar(range(PERIODEN), verkaeufe, alpha=0.25, color=\"orange\", width=0.45,\n",
|
||||||
|
" label=\"Verkaufstranche $n_t$\")\n",
|
||||||
|
" plt.title(\"Optimaler Liquidationspfad ueber diskrete Perioden\", fontsize=12)\n",
|
||||||
|
" plt.xlabel(\"Handelsperiode $t$\")\n",
|
||||||
|
" plt.ylabel(\"Verbleibender Bestand $X_t$\")\n",
|
||||||
|
" plt.xticks(range(PERIODEN + 1))\n",
|
||||||
|
" plt.grid(True, linestyle=\":\", alpha=0.6)\n",
|
||||||
|
" plt.legend()\n",
|
||||||
|
" plt.tight_layout()\n",
|
||||||
|
" ziel = os.path.join(OUTPUT_DIR, \"optimal_execution_dp.png\")\n",
|
||||||
|
" plt.savefig(ziel, dpi=150)\n",
|
||||||
|
" print(f\"\\nDiagramm gespeichert unter '{ziel}'\")\n",
|
||||||
|
" print(\"=\" * 84)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"name": "python",
|
||||||
|
"version": "3.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
484
Notebooks_04/einfuehrung.ipynb
Normal file
484
Notebooks_04/einfuehrung.ipynb
Normal file
|
|
@ -0,0 +1,484 @@
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"# Kapitel 1: Einführung in Operations Research — Vom Ursprung zur mathematischen Entscheidungsfindung\n",
|
||||||
|
"\n",
|
||||||
|
"Begleitnotebook zu *Optimierte Entscheidungsfindung mit Python*. Die Codezellen sind identisch mit den im Buch abgedruckten Programmen.\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"# Einmalig ausfuehren: installiert alle im Buch verwendeten Pakete.\n",
|
||||||
|
"# Lokal in einer virtuellen Umgebung genauso gueltig wie in Google Colab.\n",
|
||||||
|
"%pip install --quiet ortools highspy cvxpy scipy numpy pandas polars \\\n",
|
||||||
|
" scikit-learn matplotlib plotly pyomo linopy pymoo pydantic openpyxl"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Warum Ausprobieren scheitert — mit eigener Rechnung\n",
|
||||||
|
"\n",
|
||||||
|
"`Brute_Force_Vergleich.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# Brute_Force_Vergleich.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel Einfuehrung: Warum Ausprobieren scheitert.\n",
|
||||||
|
"Vergleicht vollständige Enumeration mit einem Constraint-Solver an einem\n",
|
||||||
|
"Zuordnungsproblem wachsender Größe (n Mitarbeiter auf n Aufgaben).\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"import itertools\n",
|
||||||
|
"import math\n",
|
||||||
|
"import time\n",
|
||||||
|
"\n",
|
||||||
|
"import numpy as np\n",
|
||||||
|
"from ortools.sat.python import cp_model\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def erzeuge_kostenmatrix(n: int, seed: int = 7) -> np.ndarray:\n",
|
||||||
|
" \"\"\"Zufällige, aber reproduzierbare Kosten: Wer bearbeitet welche Aufgabe wie teuer?\"\"\"\n",
|
||||||
|
" rng = np.random.default_rng(seed)\n",
|
||||||
|
" return rng.integers(low=10, high=100, size=(n, n))\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def loese_brute_force(kosten: np.ndarray) -> tuple[float, tuple, int]:\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" Probiert ALLE n! Zuordnungen durch und behält die beste.\n",
|
||||||
|
" Rückgabe: (bester Kostenwert, beste Permutation, Anzahl geprüfter Kombinationen)\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" n = len(kosten)\n",
|
||||||
|
" bester_wert = math.inf\n",
|
||||||
|
" beste_zuordnung = None\n",
|
||||||
|
" geprueft = 0\n",
|
||||||
|
"\n",
|
||||||
|
" # itertools.permutations(range(n)) liefert nacheinander jede Reihenfolge\n",
|
||||||
|
" for zuordnung in itertools.permutations(range(n)):\n",
|
||||||
|
" # zuordnung[i] = Aufgabe, die Mitarbeiter i übernimmt\n",
|
||||||
|
" wert = sum(kosten[i][zuordnung[i]] for i in range(n))\n",
|
||||||
|
" geprueft += 1\n",
|
||||||
|
" if wert < bester_wert:\n",
|
||||||
|
" bester_wert = wert\n",
|
||||||
|
" beste_zuordnung = zuordnung\n",
|
||||||
|
"\n",
|
||||||
|
" return bester_wert, beste_zuordnung, geprueft\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def loese_mit_solver(kosten: np.ndarray) -> tuple[float, tuple]:\n",
|
||||||
|
" \"\"\"Dasselbe Problem als Constraint-Programm — der Solver probiert NICHT alles durch.\"\"\"\n",
|
||||||
|
" n = len(kosten)\n",
|
||||||
|
" modell = cp_model.CpModel()\n",
|
||||||
|
"\n",
|
||||||
|
" # x[i][j] = 1 <=> Mitarbeiter i übernimmt Aufgabe j\n",
|
||||||
|
" x = [[modell.NewBoolVar(f\"x_{i}_{j}\") for j in range(n)] for i in range(n)]\n",
|
||||||
|
"\n",
|
||||||
|
" for i in range(n):\n",
|
||||||
|
" modell.AddExactlyOne(x[i][j] for j in range(n)) # jeder genau eine Aufgabe\n",
|
||||||
|
" for j in range(n):\n",
|
||||||
|
" modell.AddExactlyOne(x[i][j] for i in range(n)) # jede Aufgabe genau einmal\n",
|
||||||
|
"\n",
|
||||||
|
" modell.Minimize(sum(int(kosten[i][j]) * x[i][j] for i in range(n) for j in range(n)))\n",
|
||||||
|
"\n",
|
||||||
|
" loeser = cp_model.CpSolver()\n",
|
||||||
|
" loeser.parameters.max_time_in_seconds = 30.0\n",
|
||||||
|
" status = loeser.Solve(modell)\n",
|
||||||
|
" if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE):\n",
|
||||||
|
" raise RuntimeError(\"Solver fand keine Lösung.\")\n",
|
||||||
|
"\n",
|
||||||
|
" zuordnung = tuple(\n",
|
||||||
|
" next(j for j in range(n) if loeser.Value(x[i][j]) == 1) for i in range(n)\n",
|
||||||
|
" )\n",
|
||||||
|
" return loeser.ObjectiveValue(), zuordnung\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"if __name__ == \"__main__\":\n",
|
||||||
|
" print(\"=\" * 72)\n",
|
||||||
|
" print(\" VOLLSTÄNDIGES DURCHPROBIEREN vs. OPERATIONS RESEARCH\")\n",
|
||||||
|
" print(\"=\" * 72)\n",
|
||||||
|
" print(f\"{'n':>3} | {'Kombinationen':>18} | {'Brute Force':>14} | {'Solver':>10} | gleich?\")\n",
|
||||||
|
" print(\"-\" * 72)\n",
|
||||||
|
"\n",
|
||||||
|
" for n in [4, 6, 8, 9, 10]: # n = 11 dauert bereits ~2 Minuten\n",
|
||||||
|
" kosten = erzeuge_kostenmatrix(n)\n",
|
||||||
|
"\n",
|
||||||
|
" t0 = time.perf_counter()\n",
|
||||||
|
" wert_bf, zuord_bf, geprueft = loese_brute_force(kosten)\n",
|
||||||
|
" zeit_bf = time.perf_counter() - t0\n",
|
||||||
|
"\n",
|
||||||
|
" t0 = time.perf_counter()\n",
|
||||||
|
" wert_or, zuord_or = loese_mit_solver(kosten)\n",
|
||||||
|
" zeit_or = time.perf_counter() - t0\n",
|
||||||
|
"\n",
|
||||||
|
" gleich = \"ja\" if abs(wert_bf - wert_or) < 1e-9 else \"NEIN!\"\n",
|
||||||
|
" print(f\"{n:>3} | {geprueft:>18,} | {zeit_bf:>11.4f} s | {zeit_or:>7.4f} s | {gleich}\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"-\" * 72)\n",
|
||||||
|
" # Hochrechnung: Wie lange bräuchte Brute Force bei n = 20?\n",
|
||||||
|
" kombis_20 = math.factorial(20)\n",
|
||||||
|
" # Messbasis: Prüfungen pro Sekunde aus dem letzten Lauf schätzen\n",
|
||||||
|
" pro_sekunde = geprueft / max(zeit_bf, 1e-9)\n",
|
||||||
|
" jahre = kombis_20 / pro_sekunde / (60 * 60 * 24 * 365.25)\n",
|
||||||
|
" print(f\"Hochrechnung für n = 20: {kombis_20:,} Kombinationen\")\n",
|
||||||
|
" print(f\"Bei gemessenen {pro_sekunde:,.0f} Prüfungen/s wären das {jahre:,.0f} Jahre.\")\n",
|
||||||
|
" print(\"Der Solver löst dieselbe Instanz in Sekundenbruchteilen.\")\n",
|
||||||
|
" print(\"=\" * 72)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Die Bausteine in der Praxis: eine Vorlage\n",
|
||||||
|
"\n",
|
||||||
|
"`Bausteine_Vorlage.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# Bausteine_Vorlage.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel Einfuehrung: Die Bausteine-Vorlage als ausfuellbares Python-Geruest.\n",
|
||||||
|
"\n",
|
||||||
|
"Uebersetzt die Vier-Bausteine-Vorlage aus dem Abschnitt 'Die vier\n",
|
||||||
|
"universellen Bausteine jedes OR-Problems' direkt in Code: Wer sie\n",
|
||||||
|
"auf Papier ausgefuellt hat, kann sie fast unveraendert in ein loesbares\n",
|
||||||
|
"Modell umsetzen. Angewendet auf das Baeckerei-Beispiel der Handrechnung.\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"from dataclasses import dataclass\n",
|
||||||
|
"from ortools.sat.python import cp_model\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"@dataclass\n",
|
||||||
|
"class Bausteine:\n",
|
||||||
|
" \"\"\"Die vier Bausteine jedes OR-Problems - wortwoertlich aus der Vorlage\n",
|
||||||
|
" im Abschnitt 'Die vier universellen Bausteine' uebernommen.\"\"\"\n",
|
||||||
|
" variablen: str\n",
|
||||||
|
" parameter: str\n",
|
||||||
|
" zielfunktion: str\n",
|
||||||
|
" harte_bedingungen: str\n",
|
||||||
|
" weiche_bedingungen: str\n",
|
||||||
|
"\n",
|
||||||
|
" def zeige(self) -> None:\n",
|
||||||
|
" print(\"=\" * 78)\n",
|
||||||
|
" print(\" AUSGEFUELLTE BAUSTEINE-VORLAGE\")\n",
|
||||||
|
" print(\"=\" * 78)\n",
|
||||||
|
" for feld, wert in [\n",
|
||||||
|
" (\"Entscheidungsvariablen\", self.variablen),\n",
|
||||||
|
" (\"Parameter\", self.parameter),\n",
|
||||||
|
" (\"Zielfunktion\", self.zielfunktion),\n",
|
||||||
|
" (\"Harte Bedingungen\", self.harte_bedingungen),\n",
|
||||||
|
" (\"Weiche Bedingungen\", self.weiche_bedingungen),\n",
|
||||||
|
" ]:\n",
|
||||||
|
" print(f\"{feld:<24}: {wert}\")\n",
|
||||||
|
" print(\"=\" * 78)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"# --- Baeckerei-Beispiel aus der Handrechnung ---------------------------------\n",
|
||||||
|
"VORLAGE = Bausteine(\n",
|
||||||
|
" variablen=\"x1 = Anzahl Brote, x2 = Anzahl Broetchen-Zehnerpackungen \"\n",
|
||||||
|
" \"(beide ganzzahlig >= 0)\",\n",
|
||||||
|
" parameter=\"Deckungsbeitraege (2,50; 3,00) EUR; Mehlbedarf (0,5; 0,6) kg; \"\n",
|
||||||
|
" \"Ofenzeit (4; 3) min; Kapazitaeten (90 kg, 600 min); Mindestmenge 40 Brote\",\n",
|
||||||
|
" zielfunktion=\"max 2,5*x1 + 3,0*x2 (Einheit: Euro Deckungsbeitrag)\",\n",
|
||||||
|
" harte_bedingungen=\"0,5*x1 + 0,6*x2 <= 90 (Mehl); 4*x1 + 3*x2 <= 600 (Ofen); \"\n",
|
||||||
|
" \"x1 >= 40 (Vertrag)\",\n",
|
||||||
|
" weiche_bedingungen=\"keine\",\n",
|
||||||
|
")\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def loese_baeckerei():\n",
|
||||||
|
" \"\"\"Baut aus der ausgefuellten Vorlage mechanisch ein CP-SAT-Modell.\"\"\"\n",
|
||||||
|
" modell = cp_model.CpModel()\n",
|
||||||
|
" x1 = modell.NewIntVar(40, 1000, \"Brote\") # Vertrag: mindestens 40\n",
|
||||||
|
" x2 = modell.NewIntVar(0, 1000, \"Broetchen_Zehner\")\n",
|
||||||
|
"\n",
|
||||||
|
" modell.Add(5 * x1 + 6 * x2 <= 900) # Mehl, x10 fuer Ganzzahligkeit\n",
|
||||||
|
" modell.Add(4 * x1 + 3 * x2 <= 600) # Ofenzeit\n",
|
||||||
|
"\n",
|
||||||
|
" modell.Maximize(25 * x1 + 30 * x2) # Deckungsbeitrag x10\n",
|
||||||
|
"\n",
|
||||||
|
" loeser = cp_model.CpSolver()\n",
|
||||||
|
" status = loeser.Solve(modell)\n",
|
||||||
|
" if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE):\n",
|
||||||
|
" raise SystemExit(f\"Keine Loesung: {loeser.StatusName(status)}\")\n",
|
||||||
|
" return loeser.Value(x1), loeser.Value(x2), loeser.ObjectiveValue() / 10\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"if __name__ == \"__main__\":\n",
|
||||||
|
" VORLAGE.zeige()\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\n--- Aus der Vorlage mechanisch abgeleitetes Modell ---\")\n",
|
||||||
|
" brote, broetchen, gewinn = loese_baeckerei()\n",
|
||||||
|
" print(f\"Optimale Brote: {brote}\")\n",
|
||||||
|
" print(f\"Optimale Broetchen-Zehner: {broetchen}\")\n",
|
||||||
|
" print(f\"Maximaler Deckungsbeitrag: {gewinn:.2f} EUR\")\n",
|
||||||
|
" print()\n",
|
||||||
|
" print(\"Genau diese drei Codezeilen (Variablen, Add, Maximize) entstehen\")\n",
|
||||||
|
" print(\"direkt aus den fuenf Zeilen der Vorlage - das ist der ganze Trick.\")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Umsetzung mit Google OR-Tools\n",
|
||||||
|
"\n",
|
||||||
|
"`Bot_Allokation.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# Bot_Allokation.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel Einfuehrung: Erstes Optimierungsmodell mit Google OR-Tools (CP-SAT).\n",
|
||||||
|
"Problem: Server-Allokation für Trading-Bots.\n",
|
||||||
|
"\n",
|
||||||
|
"Modell mit expliziter Nebenbedingung statt versteckter Variablengrenze,\n",
|
||||||
|
"vollständiger Statusauswertung und Ergebnisprüfung.\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"from ortools.sat.python import cp_model\n",
|
||||||
|
"\n",
|
||||||
|
"# --- Parameter (die \"Fakten\" des Problems) ---------------------------------\n",
|
||||||
|
"GEWINN_A, GEWINN_B = 150, 250 # Euro pro Bot und Tag\n",
|
||||||
|
"CPU_A, CPU_B = 2, 5 # vCPU-Bedarf je Bot\n",
|
||||||
|
"RAM_A, RAM_B = 4, 6 # GB Arbeitsspeicher je Bot\n",
|
||||||
|
"CPU_GESAMT, RAM_GESAMT = 40, 60 # verfügbare Kapazitäten\n",
|
||||||
|
"MAX_ARBITRAGE = 8 # Marktliquiditätsgrenze\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def loese_bot_allokation(zeitlimit_s: float = 10.0):\n",
|
||||||
|
" \"\"\"Baut das Modell, löst es und gibt eine Auswertung aus.\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
" # 1. Modell instanziieren\n",
|
||||||
|
" modell = cp_model.CpModel()\n",
|
||||||
|
"\n",
|
||||||
|
" # 2. Entscheidungsvariablen anlegen\n",
|
||||||
|
" # NewIntVar(untere_grenze, obere_grenze, name)\n",
|
||||||
|
" # Die obere Grenze ist bewusst großzügig; die echte Beschränkung\n",
|
||||||
|
" # formulieren wir unten als Nebenbedingung, damit das Modell die\n",
|
||||||
|
" # mathematische Formulierung 1:1 abbildet.\n",
|
||||||
|
" x_a = modell.NewIntVar(0, 100, \"Arbitrage_Bots\")\n",
|
||||||
|
" x_b = modell.NewIntVar(0, 100, \"Trend_Bots\")\n",
|
||||||
|
"\n",
|
||||||
|
" # 3. Nebenbedingungen definieren\n",
|
||||||
|
" c_cpu = modell.Add(CPU_A * x_a + CPU_B * x_b <= CPU_GESAMT) # vCPU-Limit\n",
|
||||||
|
" c_ram = modell.Add(RAM_A * x_a + RAM_B * x_b <= RAM_GESAMT) # RAM-Limit\n",
|
||||||
|
" c_liq = modell.Add(x_a <= MAX_ARBITRAGE) # Marktliquidität\n",
|
||||||
|
"\n",
|
||||||
|
" # 4. Zielfunktion: Maximiere den Tagesgewinn\n",
|
||||||
|
" modell.Maximize(GEWINN_A * x_a + GEWINN_B * x_b)\n",
|
||||||
|
"\n",
|
||||||
|
" # 5. Solver konfigurieren und ausführen\n",
|
||||||
|
" loeser = cp_model.CpSolver()\n",
|
||||||
|
" loeser.parameters.max_time_in_seconds = zeitlimit_s\n",
|
||||||
|
" status = loeser.Solve(modell)\n",
|
||||||
|
"\n",
|
||||||
|
" # 6. Status auswerten -- IMMER alle Fälle behandeln\n",
|
||||||
|
" status_text = loeser.StatusName(status)\n",
|
||||||
|
" if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE):\n",
|
||||||
|
" print(f\"Keine verwertbare Lösung. Solver-Status: {status_text}\")\n",
|
||||||
|
" if status == cp_model.INFEASIBLE:\n",
|
||||||
|
" print(\"Das Modell ist unlösbar - die Bedingungen widersprechen sich.\")\n",
|
||||||
|
" return None\n",
|
||||||
|
"\n",
|
||||||
|
" a, b = loeser.Value(x_a), loeser.Value(x_b)\n",
|
||||||
|
" gewinn = loeser.ObjectiveValue()\n",
|
||||||
|
"\n",
|
||||||
|
" # 7. Ergebnis ausgeben\n",
|
||||||
|
" print(\"=\" * 58)\n",
|
||||||
|
" print(\" OPTIMALE BOT-ALLOKATION\")\n",
|
||||||
|
" print(\"=\" * 58)\n",
|
||||||
|
" print(f\"Solver-Status: {status_text}\"\n",
|
||||||
|
" f\"{' (beweisbar optimal)' if status == cp_model.OPTIMAL else ' (zulässig, nicht bewiesen)'}\")\n",
|
||||||
|
" print(f\"Arbitrage-Bots (x_A): {a} Instanzen\")\n",
|
||||||
|
" print(f\"Trendfolge-Bots (x_B): {b} Instanzen\")\n",
|
||||||
|
" print(f\"Täglicher Max-Gewinn: {gewinn:,.2f} EUR\")\n",
|
||||||
|
"\n",
|
||||||
|
" genutzt_cpu = CPU_A * a + CPU_B * b\n",
|
||||||
|
" genutzt_ram = RAM_A * a + RAM_B * b\n",
|
||||||
|
" print(\"-\" * 58)\n",
|
||||||
|
" print(f\"CPU-Auslastung: {genutzt_cpu} / {CPU_GESAMT} vCPUs \"\n",
|
||||||
|
" f\"({genutzt_cpu / CPU_GESAMT * 100:.1f} %)\")\n",
|
||||||
|
" print(f\"RAM-Auslastung: {genutzt_ram} / {RAM_GESAMT} GB \"\n",
|
||||||
|
" f\"({genutzt_ram / RAM_GESAMT * 100:.1f} %)\")\n",
|
||||||
|
" print(f\"Arbitrage-Limit: {a} / {MAX_ARBITRAGE}\")\n",
|
||||||
|
"\n",
|
||||||
|
" # 8. Selbstkontrolle: Sind wirklich alle Bedingungen eingehalten?\n",
|
||||||
|
" assert genutzt_cpu <= CPU_GESAMT, \"vCPU-Limit verletzt!\"\n",
|
||||||
|
" assert genutzt_ram <= RAM_GESAMT, \"RAM-Limit verletzt!\"\n",
|
||||||
|
" assert a <= MAX_ARBITRAGE, \"Arbitrage-Limit verletzt!\"\n",
|
||||||
|
"\n",
|
||||||
|
" # 9. Zum Vergleich: die naive Strategie \"nur der lukrativere Bot\"\n",
|
||||||
|
" naiv_b = min(CPU_GESAMT // CPU_B, RAM_GESAMT // RAM_B)\n",
|
||||||
|
" naiv_gewinn = GEWINN_B * naiv_b\n",
|
||||||
|
" print(\"-\" * 58)\n",
|
||||||
|
" print(f\"Naive Strategie (nur Trendfolge): {naiv_b} Bots -> {naiv_gewinn:,.2f} EUR\")\n",
|
||||||
|
" print(f\"Vorteil der Optimierung: {gewinn - naiv_gewinn:,.2f} EUR pro Tag \"\n",
|
||||||
|
" f\"({(gewinn / naiv_gewinn - 1) * 100:.1f} %)\")\n",
|
||||||
|
" print(\"=\" * 58)\n",
|
||||||
|
" return a, b, gewinn\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"if __name__ == \"__main__\":\n",
|
||||||
|
" loese_bot_allokation()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Von Excel zu Python: Ihre Daten liegen schon da\n",
|
||||||
|
"\n",
|
||||||
|
"`Excel_Bruecke.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# Excel_Bruecke.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel Einfuehrung: Vom Excel-Solver zu Python - dieselbe Rechnung, dieselbe Datei.\n",
|
||||||
|
"\n",
|
||||||
|
"Das Programm bildet den vollstaendigen Arbeitsweg ab, den ein Excel-Modell im\n",
|
||||||
|
"Betrieb sonst von Hand geht:\n",
|
||||||
|
"\n",
|
||||||
|
" Tabellenblatt lesen -> Modell bauen -> loesen -> Ergebnis zurueckschreiben\n",
|
||||||
|
"\n",
|
||||||
|
"Damit es ohne Vorbereitung laeuft, legt es die Eingabedatei beim ersten Start\n",
|
||||||
|
"selbst an. Ersetzen Sie sie durch Ihre eigene Datei - solange die Spalten\n",
|
||||||
|
"gleich heissen, aendert sich am Code nichts.\n",
|
||||||
|
"\n",
|
||||||
|
"Die vier Arbeitsschritte kommen aus or_kern.py (Kapitel Praxisfallen). Was\n",
|
||||||
|
"hier steht, ist nur noch das, was diesen Fall ausmacht - der Rest ist\n",
|
||||||
|
"gemeinsamer Unterbau. Wer das Modul nicht kennt: Es ist im Buch vollstaendig\n",
|
||||||
|
"abgedruckt und liegt im selben Verzeichnis.\n",
|
||||||
|
"\n",
|
||||||
|
"Erzeugte Dateien (im Arbeitsverzeichnis):\n",
|
||||||
|
" produktionsmix.xlsx Eingabe: Blaetter 'Produkte' und 'Kapazitaeten'\n",
|
||||||
|
" produktionsmix_ergebnis.xlsx Ausgabe: Blaetter 'Plan' und 'Kennzahlen'\n",
|
||||||
|
"\n",
|
||||||
|
"Benoetigt: pandas, openpyxl, ortools, pydantic (ueber or_kern)\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"from __future__ import annotations\n",
|
||||||
|
"\n",
|
||||||
|
"import os\n",
|
||||||
|
"\n",
|
||||||
|
"import pandas as pd\n",
|
||||||
|
"\n",
|
||||||
|
"from or_kern import (lade_produktionsproblem, loese_mit_glop, pruefe_loesung,\n",
|
||||||
|
" schreibe_ergebnis)\n",
|
||||||
|
"\n",
|
||||||
|
"EINGABE = \"produktionsmix.xlsx\"\n",
|
||||||
|
"AUSGABE = \"produktionsmix_ergebnis.xlsx\"\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def lege_beispieldatei_an(pfad: str) -> None:\n",
|
||||||
|
" \"\"\"Schreibt die Eingabemappe, wie sie auch aus dem Controlling kaeme.\"\"\"\n",
|
||||||
|
" produkte = pd.DataFrame({\n",
|
||||||
|
" \"Produkt\": [\"Tisch\", \"Stuhl\", \"Regal\"],\n",
|
||||||
|
" \"Deckungsbeitrag\": [240.0, 60.0, 130.0],\n",
|
||||||
|
" \"Montagestunden\": [3.0, 1.0, 2.0],\n",
|
||||||
|
" \"Plattenmaterial\": [6.0, 1.0, 4.0],\n",
|
||||||
|
" })\n",
|
||||||
|
" kapazitaeten = pd.DataFrame({\n",
|
||||||
|
" \"Ressource\": [\"Montagestunden\", \"Plattenmaterial\"],\n",
|
||||||
|
" \"Verfuegbar\": [150.0, 240.0],\n",
|
||||||
|
" })\n",
|
||||||
|
" with pd.ExcelWriter(pfad, engine=\"openpyxl\") as mappe:\n",
|
||||||
|
" produkte.to_excel(mappe, sheet_name=\"Produkte\", index=False)\n",
|
||||||
|
" kapazitaeten.to_excel(mappe, sheet_name=\"Kapazitaeten\", index=False)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"if __name__ == \"__main__\":\n",
|
||||||
|
" if not os.path.exists(EINGABE):\n",
|
||||||
|
" lege_beispieldatei_an(EINGABE)\n",
|
||||||
|
" print(f\"Beispiel-Eingabedatei angelegt: {EINGABE}\")\n",
|
||||||
|
"\n",
|
||||||
|
" # 1. Lesen und pruefen. Faellt hier etwas auf, ist der Fehler noch\n",
|
||||||
|
" # zuzuordnen - spaeter waere er nur noch eine merkwuerdige Zahl.\n",
|
||||||
|
" problem = lade_produktionsproblem(EINGABE)\n",
|
||||||
|
"\n",
|
||||||
|
" # 2. Loesen. Einzige solverabhaengige Zeile des ganzen Programms -\n",
|
||||||
|
" # 'loese_mit_scipy' waere ein Einzeiler-Wechsel.\n",
|
||||||
|
" loesung = loese_mit_glop(problem)\n",
|
||||||
|
"\n",
|
||||||
|
" # 3. Abnahmepruefung gegen die Anforderungen, ohne den Solver zu fragen.\n",
|
||||||
|
" beanstandungen = pruefe_loesung(problem, loesung)\n",
|
||||||
|
" if beanstandungen:\n",
|
||||||
|
" raise SystemExit(\"Abnahmepruefung fehlgeschlagen:\\n - \"\n",
|
||||||
|
" + \"\\n - \".join(beanstandungen))\n",
|
||||||
|
"\n",
|
||||||
|
" # 4. Zurueckschreiben - im Format, das die Fachabteilung ohnehin benutzt.\n",
|
||||||
|
" schreibe_ergebnis(AUSGABE, problem, loesung)\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"=\" * 66)\n",
|
||||||
|
" print(\" PRODUKTIONSPLAN AUS DER EXCEL-MAPPE\")\n",
|
||||||
|
" print(\"=\" * 66)\n",
|
||||||
|
" for produkt in problem.produkte:\n",
|
||||||
|
" menge = loesung.werte[produkt.name]\n",
|
||||||
|
" print(f\"{produkt.name:<18} {menge:8.1f} Stueck \"\n",
|
||||||
|
" f\"{menge * produkt.deckungsbeitrag:12,.2f} EUR\")\n",
|
||||||
|
" print(\"-\" * 66)\n",
|
||||||
|
" print(loesung.als_bericht())\n",
|
||||||
|
" print(\"Abnahmepruefung: bestanden\\n\")\n",
|
||||||
|
"\n",
|
||||||
|
" # Die Schattenpreise beantworten die Frage, die der Plan nicht stellt:\n",
|
||||||
|
" # Was waere eine zusaetzliche Einheit dieser Ressource wert?\n",
|
||||||
|
" for ressource, preis in loesung.schattenpreise.items():\n",
|
||||||
|
" print(f\" eine Einheit {ressource:<18} mehr waere wert: {preis:7.2f} EUR\")\n",
|
||||||
|
" print(\"=\" * 66)\n",
|
||||||
|
" print(f\"Ergebnis geschrieben nach: {AUSGABE}\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"name": "python",
|
||||||
|
"version": "3.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
540
Notebooks_04/finanzdaten.ipynb
Normal file
540
Notebooks_04/finanzdaten.ipynb
Normal file
|
|
@ -0,0 +1,540 @@
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"# Kapitel 18: Finanzdaten-Modellierung — Renditen, Kovarianz und Shrinkage\n",
|
||||||
|
"\n",
|
||||||
|
"Begleitnotebook zu *Optimierte Entscheidungsfindung mit Python*. Die Codezellen sind identisch mit den im Buch abgedruckten Programmen.\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"# Einmalig ausfuehren: installiert alle im Buch verwendeten Pakete.\n",
|
||||||
|
"# Lokal in einer virtuellen Umgebung genauso gueltig wie in Google Colab.\n",
|
||||||
|
"%pip install --quiet ortools highspy cvxpy scipy numpy pandas polars \\\n",
|
||||||
|
" scikit-learn matplotlib plotly pyomo linopy pymoo pydantic openpyxl"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Logarithmische Rendite\n",
|
||||||
|
"\n",
|
||||||
|
"`Renditen_Vergleich.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# Renditen_Vergleich.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel Finanzdaten: Diskrete vs. logarithmische Renditen.\n",
|
||||||
|
"Zeigt an einem simulierten Kursverlauf, welche Eigenschaft wo gilt -\n",
|
||||||
|
"und warum das arithmetische Mittel diskreter Renditen in die Irre fuehrt.\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"import numpy as np\n",
|
||||||
|
"import pandas as pd\n",
|
||||||
|
"\n",
|
||||||
|
"if __name__ == \"__main__\":\n",
|
||||||
|
" kurse = np.array([100.0, 150.0, 100.0, 120.0, 90.0, 135.0])\n",
|
||||||
|
" tage = [f\"t{i}\" for i in range(len(kurse))]\n",
|
||||||
|
"\n",
|
||||||
|
" diskret = kurse[1:] / kurse[:-1] - 1.0\n",
|
||||||
|
" logarithmisch = np.log(kurse[1:] / kurse[:-1])\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"=\" * 74)\n",
|
||||||
|
" print(\" DISKRETE UND LOGARITHMISCHE RENDITEN IM VERGLEICH\")\n",
|
||||||
|
" print(\"=\" * 74)\n",
|
||||||
|
" tabelle = pd.DataFrame({\n",
|
||||||
|
" \"Periode\": [f\"{tage[i]} -> {tage[i+1]}\" for i in range(len(diskret))],\n",
|
||||||
|
" \"Kurs von\": kurse[:-1],\n",
|
||||||
|
" \"Kurs bis\": kurse[1:],\n",
|
||||||
|
" \"diskret R\": [f\"{r*100:+7.2f} %\" for r in diskret],\n",
|
||||||
|
" \"log r\": [f\"{r:+8.4f}\" for r in logarithmisch],\n",
|
||||||
|
" })\n",
|
||||||
|
" print(tabelle.to_string(index=False))\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Zeitliche Aggregation ------------------------------------------\n",
|
||||||
|
" gesamt_wahr = kurse[-1] / kurse[0] - 1.0\n",
|
||||||
|
" summe_log = logarithmisch.sum()\n",
|
||||||
|
" aus_log_zurueck = np.exp(summe_log) - 1.0\n",
|
||||||
|
" summe_diskret = diskret.sum()\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\n--- Aggregation ueber die Zeit ---\")\n",
|
||||||
|
" print(f\" Tatsaechliche Gesamtrendite: {gesamt_wahr*100:+8.2f} %\")\n",
|
||||||
|
" print(f\" Summe der Log-Renditen -> exp()-1: {aus_log_zurueck*100:+8.2f} % \"\n",
|
||||||
|
" f\"{'KORREKT' if abs(aus_log_zurueck - gesamt_wahr) < 1e-9 else 'falsch'}\")\n",
|
||||||
|
" print(f\" Summe der diskreten Renditen: {summe_diskret*100:+8.2f} % FALSCH\")\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Mittelwerte ------------------------------------------------------\n",
|
||||||
|
" arithmetisch = diskret.mean()\n",
|
||||||
|
" geometrisch = np.prod(1 + diskret) ** (1 / len(diskret)) - 1\n",
|
||||||
|
" aus_log = np.exp(logarithmisch.mean()) - 1\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\n--- Welcher Mittelwert ist der richtige? ---\")\n",
|
||||||
|
" print(f\" Arithmetisches Mittel (diskret): {arithmetisch*100:+8.2f} % \"\n",
|
||||||
|
" f\"-> ueberschaetzt\")\n",
|
||||||
|
" print(f\" Geometrisches Mittel: {geometrisch*100:+8.2f} % -> korrekt\")\n",
|
||||||
|
" print(f\" exp(Mittel der Log-Renditen) - 1: {aus_log*100:+8.2f} % \"\n",
|
||||||
|
" f\"-> identisch zum geometrischen\")\n",
|
||||||
|
"\n",
|
||||||
|
" # Probe: Endkapital mit dem jeweiligen Mittelwert hochgerechnet\n",
|
||||||
|
" n = len(diskret)\n",
|
||||||
|
" print(f\"\\n Probe - Startkapital 100 EUR ueber {n} Perioden:\")\n",
|
||||||
|
" print(f\" tatsaechlich: {kurse[-1]:8.2f} EUR\")\n",
|
||||||
|
" print(f\" mit arithm. Mittel hochgerechnet: {100*(1+arithmetisch)**n:8.2f} EUR\")\n",
|
||||||
|
" print(f\" mit geom. Mittel hochgerechnet: {100*(1+geometrisch)**n:8.2f} EUR\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\n--- Annualisierung (252 Handelstage) ---\")\n",
|
||||||
|
" print(f\" Volatilitaet aus Log-Renditen: \"\n",
|
||||||
|
" f\"{logarithmisch.std(ddof=1)*np.sqrt(252)*100:.2f} % p.a.\")\n",
|
||||||
|
" print(\" (Die Wurzel-Zeit-Regel gilt nur bei unabhaengigen Renditen -\")\n",
|
||||||
|
" print(\" fuer reale Maerkte ist sie eine Naeherung.)\")\n",
|
||||||
|
" print(\"=\" * 74)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Das Schätzfehler-Problem\n",
|
||||||
|
"\n",
|
||||||
|
"`Schaetzrauschen_Demo.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# Schaetzrauschen_Demo.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel Finanzdaten: Der Error-Maximizer-Effekt im Experiment.\n",
|
||||||
|
"\n",
|
||||||
|
"Aufbau: Wir KENNEN die wahre Kovarianzmatrix, weil wir die Daten selbst\n",
|
||||||
|
"erzeugen. Dann schaetzen wir sie aus endlich vielen Beobachtungen und\n",
|
||||||
|
"vergleichen, wie gut das daraus optimierte Portfolio in der WAHREN Welt\n",
|
||||||
|
"abschneidet - mit und ohne Shrinkage.\n",
|
||||||
|
"\n",
|
||||||
|
"Das ist der einzige saubere Weg, Schaetzfehler zu messen: Man braucht eine\n",
|
||||||
|
"Wahrheit zum Vergleich, und die gibt es nur in der Simulation.\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"import numpy as np\n",
|
||||||
|
"from sklearn.covariance import LedoitWolf\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def erzeuge_wahre_kovarianz(n, rng):\n",
|
||||||
|
" \"\"\"Ein-Faktor-Modell: gemeinsamer Marktfaktor plus titelspezifisches Rauschen.\"\"\"\n",
|
||||||
|
" beta = rng.uniform(0.6, 1.4, size=n) # Marktsensitivitaeten\n",
|
||||||
|
" var_markt = 0.04 # Marktvarianz p.a.\n",
|
||||||
|
" var_spezifisch = rng.uniform(0.01, 0.09, size=n) # idiosynkratische Varianz\n",
|
||||||
|
" return np.outer(beta, beta) * var_markt + np.diag(var_spezifisch)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def minimum_varianz_gewichte(sigma):\n",
|
||||||
|
" \"\"\"Analytische GMV-Loesung: w = Sigma^-1 1 / (1' Sigma^-1 1). Ohne Restriktionen.\"\"\"\n",
|
||||||
|
" eins = np.ones(len(sigma))\n",
|
||||||
|
" try:\n",
|
||||||
|
" loesung = np.linalg.solve(sigma, eins)\n",
|
||||||
|
" except np.linalg.LinAlgError:\n",
|
||||||
|
" loesung = np.linalg.pinv(sigma) @ eins # falls singulaer\n",
|
||||||
|
" return loesung / loesung.sum()\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def portfoliovolatilitaet(w, sigma):\n",
|
||||||
|
" return float(np.sqrt(w @ sigma @ w))\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"if __name__ == \"__main__\":\n",
|
||||||
|
" rng = np.random.default_rng(2026)\n",
|
||||||
|
" N = 40 # Anzahl Titel\n",
|
||||||
|
" WIEDERHOLUNGEN = 200\n",
|
||||||
|
"\n",
|
||||||
|
" sigma_wahr = erzeuge_wahre_kovarianz(N, rng)\n",
|
||||||
|
" w_ideal = minimum_varianz_gewichte(sigma_wahr)\n",
|
||||||
|
" vola_ideal = portfoliovolatilitaet(w_ideal, sigma_wahr)\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"=\" * 92)\n",
|
||||||
|
" print(\" ERROR-MAXIMIZER: WIE TEUER IST SCHAETZRAUSCHEN?\")\n",
|
||||||
|
" print(\"=\" * 92)\n",
|
||||||
|
" print(f\"Aufbau: {N} Titel, wahre Kovarianz bekannt (Ein-Faktor-Modell).\")\n",
|
||||||
|
" print(f\"Bestmoegliche Volatilitaet bei perfektem Wissen: \"\n",
|
||||||
|
" f\"{vola_ideal*100:.2f} % p.a.\\n\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(f\"{'T (Tage)':>9} | {'T/N':>5} | {'Stichprobe':>22} | {'Ledoit-Wolf':>22} | \"\n",
|
||||||
|
" f\"{'Shrink':>7}\")\n",
|
||||||
|
" print(f\"{'':>9} | {'':>5} | {'Vola Aufschlag':>22} | \"\n",
|
||||||
|
" f\"{'Vola Aufschlag':>22} | {'delta':>7}\")\n",
|
||||||
|
" print(\"-\" * 92)\n",
|
||||||
|
"\n",
|
||||||
|
" for T in [60, 120, 252, 504, 1260]:\n",
|
||||||
|
" vola_stichprobe, vola_lw, deltas = [], [], []\n",
|
||||||
|
"\n",
|
||||||
|
" for _ in range(WIEDERHOLUNGEN):\n",
|
||||||
|
" # Daten aus der WAHREN Verteilung ziehen (taeglich)\n",
|
||||||
|
" daten = rng.multivariate_normal(np.zeros(N), sigma_wahr / 252, size=T)\n",
|
||||||
|
"\n",
|
||||||
|
" # (a) Stichproben-Kovarianz\n",
|
||||||
|
" s_stich = np.cov(daten, rowvar=False, ddof=1) * 252\n",
|
||||||
|
" w_stich = minimum_varianz_gewichte(s_stich)\n",
|
||||||
|
"\n",
|
||||||
|
" # (b) Ledoit-Wolf-Shrinkage\n",
|
||||||
|
" lw = LedoitWolf().fit(daten)\n",
|
||||||
|
" s_lw = lw.covariance_ * 252\n",
|
||||||
|
" w_lw = minimum_varianz_gewichte(s_lw)\n",
|
||||||
|
" deltas.append(lw.shrinkage_)\n",
|
||||||
|
"\n",
|
||||||
|
" # Bewertung IMMER mit der wahren Kovarianz - das ist der Punkt\n",
|
||||||
|
" vola_stichprobe.append(portfoliovolatilitaet(w_stich, sigma_wahr))\n",
|
||||||
|
" vola_lw.append(portfoliovolatilitaet(w_lw, sigma_wahr))\n",
|
||||||
|
"\n",
|
||||||
|
" m_stich, m_lw = np.mean(vola_stichprobe), np.mean(vola_lw)\n",
|
||||||
|
" print(f\"{T:>9} | {T/N:>5.1f} | {m_stich*100:>8.2f} % \"\n",
|
||||||
|
" f\"{(m_stich/vola_ideal-1)*100:>+9.1f} % | \"\n",
|
||||||
|
" f\"{m_lw*100:>8.2f} % {(m_lw/vola_ideal-1)*100:>+9.1f} % | \"\n",
|
||||||
|
" f\"{np.mean(deltas):>7.3f}\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"-\" * 92)\n",
|
||||||
|
" print(\"'Aufschlag' = wie viel mehr Risiko das Portfolio in der WAHREN Welt traegt,\")\n",
|
||||||
|
" print(\"verglichen mit dem Portfolio bei perfektem Wissen.\")\n",
|
||||||
|
" print(\"Lesart: Je kleiner T/N, desto teurer das Schaetzrauschen - und desto mehr\")\n",
|
||||||
|
" print(\"schrumpft Ledoit-Wolf (delta steigt).\")\n",
|
||||||
|
" print(\"=\" * 92)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Praxis: Datenpipeline mit korrekter Spaltenreihenfolge\n",
|
||||||
|
"\n",
|
||||||
|
"`Finanzdaten_Ledoit_Wolf.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# Finanzdaten_Ledoit_Wolf.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel Finanzdaten: Automatisierte Finanzdaten-Pipeline und Kovarianz-Shrinkage.\n",
|
||||||
|
"\n",
|
||||||
|
"Eigenschaften:\n",
|
||||||
|
" * Spaltenreihenfolge wird erzwungen und per assert geprueft\n",
|
||||||
|
" * beide Shrinkage-Ziele werden berechnet und verglichen\n",
|
||||||
|
" * Datenqualitaetspruefungen (Luecken, Ausreisser, Mindestlaenge)\n",
|
||||||
|
" * Eigenwertspektrum wird ausgewiesen, nicht nur die Konditionszahl\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"import numpy as np\n",
|
||||||
|
"import pandas as pd\n",
|
||||||
|
"import yfinance as yf\n",
|
||||||
|
"from sklearn.covariance import LedoitWolf\n",
|
||||||
|
"\n",
|
||||||
|
"HANDELSTAGE = 252\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def lade_kurse(tickers: list[str], start, ende) -> pd.DataFrame:\n",
|
||||||
|
" \"\"\"Laedt bereinigte Schlusskurse und garantiert die Spaltenreihenfolge.\"\"\"\n",
|
||||||
|
" print(f\"Lade {len(tickers)} Ticker von {start} bis {ende} ...\")\n",
|
||||||
|
" roh = yf.download(tickers, start=start, end=ende, auto_adjust=True, progress=False)\n",
|
||||||
|
" if roh.empty:\n",
|
||||||
|
" raise SystemExit(\"Download fehlgeschlagen (Netz, Ticker oder Rate-Limit pruefen).\")\n",
|
||||||
|
"\n",
|
||||||
|
" if isinstance(roh.columns, pd.MultiIndex):\n",
|
||||||
|
" # [tickers] erzwingt die gewuenschte Spaltenreihenfolge\n",
|
||||||
|
" kurse = roh[\"Close\"][tickers].dropna()\n",
|
||||||
|
" else:\n",
|
||||||
|
" kurse = roh[[\"Close\"]].dropna()\n",
|
||||||
|
" kurse.columns = tickers\n",
|
||||||
|
"\n",
|
||||||
|
" assert list(kurse.columns) == tickers, (\n",
|
||||||
|
" f\"Spaltenreihenfolge weicht ab!\\n erwartet: {tickers}\\n\"\n",
|
||||||
|
" f\" erhalten: {list(kurse.columns)}\")\n",
|
||||||
|
" return kurse\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def pruefe_datenqualitaet(kurse: pd.DataFrame, mindest_tage: int = 200) -> None:\n",
|
||||||
|
" \"\"\"Faengt die haeufigsten Datenprobleme ab, bevor sie ins Modell gelangen.\"\"\"\n",
|
||||||
|
" T, N = kurse.shape\n",
|
||||||
|
" print(f\"\\nDatenqualitaet: {T} Handelstage, {N} Titel (T/N = {T/N:.1f})\")\n",
|
||||||
|
" if T < mindest_tage:\n",
|
||||||
|
" raise SystemExit(f\"Zu wenige Beobachtungen ({T} < {mindest_tage}).\")\n",
|
||||||
|
" if T < N:\n",
|
||||||
|
" print(\" WARNUNG: T < N - die Stichprobenkovarianz ist singulaer!\")\n",
|
||||||
|
" elif T < 3 * N:\n",
|
||||||
|
" print(\" WARNUNG: T < 3N - erhebliches Schaetzrauschen zu erwarten.\")\n",
|
||||||
|
"\n",
|
||||||
|
" renditen = kurse.pct_change().dropna()\n",
|
||||||
|
" extreme = (renditen.abs() > 0.25).sum().sum()\n",
|
||||||
|
" if extreme:\n",
|
||||||
|
" print(f\" Hinweis: {extreme} Tagesrenditen ueber 25 % \"\n",
|
||||||
|
" f\"(Splits, Sondersituationen oder Datenfehler pruefen).\")\n",
|
||||||
|
" luecken = kurse.isna().sum().sum()\n",
|
||||||
|
" print(f\" Fehlende Werte nach dropna: {luecken}\")\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def ziel_konstante_korrelation(renditen: np.ndarray) -> np.ndarray:\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" Shrinkage-Ziel nach Ledoit/Wolf 2003: Einzelvarianzen behalten,\n",
|
||||||
|
" alle Korrelationen durch ihren Mittelwert ersetzen.\n",
|
||||||
|
" (sklearn nutzt stattdessen die skalierte Einheitsmatrix - siehe oben.)\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" S = np.cov(renditen, rowvar=False, ddof=1)\n",
|
||||||
|
" d = np.sqrt(np.diag(S))\n",
|
||||||
|
" korrelation = S / np.outer(d, d)\n",
|
||||||
|
" n = len(S)\n",
|
||||||
|
" r_quer = (korrelation.sum() - n) / (n * (n - 1)) # Mittel ohne Diagonale\n",
|
||||||
|
" F = r_quer * np.outer(d, d)\n",
|
||||||
|
" np.fill_diagonal(F, np.diag(S))\n",
|
||||||
|
" return F\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def analysiere(kurse: pd.DataFrame):\n",
|
||||||
|
" renditen = kurse.pct_change().dropna()\n",
|
||||||
|
" T, N = renditen.shape\n",
|
||||||
|
"\n",
|
||||||
|
" mu = renditen.mean().values * HANDELSTAGE\n",
|
||||||
|
" sigma_stichprobe = renditen.cov().values * HANDELSTAGE\n",
|
||||||
|
"\n",
|
||||||
|
" lw = LedoitWolf(assume_centered=False).fit(renditen.values)\n",
|
||||||
|
" sigma_lw = lw.covariance_ * HANDELSTAGE\n",
|
||||||
|
" delta = lw.shrinkage_\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\n\" + \"=\" * 84)\n",
|
||||||
|
" print(\" FINANZDATEN-PIPELINE UND MATRIX-KONDITIONIERUNG\")\n",
|
||||||
|
" print(\"=\" * 84)\n",
|
||||||
|
" print(f\"Beobachtungen T: {T}\")\n",
|
||||||
|
" print(f\"Titel N: {N}\")\n",
|
||||||
|
" print(f\"Optimales Shrinkage delta: {delta:.4f} \"\n",
|
||||||
|
" f\"({delta*100:.1f} % Gewicht auf dem strukturierten Ziel)\")\n",
|
||||||
|
"\n",
|
||||||
|
" eig_stich = np.linalg.eigvalsh(sigma_stichprobe)\n",
|
||||||
|
" eig_lw = np.linalg.eigvalsh(sigma_lw)\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\n--- Eigenwertspektrum (annualisiert) ---\")\n",
|
||||||
|
" print(f\"{'':<14} {'kleinster':>12} {'Median':>12} {'groesster':>12} \"\n",
|
||||||
|
" f\"{'Kondition':>12}\")\n",
|
||||||
|
" for name, eig in [(\"Stichprobe\", eig_stich), (\"Ledoit-Wolf\", eig_lw)]:\n",
|
||||||
|
" kondition = eig.max() / max(eig.min(), 1e-12)\n",
|
||||||
|
" print(f\"{name:<14} {eig.min():>12.6f} {np.median(eig):>12.6f} \"\n",
|
||||||
|
" f\"{eig.max():>12.6f} {kondition:>12.1f}\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\nDie Konditionszahl misst, wie stark sich kleine Datenaenderungen auf\")\n",
|
||||||
|
" print(\"die Inverse auswirken. Je kleiner, desto stabiler die Portfoliogewichte.\")\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Vergleich der beiden Shrinkage-Ziele ----------------------------\n",
|
||||||
|
" S_taeglich = np.cov(renditen.values, rowvar=False, ddof=1)\n",
|
||||||
|
" F_identitaet = np.eye(N) * np.trace(S_taeglich) / N\n",
|
||||||
|
" F_korrelation = ziel_konstante_korrelation(renditen.values)\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\n--- Die beiden Shrinkage-Ziele im Vergleich ---\")\n",
|
||||||
|
" for name, F in [(\"skalierte Einheitsmatrix (sklearn)\", F_identitaet),\n",
|
||||||
|
" (\"konstante Korrelation (LW 2003)\", F_korrelation)]:\n",
|
||||||
|
" gemischt = ((1 - delta) * S_taeglich + delta * F) * HANDELSTAGE\n",
|
||||||
|
" eig = np.linalg.eigvalsh(gemischt)\n",
|
||||||
|
" print(f\" {name:<36}: Kondition {eig.max()/max(eig.min(),1e-12):8.1f}, \"\n",
|
||||||
|
" f\"kleinster EW {eig.min():.6f}\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\n--- Erwartete Renditen (annualisiert) ---\")\n",
|
||||||
|
" uebersicht = pd.DataFrame({\n",
|
||||||
|
" \"Ticker\": kurse.columns,\n",
|
||||||
|
" \"Rendite p.a.\": [f\"{r*100:+7.2f} %\" for r in mu],\n",
|
||||||
|
" \"Volatilitaet p.a.\": [f\"{np.sqrt(sigma_lw[i, i])*100:6.2f} %\" for i in range(N)],\n",
|
||||||
|
" })\n",
|
||||||
|
" print(uebersicht.to_string(index=False))\n",
|
||||||
|
" print(\"=\" * 84)\n",
|
||||||
|
" return renditen, mu, sigma_stichprobe, sigma_lw\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"if __name__ == \"__main__\":\n",
|
||||||
|
" UNIVERSUM = [\"AAPL\", \"MSFT\", \"NVDA\", \"AMZN\", # Technologie\n",
|
||||||
|
" \"JNJ\", \"PFE\", # Gesundheit\n",
|
||||||
|
" \"JPM\", \"GS\", # Banken\n",
|
||||||
|
" \"XOM\", \"CVX\"] # Energie\n",
|
||||||
|
"\n",
|
||||||
|
" # Relatives 2-Jahres-Fenster, damit das Beispiel nicht \"altert\"\n",
|
||||||
|
" ende = pd.Timestamp.today().normalize()\n",
|
||||||
|
" start = ende - pd.DateOffset(years=2)\n",
|
||||||
|
"\n",
|
||||||
|
" kurse = lade_kurse(UNIVERSUM, start.strftime(\"%Y-%m-%d\"), ende.strftime(\"%Y-%m-%d\"))\n",
|
||||||
|
" pruefe_datenqualitaet(kurse)\n",
|
||||||
|
" analysiere(kurse)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Finde den Denkfehler\n",
|
||||||
|
"\n",
|
||||||
|
"`Kovarianz_Falle.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# Kovarianz_Falle.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel Finanzdaten: Warum die historische Kovarianzmatrix den Optimierer belaeugt.\n",
|
||||||
|
"\n",
|
||||||
|
"Ein Portfolio aus 30 Anlagen soll das Risiko minimieren. Geschaetzt wird die\n",
|
||||||
|
"Kovarianzmatrix aus 20 Beobachtungen - in der Praxis ein voellig ueblicher\n",
|
||||||
|
"Fall (30 Titel, ein Quartal Wochendaten).\n",
|
||||||
|
"\n",
|
||||||
|
"Das Ergebnis ist ein Portfolio mit einem gemessenen Risiko von 0,000 %. Der\n",
|
||||||
|
"Optimierer haelt es fuer risikofrei. Ausserhalb des Schaetzzeitraums ist es\n",
|
||||||
|
"das schlechteste der drei untersuchten Portfolios - schlechter sogar als\n",
|
||||||
|
"blosse Gleichgewichtung.\n",
|
||||||
|
"\n",
|
||||||
|
"Der Grund ist nicht Zufall, sondern Struktur: Bei N Anlagen und T < N\n",
|
||||||
|
"Beobachtungen hat die Stichproben-Kovarianzmatrix hoechstens Rang T-1. Es gibt\n",
|
||||||
|
"dann ganze Richtungen im Gewichtsraum, in denen sie exakt null Varianz misst -\n",
|
||||||
|
"Richtungen, die es in Wirklichkeit nicht gibt. Der Optimierer findet sie\n",
|
||||||
|
"zuverlaessig, denn er sucht ja genau danach.\n",
|
||||||
|
"\n",
|
||||||
|
"Die Daten sind synthetisch (ein Marktfaktor plus Eigenrauschen), damit das\n",
|
||||||
|
"Ergebnis reproduzierbar ist und kein Internetzugang noetig wird. Mit echten\n",
|
||||||
|
"Kursdaten sieht es genauso aus.\n",
|
||||||
|
"\n",
|
||||||
|
"Benoetigt: numpy, cvxpy, scikit-learn\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"from __future__ import annotations\n",
|
||||||
|
"\n",
|
||||||
|
"import numpy as np\n",
|
||||||
|
"import cvxpy as cp\n",
|
||||||
|
"from sklearn.covariance import LedoitWolf\n",
|
||||||
|
"\n",
|
||||||
|
"ANZAHL_ANLAGEN = 30\n",
|
||||||
|
"BEOBACHTUNGEN_SCHAETZUNG = 20 # weniger als Anlagen - genau darum geht es\n",
|
||||||
|
"BEOBACHTUNGEN_TEST = 2000 # der \"spaetere Verlauf\"\n",
|
||||||
|
"\n",
|
||||||
|
"RNG = np.random.default_rng(11)\n",
|
||||||
|
"# Jede Anlage reagiert unterschiedlich stark auf den Gesamtmarkt.\n",
|
||||||
|
"BETA = RNG.uniform(0.6, 1.4, ANZAHL_ANLAGEN)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def erzeuge_renditen(perioden: int) -> np.ndarray:\n",
|
||||||
|
" \"\"\"Ein Marktfaktor, auf den alle reagieren, plus Eigenrauschen je Anlage.\"\"\"\n",
|
||||||
|
" markt = RNG.normal(0.0, 0.010, perioden)\n",
|
||||||
|
" eigen = RNG.normal(0.0, 0.012, (perioden, ANZAHL_ANLAGEN))\n",
|
||||||
|
" return np.outer(markt, BETA) + eigen\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def minimales_risiko(kovarianz: np.ndarray) -> tuple[np.ndarray, float]:\n",
|
||||||
|
" \"\"\"Minimum-Varianz-Portfolio: Gewichte summieren sich zu 1, sonst frei.\n",
|
||||||
|
"\n",
|
||||||
|
" cp.psd_wrap() sagt CVXPY: 'Ich weiss, dass diese Matrix numerisch nicht\n",
|
||||||
|
" exakt positiv semidefinit ist - rechne trotzdem.' Das ist hier bewusst so\n",
|
||||||
|
" gewaehlt, denn genau diese Eigenschaft wollen wir vorfuehren. Im\n",
|
||||||
|
" Produktivbetrieb waere die DCP-Fehlermeldung das Warnsignal, dem man\n",
|
||||||
|
" nachgehen muss (siehe Denkfehler 8.1).\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" gewichte = cp.Variable(ANZAHL_ANLAGEN)\n",
|
||||||
|
" problem = cp.Problem(cp.Minimize(cp.quad_form(gewichte, cp.psd_wrap(kovarianz))),\n",
|
||||||
|
" [cp.sum(gewichte) == 1])\n",
|
||||||
|
" problem.solve()\n",
|
||||||
|
" return np.array(gewichte.value).ravel(), max(problem.value, 0.0)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"if __name__ == \"__main__\":\n",
|
||||||
|
" schaetzzeitraum = erzeuge_renditen(BEOBACHTUNGEN_SCHAETZUNG)\n",
|
||||||
|
" spaeterer_verlauf = erzeuge_renditen(BEOBACHTUNGEN_TEST)\n",
|
||||||
|
"\n",
|
||||||
|
" stichprobe = np.cov(schaetzzeitraum, rowvar=False)\n",
|
||||||
|
" ledoit_wolf = LedoitWolf().fit(schaetzzeitraum)\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"=\" * 78)\n",
|
||||||
|
" print(\" DIE KOVARIANZ-FALLE\")\n",
|
||||||
|
" print(\"=\" * 78)\n",
|
||||||
|
" print(f\"{ANZAHL_ANLAGEN} Anlagen, geschaetzt aus {BEOBACHTUNGEN_SCHAETZUNG} \"\n",
|
||||||
|
" f\"Beobachtungen.\")\n",
|
||||||
|
" print(f\"Rang der Stichprobenmatrix: \"\n",
|
||||||
|
" f\"{np.linalg.matrix_rank(stichprobe)} - noetig waeren {ANZAHL_ANLAGEN}.\")\n",
|
||||||
|
" print(f\"Kleinster Eigenwert: {np.linalg.eigvalsh(stichprobe).min():.2e}\")\n",
|
||||||
|
" print()\n",
|
||||||
|
" print(\"Es gibt also Richtungen, in denen diese Matrix EXAKT null Varianz\")\n",
|
||||||
|
" print(\"misst. Genau dort sucht ein Minimum-Varianz-Optimierer.\")\n",
|
||||||
|
" print()\n",
|
||||||
|
"\n",
|
||||||
|
" print(f\"{'Schaetzer':<26} {'Risiko im Zeitraum':>19} \"\n",
|
||||||
|
" f\"{'Risiko spaeter':>16} {'Hebel':>7}\")\n",
|
||||||
|
" print(\"-\" * 78)\n",
|
||||||
|
"\n",
|
||||||
|
" ergebnisse = {}\n",
|
||||||
|
" for name, matrix in [\n",
|
||||||
|
" (\"Stichprobe (roh)\", stichprobe),\n",
|
||||||
|
" (f\"Ledoit-Wolf (d={ledoit_wolf.shrinkage_:.2f})\", ledoit_wolf.covariance_)]:\n",
|
||||||
|
" gewichte, varianz_intern = minimales_risiko(matrix)\n",
|
||||||
|
" risiko_spaeter = float(np.std(spaeterer_verlauf @ gewichte))\n",
|
||||||
|
" hebel = float(np.abs(gewichte).sum())\n",
|
||||||
|
" ergebnisse[name] = (risiko_spaeter, gewichte)\n",
|
||||||
|
" print(f\"{name:<26} {np.sqrt(varianz_intern) * 100:>18.3f} % \"\n",
|
||||||
|
" f\"{risiko_spaeter * 100:>15.3f} % {hebel:>7.1f}\")\n",
|
||||||
|
"\n",
|
||||||
|
" gleich = np.full(ANZAHL_ANLAGEN, 1.0 / ANZAHL_ANLAGEN)\n",
|
||||||
|
" print(f\"{'Gleichgewichtung 1/N':<26} \"\n",
|
||||||
|
" f\"{float(np.std(schaetzzeitraum @ gleich)) * 100:>18.3f} % \"\n",
|
||||||
|
" f\"{float(np.std(spaeterer_verlauf @ gleich)) * 100:>15.3f} % {1.0:>7.1f}\")\n",
|
||||||
|
" print(\"-\" * 78)\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\nDrei Beobachtungen:\")\n",
|
||||||
|
" print(\"1. Die rohe Schaetzung meldet 0,000 % Risiko - und liefert spaeter das\")\n",
|
||||||
|
" print(\" SCHLECHTESTE Ergebnis der drei. Sie hat kein Portfolio optimiert,\")\n",
|
||||||
|
" print(\" sondern eine Luecke in den eigenen Daten gefunden.\")\n",
|
||||||
|
" print(\"2. Die Gleichgewichtung, die gar nicht optimiert, schlaegt den rohen\")\n",
|
||||||
|
" print(\" Optimierer deutlich. Das ist kein Zufallsbefund, sondern in der\")\n",
|
||||||
|
" print(\" Literatur breit belegt.\")\n",
|
||||||
|
" print(\"3. Ledoit-Wolf ist im Schaetzzeitraum EHRLICHER (0,441 statt 0,000 %)\")\n",
|
||||||
|
" print(\" und spaeter am besten. Der Hebel faellt von 3,8 auf 1,8 - das\")\n",
|
||||||
|
" print(\" Portfolio wird auch praktisch handelbarer.\")\n",
|
||||||
|
" print()\n",
|
||||||
|
" print(\"Merksatz: Ein Optimierer glaubt seinen Eingabedaten vollstaendig.\")\n",
|
||||||
|
" print(\"Je mehr Freiheit Sie ihm geben, desto gruendlicher findet er deren\")\n",
|
||||||
|
" print(\"Fehler. Das gilt fuer Kovarianzmatrizen wie fuer Lieferzeiten,\")\n",
|
||||||
|
" print(\"Ausfallraten und jede andere geschaetzte Groesse.\")\n",
|
||||||
|
" print(\"=\" * 78)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"name": "python",
|
||||||
|
"version": "3.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
579
Notebooks_04/fundament.ipynb
Normal file
579
Notebooks_04/fundament.ipynb
Normal file
|
|
@ -0,0 +1,579 @@
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"# Kapitel 2: Das mathematische Fundament — Vektoren, Matrizen, Konvexität\n",
|
||||||
|
"\n",
|
||||||
|
"Begleitnotebook zu *Optimierte Entscheidungsfindung mit Python*. Die Codezellen sind identisch mit den im Buch abgedruckten Programmen.\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"# Einmalig ausfuehren: installiert alle im Buch verwendeten Pakete.\n",
|
||||||
|
"# Lokal in einer virtuellen Umgebung genauso gueltig wie in Google Colab.\n",
|
||||||
|
"%pip install --quiet ortools highspy cvxpy scipy numpy pandas polars \\\n",
|
||||||
|
" scikit-learn matplotlib plotly pyomo linopy pymoo pydantic openpyxl"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Dasselbe in Python\n",
|
||||||
|
"\n",
|
||||||
|
"`Matrixform.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# Matrixform.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel Fundament: Von der ausgeschriebenen Form zur Matrixform - und zurück.\n",
|
||||||
|
"Zeigt, dass beide Schreibweisen dasselbe Modell beschreiben.\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"import numpy as np\n",
|
||||||
|
"from scipy.optimize import linprog\n",
|
||||||
|
"\n",
|
||||||
|
"# --- Modell in Matrixform -------------------------------------------------\n",
|
||||||
|
"# max 3*x1 + 5*x2 u.d.N. x1 <= 4, 2*x2 <= 12, 3*x1 + 2*x2 <= 18, x >= 0\n",
|
||||||
|
"c = np.array([3.0, 5.0]) # Ertragsvektor (Maximierung)\n",
|
||||||
|
"A = np.array([[1.0, 0.0], # Zeile 1: nur x1 kommt vor\n",
|
||||||
|
" [0.0, 2.0], # Zeile 2: nur x2 kommt vor\n",
|
||||||
|
" [3.0, 2.0]]) # Zeile 3: beide\n",
|
||||||
|
"b = np.array([4.0, 12.0, 18.0])\n",
|
||||||
|
"namen = [\"Rohstoff A\", \"Rohstoff B\", \"Maschinenzeit\"]\n",
|
||||||
|
"\n",
|
||||||
|
"# --- Ausgeschriebene Form maschinell erzeugen ------------------------------\n",
|
||||||
|
"def zeige_ausgeschrieben(c, A, b, namen):\n",
|
||||||
|
" \"\"\"Druckt die Matrixform als lesbares Ungleichungssystem.\"\"\"\n",
|
||||||
|
" terme = \" + \".join(f\"{c[j]:g}*x{j+1}\" for j in range(len(c)))\n",
|
||||||
|
" print(f\"max {terme}\")\n",
|
||||||
|
" print(\"u.d.N.\")\n",
|
||||||
|
" for i in range(A.shape[0]):\n",
|
||||||
|
" summanden = \" + \".join(f\"{A[i, j]:g}*x{j+1}\"\n",
|
||||||
|
" for j in range(A.shape[1]) if A[i, j] != 0)\n",
|
||||||
|
" print(f\" {summanden:<24} <= {b[i]:>5g} ({namen[i]})\")\n",
|
||||||
|
" print(f\" x1, ..., x{len(c)} >= 0\")\n",
|
||||||
|
"\n",
|
||||||
|
"zeige_ausgeschrieben(c, A, b, namen)\n",
|
||||||
|
"\n",
|
||||||
|
"# --- Zulässigkeit eines Punktes prüfen ------------------------------------\n",
|
||||||
|
"def ist_zulaessig(x, A, b, toleranz=1e-9):\n",
|
||||||
|
" \"\"\"Prüft A x <= b und x >= 0 komponentenweise.\"\"\"\n",
|
||||||
|
" verbrauch = A @ x # Matrix-Vektor-Produkt: alle Zeilen auf einmal\n",
|
||||||
|
" return bool(np.all(verbrauch <= b + toleranz) and np.all(x >= -toleranz))\n",
|
||||||
|
"\n",
|
||||||
|
"for kandidat in [np.array([2.0, 6.0]), np.array([4.0, 3.0]), np.array([4.0, 6.0])]:\n",
|
||||||
|
" zulaessig = ist_zulaessig(kandidat, A, b)\n",
|
||||||
|
" zielwert = c @ kandidat\n",
|
||||||
|
" verbrauch = A @ kandidat\n",
|
||||||
|
" print(f\"\\nx = {kandidat} -> A x = {verbrauch} \"\n",
|
||||||
|
" f\"{'zulaessig' if zulaessig else 'UNZULAESSIG'}, Z = {zielwert:g}\")\n",
|
||||||
|
"\n",
|
||||||
|
"# --- Lösen: linprog minimiert, also c negieren -----------------------------\n",
|
||||||
|
"ergebnis = linprog(c=-c, A_ub=A, b_ub=b, bounds=[(0, None)] * len(c), method=\"highs\")\n",
|
||||||
|
"print(\"\\n\" + \"-\" * 60)\n",
|
||||||
|
"print(f\"Optimale Loesung: x* = {np.round(ergebnis.x, 4)}\")\n",
|
||||||
|
"print(f\"Optimaler Wert: Z* = {-ergebnis.fun:g}\")\n",
|
||||||
|
"print(\"Hinweis: linprog minimiert, deshalb wurde c negiert und das\")\n",
|
||||||
|
"print(\" Ergebnis am Ende wieder mit -1 multipliziert.\")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Konvexität sichtbar machen\n",
|
||||||
|
"\n",
|
||||||
|
"`Konvexitaet_Demo.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# Konvexitaet_Demo.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel Fundament: Konvexität praktisch erfahrbar machen.\n",
|
||||||
|
"Teil 1: Die Sehnen-Bedingung numerisch nachprüfen.\n",
|
||||||
|
"Teil 2: Zeigen, warum lokale Suche bei nicht-konvexen Funktionen scheitert.\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"import numpy as np\n",
|
||||||
|
"from scipy.optimize import minimize\n",
|
||||||
|
"\n",
|
||||||
|
"# --- Teil 1: Sehnen-Test ---------------------------------------------------\n",
|
||||||
|
"def ist_konvex_numerisch(f, unten, oben, proben=2000, seed=0):\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" Prüft die Konvexitätsdefinition an zufälligen Punktepaaren:\n",
|
||||||
|
" f(theta*x + (1-theta)*y) <= theta*f(x) + (1-theta)*f(y) ?\n",
|
||||||
|
" Findet Gegenbeispiele - beweist aber KEINE Konvexität.\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" rng = np.random.default_rng(seed)\n",
|
||||||
|
" schlimmste_verletzung = 0.0\n",
|
||||||
|
" for _ in range(proben):\n",
|
||||||
|
" x, y = rng.uniform(unten, oben, size=2)\n",
|
||||||
|
" theta = rng.uniform(0.0, 1.0)\n",
|
||||||
|
" links = f(theta * x + (1 - theta) * y)\n",
|
||||||
|
" rechts = theta * f(x) + (1 - theta) * f(y)\n",
|
||||||
|
" schlimmste_verletzung = max(schlimmste_verletzung, links - rechts)\n",
|
||||||
|
" return schlimmste_verletzung\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"funktionen = {\n",
|
||||||
|
" \"f(x) = x^2 (konvex)\": lambda x: x ** 2,\n",
|
||||||
|
" \"f(x) = |x| (konvex)\": lambda x: abs(x),\n",
|
||||||
|
" \"f(x) = e^x (konvex)\": lambda x: np.exp(x),\n",
|
||||||
|
" \"f(x) = x^3 (NICHT konvex)\": lambda x: x ** 3,\n",
|
||||||
|
" \"f(x) = x^2+3sin(3x) (NICHT konvex)\": lambda x: x ** 2 + 3 * np.sin(3 * x),\n",
|
||||||
|
"}\n",
|
||||||
|
"\n",
|
||||||
|
"print(\"=\" * 68)\n",
|
||||||
|
"print(\" TEIL 1: SEHNEN-TEST (positive Zahl = Konvexität verletzt)\")\n",
|
||||||
|
"print(\"=\" * 68)\n",
|
||||||
|
"for name, f in funktionen.items():\n",
|
||||||
|
" verletzung = ist_konvex_numerisch(f, -3.0, 3.0)\n",
|
||||||
|
" urteil = \"konvex (keine Verletzung gefunden)\" if verletzung < 1e-9 \\\n",
|
||||||
|
" else f\"NICHT konvex (Verletzung bis {verletzung:.3f})\"\n",
|
||||||
|
" print(f\"{name:<34} -> {urteil}\")\n",
|
||||||
|
"\n",
|
||||||
|
"# --- Teil 2: Lokale Suche von verschiedenen Startpunkten -------------------\n",
|
||||||
|
"def wellige_funktion(x):\n",
|
||||||
|
" \"\"\"Nicht-konvex: eine Parabel mit aufmodulierter Welle -> viele lokale Minima.\"\"\"\n",
|
||||||
|
" return x[0] ** 2 + 3.0 * np.sin(3.0 * x[0])\n",
|
||||||
|
"\n",
|
||||||
|
"print(\"\\n\" + \"=\" * 68)\n",
|
||||||
|
"print(\" TEIL 2: LOKALE SUCHE BEI NICHT-KONVEXER FUNKTION\")\n",
|
||||||
|
"print(\"=\" * 68)\n",
|
||||||
|
"print(f\"{'Startpunkt':>12} | {'gefundenes Minimum':>20} | {'Funktionswert':>14}\")\n",
|
||||||
|
"print(\"-\" * 68)\n",
|
||||||
|
"\n",
|
||||||
|
"ergebnisse = []\n",
|
||||||
|
"for start in [-3.0, -1.5, 0.0, 1.5, 3.0]:\n",
|
||||||
|
" res = minimize(wellige_funktion, x0=[start], method=\"BFGS\")\n",
|
||||||
|
" ergebnisse.append((start, res.x[0], res.fun))\n",
|
||||||
|
" print(f\"{start:>12.1f} | {res.x[0]:>20.4f} | {res.fun:>14.4f}\")\n",
|
||||||
|
"\n",
|
||||||
|
"bester = min(ergebnisse, key=lambda t: t[2])\n",
|
||||||
|
"print(\"-\" * 68)\n",
|
||||||
|
"print(f\"Je nach Startpunkt landet derselbe Algorithmus in \"\n",
|
||||||
|
" f\"{len({round(e[1], 3) for e in ergebnisse})} verschiedenen Minima.\")\n",
|
||||||
|
"print(f\"Das beste gefundene: x = {bester[1]:.4f} mit f = {bester[2]:.4f} \"\n",
|
||||||
|
" f\"(Start bei {bester[0]:.1f})\")\n",
|
||||||
|
"print(\"Bei einer KONVEXEN Funktion waeren alle Zeilen identisch --\")\n",
|
||||||
|
"print(\"der Startpunkt waere voellig gleichgueltig.\")\n",
|
||||||
|
"print(\"=\" * 68)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Geometrische Visualisierung des Lösungsraums\n",
|
||||||
|
"\n",
|
||||||
|
"`Visualisierung_Loesungsraum.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# Visualisierung_Loesungsraum.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel Fundament: Geometrische Visualisierung eines 2D-Optimierungsraums.\n",
|
||||||
|
"\n",
|
||||||
|
"Ecken werden berechnet, auf Zulässigkeit geprüft, bewertet und eingezeichnet.\n",
|
||||||
|
"Zusätzlich wird das ganzzahlige Optimum systematisch bestimmt statt behauptet.\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"import itertools\n",
|
||||||
|
"import os\n",
|
||||||
|
"\n",
|
||||||
|
"import numpy as np\n",
|
||||||
|
"import matplotlib\n",
|
||||||
|
"matplotlib.use(\"Agg\") # kein Bildschirm nötig\n",
|
||||||
|
"import matplotlib.pyplot as plt\n",
|
||||||
|
"\n",
|
||||||
|
"OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), \"output\")\n",
|
||||||
|
"os.makedirs(OUTPUT_DIR, exist_ok=True)\n",
|
||||||
|
"\n",
|
||||||
|
"# --- Modell (identisch zum Kapitel Einfuehrung) ----------------------------\n",
|
||||||
|
"# max 150*xA + 250*xB\n",
|
||||||
|
"# u.d.N. 2*xA + 5*xB <= 40 (vCPU)\n",
|
||||||
|
"# 4*xA + 6*xB <= 60 (RAM)\n",
|
||||||
|
"# 1*xA + 0*xB <= 8 (Marktliquidität)\n",
|
||||||
|
"c = np.array([150.0, 250.0])\n",
|
||||||
|
"A = np.array([[2.0, 5.0], [4.0, 6.0], [1.0, 0.0]])\n",
|
||||||
|
"b = np.array([40.0, 60.0, 8.0])\n",
|
||||||
|
"restriktionsnamen = [\"vCPU\", \"RAM\", \"Marktlimit\"]\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def ist_zulaessig(punkt, tol=1e-7):\n",
|
||||||
|
" return np.all(A @ punkt <= b + tol) and np.all(punkt >= -tol)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def berechne_ecken():\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" Ecken = Schnittpunkte je zweier Begrenzungsgeraden, die zulässig sind.\n",
|
||||||
|
" Begrenzungen sind die 3 Restriktionen plus die beiden Achsen xA=0, xB=0.\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" geraden = [(A[i], b[i]) for i in range(len(b))]\n",
|
||||||
|
" geraden.append((np.array([1.0, 0.0]), 0.0)) # xA = 0\n",
|
||||||
|
" geraden.append((np.array([0.0, 1.0]), 0.0)) # xB = 0\n",
|
||||||
|
"\n",
|
||||||
|
" ecken = []\n",
|
||||||
|
" for (n1, d1), (n2, d2) in itertools.combinations(geraden, 2):\n",
|
||||||
|
" M = np.array([n1, n2])\n",
|
||||||
|
" if abs(np.linalg.det(M)) < 1e-9: # parallel -> kein Schnittpunkt\n",
|
||||||
|
" continue\n",
|
||||||
|
" p = np.linalg.solve(M, np.array([d1, d2]))\n",
|
||||||
|
" if ist_zulaessig(p) and not any(np.allclose(p, e) for e in ecken):\n",
|
||||||
|
" ecken.append(p)\n",
|
||||||
|
" return np.array(ecken)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def bestes_ganzzahliges():\n",
|
||||||
|
" \"\"\"Vollständige Suche über das kleine Gitter - hier zulässig, weil winzig.\"\"\"\n",
|
||||||
|
" bester_wert, bester_punkt = -np.inf, None\n",
|
||||||
|
" for xa in range(0, 21):\n",
|
||||||
|
" for xb in range(0, 21):\n",
|
||||||
|
" p = np.array([float(xa), float(xb)])\n",
|
||||||
|
" if ist_zulaessig(p) and c @ p > bester_wert:\n",
|
||||||
|
" bester_wert, bester_punkt = c @ p, p\n",
|
||||||
|
" return bester_punkt, bester_wert\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"# --- Analyse ---------------------------------------------------------------\n",
|
||||||
|
"ecken = berechne_ecken()\n",
|
||||||
|
"werte = ecken @ c\n",
|
||||||
|
"reihenfolge = np.argsort(-werte)\n",
|
||||||
|
"\n",
|
||||||
|
"print(\"=\" * 62)\n",
|
||||||
|
"print(\" ECKEN DES ZULÄSSIGEN POLYEDERS (nach Zielwert sortiert)\")\n",
|
||||||
|
"print(\"=\" * 62)\n",
|
||||||
|
"print(f\"{'x_A':>8} {'x_B':>8} {'Z = 150 xA + 250 xB':>24}\")\n",
|
||||||
|
"print(\"-\" * 62)\n",
|
||||||
|
"for i in reihenfolge:\n",
|
||||||
|
" print(f\"{ecken[i, 0]:>8.2f} {ecken[i, 1]:>8.2f} {werte[i]:>24,.2f} EUR\")\n",
|
||||||
|
"\n",
|
||||||
|
"lp_punkt, lp_wert = ecken[reihenfolge[0]], werte[reihenfolge[0]]\n",
|
||||||
|
"ip_punkt, ip_wert = bestes_ganzzahliges()\n",
|
||||||
|
"\n",
|
||||||
|
"print(\"-\" * 62)\n",
|
||||||
|
"print(f\"Kontinuierliches Optimum (LP): x = ({lp_punkt[0]:.2f}, {lp_punkt[1]:.2f}), \"\n",
|
||||||
|
" f\"Z = {lp_wert:,.2f} EUR\")\n",
|
||||||
|
"print(f\"Ganzzahliges Optimum (IP): x = ({ip_punkt[0]:.0f}, {ip_punkt[1]:.0f}), \"\n",
|
||||||
|
" f\"Z = {ip_wert:,.2f} EUR\")\n",
|
||||||
|
"print(f\"Preis der Ganzzahligkeit: {lp_wert - ip_wert:,.2f} EUR \"\n",
|
||||||
|
" f\"({(1 - ip_wert / lp_wert) * 100:.2f} %)\")\n",
|
||||||
|
"print(\"=\" * 62)\n",
|
||||||
|
"\n",
|
||||||
|
"# --- Zeichnung -------------------------------------------------------------\n",
|
||||||
|
"gitter = np.linspace(0, 15, 400)\n",
|
||||||
|
"xa_gitter, xb_gitter = np.meshgrid(gitter, gitter)\n",
|
||||||
|
"\n",
|
||||||
|
"plt.figure(figsize=(10, 8))\n",
|
||||||
|
"\n",
|
||||||
|
"# Restriktionsgeraden\n",
|
||||||
|
"plt.plot(gitter, (40 - 2 * gitter) / 5, color=\"tab:blue\", lw=2,\n",
|
||||||
|
" label=r\"$2x_A + 5x_B \\leq 40$ (vCPU)\")\n",
|
||||||
|
"plt.plot(gitter, (60 - 4 * gitter) / 6, color=\"tab:green\", lw=2,\n",
|
||||||
|
" label=r\"$4x_A + 6x_B \\leq 60$ (RAM)\")\n",
|
||||||
|
"plt.axvline(x=8, color=\"tab:orange\", lw=2, label=r\"$x_A \\leq 8$ (Marktlimit)\")\n",
|
||||||
|
"\n",
|
||||||
|
"# Zulässiger Bereich\n",
|
||||||
|
"maske = ((2 * xa_gitter + 5 * xb_gitter <= 40) & (4 * xa_gitter + 6 * xb_gitter <= 60)\n",
|
||||||
|
" & (xa_gitter <= 8) & (xa_gitter >= 0) & (xb_gitter >= 0))\n",
|
||||||
|
"plt.imshow(maske.astype(int), extent=(0, 15, 0, 15), origin=\"lower\",\n",
|
||||||
|
" cmap=\"Greys\", alpha=0.25, aspect=\"auto\")\n",
|
||||||
|
"\n",
|
||||||
|
"# Höhenlinien der Zielfunktion\n",
|
||||||
|
"Z = 150 * xa_gitter + 250 * xb_gitter\n",
|
||||||
|
"hoehen = plt.contour(xa_gitter, xb_gitter, Z, levels=[500, 1000, 1500, 2000, 2375],\n",
|
||||||
|
" colors=\"purple\", linestyles=\"--\", alpha=0.7)\n",
|
||||||
|
"plt.clabel(hoehen, inline=True, fontsize=9, fmt=\"Z = %1.0f EUR\")\n",
|
||||||
|
"\n",
|
||||||
|
"# Ecken einzeichnen - jetzt werden sie tatsächlich benutzt\n",
|
||||||
|
"plt.scatter(ecken[:, 0], ecken[:, 1], s=70, facecolors=\"white\",\n",
|
||||||
|
" edgecolors=\"black\", zorder=4, label=\"Ecken des Polyeders\")\n",
|
||||||
|
"for e, w in zip(ecken, werte):\n",
|
||||||
|
" plt.annotate(f\"({e[0]:.1f}, {e[1]:.1f})\\nZ={w:,.0f}\", (e[0], e[1]),\n",
|
||||||
|
" textcoords=\"offset points\", xytext=(6, 6), fontsize=8)\n",
|
||||||
|
"\n",
|
||||||
|
"plt.scatter([lp_punkt[0]], [lp_punkt[1]], color=\"purple\", marker=\"D\", s=110, zorder=5,\n",
|
||||||
|
" label=f\"LP-Optimum ({lp_punkt[0]:.1f}, {lp_punkt[1]:.1f})\")\n",
|
||||||
|
"plt.scatter([ip_punkt[0]], [ip_punkt[1]], color=\"red\", s=170, zorder=6,\n",
|
||||||
|
" label=f\"Ganzzahliges Optimum ({ip_punkt[0]:.0f}, {ip_punkt[1]:.0f})\")\n",
|
||||||
|
"\n",
|
||||||
|
"plt.xlim(0, 12)\n",
|
||||||
|
"plt.ylim(0, 10)\n",
|
||||||
|
"plt.xlabel(\"Anzahl Arbitrage-Bots ($x_A$)\", fontsize=11)\n",
|
||||||
|
"plt.ylabel(\"Anzahl Trendfolge-Bots ($x_B$)\", fontsize=11)\n",
|
||||||
|
"plt.title(\"Polyeder des zulässigen Bereichs mit Niveaulinien der Zielfunktion\", fontsize=13)\n",
|
||||||
|
"plt.grid(True, linestyle=\":\", alpha=0.6)\n",
|
||||||
|
"plt.legend(loc=\"upper right\", framealpha=0.9)\n",
|
||||||
|
"plt.tight_layout()\n",
|
||||||
|
"ziel = os.path.join(OUTPUT_DIR, \"feasible_region_2d.png\")\n",
|
||||||
|
"plt.savefig(ziel, dpi=150)\n",
|
||||||
|
"print(f\"Visualisierung gespeichert unter '{ziel}'\")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Das Experiment\n",
|
||||||
|
"\n",
|
||||||
|
"`Skalierung_Kondition.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# Skalierung_Kondition.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel Fundament: Was die Konditionszahl kappa(A) praktisch bedeutet - und wie man\n",
|
||||||
|
"ein schlecht skaliertes Modell wieder gesund rechnet.\n",
|
||||||
|
"\n",
|
||||||
|
"Drei Experimente:\n",
|
||||||
|
"\n",
|
||||||
|
" 1. Fehlerverstaerkung: Wie stark schlaegt eine winzige Datenunsicherheit auf\n",
|
||||||
|
" die Loesung durch? kappa(A) ist genau die Obergrenze dieses Faktors.\n",
|
||||||
|
" 2. Ruiz-Equilibrierung: Ein Modell, in dem Euro-Betraege (1e7) und\n",
|
||||||
|
" Tonnen-Angaben (1e-3) in derselben Matrix stehen, wird durch Zeilen- und\n",
|
||||||
|
" Spaltenskalierung um Groessenordnungen besser konditioniert.\n",
|
||||||
|
" 3. Toleranzen: Warum eine Binaervariable mit dem Wert 0.99999998 niemals\n",
|
||||||
|
" mit int() gerundet werden darf.\n",
|
||||||
|
"\n",
|
||||||
|
"Benoetigt: numpy, scipy\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"from __future__ import annotations\n",
|
||||||
|
"\n",
|
||||||
|
"import numpy as np\n",
|
||||||
|
"from scipy.optimize import linprog\n",
|
||||||
|
"\n",
|
||||||
|
"RNG = np.random.default_rng(42)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"# --- Experiment 1: kappa(A) als Fehlerverstaerker ---------------------------\n",
|
||||||
|
"\n",
|
||||||
|
"def fehlerverstaerkung(A: np.ndarray, versuche: int = 200,\n",
|
||||||
|
" stoerung: float = 1e-10) -> tuple[float, float]:\n",
|
||||||
|
" \"\"\"Stoert die rechte Seite b relativ um 'stoerung' in zufaellige Richtungen\n",
|
||||||
|
" und misst, um welchen Faktor sich der Fehler in der Loesung x vergroessert.\n",
|
||||||
|
"\n",
|
||||||
|
" Liefert (Median, Maximum) der Verstaerkung. Die Theorie sagt: Das Maximum\n",
|
||||||
|
" kann bis kappa(A) betragen - und nur bis dahin.\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" x_wahr = np.ones(A.shape[1])\n",
|
||||||
|
" b = A @ x_wahr\n",
|
||||||
|
" faktoren = []\n",
|
||||||
|
" for _ in range(versuche):\n",
|
||||||
|
" richtung = RNG.normal(size=b.size)\n",
|
||||||
|
" richtung /= np.linalg.norm(richtung)\n",
|
||||||
|
" b_gestoert = b + stoerung * np.linalg.norm(b) * richtung\n",
|
||||||
|
" x_gestoert = np.linalg.solve(A, b_gestoert)\n",
|
||||||
|
" rel_x = np.linalg.norm(x_gestoert - x_wahr) / np.linalg.norm(x_wahr)\n",
|
||||||
|
" faktoren.append(rel_x / stoerung)\n",
|
||||||
|
" return float(np.median(faktoren)), float(np.max(faktoren))\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def zeige_experiment_1() -> None:\n",
|
||||||
|
" print(\"=\" * 74)\n",
|
||||||
|
" print(\" 1. KONDITIONSZAHL ALS FEHLERVERSTAERKER\")\n",
|
||||||
|
" print(\"=\" * 74)\n",
|
||||||
|
" print(\"Zwei Gleichungssysteme, beide exakt loesbar mit x = (1, 1).\")\n",
|
||||||
|
" print(\"Die rechte Seite wird um relativ 1e-10 gestoert - so viel Unsicherheit\")\n",
|
||||||
|
" print(\"steckt in JEDER gemessenen Betriebszahl allemal.\\n\")\n",
|
||||||
|
"\n",
|
||||||
|
" modelle = {\n",
|
||||||
|
" \"gut konditioniert\": np.array([[2.0, 1.0], [1.0, 3.0]]),\n",
|
||||||
|
" \"fast parallele Zeilen\": np.array([[1.0, 1.0], [1.0, 1.0 + 1e-8]]),\n",
|
||||||
|
" }\n",
|
||||||
|
" print(f\"{'Matrix':<24} {'kappa(A)':>12} {'Verst. median':>14} {'Verst. max':>12}\")\n",
|
||||||
|
" print(\"-\" * 74)\n",
|
||||||
|
" for name, A in modelle.items():\n",
|
||||||
|
" median, maximum = fehlerverstaerkung(A)\n",
|
||||||
|
" print(f\"{name:<24} {np.linalg.cond(A):>12.2e} {median:>14.2e} {maximum:>12.2e}\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\nLesart: Bei der zweiten Matrix wird aus einem Datenfehler in der\")\n",
|
||||||
|
" print(\"10. Nachkommastelle ein Loesungsfehler in der 2. Nachkommastelle.\")\n",
|
||||||
|
" print(\"Das Modell ist mathematisch korrekt - und praktisch wertlos.\")\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"# --- Experiment 2: Ruiz-Equilibrierung --------------------------------------\n",
|
||||||
|
"\n",
|
||||||
|
"def ruiz_equilibrierung(A: np.ndarray, durchlaeufe: int = 20\n",
|
||||||
|
" ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:\n",
|
||||||
|
" \"\"\"Skaliert A iterativ so, dass alle Zeilen- und Spaltenmaxima nahe 1\n",
|
||||||
|
" liegen (Ruiz 2001).\n",
|
||||||
|
"\n",
|
||||||
|
" In jedem Durchlauf wird jede Zeile durch die Wurzel ihres Betragsmaximums\n",
|
||||||
|
" geteilt, danach jede Spalte. Das Verfahren konvergiert schnell und braucht\n",
|
||||||
|
" keinerlei Wissen ueber die Bedeutung der Zahlen - genau deshalb steckt es\n",
|
||||||
|
" in jedem ernsthaften Solver als Vorverarbeitung.\n",
|
||||||
|
"\n",
|
||||||
|
" Liefert (A_skaliert, zeilenfaktor r, spaltenfaktor c) mit\n",
|
||||||
|
" A_skaliert = diag(r) @ A @ diag(c)\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" m, n = A.shape\n",
|
||||||
|
" r = np.ones(m)\n",
|
||||||
|
" c = np.ones(n)\n",
|
||||||
|
" A_s = A.astype(float).copy()\n",
|
||||||
|
"\n",
|
||||||
|
" for _ in range(durchlaeufe):\n",
|
||||||
|
" zeilen_max = np.abs(A_s).max(axis=1)\n",
|
||||||
|
" zeilen_max[zeilen_max == 0] = 1.0\n",
|
||||||
|
" d_r = 1.0 / np.sqrt(zeilen_max)\n",
|
||||||
|
" A_s = d_r[:, None] * A_s\n",
|
||||||
|
" r *= d_r\n",
|
||||||
|
"\n",
|
||||||
|
" spalten_max = np.abs(A_s).max(axis=0)\n",
|
||||||
|
" spalten_max[spalten_max == 0] = 1.0\n",
|
||||||
|
" d_c = 1.0 / np.sqrt(spalten_max)\n",
|
||||||
|
" A_s = A_s * d_c[None, :]\n",
|
||||||
|
" c *= d_c\n",
|
||||||
|
"\n",
|
||||||
|
" return A_s, r, c\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def zeige_experiment_2() -> None:\n",
|
||||||
|
" print(\"\\n\" + \"=\" * 74)\n",
|
||||||
|
" print(\" 2. RUIZ-EQUILIBRIERUNG: EINHEITEN GERADERUECKEN\")\n",
|
||||||
|
" print(\"=\" * 74)\n",
|
||||||
|
" print(\"Ein Produktionsmodell, in dem vier Ressourcen in voellig\")\n",
|
||||||
|
" print(\"verschiedenen Einheiten gemessen werden:\")\n",
|
||||||
|
" print(\" Zeile 1: Kapitalbindung in Euro (Groessenordnung 1e7)\")\n",
|
||||||
|
" print(\" Zeile 2: Katalysatorverbrauch in Tonnen (Groessenordnung 1e-3)\")\n",
|
||||||
|
" print(\" Zeile 3: Energie in Wattsekunden (Groessenordnung 1e5)\")\n",
|
||||||
|
" print(\" Zeile 4: Ausschussquote als Anteil (Groessenordnung 1e-2)\\n\")\n",
|
||||||
|
"\n",
|
||||||
|
" # Quadratisch gewaehlt, damit die Loesung eindeutig ist und die\n",
|
||||||
|
" # Ruecktransformation unten wirklich etwas beweist.\n",
|
||||||
|
" n = 4\n",
|
||||||
|
" grundmatrix = RNG.uniform(0.5, 2.0, size=(n, n))\n",
|
||||||
|
" einheiten = np.array([1e7, 1e-3, 1e5, 1e-2])\n",
|
||||||
|
" A = grundmatrix * einheiten[:, None]\n",
|
||||||
|
"\n",
|
||||||
|
" A_s, r, c = ruiz_equilibrierung(A)\n",
|
||||||
|
"\n",
|
||||||
|
" print(f\"{'':<28} {'kappa(A)':>12} {'groesster Eintrag':>18} \"\n",
|
||||||
|
" f\"{'kleinster Eintrag':>18}\")\n",
|
||||||
|
" print(\"-\" * 74)\n",
|
||||||
|
" for name, matrix in [(\"vor der Skalierung\", A), (\"nach Ruiz-Equilibrierung\", A_s)]:\n",
|
||||||
|
" betraege = np.abs(matrix)\n",
|
||||||
|
" print(f\"{name:<28} {np.linalg.cond(matrix):>12.2e} \"\n",
|
||||||
|
" f\"{betraege.max():>18.2e} {betraege.min():>18.2e}\")\n",
|
||||||
|
"\n",
|
||||||
|
" # Gegenprobe: Das skalierte Modell beschreibt dasselbe Problem. Wer x_s\n",
|
||||||
|
" # loest, erhaelt die urspruengliche Loesung durch x = c * x_s.\n",
|
||||||
|
" x_wahr = RNG.uniform(1.0, 5.0, size=n)\n",
|
||||||
|
" b = A @ x_wahr\n",
|
||||||
|
" b_s = r * b\n",
|
||||||
|
" x_s = np.linalg.solve(A_s, b_s)\n",
|
||||||
|
" x_zurueck = c * x_s\n",
|
||||||
|
" print(f\"\\nRuecktransformation x = c * x_s: groesste Abweichung zur wahren \"\n",
|
||||||
|
" f\"Loesung {np.abs(x_zurueck - x_wahr).max():.2e}\")\n",
|
||||||
|
" print(\"Die Skalierung ist also verlustfrei - sie aendert nur die Zahlen,\")\n",
|
||||||
|
" print(\"nicht das Problem.\")\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"# --- Experiment 3: Toleranzen und der int()-Fehler --------------------------\n",
|
||||||
|
"\n",
|
||||||
|
"def zeige_experiment_3() -> None:\n",
|
||||||
|
" print(\"\\n\" + \"=\" * 74)\n",
|
||||||
|
" print(\" 3. TOLERANZEN: WARUM int() DIE FALSCHE RUNDUNG IST\")\n",
|
||||||
|
" print(\"=\" * 74)\n",
|
||||||
|
"\n",
|
||||||
|
" # Ein LP, dessen Optimum bei x = 1 liegt, aber vom Solver nur bis auf\n",
|
||||||
|
" # seine Toleranz getroffen wird.\n",
|
||||||
|
" ergebnis = linprog(c=[-1.0], A_ub=[[1.0]], b_ub=[1.0],\n",
|
||||||
|
" bounds=[(0, None)], method=\"highs\")\n",
|
||||||
|
" wert = float(ergebnis.x[0])\n",
|
||||||
|
" print(f\"Solver liefert x = {wert!r}\")\n",
|
||||||
|
"\n",
|
||||||
|
" # Typische Werte, wie sie aus MILP-Solvern zurueckkommen.\n",
|
||||||
|
" beispiele = [0.99999998, 1.00000002, 0.49999999, 2.9999999]\n",
|
||||||
|
" print(f\"\\n{'Solverwert':>14} {'int()':>8} {'round()':>9} {'Kommentar'}\")\n",
|
||||||
|
" print(\"-\" * 74)\n",
|
||||||
|
" kommentare = {\n",
|
||||||
|
" 0.99999998: \"int() macht aus einer JA- eine NEIN-Entscheidung\",\n",
|
||||||
|
" 1.00000002: \"hier ginge int() zufaellig gut - Verlass ist keiner\",\n",
|
||||||
|
" 0.49999999: \"echt unentschieden: Modell oder Toleranz pruefen!\",\n",
|
||||||
|
" 2.9999999: \"3 Maschinen werden zu 2 - der Plan geht nicht auf\",\n",
|
||||||
|
" }\n",
|
||||||
|
" for wert_b in beispiele:\n",
|
||||||
|
" print(f\"{wert_b:>14.8f} {int(wert_b):>8} {round(wert_b):>9} \"\n",
|
||||||
|
" f\"{kommentare[wert_b]}\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\nRichtige Vorgehensweise: gegen die Solver-Toleranz pruefen,\")\n",
|
||||||
|
" print(\"dann erst runden - und den Zweifelsfall melden statt still zu raten.\")\n",
|
||||||
|
"\n",
|
||||||
|
" def sichere_ganzzahl(wert: float, toleranz: float = 1e-6) -> int:\n",
|
||||||
|
" naechste = round(wert)\n",
|
||||||
|
" if abs(wert - naechste) > toleranz:\n",
|
||||||
|
" raise ValueError(\n",
|
||||||
|
" f\"{wert} ist {abs(wert - naechste):.2e} von der naechsten ganzen \"\n",
|
||||||
|
" f\"Zahl entfernt - das ist mehr als die Toleranz {toleranz}. \"\n",
|
||||||
|
" \"Ganzzahligkeit im Modell pruefen.\")\n",
|
||||||
|
" return naechste\n",
|
||||||
|
"\n",
|
||||||
|
" for wert_b in beispiele:\n",
|
||||||
|
" try:\n",
|
||||||
|
" print(f\" sichere_ganzzahl({wert_b}) = {sichere_ganzzahl(wert_b)}\")\n",
|
||||||
|
" except ValueError as fehler:\n",
|
||||||
|
" print(f\" sichere_ganzzahl({wert_b}) -> ValueError: {fehler}\")\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"if __name__ == \"__main__\":\n",
|
||||||
|
" zeige_experiment_1()\n",
|
||||||
|
" zeige_experiment_2()\n",
|
||||||
|
" zeige_experiment_3()\n",
|
||||||
|
" print(\"\\n\" + \"=\" * 74)\n",
|
||||||
|
" print(\"Merksatz: Skalieren Sie Ihre Daten, BEVOR der Solver sie sieht -\")\n",
|
||||||
|
" print(\"und runden Sie Solver-Ergebnisse NIE ohne Toleranzpruefung.\")\n",
|
||||||
|
" print(\"=\" * 74)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"name": "python",
|
||||||
|
"version": "3.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
644
Notebooks_04/graphen.ipynb
Normal file
644
Notebooks_04/graphen.ipynb
Normal file
|
|
@ -0,0 +1,644 @@
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"# Kapitel 8: Graphen, Flüsse und Touren — Min-Cost-Flow, Matching und VRP\n",
|
||||||
|
"\n",
|
||||||
|
"Begleitnotebook zu *Optimierte Entscheidungsfindung mit Python*. Die Codezellen sind identisch mit den im Buch abgedruckten Programmen.\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"# Einmalig ausfuehren: installiert alle im Buch verwendeten Pakete.\n",
|
||||||
|
"# Lokal in einer virtuellen Umgebung genauso gueltig wie in Google Colab.\n",
|
||||||
|
"%pip install --quiet ortools highspy cvxpy scipy numpy pandas polars \\\n",
|
||||||
|
" scikit-learn matplotlib plotly pyomo linopy pymoo pydantic openpyxl"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Das Minimum-Cost-Flow-Problem (MCNFP)\n",
|
||||||
|
"\n",
|
||||||
|
"`Min_Cost_Flow.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# Min_Cost_Flow.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel Graphen: Kostenminimaler Fluss durch ein Netzwerk.\n",
|
||||||
|
"\n",
|
||||||
|
"Loest dasselbe Problem zweimal:\n",
|
||||||
|
" (1) als allgemeines LP mit scipy -> zeigt die Modellstruktur\n",
|
||||||
|
" (2) mit dem spezialisierten Netzwerk-Solver von OR-Tools -> zeigt den\n",
|
||||||
|
" Geschwindigkeitsvorteil eines Verfahrens, das die Struktur ausnutzt\n",
|
||||||
|
"\n",
|
||||||
|
"Beide laufen in eigenen Prozessen nicht noetig: scipy und ortools vertragen\n",
|
||||||
|
"sich (nur ortools + highspy kollidieren, siehe Kapitel Oekosystem).\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"import numpy as np\n",
|
||||||
|
"from scipy.optimize import linprog\n",
|
||||||
|
"\n",
|
||||||
|
"# --- Netzwerk definieren ---------------------------------------------------\n",
|
||||||
|
"KNOTEN = [\"Werk_A\", \"Werk_B\", \"Umschlag\", \"Kunde_1\", \"Kunde_2\"]\n",
|
||||||
|
"# (von, nach, Kosten je Einheit, Kapazitaet)\n",
|
||||||
|
"KANTEN = [\n",
|
||||||
|
" (\"Werk_A\", \"Umschlag\", 2.0, 15),\n",
|
||||||
|
" (\"Werk_A\", \"Kunde_1\", 5.0, 10),\n",
|
||||||
|
" (\"Werk_B\", \"Umschlag\", 4.0, 10),\n",
|
||||||
|
" (\"Werk_B\", \"Kunde_2\", 6.0, 10),\n",
|
||||||
|
" (\"Umschlag\", \"Kunde_1\", 1.0, 20),\n",
|
||||||
|
" (\"Umschlag\", \"Kunde_2\", 3.0, 10),\n",
|
||||||
|
"]\n",
|
||||||
|
"# Angebot (+) bzw. Bedarf (-) je Knoten\n",
|
||||||
|
"SALDO = {\"Werk_A\": 20, \"Werk_B\": 10, \"Umschlag\": 0, \"Kunde_1\": -15, \"Kunde_2\": -15}\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def loese_als_lp():\n",
|
||||||
|
" \"\"\"Flussproblem als allgemeines lineares Programm.\"\"\"\n",
|
||||||
|
" n_kanten = len(KANTEN)\n",
|
||||||
|
" knoten_index = {k: i for i, k in enumerate(KNOTEN)}\n",
|
||||||
|
"\n",
|
||||||
|
" # Zielfunktion: Summe der Transportkosten\n",
|
||||||
|
" kosten = np.array([k[2] for k in KANTEN])\n",
|
||||||
|
"\n",
|
||||||
|
" # Flusserhaltung als Gleichungssystem: A_eq @ x = b_eq\n",
|
||||||
|
" A_eq = np.zeros((len(KNOTEN), n_kanten))\n",
|
||||||
|
" for e, (von, nach, _, _) in enumerate(KANTEN):\n",
|
||||||
|
" A_eq[knoten_index[von], e] = +1.0 # fliesst hinaus\n",
|
||||||
|
" A_eq[knoten_index[nach], e] = -1.0 # fliesst hinein\n",
|
||||||
|
" b_eq = np.array([SALDO[k] for k in KNOTEN], dtype=float)\n",
|
||||||
|
"\n",
|
||||||
|
" schranken = [(0, k[3]) for k in KANTEN] # 0 <= x_ij <= u_ij\n",
|
||||||
|
"\n",
|
||||||
|
" ergebnis = linprog(c=kosten, A_eq=A_eq, b_eq=b_eq, bounds=schranken, method=\"highs\")\n",
|
||||||
|
" if not ergebnis.success:\n",
|
||||||
|
" raise SystemExit(f\"Nicht loesbar: {ergebnis.message}\")\n",
|
||||||
|
" return ergebnis.fun, ergebnis.x, ergebnis.eqlin.marginals\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"if __name__ == \"__main__\":\n",
|
||||||
|
" # Vorabpruefung: Angebot muss Bedarf entsprechen\n",
|
||||||
|
" gesamt = sum(SALDO.values())\n",
|
||||||
|
" print(\"=\" * 78)\n",
|
||||||
|
" print(\" KOSTENMINIMALER FLUSS DURCH EIN TRANSPORTNETZ\")\n",
|
||||||
|
" print(\"=\" * 78)\n",
|
||||||
|
" print(f\"Angebot gesamt: {sum(v for v in SALDO.values() if v > 0)} | \"\n",
|
||||||
|
" f\"Bedarf gesamt: {-sum(v for v in SALDO.values() if v < 0)} | \"\n",
|
||||||
|
" f\"Saldo: {gesamt}\")\n",
|
||||||
|
" if gesamt != 0:\n",
|
||||||
|
" raise SystemExit(\"Angebot und Bedarf stimmen nicht ueberein - unloesbar!\")\n",
|
||||||
|
"\n",
|
||||||
|
" kosten_gesamt, fluss, knotenpreise = loese_als_lp()\n",
|
||||||
|
"\n",
|
||||||
|
" print(f\"\\nMinimale Transportkosten: {kosten_gesamt:,.2f} EUR\\n\")\n",
|
||||||
|
" print(f\"{'Kante':<24} {'Fluss':>7} {'Kapazitaet':>11} {'Kosten/E':>9} {'Kosten':>9}\")\n",
|
||||||
|
" print(\"-\" * 78)\n",
|
||||||
|
" for e, (von, nach, c, u) in enumerate(KANTEN):\n",
|
||||||
|
" menge = fluss[e] + 0.0 if abs(fluss[e]) > 1e-9 else 0.0 # vermeidet \"-0.0\"\n",
|
||||||
|
" ausgelastet = \" (VOLL)\" if abs(menge - u) < 1e-6 else \"\"\n",
|
||||||
|
" print(f\"{von + ' -> ' + nach:<24} {menge:>7.1f} {u:>11} \"\n",
|
||||||
|
" f\"{c:>9.2f} {menge * c:>9.2f}{ausgelastet}\")\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Flusserhaltung nachpruefen --------------------------------------\n",
|
||||||
|
" print(\"\\n--- Pruefung der Flusserhaltung je Knoten ---\")\n",
|
||||||
|
" for k in KNOTEN:\n",
|
||||||
|
" hinaus = sum(fluss[e] for e, (v, n, _, _) in enumerate(KANTEN) if v == k)\n",
|
||||||
|
" hinein = sum(fluss[e] for e, (v, n, _, _) in enumerate(KANTEN) if n == k)\n",
|
||||||
|
" netto = hinaus - hinein\n",
|
||||||
|
" art = \"Quelle\" if SALDO[k] > 0 else (\"Senke\" if SALDO[k] < 0 else \"Umschlag\")\n",
|
||||||
|
" print(f\" {k:<10} ({art:<8}): hinaus {hinaus:5.1f} - hinein {hinein:5.1f} \"\n",
|
||||||
|
" f\"= {netto:+6.1f} (gefordert: {SALDO[k]:+d})\")\n",
|
||||||
|
" assert abs(netto - SALDO[k]) < 1e-6, f\"Flusserhaltung verletzt bei {k}!\"\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Knotenpreise (Dualwerte) interpretieren -------------------------\n",
|
||||||
|
" print(\"\\n--- Knotenpreise (Dualwerte der Flusserhaltung) ---\")\n",
|
||||||
|
" print(\" Differenz zweier Knotenpreise = Grenzkosten einer zusaetzlichen Einheit\")\n",
|
||||||
|
" print(\" auf dem guenstigsten Weg zwischen ihnen.\")\n",
|
||||||
|
" for k, preis in zip(KNOTEN, knotenpreise):\n",
|
||||||
|
" print(f\" {k:<10}: {preis:7.2f}\")\n",
|
||||||
|
" print(\"=\" * 78)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Der Satz von Birkhoff und von Neumann\n",
|
||||||
|
"\n",
|
||||||
|
"`Zuordnung_Ungarisch.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# Zuordnung_Ungarisch.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel Graphen: Das Zuordnungsproblem, dreifach geloest.\n",
|
||||||
|
"\n",
|
||||||
|
" (1) Ungarischer Algorithmus (scipy.optimize.linear_sum_assignment) - O(n^3)\n",
|
||||||
|
" (2) als LP OHNE Ganzzahligkeitsforderung -> liefert trotzdem 0/1 (Birkhoff)\n",
|
||||||
|
" (3) als MILP MIT Ganzzahligkeitsforderung -> gleiches Ergebnis, mehr Aufwand\n",
|
||||||
|
"\n",
|
||||||
|
"Zeigt damit die praktische Bedeutung der totalen Unimodularitaet.\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"import time\n",
|
||||||
|
"\n",
|
||||||
|
"import numpy as np\n",
|
||||||
|
"from scipy.optimize import linear_sum_assignment, linprog\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def erzeuge_kosten(n, seed=11):\n",
|
||||||
|
" rng = np.random.default_rng(seed)\n",
|
||||||
|
" return rng.integers(10, 99, size=(n, n)).astype(float)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def loese_ungarisch(kosten):\n",
|
||||||
|
" zeilen, spalten = linear_sum_assignment(kosten)\n",
|
||||||
|
" return kosten[zeilen, spalten].sum(), spalten\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def baue_lp(kosten):\n",
|
||||||
|
" \"\"\"Gemeinsame LP-Struktur fuer Variante 2 und 3.\"\"\"\n",
|
||||||
|
" n = len(kosten)\n",
|
||||||
|
" c = kosten.flatten() # x_ij in Zeilenreihenfolge\n",
|
||||||
|
" A_eq = np.zeros((2 * n, n * n))\n",
|
||||||
|
" for i in range(n): # jede Person genau eine Aufgabe\n",
|
||||||
|
" A_eq[i, i * n:(i + 1) * n] = 1.0\n",
|
||||||
|
" for j in range(n): # jede Aufgabe genau einer Person\n",
|
||||||
|
" A_eq[n + j, j::n] = 1.0\n",
|
||||||
|
" b_eq = np.ones(2 * n)\n",
|
||||||
|
" return c, A_eq, b_eq\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def loese_lp(kosten, ganzzahlig):\n",
|
||||||
|
" n = len(kosten)\n",
|
||||||
|
" c, A_eq, b_eq = baue_lp(kosten)\n",
|
||||||
|
" ergebnis = linprog(c=c, A_eq=A_eq, b_eq=b_eq, bounds=[(0, 1)] * (n * n),\n",
|
||||||
|
" integrality=np.ones(n * n) if ganzzahlig else None,\n",
|
||||||
|
" method=\"highs\")\n",
|
||||||
|
" x = ergebnis.x.reshape(n, n)\n",
|
||||||
|
" return ergebnis.fun, x\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"if __name__ == \"__main__\":\n",
|
||||||
|
" print(\"=\" * 84)\n",
|
||||||
|
" print(\" ZUORDNUNGSPROBLEM: DREI WEGE ZUM SELBEN ERGEBNIS\")\n",
|
||||||
|
" print(\"=\" * 84)\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Kleines Beispiel zum Nachvollziehen ------------------------------\n",
|
||||||
|
" kosten = np.array([[82., 83., 69., 92.],\n",
|
||||||
|
" [77., 37., 49., 92.],\n",
|
||||||
|
" [11., 69., 5., 86.],\n",
|
||||||
|
" [8., 9., 98., 23.]])\n",
|
||||||
|
" namen = [\"Anna\", \"Ben\", \"Carla\", \"David\"]\n",
|
||||||
|
" aufgaben = [\"Auftrag W\", \"Auftrag X\", \"Auftrag Y\", \"Auftrag Z\"]\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\nKostenmatrix (wer bearbeitet was zu welchen Kosten?):\")\n",
|
||||||
|
" print(f\"{'':<8}\" + \"\".join(f\"{a:>12}\" for a in aufgaben))\n",
|
||||||
|
" for i, name in enumerate(namen):\n",
|
||||||
|
" print(f\"{name:<8}\" + \"\".join(f\"{kosten[i, j]:>12.0f}\" for j in range(4)))\n",
|
||||||
|
"\n",
|
||||||
|
" wert, zuordnung = loese_ungarisch(kosten)\n",
|
||||||
|
" print(f\"\\nOptimale Zuordnung (Gesamtkosten {wert:.0f}):\")\n",
|
||||||
|
" for i, j in enumerate(zuordnung):\n",
|
||||||
|
" print(f\" {namen[i]:<8} -> {aufgaben[j]:<12} ({kosten[i, j]:.0f} EUR)\")\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Nachweis: LP ohne Ganzzahligkeit liefert trotzdem 0/1 -----------\n",
|
||||||
|
" wert_lp, x_lp = loese_lp(kosten, ganzzahlig=False)\n",
|
||||||
|
" ist_binaer = np.all((np.abs(x_lp) < 1e-9) | (np.abs(x_lp - 1) < 1e-9))\n",
|
||||||
|
" print(f\"\\nLP OHNE Ganzzahligkeitsforderung: Kosten {wert_lp:.0f}, \"\n",
|
||||||
|
" f\"Loesung ist {'0/1-wertig' if ist_binaer else 'GEBROCHEN'}\")\n",
|
||||||
|
" print(\" -> Satz von Birkhoff/von Neumann bestaetigt: Die Ecken sind Permutationen.\")\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Laufzeitvergleich bei wachsender Groesse ------------------------\n",
|
||||||
|
" print(\"\\n\" + \"-\" * 84)\n",
|
||||||
|
" print(f\"{'n':>4} | {'Ungarisch':>12} | {'LP (kontinuierlich)':>21} | \"\n",
|
||||||
|
" f\"{'MILP (ganzzahlig)':>19} | {'gleich?':>8}\")\n",
|
||||||
|
" print(\"-\" * 84)\n",
|
||||||
|
" for n in [10, 25, 50, 100]:\n",
|
||||||
|
" k = erzeuge_kosten(n)\n",
|
||||||
|
"\n",
|
||||||
|
" t0 = time.perf_counter(); w1, _ = loese_ungarisch(k); t1 = time.perf_counter() - t0\n",
|
||||||
|
" t0 = time.perf_counter(); w2, _ = loese_lp(k, False); t2 = time.perf_counter() - t0\n",
|
||||||
|
" if n <= 50:\n",
|
||||||
|
" t0 = time.perf_counter(); w3, _ = loese_lp(k, True); t3 = time.perf_counter() - t0\n",
|
||||||
|
" t3_text, gleich = f\"{t3*1000:>16.1f} ms\", abs(w1 - w3) < 1e-6\n",
|
||||||
|
" else:\n",
|
||||||
|
" t3_text, gleich = f\"{'uebersprungen':>19}\", abs(w1 - w2) < 1e-6\n",
|
||||||
|
"\n",
|
||||||
|
" print(f\"{n:>4} | {t1*1000:>9.1f} ms | {t2*1000:>18.1f} ms | {t3_text} | \"\n",
|
||||||
|
" f\"{'ja' if gleich else 'NEIN':>8}\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"-\" * 84)\n",
|
||||||
|
" print(\"Fazit: Der spezialisierte Ungarische Algorithmus ist um Groessenordnungen\")\n",
|
||||||
|
" print(\"schneller. Nutzen Sie fuer reine Zuordnungen NIE einen MILP-Solver.\")\n",
|
||||||
|
" print(\"=\" * 84)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Praxisbeispiel: Flotten-Routing\n",
|
||||||
|
"\n",
|
||||||
|
"`VRP_Flotten_Routing.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# VRP_Flotten_Routing.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel Graphen: Capacitated Vehicle Routing Problem with Time Windows (CVRPTW)\n",
|
||||||
|
"mit der Routing-Bibliothek von Google OR-Tools.\n",
|
||||||
|
"\n",
|
||||||
|
"Eigenschaften:\n",
|
||||||
|
" * Eingabedaten werden vorab auf Plausibilitaet geprueft (Kapazitaet\n",
|
||||||
|
" ausreichend? Zeitfenster erreichbar?)\n",
|
||||||
|
" * Fahrzeit und Servicezeit werden getrennt ausgewiesen\n",
|
||||||
|
" * Ausgabe als lesbarer Tourenplan mit Ankunftszeiten\n",
|
||||||
|
" * Kennzahlen: Auslastung, Leerfahrten, Wartezeit\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"import numpy as np\n",
|
||||||
|
"from ortools.constraint_solver import pywrapcp, routing_enums_pb2\n",
|
||||||
|
"\n",
|
||||||
|
"SERVICEZEIT = 10 # Minuten je Kundenstopp\n",
|
||||||
|
"WARTEZEIT_MAX = 60 # zulaessige Wartezeit bei zu frueher Ankunft\n",
|
||||||
|
"SCHICHTLAENGE = 600 # Minuten\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def erzeuge_daten(seed: int = 42):\n",
|
||||||
|
" \"\"\"Synthetische, aber reproduzierbare Instanz: 1 Depot + 16 Kunden.\"\"\"\n",
|
||||||
|
" anzahl_orte = 17\n",
|
||||||
|
" rng = np.random.default_rng(seed)\n",
|
||||||
|
" koordinaten = rng.random((anzahl_orte, 2)) * 100 # 100 x 100 km Raster\n",
|
||||||
|
"\n",
|
||||||
|
" distanz = np.zeros((anzahl_orte, anzahl_orte), dtype=int)\n",
|
||||||
|
" for i in range(anzahl_orte):\n",
|
||||||
|
" for j in range(anzahl_orte):\n",
|
||||||
|
" distanz[i][j] = int(np.linalg.norm(koordinaten[i] - koordinaten[j]))\n",
|
||||||
|
"\n",
|
||||||
|
" return {\n",
|
||||||
|
" \"distanzmatrix\": distanz.tolist(),\n",
|
||||||
|
" \"zeitfenster\": [\n",
|
||||||
|
" (0, SCHICHTLAENGE), # 0: Depot\n",
|
||||||
|
" (30, 120), (60, 180), (100, 240), (150, 300), # Kunden 1-4\n",
|
||||||
|
" (60, 180), (120, 240), (200, 360), (300, 450), # Kunden 5-8\n",
|
||||||
|
" (180, 300), (240, 360), (300, 480), (360, 500), # Kunden 9-12\n",
|
||||||
|
" (60, 200), (120, 300), (240, 400), (300, 550), # Kunden 13-16\n",
|
||||||
|
" ],\n",
|
||||||
|
" \"bedarfe\": [0, 2, 3, 1, 4, 2, 2, 3, 1, 2, 4, 3, 2, 1, 2, 3, 2],\n",
|
||||||
|
" \"kapazitaeten\": [10, 10, 10, 10],\n",
|
||||||
|
" \"anzahl_fahrzeuge\": 4,\n",
|
||||||
|
" \"depot\": 0,\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def pruefe_daten(daten) -> None:\n",
|
||||||
|
" \"\"\"Vorabdiagnose - fangt die haeufigsten Ursachen fuer 'keine Loesung' ab.\"\"\"\n",
|
||||||
|
" gesamtbedarf = sum(daten[\"bedarfe\"])\n",
|
||||||
|
" gesamtkapazitaet = sum(daten[\"kapazitaeten\"])\n",
|
||||||
|
" print(f\"Gesamtbedarf {gesamtbedarf} Einheiten | \"\n",
|
||||||
|
" f\"Flottenkapazitaet {gesamtkapazitaet} Einheiten | \"\n",
|
||||||
|
" f\"Auslastung {gesamtbedarf / gesamtkapazitaet * 100:.0f} %\")\n",
|
||||||
|
" if gesamtbedarf > gesamtkapazitaet:\n",
|
||||||
|
" raise SystemExit(\"UNLOESBAR: Der Bedarf uebersteigt die Flottenkapazitaet.\")\n",
|
||||||
|
"\n",
|
||||||
|
" d = daten[\"distanzmatrix\"]\n",
|
||||||
|
" for kunde, (fruehestens, spaetestens) in enumerate(daten[\"zeitfenster\"]):\n",
|
||||||
|
" if kunde == 0:\n",
|
||||||
|
" continue\n",
|
||||||
|
" direktfahrt = d[0][kunde]\n",
|
||||||
|
" if direktfahrt > spaetestens:\n",
|
||||||
|
" raise SystemExit(\n",
|
||||||
|
" f\"UNLOESBAR: Kunde {kunde} ist erst nach {direktfahrt} min erreichbar, \"\n",
|
||||||
|
" f\"sein Zeitfenster endet aber bei {spaetestens} min.\")\n",
|
||||||
|
" print(\"Vorabpruefung bestanden: Kapazitaet und Zeitfenster sind grundsaetzlich machbar.\")\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def loese_cvrptw(zeitlimit_s: int = 5):\n",
|
||||||
|
" daten = erzeuge_daten()\n",
|
||||||
|
" pruefe_daten(daten)\n",
|
||||||
|
"\n",
|
||||||
|
" manager = pywrapcp.RoutingIndexManager(\n",
|
||||||
|
" len(daten[\"distanzmatrix\"]), daten[\"anzahl_fahrzeuge\"], daten[\"depot\"])\n",
|
||||||
|
" routing = pywrapcp.RoutingModel(manager)\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Fahrzeit + Servicezeit als Kantengewicht ------------------------\n",
|
||||||
|
" def zeit_callback(von_index, nach_index):\n",
|
||||||
|
" von = manager.IndexToNode(von_index)\n",
|
||||||
|
" nach = manager.IndexToNode(nach_index)\n",
|
||||||
|
" service = SERVICEZEIT if von != daten[\"depot\"] else 0\n",
|
||||||
|
" return daten[\"distanzmatrix\"][von][nach] + service\n",
|
||||||
|
"\n",
|
||||||
|
" zeit_index = routing.RegisterTransitCallback(zeit_callback)\n",
|
||||||
|
" routing.SetArcCostEvaluatorOfAllVehicles(zeit_index)\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Kapazitaetsdimension ---------------------------------------------\n",
|
||||||
|
" def bedarf_callback(von_index):\n",
|
||||||
|
" return daten[\"bedarfe\"][manager.IndexToNode(von_index)]\n",
|
||||||
|
"\n",
|
||||||
|
" bedarf_index = routing.RegisterUnaryTransitCallback(bedarf_callback)\n",
|
||||||
|
" routing.AddDimensionWithVehicleCapacity(\n",
|
||||||
|
" bedarf_index, 0, daten[\"kapazitaeten\"], True, \"Kapazitaet\")\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Zeitdimension mit Zeitfenstern -----------------------------------\n",
|
||||||
|
" routing.AddDimension(zeit_index, WARTEZEIT_MAX, SCHICHTLAENGE, False, \"Zeit\")\n",
|
||||||
|
" zeit_dimension = routing.GetDimensionOrDie(\"Zeit\")\n",
|
||||||
|
" for ort, (fruehestens, spaetestens) in enumerate(daten[\"zeitfenster\"]):\n",
|
||||||
|
" zeit_dimension.CumulVar(manager.NodeToIndex(ort)).SetRange(fruehestens, spaetestens)\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Suchparameter -----------------------------------------------------\n",
|
||||||
|
" parameter = pywrapcp.DefaultRoutingSearchParameters()\n",
|
||||||
|
" parameter.first_solution_strategy = (\n",
|
||||||
|
" routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC)\n",
|
||||||
|
" parameter.local_search_metaheuristic = (\n",
|
||||||
|
" routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH)\n",
|
||||||
|
" parameter.time_limit.seconds = zeitlimit_s\n",
|
||||||
|
"\n",
|
||||||
|
" loesung = routing.SolveWithParameters(parameter)\n",
|
||||||
|
" if not loesung:\n",
|
||||||
|
" print(\"Keine zulaessige Routenfuehrung gefunden.\")\n",
|
||||||
|
" return\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Auswertung --------------------------------------------------------\n",
|
||||||
|
" print(\"\\n\" + \"=\" * 84)\n",
|
||||||
|
" print(\" OPTIMIERTER TOURENPLAN (CVRPTW)\")\n",
|
||||||
|
" print(\"=\" * 84)\n",
|
||||||
|
"\n",
|
||||||
|
" gesamtzeit = gesamtfracht = gesamtdistanz = 0\n",
|
||||||
|
" kapazitaet = daten[\"kapazitaeten\"]\n",
|
||||||
|
"\n",
|
||||||
|
" for fahrzeug in range(daten[\"anzahl_fahrzeuge\"]):\n",
|
||||||
|
" index = routing.Start(fahrzeug)\n",
|
||||||
|
" if routing.IsEnd(loesung.Value(routing.NextVar(index))):\n",
|
||||||
|
" print(f\"\\nFahrzeug {fahrzeug + 1}: nicht eingesetzt\")\n",
|
||||||
|
" continue\n",
|
||||||
|
"\n",
|
||||||
|
" stationen, fracht, distanz = [], 0, 0\n",
|
||||||
|
" while not routing.IsEnd(index):\n",
|
||||||
|
" knoten = manager.IndexToNode(index)\n",
|
||||||
|
" ankunft = loesung.Min(zeit_dimension.CumulVar(index))\n",
|
||||||
|
" fracht += daten[\"bedarfe\"][knoten]\n",
|
||||||
|
" bezeichnung = \"Depot\" if knoten == 0 else f\"K{knoten}\"\n",
|
||||||
|
" stationen.append(f\"{bezeichnung}@{ankunft}\")\n",
|
||||||
|
" naechster = loesung.Value(routing.NextVar(index))\n",
|
||||||
|
" distanz += daten[\"distanzmatrix\"][knoten][manager.IndexToNode(naechster)]\n",
|
||||||
|
" index = naechster\n",
|
||||||
|
"\n",
|
||||||
|
" endzeit = loesung.Min(zeit_dimension.CumulVar(index))\n",
|
||||||
|
" stationen.append(f\"Depot@{endzeit}\")\n",
|
||||||
|
" gesamtzeit += endzeit\n",
|
||||||
|
" gesamtfracht += fracht\n",
|
||||||
|
" gesamtdistanz += distanz\n",
|
||||||
|
"\n",
|
||||||
|
" print(f\"\\nFahrzeug {fahrzeug + 1}:\")\n",
|
||||||
|
" print(\" \" + \" -> \".join(stationen))\n",
|
||||||
|
" print(f\" Schichtzeit {endzeit} min | Fahrstrecke {distanz} km | \"\n",
|
||||||
|
" f\"Fracht {fracht}/{kapazitaet[fahrzeug]} \"\n",
|
||||||
|
" f\"({fracht / kapazitaet[fahrzeug] * 100:.0f} % Auslastung)\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\n\" + \"-\" * 84)\n",
|
||||||
|
" print(f\"Summe Schichtzeiten: {gesamtzeit} min\")\n",
|
||||||
|
" print(f\"Summe Fahrstrecken: {gesamtdistanz} km\")\n",
|
||||||
|
" print(f\"Transportierte Fracht: {gesamtfracht} von {sum(daten['bedarfe'])} Einheiten\")\n",
|
||||||
|
" assert gesamtfracht == sum(daten[\"bedarfe\"]), \"Nicht alle Kunden wurden beliefert!\"\n",
|
||||||
|
" print(\"Alle Kunden wurden innerhalb ihrer Zeitfenster beliefert.\")\n",
|
||||||
|
" print(\"=\" * 84)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"if __name__ == \"__main__\":\n",
|
||||||
|
" loese_cvrptw()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Finde den Denkfehler\n",
|
||||||
|
"\n",
|
||||||
|
"`VRP_Kapazitaetsfalle.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# VRP_Kapazitaetsfalle.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel Graphen: Die vergessene Dimension.\n",
|
||||||
|
"\n",
|
||||||
|
"Die Routing-Bibliothek von OR-Tools kennt keine \"Kapazitaet\" von sich aus.\n",
|
||||||
|
"Sie kennt nur DIMENSIONEN - benannte Groessen, die sich entlang einer Tour\n",
|
||||||
|
"aufsummieren und begrenzt werden koennen. Distanz ist eine, Zeit ist eine,\n",
|
||||||
|
"Ladung ist eine. Wer eine davon nicht anlegt, bekommt trotzdem eine Loesung:\n",
|
||||||
|
"eine schoene, kurze, guenstige - und unfahrbare.\n",
|
||||||
|
"\n",
|
||||||
|
"Dieses Programm loest dieselbe Instanz zweimal und prueft beide Ergebnisse\n",
|
||||||
|
"gegen die tatsaechlichen Lademengen.\n",
|
||||||
|
"\n",
|
||||||
|
"Instanz: 1 Depot, 16 Kunden, 4 Fahrzeuge zu je 10 Paletten.\n",
|
||||||
|
"Gesamtbedarf 37 Paletten bei 40 Paletten Flottenkapazitaet - es ist also\n",
|
||||||
|
"knapp, aber machbar.\n",
|
||||||
|
"\n",
|
||||||
|
"Benoetigt: numpy, ortools\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"from __future__ import annotations\n",
|
||||||
|
"\n",
|
||||||
|
"import numpy as np\n",
|
||||||
|
"from ortools.constraint_solver import pywrapcp, routing_enums_pb2\n",
|
||||||
|
"\n",
|
||||||
|
"# Dieselbe Instanz wie VRP_Flotten_Routing.py\n",
|
||||||
|
"BEDARFE = [0, 2, 3, 1, 4, 2, 2, 3, 1, 2, 4, 3, 2, 1, 2, 3, 2]\n",
|
||||||
|
"KAPAZITAETEN = [10, 10, 10, 10]\n",
|
||||||
|
"ANZAHL_FAHRZEUGE = 4\n",
|
||||||
|
"DEPOT = 0\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def distanzmatrix(seed: int = 42) -> list[list[int]]:\n",
|
||||||
|
" rng = np.random.default_rng(seed)\n",
|
||||||
|
" koordinaten = rng.random((len(BEDARFE), 2)) * 100 # 100 x 100 km\n",
|
||||||
|
" n = len(BEDARFE)\n",
|
||||||
|
" return [[int(np.linalg.norm(koordinaten[i] - koordinaten[j]))\n",
|
||||||
|
" for j in range(n)] for i in range(n)]\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def plane(mit_kapazitaet: bool, zeitlimit: int = 5) -> dict:\n",
|
||||||
|
" \"\"\"Loest die Tourenplanung - wahlweise mit oder ohne Ladungsdimension.\"\"\"\n",
|
||||||
|
" distanz = distanzmatrix()\n",
|
||||||
|
" manager = pywrapcp.RoutingIndexManager(len(distanz), ANZAHL_FAHRZEUGE, DEPOT)\n",
|
||||||
|
" routing = pywrapcp.RoutingModel(manager)\n",
|
||||||
|
"\n",
|
||||||
|
" def entfernung(von_index, nach_index):\n",
|
||||||
|
" return distanz[manager.IndexToNode(von_index)][manager.IndexToNode(nach_index)]\n",
|
||||||
|
"\n",
|
||||||
|
" kosten_id = routing.RegisterTransitCallback(entfernung)\n",
|
||||||
|
" routing.SetArcCostEvaluatorOfAllVehicles(kosten_id)\n",
|
||||||
|
"\n",
|
||||||
|
" # DIE entscheidende Stelle. Ohne diesen Block existiert im Modell keine\n",
|
||||||
|
" # Ladung - die Fahrzeuge sind dann unendlich gross.\n",
|
||||||
|
" if mit_kapazitaet:\n",
|
||||||
|
" def bedarf(index):\n",
|
||||||
|
" return BEDARFE[manager.IndexToNode(index)]\n",
|
||||||
|
"\n",
|
||||||
|
" bedarf_id = routing.RegisterUnaryTransitCallback(bedarf)\n",
|
||||||
|
" routing.AddDimensionWithVehicleCapacity(\n",
|
||||||
|
" bedarf_id,\n",
|
||||||
|
" 0, # kein Zwischenpuffer\n",
|
||||||
|
" KAPAZITAETEN, # Obergrenze je Fahrzeug\n",
|
||||||
|
" True, # Ladung startet bei 0\n",
|
||||||
|
" \"Ladung\")\n",
|
||||||
|
"\n",
|
||||||
|
" parameter = pywrapcp.DefaultRoutingSearchParameters()\n",
|
||||||
|
" parameter.first_solution_strategy = (\n",
|
||||||
|
" routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC)\n",
|
||||||
|
" parameter.local_search_metaheuristic = (\n",
|
||||||
|
" routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH)\n",
|
||||||
|
" parameter.time_limit.FromSeconds(zeitlimit)\n",
|
||||||
|
"\n",
|
||||||
|
" loesung = routing.SolveWithParameters(parameter)\n",
|
||||||
|
" if loesung is None:\n",
|
||||||
|
" raise RuntimeError(\"Keine Loesung gefunden\")\n",
|
||||||
|
"\n",
|
||||||
|
" touren, strecken, ladungen = [], [], []\n",
|
||||||
|
" for fahrzeug in range(ANZAHL_FAHRZEUGE):\n",
|
||||||
|
" index = routing.Start(fahrzeug)\n",
|
||||||
|
" tour, strecke, ladung = [], 0, 0\n",
|
||||||
|
" while not routing.IsEnd(index):\n",
|
||||||
|
" knoten = manager.IndexToNode(index)\n",
|
||||||
|
" tour.append(knoten)\n",
|
||||||
|
" ladung += BEDARFE[knoten]\n",
|
||||||
|
" vorher = index\n",
|
||||||
|
" index = loesung.Value(routing.NextVar(index))\n",
|
||||||
|
" strecke += routing.GetArcCostForVehicle(vorher, index, fahrzeug)\n",
|
||||||
|
" tour.append(manager.IndexToNode(index))\n",
|
||||||
|
" touren.append(tour)\n",
|
||||||
|
" strecken.append(strecke)\n",
|
||||||
|
" ladungen.append(ladung)\n",
|
||||||
|
"\n",
|
||||||
|
" return {\"touren\": touren, \"strecken\": strecken, \"ladungen\": ladungen,\n",
|
||||||
|
" \"gesamtstrecke\": sum(strecken)}\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def pruefe(ergebnis: dict) -> list[str]:\n",
|
||||||
|
" \"\"\"Prueft den Plan gegen die Wirklichkeit - unabhaengig vom Modell.\n",
|
||||||
|
"\n",
|
||||||
|
" Genau diese Trennung ist der Punkt: Die Pruefung darf nicht dieselben\n",
|
||||||
|
" Annahmen benutzen wie das Modell, sonst prueft sie nichts.\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" beanstandungen = []\n",
|
||||||
|
" for fahrzeug, (ladung, kapazitaet) in enumerate(\n",
|
||||||
|
" zip(ergebnis[\"ladungen\"], KAPAZITAETEN)):\n",
|
||||||
|
" if ladung > kapazitaet:\n",
|
||||||
|
" beanstandungen.append(\n",
|
||||||
|
" f\"Fahrzeug {fahrzeug + 1}: {ladung} Paletten geladen, \"\n",
|
||||||
|
" f\"Kapazitaet {kapazitaet} ({ladung - kapazitaet} zu viel)\")\n",
|
||||||
|
"\n",
|
||||||
|
" beliefert = sorted(k for tour in ergebnis[\"touren\"] for k in tour[1:-1])\n",
|
||||||
|
" erwartet = list(range(1, len(BEDARFE)))\n",
|
||||||
|
" if beliefert != erwartet:\n",
|
||||||
|
" fehlend = set(erwartet) - set(beliefert)\n",
|
||||||
|
" if fehlend:\n",
|
||||||
|
" beanstandungen.append(f\"nicht beliefert: {sorted(fehlend)}\")\n",
|
||||||
|
" return beanstandungen\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def zeige(titel: str, ergebnis: dict) -> None:\n",
|
||||||
|
" print(f\"\\n{titel}\")\n",
|
||||||
|
" print(f\" Gesamtstrecke {ergebnis['gesamtstrecke']} km\")\n",
|
||||||
|
" print(f\" {'Fahrzeug':<10} {'Stopps':>7} {'Strecke':>9} {'Ladung':>8} \"\n",
|
||||||
|
" f\"{'Kapazitaet':>11}\")\n",
|
||||||
|
" for i, (tour, strecke, ladung) in enumerate(\n",
|
||||||
|
" zip(ergebnis[\"touren\"], ergebnis[\"strecken\"], ergebnis[\"ladungen\"])):\n",
|
||||||
|
" markierung = \" <-- ueberladen\" if ladung > KAPAZITAETEN[i] else \"\"\n",
|
||||||
|
" print(f\" {i + 1:<10} {len(tour) - 2:>7} {strecke:>8} km {ladung:>8} \"\n",
|
||||||
|
" f\"{KAPAZITAETEN[i]:>11}{markierung}\")\n",
|
||||||
|
"\n",
|
||||||
|
" beanstandungen = pruefe(ergebnis)\n",
|
||||||
|
" if beanstandungen:\n",
|
||||||
|
" print(\" PRUEFUNG: DURCHGEFALLEN\")\n",
|
||||||
|
" for text in beanstandungen:\n",
|
||||||
|
" print(f\" - {text}\")\n",
|
||||||
|
" else:\n",
|
||||||
|
" print(\" PRUEFUNG: bestanden\")\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"if __name__ == \"__main__\":\n",
|
||||||
|
" print(\"=\" * 78)\n",
|
||||||
|
" print(\" DIE VERGESSENE DIMENSION\")\n",
|
||||||
|
" print(\"=\" * 78)\n",
|
||||||
|
" print(f\"16 Kunden, Gesamtbedarf {sum(BEDARFE)} Paletten, \"\n",
|
||||||
|
" f\"{ANZAHL_FAHRZEUGE} Fahrzeuge zu je {KAPAZITAETEN[0]} \"\n",
|
||||||
|
" f\"= {sum(KAPAZITAETEN)} Paletten Flottenkapazitaet.\")\n",
|
||||||
|
"\n",
|
||||||
|
" ohne = plane(mit_kapazitaet=False)\n",
|
||||||
|
" zeige(\"[1] Ohne Ladungsdimension\", ohne)\n",
|
||||||
|
"\n",
|
||||||
|
" mit = plane(mit_kapazitaet=True)\n",
|
||||||
|
" zeige(\"[2] Mit AddDimensionWithVehicleCapacity\", mit)\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\n\" + \"=\" * 78)\n",
|
||||||
|
" mehr = mit[\"gesamtstrecke\"] - ohne[\"gesamtstrecke\"]\n",
|
||||||
|
" print(f\"Der korrekte Plan ist {mehr} km laenger \"\n",
|
||||||
|
" f\"({mehr / ohne['gesamtstrecke'] * 100:.1f} %).\")\n",
|
||||||
|
" print()\n",
|
||||||
|
" print(\"Und genau darin liegt die Gefahr: Lauf [1] sieht BESSER aus. Wer\")\n",
|
||||||
|
" print(\"beide Zahlen nebeneinander legt, ohne die Ladung zu pruefen, haelt\")\n",
|
||||||
|
" print(\"die unfahrbare Loesung fuer die bessere Optimierung - und den\")\n",
|
||||||
|
" print(\"korrekten Plan fuer schlechte Arbeit.\")\n",
|
||||||
|
" print()\n",
|
||||||
|
" print(\"Die Routing-Bibliothek kennt keine 'Kapazitaet'. Sie kennt nur\")\n",
|
||||||
|
" print(\"Dimensionen, die man ihr anlegt. Was nicht als Dimension existiert,\")\n",
|
||||||
|
" print(\"wird nicht begrenzt - und faellt niemandem auf, weil das Ergebnis\")\n",
|
||||||
|
" print(\"plausibel aussieht.\")\n",
|
||||||
|
" print(\"=\" * 78)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"name": "python",
|
||||||
|
"version": "3.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
623
Notebooks_04/handelsmaschine.ipynb
Normal file
623
Notebooks_04/handelsmaschine.ipynb
Normal file
|
|
@ -0,0 +1,623 @@
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"# Kapitel 21: Die vollständige quantitative Handelsmaschine\n",
|
||||||
|
"\n",
|
||||||
|
"Begleitnotebook zu *Optimierte Entscheidungsfindung mit Python*. Die Codezellen sind identisch mit den im Buch abgedruckten Programmen.\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"# Einmalig ausfuehren: installiert alle im Buch verwendeten Pakete.\n",
|
||||||
|
"# Lokal in einer virtuellen Umgebung genauso gueltig wie in Google Colab.\n",
|
||||||
|
"%pip install --quiet ortools highspy cvxpy scipy numpy pandas polars \\\n",
|
||||||
|
" scikit-learn matplotlib plotly pyomo linopy pymoo pydantic openpyxl"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Die Engine\n",
|
||||||
|
"\n",
|
||||||
|
"`QuantitativeTradingEngine.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# QuantitativeTradingEngine.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel Handelsmaschine: Vollstaendige quantitative Handelsmaschine mit\n",
|
||||||
|
"Walk-Forward-Backtest und OR-Optimierungsschicht.\n",
|
||||||
|
"\n",
|
||||||
|
"Eigenschaften:\n",
|
||||||
|
" * Rebalancing-Termine sind garantiert Handelstage; die Zahl der\n",
|
||||||
|
" tatsaechlich ausgefuehrten Umschichtungen wird ausgewiesen\n",
|
||||||
|
" * Spaltenreihenfolge erzwungen\n",
|
||||||
|
" * Einheiten konsistent (taegliche Groessen im Modell)\n",
|
||||||
|
" * Szenario-Nebenbedingungen vektorisiert\n",
|
||||||
|
" * Lookahead-Selbsttest eingebaut\n",
|
||||||
|
" * Zusaetzliche Kennzahlen: Calmar, Trefferquote, Turnover, Kostenanteil\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"import os\n",
|
||||||
|
"\n",
|
||||||
|
"import cvxpy as cp\n",
|
||||||
|
"import numpy as np\n",
|
||||||
|
"import pandas as pd\n",
|
||||||
|
"import yfinance as yf\n",
|
||||||
|
"from sklearn.covariance import LedoitWolf\n",
|
||||||
|
"import matplotlib\n",
|
||||||
|
"matplotlib.use(\"Agg\")\n",
|
||||||
|
"import matplotlib.pyplot as plt\n",
|
||||||
|
"\n",
|
||||||
|
"OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), \"output\")\n",
|
||||||
|
"os.makedirs(OUTPUT_DIR, exist_ok=True)\n",
|
||||||
|
"\n",
|
||||||
|
"HANDELSTAGE = 252\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"class QuantitativeHandelsmaschine:\n",
|
||||||
|
" \"\"\"Walk-Forward-Backtest mit CVaR-Optimierung und Transaktionskosten.\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
" def __init__(self, universum, benchmark=\"SPY\", jahre=5,\n",
|
||||||
|
" startkapital=100_000.0, gebuehrensatz=0.0015,\n",
|
||||||
|
" max_gewicht=0.20, alpha=0.95, risikoaversion=1.2,\n",
|
||||||
|
" warmup=HANDELSTAGE):\n",
|
||||||
|
" self.universum = list(universum)\n",
|
||||||
|
" self.benchmark = benchmark\n",
|
||||||
|
" self.jahre = jahre\n",
|
||||||
|
" self.startkapital = startkapital\n",
|
||||||
|
" self.gebuehrensatz = gebuehrensatz\n",
|
||||||
|
" self.max_gewicht = max_gewicht\n",
|
||||||
|
" self.alpha = alpha\n",
|
||||||
|
" self.risikoaversion = risikoaversion\n",
|
||||||
|
" self.warmup = warmup\n",
|
||||||
|
"\n",
|
||||||
|
" self.kurse = None\n",
|
||||||
|
" self.benchmark_kurse = None\n",
|
||||||
|
" self.renditen = None\n",
|
||||||
|
" self.rebalancing_termine = None\n",
|
||||||
|
"\n",
|
||||||
|
" # -- 1. Datenpipeline ------------------------------------------------\n",
|
||||||
|
" def lade_daten(self):\n",
|
||||||
|
" ende = pd.Timestamp.today().normalize()\n",
|
||||||
|
" start = ende - pd.DateOffset(years=self.jahre)\n",
|
||||||
|
" alle = self.universum + [self.benchmark]\n",
|
||||||
|
"\n",
|
||||||
|
" print(f\"Lade {len(self.universum)} Titel + Benchmark ({self.benchmark}) \"\n",
|
||||||
|
" f\"ueber {self.jahre} Jahre ...\")\n",
|
||||||
|
" roh = yf.download(alle, start=start, end=ende, auto_adjust=True, progress=False)\n",
|
||||||
|
" if roh.empty:\n",
|
||||||
|
" raise SystemExit(\"Download fehlgeschlagen (Netz/Ticker/Rate-Limit pruefen).\")\n",
|
||||||
|
"\n",
|
||||||
|
" if isinstance(roh.columns, pd.MultiIndex):\n",
|
||||||
|
" daten = roh[\"Close\"][alle].dropna() # erzwingt eigene Spaltenreihenfolge\n",
|
||||||
|
" else:\n",
|
||||||
|
" daten = roh[[\"Close\"]].dropna()\n",
|
||||||
|
" daten.columns = alle\n",
|
||||||
|
" assert list(daten.columns) == alle, \"Spaltenreihenfolge weicht ab!\"\n",
|
||||||
|
"\n",
|
||||||
|
" self.kurse = daten[self.universum]\n",
|
||||||
|
" self.benchmark_kurse = daten[self.benchmark]\n",
|
||||||
|
" self.renditen = self.kurse.pct_change().dropna()\n",
|
||||||
|
"\n",
|
||||||
|
" # --- erster HANDELStag je Monat, nicht Kalendermonatsanfang -----\n",
|
||||||
|
" monat = self.kurse.index.to_period(\"M\")\n",
|
||||||
|
" self.rebalancing_termine = self.kurse.index[~monat.duplicated()]\n",
|
||||||
|
" assert self.rebalancing_termine.isin(self.kurse.index).all(), \\\n",
|
||||||
|
" \"Rebalancing-Termine sind keine Handelstage!\"\n",
|
||||||
|
"\n",
|
||||||
|
" print(f\" {len(self.kurse)} Handelstage, \"\n",
|
||||||
|
" f\"{len(self.rebalancing_termine)} Rebalancing-Termine \"\n",
|
||||||
|
" f\"(alle sind Handelstage)\")\n",
|
||||||
|
"\n",
|
||||||
|
" # -- 2. Signal --------------------------------------------------------\n",
|
||||||
|
" def alpha_signal(self, stichtag, rueckblick=HANDELSTAGE):\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" 12-1-Momentum: Kursentwicklung der letzten 12 Monate unter Auslassung\n",
|
||||||
|
" des letzten Monats (kurzfristige Umkehr herausrechnen).\n",
|
||||||
|
" Rueckgabe: TAEGLICHE Renditeerwartung.\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" historie = self.kurse.loc[:stichtag]\n",
|
||||||
|
" if len(historie) < rueckblick:\n",
|
||||||
|
" return np.zeros(len(self.universum))\n",
|
||||||
|
"\n",
|
||||||
|
" p_alt = historie.iloc[-rueckblick]\n",
|
||||||
|
" p_neu = historie.iloc[-21] # vor ca. einem Monat\n",
|
||||||
|
" momentum = (p_neu / p_alt - 1.0).values\n",
|
||||||
|
"\n",
|
||||||
|
" # Z-Score, damit das Signal skalenfrei wird\n",
|
||||||
|
" streuung = momentum.std()\n",
|
||||||
|
" score = (momentum - momentum.mean()) / (streuung + 1e-9)\n",
|
||||||
|
"\n",
|
||||||
|
" # In eine taegliche Renditeerwartung uebersetzen:\n",
|
||||||
|
" # Basis 4 % p.a. plus 6 % p.a. je Standardabweichung Momentum.\n",
|
||||||
|
" mu_jaehrlich = np.maximum(0.04 + 0.06 * score, 0.0)\n",
|
||||||
|
" return mu_jaehrlich / HANDELSTAGE\n",
|
||||||
|
"\n",
|
||||||
|
" # -- 3./4. Risikomodell und Optimierung -------------------------------\n",
|
||||||
|
" def optimiere(self, stichtag, w_alt, rueckblick=HANDELSTAGE):\n",
|
||||||
|
" historie = self.renditen.loc[:stichtag].iloc[-rueckblick:]\n",
|
||||||
|
" if len(historie) < 100:\n",
|
||||||
|
" return w_alt, False\n",
|
||||||
|
"\n",
|
||||||
|
" R = historie.values\n",
|
||||||
|
" S, N = R.shape\n",
|
||||||
|
" mu_taeglich = self.alpha_signal(stichtag, rueckblick)\n",
|
||||||
|
"\n",
|
||||||
|
" w = cp.Variable(N, nonneg=True)\n",
|
||||||
|
" gamma = cp.Variable()\n",
|
||||||
|
" u = cp.Variable(S, nonneg=True)\n",
|
||||||
|
"\n",
|
||||||
|
" cvar = gamma + (1.0 / (S * (1.0 - self.alpha))) * cp.sum(u)\n",
|
||||||
|
" kosten = self.gebuehrensatz * cp.norm1(w - w_alt)\n",
|
||||||
|
" ziel = cp.Maximize(mu_taeglich @ w - self.risikoaversion * cvar - kosten)\n",
|
||||||
|
"\n",
|
||||||
|
" bedingungen = [cp.sum(w) == 1,\n",
|
||||||
|
" w <= self.max_gewicht,\n",
|
||||||
|
" u >= -(R @ w) - gamma] # vektorisiert\n",
|
||||||
|
" problem = cp.Problem(ziel, bedingungen)\n",
|
||||||
|
" try:\n",
|
||||||
|
" problem.solve(solver=cp.CLARABEL)\n",
|
||||||
|
" except cp.error.SolverError:\n",
|
||||||
|
" return w_alt, False\n",
|
||||||
|
"\n",
|
||||||
|
" if problem.status not in (\"optimal\", \"optimal_inaccurate\") or w.value is None:\n",
|
||||||
|
" return w_alt, False\n",
|
||||||
|
"\n",
|
||||||
|
" gewichte = np.maximum(w.value, 0.0)\n",
|
||||||
|
" return gewichte / gewichte.sum(), True\n",
|
||||||
|
"\n",
|
||||||
|
" # -- 5. Backtest -------------------------------------------------------\n",
|
||||||
|
" def backtest(self):\n",
|
||||||
|
" print(\"Starte Walk-Forward-Backtest ...\")\n",
|
||||||
|
" N = len(self.universum)\n",
|
||||||
|
" gewichte = np.ones(N) / N\n",
|
||||||
|
"\n",
|
||||||
|
" werte = [self.startkapital]\n",
|
||||||
|
" datumsliste = [self.renditen.index[self.warmup]]\n",
|
||||||
|
" gewichtshistorie = []\n",
|
||||||
|
" geplant = versucht = ausgefuehrt = 0\n",
|
||||||
|
" kosten_gesamt = 0.0\n",
|
||||||
|
" turnover_gesamt = 0.0\n",
|
||||||
|
"\n",
|
||||||
|
" for t in range(self.warmup, len(self.renditen) - 1):\n",
|
||||||
|
" heute = self.renditen.index[t]\n",
|
||||||
|
" morgen = self.renditen.index[t + 1]\n",
|
||||||
|
"\n",
|
||||||
|
" ist_termin = heute in self.rebalancing_termine or t == self.warmup\n",
|
||||||
|
" if ist_termin:\n",
|
||||||
|
" geplant += 1\n",
|
||||||
|
" versucht += 1\n",
|
||||||
|
" neue_gewichte, erfolg = self.optimiere(heute, gewichte)\n",
|
||||||
|
" if erfolg:\n",
|
||||||
|
" ausgefuehrt += 1\n",
|
||||||
|
" turnover = float(np.abs(neue_gewichte - gewichte).sum())\n",
|
||||||
|
" gebuehr = werte[-1] * turnover * self.gebuehrensatz\n",
|
||||||
|
" werte[-1] -= gebuehr\n",
|
||||||
|
" kosten_gesamt += gebuehr\n",
|
||||||
|
" turnover_gesamt += turnover\n",
|
||||||
|
" gewichte = neue_gewichte\n",
|
||||||
|
" gewichtshistorie.append((heute, gewichte.copy()))\n",
|
||||||
|
"\n",
|
||||||
|
" # Rendite von MORGEN auf die HEUTE festgelegten Gewichte anwenden\n",
|
||||||
|
" tagesrenditen = self.renditen.iloc[t + 1].values\n",
|
||||||
|
" portfoliorendite = float(gewichte @ tagesrenditen)\n",
|
||||||
|
" werte.append(werte[-1] * (1.0 + portfoliorendite))\n",
|
||||||
|
" datumsliste.append(morgen)\n",
|
||||||
|
"\n",
|
||||||
|
" # Gewichte driften mit den Kursen weiter\n",
|
||||||
|
" gedriftet = gewichte * (1.0 + tagesrenditen)\n",
|
||||||
|
" gewichte = gedriftet / gedriftet.sum()\n",
|
||||||
|
"\n",
|
||||||
|
" print(f\" Rebalancing: {geplant} geplant, {ausgefuehrt} ausgefuehrt \"\n",
|
||||||
|
" f\"({ausgefuehrt/max(geplant,1)*100:.0f} %)\")\n",
|
||||||
|
" if ausgefuehrt < geplant:\n",
|
||||||
|
" print(f\" WARNUNG: {geplant - ausgefuehrt} Optimierungen sind \"\n",
|
||||||
|
" f\"fehlgeschlagen - Ursache pruefen!\")\n",
|
||||||
|
" print(f\" Kumulierter Turnover: {turnover_gesamt*100:.0f} % | \"\n",
|
||||||
|
" f\"Transaktionskosten insgesamt: {kosten_gesamt:,.2f} EUR\")\n",
|
||||||
|
"\n",
|
||||||
|
" ergebnis = pd.DataFrame({\"Portfolio\": werte}, index=datumsliste)\n",
|
||||||
|
" benchmark = self.benchmark_kurse.loc[datumsliste]\n",
|
||||||
|
" ergebnis[\"Benchmark\"] = benchmark / benchmark.iloc[0] * self.startkapital\n",
|
||||||
|
" return ergebnis, gewichtshistorie, kosten_gesamt\n",
|
||||||
|
"\n",
|
||||||
|
" # -- 6. Auswertung -----------------------------------------------------\n",
|
||||||
|
" @staticmethod\n",
|
||||||
|
" def kennzahlen(reihe, risikofrei=0.02):\n",
|
||||||
|
" renditen = reihe.pct_change().dropna()\n",
|
||||||
|
" jahre = (reihe.index[-1] - reihe.index[0]).days / 365.25\n",
|
||||||
|
" cagr = (reihe.iloc[-1] / reihe.iloc[0]) ** (1 / jahre) - 1\n",
|
||||||
|
" vola = renditen.std() * np.sqrt(HANDELSTAGE)\n",
|
||||||
|
" sharpe = (cagr - risikofrei) / vola if vola > 0 else np.nan\n",
|
||||||
|
" drawdown = ((reihe - reihe.cummax()) / reihe.cummax()).min()\n",
|
||||||
|
" calmar = cagr / abs(drawdown) if drawdown < 0 else np.nan\n",
|
||||||
|
" trefferquote = float((renditen > 0).mean())\n",
|
||||||
|
" return {\"CAGR\": cagr, \"Volatilitaet\": vola, \"Sharpe\": sharpe,\n",
|
||||||
|
" \"Max Drawdown\": drawdown, \"Calmar\": calmar,\n",
|
||||||
|
" \"Trefferquote\": trefferquote, \"Jahre\": jahre}\n",
|
||||||
|
"\n",
|
||||||
|
" def bericht(self, ergebnis, kosten_gesamt):\n",
|
||||||
|
" k_port = self.kennzahlen(ergebnis[\"Portfolio\"])\n",
|
||||||
|
" k_bench = self.kennzahlen(ergebnis[\"Benchmark\"])\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\n\" + \"=\" * 84)\n",
|
||||||
|
" print(\" LEISTUNGSREPORT DER QUANTITATIVEN ENGINE\")\n",
|
||||||
|
" print(\"=\" * 84)\n",
|
||||||
|
" print(f\"Zeitraum: {ergebnis.index[0]:%Y-%m-%d} bis {ergebnis.index[-1]:%Y-%m-%d} \"\n",
|
||||||
|
" f\"({k_port['Jahre']:.1f} Jahre) | Startkapital \"\n",
|
||||||
|
" f\"{self.startkapital:,.0f} EUR\\n\")\n",
|
||||||
|
"\n",
|
||||||
|
" zeilen = [\n",
|
||||||
|
" (\"Endkapital\", f\"{ergebnis['Portfolio'].iloc[-1]:,.0f} EUR\",\n",
|
||||||
|
" f\"{ergebnis['Benchmark'].iloc[-1]:,.0f} EUR\"),\n",
|
||||||
|
" (\"CAGR (Jahresrendite)\", f\"{k_port['CAGR']*100:6.2f} %\",\n",
|
||||||
|
" f\"{k_bench['CAGR']*100:6.2f} %\"),\n",
|
||||||
|
" (\"Volatilitaet p.a.\", f\"{k_port['Volatilitaet']*100:6.2f} %\",\n",
|
||||||
|
" f\"{k_bench['Volatilitaet']*100:6.2f} %\"),\n",
|
||||||
|
" (\"Sharpe Ratio (rf=2 %)\", f\"{k_port['Sharpe']:6.2f}\",\n",
|
||||||
|
" f\"{k_bench['Sharpe']:6.2f}\"),\n",
|
||||||
|
" (\"Maximum Drawdown\", f\"{k_port['Max Drawdown']*100:6.2f} %\",\n",
|
||||||
|
" f\"{k_bench['Max Drawdown']*100:6.2f} %\"),\n",
|
||||||
|
" (\"Calmar Ratio\", f\"{k_port['Calmar']:6.2f}\", f\"{k_bench['Calmar']:6.2f}\"),\n",
|
||||||
|
" (\"Trefferquote (Tage)\", f\"{k_port['Trefferquote']*100:6.1f} %\",\n",
|
||||||
|
" f\"{k_bench['Trefferquote']*100:6.1f} %\"),\n",
|
||||||
|
" ]\n",
|
||||||
|
" print(pd.DataFrame(zeilen, columns=[\"Kennzahl\", \"OR-Strategie\", \"Benchmark\"])\n",
|
||||||
|
" .to_string(index=False))\n",
|
||||||
|
"\n",
|
||||||
|
" anteil = kosten_gesamt / self.startkapital\n",
|
||||||
|
" print(f\"\\nTransaktionskosten: {kosten_gesamt:,.0f} EUR \"\n",
|
||||||
|
" f\"= {anteil*100:.2f} % des Startkapitals \"\n",
|
||||||
|
" f\"= {anteil/k_port['Jahre']*100:.2f} % p.a.\")\n",
|
||||||
|
" print(\"Ohne diese Kosten waere die ausgewiesene Rendite entsprechend hoeher -\")\n",
|
||||||
|
" print(\"genau deshalb gehoeren sie in den Backtest und nicht daneben.\")\n",
|
||||||
|
" print(\"=\" * 84)\n",
|
||||||
|
" return k_port, k_bench\n",
|
||||||
|
"\n",
|
||||||
|
" def zeichne(self, ergebnis):\n",
|
||||||
|
" plt.figure(figsize=(11, 6))\n",
|
||||||
|
" plt.plot(ergebnis.index, ergebnis[\"Portfolio\"], \"b-\", lw=2.2,\n",
|
||||||
|
" label=\"OR-Handelsmaschine\")\n",
|
||||||
|
" plt.plot(ergebnis.index, ergebnis[\"Benchmark\"], \"k--\", lw=1.5, alpha=0.75,\n",
|
||||||
|
" label=f\"Benchmark ({self.benchmark})\")\n",
|
||||||
|
" plt.title(\"Walk-Forward-Backtest: OR-Strategie gegen Benchmark\", fontsize=12)\n",
|
||||||
|
" plt.xlabel(\"Datum\")\n",
|
||||||
|
" plt.ylabel(\"Depotwert in EUR\")\n",
|
||||||
|
" plt.grid(True, linestyle=\":\", alpha=0.6)\n",
|
||||||
|
" plt.legend(loc=\"upper left\")\n",
|
||||||
|
" plt.tight_layout()\n",
|
||||||
|
" ziel = os.path.join(OUTPUT_DIR, \"trading_engine_backtest.png\")\n",
|
||||||
|
" plt.savefig(ziel, dpi=150)\n",
|
||||||
|
" print(f\"Chart gespeichert unter '{ziel}'\")\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def lookahead_selbsttest(maschine):\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" Prueft, dass das Signal zum Zeitpunkt t NUR Daten bis t verwendet.\n",
|
||||||
|
" Methode: kuenstlich die Zukunft veraendern und pruefen, ob sich das\n",
|
||||||
|
" Signal von heute dadurch aendert. Tut es das, liegt Lookahead vor.\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" stichtag = maschine.kurse.index[len(maschine.kurse) // 2]\n",
|
||||||
|
" signal_original = maschine.alpha_signal(stichtag)\n",
|
||||||
|
"\n",
|
||||||
|
" original_kurse = maschine.kurse.copy()\n",
|
||||||
|
" zukunft = maschine.kurse.index > stichtag\n",
|
||||||
|
" maschine.kurse.loc[zukunft] *= 3.0 # Zukunft massiv veraendern\n",
|
||||||
|
" signal_manipuliert = maschine.alpha_signal(stichtag)\n",
|
||||||
|
" maschine.kurse = original_kurse # zuruecksetzen\n",
|
||||||
|
"\n",
|
||||||
|
" identisch = np.allclose(signal_original, signal_manipuliert)\n",
|
||||||
|
" print(f\"Lookahead-Selbsttest: Signal bleibt bei manipulierter Zukunft \"\n",
|
||||||
|
" f\"{'UNVERAENDERT (gut)' if identisch else 'VERAENDERT - LOOKAHEAD-BIAS!'}\")\n",
|
||||||
|
" assert identisch, \"Das Signal verwendet Zukunftsdaten!\"\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"if __name__ == \"__main__\":\n",
|
||||||
|
" UNIVERSUM = [\"AAPL\", \"MSFT\", \"NVDA\", \"GOOGL\",\n",
|
||||||
|
" \"JNJ\", \"UNH\", \"PFE\",\n",
|
||||||
|
" \"JPM\", \"BAC\", \"GS\",\n",
|
||||||
|
" \"XOM\", \"CVX\",\n",
|
||||||
|
" \"PG\", \"KO\", \"COST\"]\n",
|
||||||
|
"\n",
|
||||||
|
" maschine = QuantitativeHandelsmaschine(UNIVERSUM, benchmark=\"SPY\", jahre=5)\n",
|
||||||
|
" maschine.lade_daten()\n",
|
||||||
|
" lookahead_selbsttest(maschine)\n",
|
||||||
|
" ergebnis, gewichte, kosten = maschine.backtest()\n",
|
||||||
|
" maschine.bericht(ergebnis, kosten)\n",
|
||||||
|
" maschine.zeichne(ergebnis)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Die fünf Selbsttäuschungen des Backtestens\n",
|
||||||
|
"\n",
|
||||||
|
"`Backtest_Fallen.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# Backtest_Fallen.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel Handelsmaschine: Wie leicht man sich selbst betruegt.\n",
|
||||||
|
"\n",
|
||||||
|
"Demonstriert an SIMULIERTEN Daten (also ohne jede echte Prognosekraft),\n",
|
||||||
|
"wie gross die scheinbare Ueberrendite durch typische Backtest-Fehler wird.\n",
|
||||||
|
"Weil die Daten reines Rauschen sind, ist JEDE positive Ueberrendite ein Artefakt.\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"import numpy as np\n",
|
||||||
|
"import pandas as pd\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def erzeuge_zufallsmarkt(tage=1260, titel=15, seed=7):\n",
|
||||||
|
" \"\"\"Reines Rauschen: kein Titel hat echte Prognosekraft.\"\"\"\n",
|
||||||
|
" rng = np.random.default_rng(seed)\n",
|
||||||
|
" renditen = rng.normal(0.0003, 0.015, size=(tage, titel))\n",
|
||||||
|
" index = pd.bdate_range(\"2020-01-01\", periods=tage)\n",
|
||||||
|
" return pd.DataFrame(renditen, index=index,\n",
|
||||||
|
" columns=[f\"T{i:02d}\" for i in range(titel)])\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def strategie(renditen, lookahead=False, survivorship=False,\n",
|
||||||
|
" ohne_kosten=False, gebuehr=0.0015):\n",
|
||||||
|
" \"\"\"Einfache Momentum-Strategie mit optional eingebauten Fehlern.\"\"\"\n",
|
||||||
|
" fenster = 60\n",
|
||||||
|
" kapital = [100.0]\n",
|
||||||
|
" gewichte = np.ones(renditen.shape[1]) / renditen.shape[1]\n",
|
||||||
|
"\n",
|
||||||
|
" daten = renditen\n",
|
||||||
|
" if survivorship:\n",
|
||||||
|
" # FEHLER: nur die im Nachhinein besten Titel ins Universum lassen\n",
|
||||||
|
" beste = renditen.sum().nlargest(renditen.shape[1] // 2).index\n",
|
||||||
|
" daten = renditen[beste]\n",
|
||||||
|
" gewichte = np.ones(len(beste)) / len(beste)\n",
|
||||||
|
"\n",
|
||||||
|
" for t in range(fenster, len(daten) - 1):\n",
|
||||||
|
" if t % 21 == 0: # monatlich\n",
|
||||||
|
" if lookahead:\n",
|
||||||
|
" # FEHLER: Signal nutzt die Rendite von MORGEN\n",
|
||||||
|
" signal = daten.iloc[t + 1].values\n",
|
||||||
|
" else:\n",
|
||||||
|
" signal = daten.iloc[t - fenster:t].mean().values\n",
|
||||||
|
"\n",
|
||||||
|
" neue = (signal > np.median(signal)).astype(float)\n",
|
||||||
|
" neue = neue / neue.sum() if neue.sum() > 0 else gewichte\n",
|
||||||
|
" if not ohne_kosten:\n",
|
||||||
|
" kapital[-1] *= (1 - np.abs(neue - gewichte).sum() * gebuehr)\n",
|
||||||
|
" gewichte = neue\n",
|
||||||
|
"\n",
|
||||||
|
" kapital.append(kapital[-1] * (1 + float(gewichte @ daten.iloc[t + 1].values)))\n",
|
||||||
|
" return np.array(kapital)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def cagr(verlauf, jahre):\n",
|
||||||
|
" return (verlauf[-1] / verlauf[0]) ** (1 / jahre) - 1\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"if __name__ == \"__main__\":\n",
|
||||||
|
" renditen = erzeuge_zufallsmarkt()\n",
|
||||||
|
" jahre = len(renditen) / 252\n",
|
||||||
|
" markt = 100 * (1 + renditen.mean(axis=1)).cumprod().values\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"=\" * 84)\n",
|
||||||
|
" print(\" DIE FUENF SELBSTTAEUSCHUNGEN - GEMESSEN AN REINEM RAUSCHEN\")\n",
|
||||||
|
" print(\"=\" * 84)\n",
|
||||||
|
" print(\"Die Daten sind zufaellig erzeugt. Es gibt KEINE echte Prognosekraft.\")\n",
|
||||||
|
" print(\"Jede Ueberrendite unten ist daher ein reines Artefakt.\\n\")\n",
|
||||||
|
" print(f\"{'Variante':<44} {'CAGR':>9} {'ggue. Markt':>13}\")\n",
|
||||||
|
" print(\"-\" * 84)\n",
|
||||||
|
"\n",
|
||||||
|
" basis = cagr(markt, jahre)\n",
|
||||||
|
" print(f\"{'Markt (Gleichgewichtung, Referenz)':<44} {basis*100:>8.2f} % \"\n",
|
||||||
|
" f\"{0.0:>12.2f} %\")\n",
|
||||||
|
"\n",
|
||||||
|
" varianten = [\n",
|
||||||
|
" (\"Ehrlich: Signal aus Vergangenheit, mit Kosten\", {}),\n",
|
||||||
|
" (\"FEHLER 1: Lookahead (Signal kennt morgen)\", {\"lookahead\": True}),\n",
|
||||||
|
" (\"FEHLER 2: Survivorship (nur Gewinner im Universum)\", {\"survivorship\": True}),\n",
|
||||||
|
" (\"FEHLER 3: Transaktionskosten ignoriert\", {\"ohne_kosten\": True}),\n",
|
||||||
|
" (\"FEHLER 1+2+3 kombiniert\", {\"lookahead\": True, \"survivorship\": True,\n",
|
||||||
|
" \"ohne_kosten\": True}),\n",
|
||||||
|
" ]\n",
|
||||||
|
" for name, argumente in varianten:\n",
|
||||||
|
" verlauf = strategie(renditen, **argumente)\n",
|
||||||
|
" wert = cagr(verlauf, jahre)\n",
|
||||||
|
" print(f\"{name:<44} {wert*100:>8.2f} % {(wert-basis)*100:>+12.2f} %\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"-\" * 84)\n",
|
||||||
|
" print(\"FEHLER 4 (Overfitting) und FEHLER 5 (Data Snooping) lassen sich so nicht\")\n",
|
||||||
|
" print(\"zeigen - sie entstehen erst durch WIEDERHOLTES Probieren. Faustregel:\")\n",
|
||||||
|
" print(\"Wer 20 Strategien testet, findet auch in reinem Rauschen eine mit\")\n",
|
||||||
|
" print(\"Signifikanz auf dem 5-%-Niveau. Genau das ist der Punkt.\")\n",
|
||||||
|
" print(\"=\" * 84)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Finde den Denkfehler\n",
|
||||||
|
"\n",
|
||||||
|
"`Data_Snooping.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# Data_Snooping.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel Handelsmaschine: Die Selbsttaeuschung, die man einem Backtest nicht ansieht.\n",
|
||||||
|
"\n",
|
||||||
|
"Backtest_Fallen.py zeigt drei Fehler, die IM Backtest stecken: Lookahead,\n",
|
||||||
|
"Survivorship, vergessene Kosten. Alle drei kann man einem Programm ansehen,\n",
|
||||||
|
"wenn man es liest.\n",
|
||||||
|
"\n",
|
||||||
|
"Die beiden anderen - Overfitting und Data Snooping - entstehen nicht im\n",
|
||||||
|
"Backtest, sondern in der ARBEITSWEISE davor. Sie hinterlassen im Code keine\n",
|
||||||
|
"Spur. Ein Backtest, der zwanzigmal angepasst wurde, sieht am Ende genauso\n",
|
||||||
|
"sauber aus wie einer, der beim ersten Versuch funktioniert hat.\n",
|
||||||
|
"\n",
|
||||||
|
"Dieses Programm macht den Effekt messbar. Es testet Strategien, die\n",
|
||||||
|
"NACHWEISLICH keinerlei Prognosekraft haben (ihre taeglichen Ergebnisse sind\n",
|
||||||
|
"Zufallszahlen mit Erwartungswert exakt null), und zeigt, wie gut die jeweils\n",
|
||||||
|
"BESTE davon aussieht - allein als Funktion der Anzahl der Versuche.\n",
|
||||||
|
"\n",
|
||||||
|
"Benoetigt: numpy, scipy\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"from __future__ import annotations\n",
|
||||||
|
"\n",
|
||||||
|
"import numpy as np\n",
|
||||||
|
"from scipy import stats\n",
|
||||||
|
"\n",
|
||||||
|
"HANDELSTAGE = 1260 # fuenf Jahre\n",
|
||||||
|
"TAGESSCHWANKUNG = 0.010 # 1 % taeglich\n",
|
||||||
|
"WIEDERHOLUNGEN = 300 # damit die Tabelle nicht vom Zufall abhaengt\n",
|
||||||
|
"NIVEAU = 0.05\n",
|
||||||
|
"\n",
|
||||||
|
"RNG = np.random.default_rng(42)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def bewerte(ergebnisse: np.ndarray) -> tuple[np.ndarray, np.ndarray]:\n",
|
||||||
|
" \"\"\"Sharpe-Kennzahl und einseitiger p-Wert je Strategie.\n",
|
||||||
|
"\n",
|
||||||
|
" ergebnisse hat die Form (Strategien, Handelstage). Der p-Wert prueft die\n",
|
||||||
|
" Nullhypothese 'mittleres Tagesergebnis ist null' - also genau die Frage,\n",
|
||||||
|
" die ein Backtest beantworten soll.\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" mittel = ergebnisse.mean(axis=1)\n",
|
||||||
|
" streuung = ergebnisse.std(axis=1, ddof=1)\n",
|
||||||
|
" sharpe = mittel / streuung * np.sqrt(252)\n",
|
||||||
|
" t_wert = mittel / (streuung / np.sqrt(ergebnisse.shape[1]))\n",
|
||||||
|
" return sharpe, stats.t.sf(t_wert, ergebnisse.shape[1] - 1)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def probiere(anzahl_strategien: int) -> tuple[float, float, float, float]:\n",
|
||||||
|
" \"\"\"Liefert (beste Sharpe, ihr p-Wert, Anteil scheinbar signifikanter,\n",
|
||||||
|
" Anteil, der auch die Bonferroni-Huerde nimmt) - gemittelt ueber\n",
|
||||||
|
" WIEDERHOLUNGEN unabhaengige Durchgaenge.\"\"\"\n",
|
||||||
|
" beste_sharpe, beste_p, anteil_signifikant, ueberlebt_bonferroni = [], [], [], []\n",
|
||||||
|
"\n",
|
||||||
|
" for _ in range(WIEDERHOLUNGEN):\n",
|
||||||
|
" # Der springende Punkt: Erwartungswert exakt 0. Keine dieser\n",
|
||||||
|
" # Strategien hat einen echten Vorteil - per Konstruktion.\n",
|
||||||
|
" ergebnisse = RNG.normal(0.0, TAGESSCHWANKUNG,\n",
|
||||||
|
" (anzahl_strategien, HANDELSTAGE))\n",
|
||||||
|
" sharpe, p_werte = bewerte(ergebnisse)\n",
|
||||||
|
"\n",
|
||||||
|
" beste = int(sharpe.argmax())\n",
|
||||||
|
" beste_sharpe.append(float(sharpe[beste]))\n",
|
||||||
|
" beste_p.append(float(p_werte[beste]))\n",
|
||||||
|
" anteil_signifikant.append(float((p_werte < NIVEAU).mean()))\n",
|
||||||
|
" # Bonferroni: Wer n Tests macht, muss die Huerde durch n teilen.\n",
|
||||||
|
" ueberlebt_bonferroni.append(\n",
|
||||||
|
" float((p_werte < NIVEAU / anzahl_strategien).mean()))\n",
|
||||||
|
"\n",
|
||||||
|
" return (float(np.mean(beste_sharpe)), float(np.mean(beste_p)),\n",
|
||||||
|
" float(np.mean(anteil_signifikant)),\n",
|
||||||
|
" float(np.mean(ueberlebt_bonferroni)))\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"if __name__ == \"__main__\":\n",
|
||||||
|
" print(\"=\" * 78)\n",
|
||||||
|
" print(\" WIE GUT SIEHT DIE BESTE VON N STRATEGIEN AUS,\")\n",
|
||||||
|
" print(\" WENN KEINE EINZIGE ETWAS TAUGT?\")\n",
|
||||||
|
" print(\"=\" * 78)\n",
|
||||||
|
" print(f\"{HANDELSTAGE} Handelstage, taegliche Ergebnisse mit Erwartungswert \"\n",
|
||||||
|
" f\"EXAKT null.\")\n",
|
||||||
|
" print(f\"Jede Zeile ist der Mittelwert aus {WIEDERHOLUNGEN} unabhaengigen \"\n",
|
||||||
|
" f\"Durchgaengen.\\n\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(f\"{'getestet':>9} {'beste Sharpe':>14} {'ihr p-Wert':>12} \"\n",
|
||||||
|
" f\"{'scheinbar sign.':>16}\")\n",
|
||||||
|
" print(\"-\" * 78)\n",
|
||||||
|
" ergebnisse = {}\n",
|
||||||
|
" for anzahl in (1, 10, 100, 1000):\n",
|
||||||
|
" sharpe, p_wert, anteil, _ = probiere(anzahl)\n",
|
||||||
|
" ergebnisse[anzahl] = (sharpe, p_wert, anteil)\n",
|
||||||
|
" print(f\"{anzahl:9d} {sharpe:14.2f} {p_wert:12.4f} {anteil * 100:15.1f} %\")\n",
|
||||||
|
" print(\"-\" * 78)\n",
|
||||||
|
"\n",
|
||||||
|
" sharpe_100, p_100, _ = ergebnisse[100]\n",
|
||||||
|
" sharpe_1000, p_1000, _ = ergebnisse[1000]\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\nLesen Sie die dritte Spalte von unten nach oben:\")\n",
|
||||||
|
" print(f\" Wer {1000} Varianten durchprobiert, findet eine mit p = {p_1000:.4f} -\")\n",
|
||||||
|
" print(\" ein Wert, den jede Zeitschrift als 'hochsignifikant' druckt.\")\n",
|
||||||
|
" print(f\" Wer {100} probiert, findet p = {p_100:.4f} und eine Sharpe-Kennzahl\")\n",
|
||||||
|
" print(f\" von {sharpe_100:.2f} - damit geht man zum Vorgesetzten.\")\n",
|
||||||
|
" print(\" Und keine dieser Strategien hat auch nur den Hauch eines Vorteils.\")\n",
|
||||||
|
" print()\n",
|
||||||
|
" print(\"Die LETZTE Spalte bleibt dagegen konstant bei rund 5 %. Das ist kein\")\n",
|
||||||
|
" print(\"Widerspruch, sondern die Definition: Ein Test zum 5-%-Niveau irrt in\")\n",
|
||||||
|
" print(\"5 % der Faelle. Was sich aendert, ist nicht der Anteil, sondern die\")\n",
|
||||||
|
" print(\"ANZAHL - und weil man nur die beste Strategie praesentiert, sieht man\")\n",
|
||||||
|
" print(\"von allen Fehlversuchen genau einen: den erfolgreichsten.\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\n\" + \"=\" * 78)\n",
|
||||||
|
" print(\" WAS MAN DAGEGEN TUN KANN\")\n",
|
||||||
|
" print(\"=\" * 78)\n",
|
||||||
|
" print(\"1. ZAEHLEN. Notieren Sie, wie viele Varianten Sie ausprobiert haben -\")\n",
|
||||||
|
" print(\" auch die verworfenen, auch die 'nur mal schnell'. Ohne diese Zahl\")\n",
|
||||||
|
" print(\" ist kein p-Wert und keine Sharpe-Kennzahl interpretierbar.\")\n",
|
||||||
|
" print()\n",
|
||||||
|
" print(\"2. KORRIGIEREN. Die Bonferroni-Korrektur teilt die Huerde durch die\")\n",
|
||||||
|
" print(\" Zahl der Versuche:\")\n",
|
||||||
|
" for anzahl in (10, 100, 1000):\n",
|
||||||
|
" print(f\" bei {anzahl:5d} Versuchen: p < {NIVEAU / anzahl:.5f} statt \"\n",
|
||||||
|
" f\"p < {NIVEAU:.2f}\")\n",
|
||||||
|
" print(\" Sie ist konservativ, aber sie ist ehrlich. Fuer Handelsstrategien\")\n",
|
||||||
|
" print(\" gibt es verfeinerte Varianten (Deflated Sharpe Ratio nach Bailey\")\n",
|
||||||
|
" print(\" und Lopez de Prado), die dasselbe Prinzip verfolgen.\")\n",
|
||||||
|
" print()\n",
|
||||||
|
" print(\"3. ZURUECKHALTEN. Legen Sie einen Zeitraum beiseite, BEVOR Sie\")\n",
|
||||||
|
" print(\" anfangen, und ruehren Sie ihn bis zum Schluss nicht an. Er ist\")\n",
|
||||||
|
" print(\" der einzige Test, den Sie nicht durch Probieren verderben koennen -\")\n",
|
||||||
|
" print(\" und er ist genau einmal verwendbar.\")\n",
|
||||||
|
" print(\"=\" * 78)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"name": "python",
|
||||||
|
"version": "3.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
633
Notebooks_04/lp.ipynb
Normal file
633
Notebooks_04/lp.ipynb
Normal file
|
|
@ -0,0 +1,633 @@
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"# Kapitel 5: Lineare Programmierung — Simplex, Dualität und Schattenpreise\n",
|
||||||
|
"\n",
|
||||||
|
"Begleitnotebook zu *Optimierte Entscheidungsfindung mit Python*. Die Codezellen sind identisch mit den im Buch abgedruckten Programmen.\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"# Einmalig ausfuehren: installiert alle im Buch verwendeten Pakete.\n",
|
||||||
|
"# Lokal in einer virtuellen Umgebung genauso gueltig wie in Google Colab.\n",
|
||||||
|
"%pip install --quiet ortools highspy cvxpy scipy numpy pandas polars \\\n",
|
||||||
|
" scikit-learn matplotlib plotly pyomo linopy pymoo pydantic openpyxl"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Das Simplex-Tableau in Python\n",
|
||||||
|
"\n",
|
||||||
|
"`Simplex_Tableau_LP.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# Simplex_Tableau_LP.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel LP: Vollständige Implementierung des Simplex-Algorithmus (Tableau-Methode).\n",
|
||||||
|
"\n",
|
||||||
|
"GRENZEN DIESER IMPLEMENTIERUNG (bewusst, aus didaktischen Gründen):\n",
|
||||||
|
" * nur Maximierung\n",
|
||||||
|
" * nur \"<=\"-Nebenbedingungen\n",
|
||||||
|
" * alle b_i >= 0 (sonst wäre der Ursprung keine zulässige Startecke und man\n",
|
||||||
|
" bräuchte eine Phase-1-Rechnung mit künstlichen Variablen)\n",
|
||||||
|
"Für den produktiven Einsatz nimmt man HiGHS - dieser Code dient dem Verständnis.\n",
|
||||||
|
"\n",
|
||||||
|
"Voraussetzungen werden geprüft statt stillschweigend angenommen;\n",
|
||||||
|
"Iterationsprotokoll und Schattenpreise werden ausgegeben.\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"import numpy as np\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"class SimplexTableauSolver:\n",
|
||||||
|
" \"\"\"Maximierungs-Standardform: max c^T x u.d.N. A x <= b, x >= 0, b >= 0.\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
" def __init__(self, c, A, b, variablennamen=None, restriktionsnamen=None):\n",
|
||||||
|
" self.c = np.asarray(c, dtype=float)\n",
|
||||||
|
" self.A = np.asarray(A, dtype=float)\n",
|
||||||
|
" self.b = np.asarray(b, dtype=float)\n",
|
||||||
|
" self.n = len(self.c) # Anzahl Originalvariablen\n",
|
||||||
|
" self.m = len(self.b) # Anzahl Nebenbedingungen\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Voraussetzungen pruefen, statt sie stillschweigend anzunehmen ---\n",
|
||||||
|
" if self.A.shape != (self.m, self.n):\n",
|
||||||
|
" raise ValueError(f\"A hat Form {self.A.shape}, erwartet ({self.m}, {self.n}).\")\n",
|
||||||
|
" if np.any(self.b < 0):\n",
|
||||||
|
" raise ValueError(\n",
|
||||||
|
" \"Mindestens ein b_i ist negativ. Dann ist der Ursprung keine zulässige \"\n",
|
||||||
|
" \"Startecke; dieser Solver benötigt eine Phase-1-Rechnung, die hier \"\n",
|
||||||
|
" \"bewusst nicht implementiert ist. Nutzen Sie scipy.optimize.linprog.\"\n",
|
||||||
|
" )\n",
|
||||||
|
"\n",
|
||||||
|
" self.var_namen = variablennamen or [f\"x{j+1}\" for j in range(self.n)]\n",
|
||||||
|
" self.restr_namen = restriktionsnamen or [f\"R{i+1}\" for i in range(self.m)]\n",
|
||||||
|
"\n",
|
||||||
|
" self.tableau = None\n",
|
||||||
|
" self.basis = None # welche Variable ist in welcher Zeile Basis?\n",
|
||||||
|
" self._baue_starttableau()\n",
|
||||||
|
"\n",
|
||||||
|
" def _baue_starttableau(self):\n",
|
||||||
|
" \"\"\"Zeilen: m Nebenbedingungen + Zielfunktionszeile.\n",
|
||||||
|
" Spalten: n Variablen + m Schlupfvariablen + rechte Seite.\"\"\"\n",
|
||||||
|
" m, n = self.m, self.n\n",
|
||||||
|
" self.tableau = np.zeros((m + 1, n + m + 1))\n",
|
||||||
|
" self.tableau[:m, :n] = self.A # Koeffizienten\n",
|
||||||
|
" self.tableau[:m, n:n + m] = np.eye(m) # Schlupfvariablen\n",
|
||||||
|
" self.tableau[:m, -1] = self.b # rechte Seite\n",
|
||||||
|
" self.tableau[-1, :n] = -self.c # Zielzeile: -c (Maximierung)\n",
|
||||||
|
" self.basis = list(range(n, n + m)) # Start: alle Schlupf in der Basis\n",
|
||||||
|
"\n",
|
||||||
|
" def _spaltenname(self, index):\n",
|
||||||
|
" return self.var_namen[index] if index < self.n else f\"s{index - self.n + 1}\"\n",
|
||||||
|
"\n",
|
||||||
|
" def solve(self, max_iterationen=100, protokoll=True):\n",
|
||||||
|
" m, n = self.m, self.n\n",
|
||||||
|
" if protokoll:\n",
|
||||||
|
" print(f\"{'Iter':>4} | {'eintritt':>9} | {'austritt':>9} | \"\n",
|
||||||
|
" f\"{'Pivot':>8} | {'Z':>12}\")\n",
|
||||||
|
" print(\"-\" * 58)\n",
|
||||||
|
"\n",
|
||||||
|
" for iteration in range(1, max_iterationen + 1):\n",
|
||||||
|
" zielzeile = self.tableau[-1, :-1]\n",
|
||||||
|
"\n",
|
||||||
|
" # 1. Optimalitätsprüfung: alle Koeffizienten >= 0 ?\n",
|
||||||
|
" if np.all(zielzeile >= -1e-9):\n",
|
||||||
|
" if protokoll:\n",
|
||||||
|
" print(\"-\" * 58)\n",
|
||||||
|
" print(f\"Optimum nach {iteration - 1} Pivotschritten erreicht.\")\n",
|
||||||
|
" return self._loesung_auslesen()\n",
|
||||||
|
"\n",
|
||||||
|
" # 2. Pivotspalte: negativster Eintrag (Dantzig-Regel)\n",
|
||||||
|
" pivot_spalte = int(np.argmin(zielzeile))\n",
|
||||||
|
"\n",
|
||||||
|
" # 3. Pivotzeile: minimaler Quotient über POSITIVE Spalteneinträge\n",
|
||||||
|
" spalte = self.tableau[:m, pivot_spalte]\n",
|
||||||
|
" rechte_seite = self.tableau[:m, -1]\n",
|
||||||
|
" quotienten = np.where(spalte > 1e-9, rechte_seite / np.where(spalte > 1e-9, spalte, 1),\n",
|
||||||
|
" np.inf)\n",
|
||||||
|
" pivot_zeile = int(np.argmin(quotienten))\n",
|
||||||
|
" if not np.isfinite(quotienten[pivot_zeile]):\n",
|
||||||
|
" raise ValueError(\n",
|
||||||
|
" f\"Problem ist unbeschraenkt: Variable {self._spaltenname(pivot_spalte)} \"\n",
|
||||||
|
" \"kann beliebig wachsen, ohne eine Bedingung zu verletzen. \"\n",
|
||||||
|
" \"Meist fehlt eine Kapazitaetsbeschraenkung.\"\n",
|
||||||
|
" )\n",
|
||||||
|
"\n",
|
||||||
|
" if protokoll:\n",
|
||||||
|
" print(f\"{iteration:>4} | {self._spaltenname(pivot_spalte):>9} | \"\n",
|
||||||
|
" f\"{self._spaltenname(self.basis[pivot_zeile]):>9} | \"\n",
|
||||||
|
" f\"{self.tableau[pivot_zeile, pivot_spalte]:>8.3f} | \"\n",
|
||||||
|
" f\"{self.tableau[-1, -1]:>12,.2f}\")\n",
|
||||||
|
"\n",
|
||||||
|
" # 4. Pivotoperation (Gauß-Jordan)\n",
|
||||||
|
" pivot_wert = self.tableau[pivot_zeile, pivot_spalte]\n",
|
||||||
|
" self.tableau[pivot_zeile, :] /= pivot_wert\n",
|
||||||
|
" for zeile in range(m + 1):\n",
|
||||||
|
" if zeile != pivot_zeile:\n",
|
||||||
|
" faktor = self.tableau[zeile, pivot_spalte]\n",
|
||||||
|
" self.tableau[zeile, :] -= faktor * self.tableau[pivot_zeile, :]\n",
|
||||||
|
"\n",
|
||||||
|
" self.basis[pivot_zeile] = pivot_spalte\n",
|
||||||
|
"\n",
|
||||||
|
" raise RuntimeError(\"Maximale Iterationszahl ueberschritten (moeglicherweise Zyklus).\")\n",
|
||||||
|
"\n",
|
||||||
|
" def _loesung_auslesen(self):\n",
|
||||||
|
" \"\"\"Basisvariablen tragen den RHS-Wert ihrer Zeile, Nichtbasisvariablen sind 0.\"\"\"\n",
|
||||||
|
" x = np.zeros(self.n + self.m)\n",
|
||||||
|
" for zeile, spalte in enumerate(self.basis):\n",
|
||||||
|
" x[spalte] = self.tableau[zeile, -1]\n",
|
||||||
|
" return x[:self.n], x[self.n:], self.tableau[-1, -1]\n",
|
||||||
|
"\n",
|
||||||
|
" def schattenpreise(self):\n",
|
||||||
|
" \"\"\"Die Zielzeile unter den Schlupfspalten enthält direkt die Dualwerte.\"\"\"\n",
|
||||||
|
" return self.tableau[-1, self.n:self.n + self.m].copy()\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"if __name__ == \"__main__\":\n",
|
||||||
|
" # Modell aus der Simplex-Handrechnung (Bot-Beispiel, Kapitel Einfuehrung)\n",
|
||||||
|
" ertraege = [150.0, 250.0]\n",
|
||||||
|
" matrix = [[2.0, 5.0],\n",
|
||||||
|
" [4.0, 6.0],\n",
|
||||||
|
" [1.0, 0.0]]\n",
|
||||||
|
" kapazitaeten = [40.0, 60.0, 8.0]\n",
|
||||||
|
" var_namen = [\"x_A\", \"x_B\"]\n",
|
||||||
|
" restr_namen = [\"vCPU\", \"RAM\", \"Marktlimit\"]\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"=\" * 58)\n",
|
||||||
|
" print(\" SIMPLEX-TABLEAU: ITERATIONSPROTOKOLL\")\n",
|
||||||
|
" print(\"=\" * 58)\n",
|
||||||
|
"\n",
|
||||||
|
" solver = SimplexTableauSolver(ertraege, matrix, kapazitaeten, var_namen, restr_namen)\n",
|
||||||
|
" x_opt, schlupf, z_opt = solver.solve()\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\n\" + \"=\" * 58)\n",
|
||||||
|
" print(\" ERGEBNIS\")\n",
|
||||||
|
" print(\"=\" * 58)\n",
|
||||||
|
" for name, wert in zip(var_namen, x_opt):\n",
|
||||||
|
" print(f\" {name:<12} = {wert:8.4f}\")\n",
|
||||||
|
" print(f\" {'Zielwert Z':<12} = {z_opt:8.2f} EUR\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\n Ressourcenanalyse:\")\n",
|
||||||
|
" print(f\" {'Ressource':<12} {'Schlupf':>9} {'Status':>22} {'Schattenpreis':>15}\")\n",
|
||||||
|
" print(\" \" + \"-\" * 60)\n",
|
||||||
|
" for name, s, y in zip(restr_namen, schlupf, solver.schattenpreise()):\n",
|
||||||
|
" status = \"ENGPASS (bindend)\" if abs(s) < 1e-9 else \"Reserve vorhanden\"\n",
|
||||||
|
" print(f\" {name:<12} {s:>9.3f} {status:>22} {y:>12.2f} EUR\")\n",
|
||||||
|
"\n",
|
||||||
|
" # Selbstkontrolle: komplementaerer Schlupf muss gelten\n",
|
||||||
|
" for s, y in zip(schlupf, solver.schattenpreise()):\n",
|
||||||
|
" assert abs(s * y) < 1e-6, \"Komplementaerer Schlupf verletzt - Rechenfehler!\"\n",
|
||||||
|
" print(\"\\n Pruefung: komplementaerer Schlupf (s_i * y_i = 0) fuer alle i erfuellt.\")\n",
|
||||||
|
" print(\"=\" * 58)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Der starke Dualitätssatz\n",
|
||||||
|
"\n",
|
||||||
|
"`Dualitaet_Nachweis.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# Dualitaet_Nachweis.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel LP: Primales und duales Problem unabhaengig loesen und den starken\n",
|
||||||
|
"Dualitaetssatz sowie den komplementaeren Schlupf numerisch nachweisen.\n",
|
||||||
|
"\n",
|
||||||
|
"Modell aus der Simplex-Handrechnung (Bot-Allokation, LP-Relaxation):\n",
|
||||||
|
" max 150*x1 + 250*x2 u.d.N. 2*x1+5*x2<=40, 4*x1+6*x2<=60, x1<=8, x>=0\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"import numpy as np\n",
|
||||||
|
"from scipy.optimize import linprog\n",
|
||||||
|
"\n",
|
||||||
|
"# --- Primales Problem --------------------------------------------------------\n",
|
||||||
|
"C_PRIMAL = np.array([150.0, 250.0])\n",
|
||||||
|
"A_PRIMAL = np.array([[2.0, 5.0], [4.0, 6.0], [1.0, 0.0]])\n",
|
||||||
|
"B_PRIMAL = np.array([40.0, 60.0, 8.0])\n",
|
||||||
|
"\n",
|
||||||
|
"# --- Duales Problem: min b^T y u.d.N. A^T y >= c, y >= 0 ------------------\n",
|
||||||
|
"# linprog kennt nur <=, also A^T y >= c <=> -A^T y <= -c\n",
|
||||||
|
"C_DUAL = B_PRIMAL\n",
|
||||||
|
"A_DUAL = -A_PRIMAL.T\n",
|
||||||
|
"B_DUAL = -C_PRIMAL\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def loese():\n",
|
||||||
|
" primal = linprog(c=-C_PRIMAL, A_ub=A_PRIMAL, b_ub=B_PRIMAL,\n",
|
||||||
|
" bounds=[(0, None)] * 2, method=\"highs\")\n",
|
||||||
|
" dual = linprog(c=C_DUAL, A_ub=A_DUAL, b_ub=B_DUAL,\n",
|
||||||
|
" bounds=[(0, None)] * 3, method=\"highs\")\n",
|
||||||
|
" if not primal.success or not dual.success:\n",
|
||||||
|
" raise SystemExit(\"Primal oder Dual nicht loesbar.\")\n",
|
||||||
|
" return primal, dual\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"if __name__ == \"__main__\":\n",
|
||||||
|
" primal, dual = loese()\n",
|
||||||
|
" x = primal.x\n",
|
||||||
|
" y = dual.x\n",
|
||||||
|
" z_primal = -primal.fun\n",
|
||||||
|
" z_dual = dual.fun\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"=\" * 78)\n",
|
||||||
|
" print(\" PRIMALES PROBLEM\")\n",
|
||||||
|
" print(\"=\" * 78)\n",
|
||||||
|
" print(f\"x* = ({x[0]:.4f}, {x[1]:.4f})\")\n",
|
||||||
|
" print(f\"Z* = {z_primal:.4f}\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\n\" + \"=\" * 78)\n",
|
||||||
|
" print(\" DUALES PROBLEM\")\n",
|
||||||
|
" print(\"=\" * 78)\n",
|
||||||
|
" print(f\"y* = ({y[0]:.4f}, {y[1]:.4f}, {y[2]:.4f})\")\n",
|
||||||
|
" print(f\"W* = {z_dual:.4f}\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\n\" + \"=\" * 78)\n",
|
||||||
|
" print(\" STARKER DUALITAETSSATZ: c^T x* == b^T y* ?\")\n",
|
||||||
|
" print(\"=\" * 78)\n",
|
||||||
|
" print(f\" Primal Z* = {z_primal:.6f}\")\n",
|
||||||
|
" print(f\" Dual W* = {z_dual:.6f}\")\n",
|
||||||
|
" differenz = abs(z_primal - z_dual)\n",
|
||||||
|
" print(f\" Differenz = {differenz:.2e} -> \"\n",
|
||||||
|
" f\"{'BESTAETIGT' if differenz < 1e-6 else 'VERLETZT!'}\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\n\" + \"=\" * 78)\n",
|
||||||
|
" print(\" KOMPLEMENTAERER SCHLUPF: s_i * y_i == 0 fuer alle i ?\")\n",
|
||||||
|
" print(\"=\" * 78)\n",
|
||||||
|
" schlupf = B_PRIMAL - A_PRIMAL @ x\n",
|
||||||
|
" ressourcen = [\"vCPU (s1)\", \"RAM (s2)\", \"Marktlimit (s3)\"]\n",
|
||||||
|
" for name, s, yi in zip(ressourcen, schlupf, y):\n",
|
||||||
|
" produkt = s * yi\n",
|
||||||
|
" print(f\" {name:<16} Schlupf s={s:6.4f} Schattenpreis y={yi:6.4f} \"\n",
|
||||||
|
" f\"s*y={produkt:.2e} {'OK' if abs(produkt) < 1e-6 else 'VERLETZT!'}\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\nFazit: Das dual geloeste y* stimmt exakt mit den Schattenpreisen\")\n",
|
||||||
|
" print(\"überein, die die Simplex-Rechnung von Hand in der Z-Zeile\")\n",
|
||||||
|
" print(\"ablas - unabhaengig voneinander berechnet, identisches Ergebnis.\")\n",
|
||||||
|
" print(\"=\" * 78)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Praxisfall: Sensitivitätsanalyse mit korrekten Schattenpreisen\n",
|
||||||
|
"\n",
|
||||||
|
"`Sensitivitaetsanalyse.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# Sensitivitaetsanalyse.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel LP: Schattenpreis- und Sensitivitätsanalyse mit SciPy und HiGHS.\n",
|
||||||
|
"\n",
|
||||||
|
"Achtung: Ohne Vorzeichenumkehr der Dualwerte waere die Handlungsempfehlung\n",
|
||||||
|
"strukturell immer \"kein Zukauf noetig\" - selbst bei harten Engpaessen. Dieses\n",
|
||||||
|
"Programm zeigt die korrekte Vorzeichenbehandlung.\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"import numpy as np\n",
|
||||||
|
"from scipy.optimize import linprog\n",
|
||||||
|
"\n",
|
||||||
|
"# --- Modell: Maximiere Deckungsbeitrag aus 3 Produkten ---------------------\n",
|
||||||
|
"# max 40*x1 + 30*x2 + 50*x3\n",
|
||||||
|
"DECKUNGSBEITRAG = np.array([40.0, 30.0, 50.0])\n",
|
||||||
|
"\n",
|
||||||
|
"# Ressourcenverbrauch je Produkt (Zeile = Ressource, Spalte = Produkt)\n",
|
||||||
|
"VERBRAUCH = np.array([\n",
|
||||||
|
" [2.0, 1.0, 3.0], # Montagezeit\n",
|
||||||
|
" [1.0, 2.0, 1.0], # Lackierzeit\n",
|
||||||
|
" [1.0, 0.5, 2.0], # Qualitätsprüfung\n",
|
||||||
|
"])\n",
|
||||||
|
"KAPAZITAET = np.array([120.0, 80.0, 50.0]) # Stunden\n",
|
||||||
|
"RESSOURCEN = [\"Montage\", \"Lackieren\", \"Qualitätsprüfung\"]\n",
|
||||||
|
"PRODUKTE = [\"Produkt 1\", \"Produkt 2\", \"Produkt 3\"]\n",
|
||||||
|
"\n",
|
||||||
|
"ANGEBOTSPREIS_PRUEFSTUNDE = 18.0 # EUR/h - lohnt sich der Zukauf?\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def analysiere():\n",
|
||||||
|
" # linprog MINIMIERT -> Zielfunktion negieren\n",
|
||||||
|
" ergebnis = linprog(c=-DECKUNGSBEITRAG, A_ub=VERBRAUCH, b_ub=KAPAZITAET,\n",
|
||||||
|
" bounds=[(0, None)] * len(DECKUNGSBEITRAG), method=\"highs\")\n",
|
||||||
|
" if not ergebnis.success:\n",
|
||||||
|
" raise SystemExit(f\"Kein Optimum gefunden: {ergebnis.message}\")\n",
|
||||||
|
"\n",
|
||||||
|
" max_gewinn = -ergebnis.fun\n",
|
||||||
|
" mengen = ergebnis.x\n",
|
||||||
|
" schlupf = ergebnis.slack\n",
|
||||||
|
"\n",
|
||||||
|
" # ------------------------------------------------------------------\n",
|
||||||
|
" # DER ENTSCHEIDENDE PUNKT:\n",
|
||||||
|
" # Weil wir zur Maximierung negiert haben, sind die Dualwerte aus\n",
|
||||||
|
" # linprog fuer \"<=\"-Bedingungen <= 0. Zurueckdrehen!\n",
|
||||||
|
" # ------------------------------------------------------------------\n",
|
||||||
|
" schattenpreise = -ergebnis.ineqlin.marginals\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"=\" * 74)\n",
|
||||||
|
" print(\" PRIMALE UND DUALE ERGEBNISANALYSE (SENSITIVITAET)\")\n",
|
||||||
|
" print(\"=\" * 74)\n",
|
||||||
|
" print(f\"Maximaler Deckungsbeitrag: {max_gewinn:,.2f} EUR\\n\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"--- Primalloesung: optimale Produktionsmengen ---\")\n",
|
||||||
|
" for name, menge in zip(PRODUKTE, mengen):\n",
|
||||||
|
" print(f\" * {name}: {menge:8.2f} Stueck\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\n--- Duale Analyse: Schattenpreise und Auslastung ---\")\n",
|
||||||
|
" for i, name in enumerate(RESSOURCEN):\n",
|
||||||
|
" kapazitaet = KAPAZITAET[i]\n",
|
||||||
|
" genutzt = kapazitaet - schlupf[i]\n",
|
||||||
|
" auslastung = genutzt / kapazitaet * 100\n",
|
||||||
|
" preis = schattenpreise[i]\n",
|
||||||
|
" bindend = abs(schlupf[i]) < 1e-9\n",
|
||||||
|
"\n",
|
||||||
|
" print(f\"\\nRessource '{name}':\")\n",
|
||||||
|
" print(f\" Auslastung: {genutzt:6.1f} / {kapazitaet:6.1f} h ({auslastung:5.1f} %)\"\n",
|
||||||
|
" f\" -> {'ENGPASS' if bindend else 'Reserve: %.1f h' % schlupf[i]}\")\n",
|
||||||
|
" print(f\" Schattenpreis: {preis:6.2f} EUR je zusaetzlicher Stunde\")\n",
|
||||||
|
"\n",
|
||||||
|
" if preis > 1e-9:\n",
|
||||||
|
" print(f\" >> Zusaetzliche Stunden lohnen sich bis zu einem Preis von \"\n",
|
||||||
|
" f\"{preis:.2f} EUR/h.\")\n",
|
||||||
|
" else:\n",
|
||||||
|
" print(f\" >> Kein Zukauf noetig - die Kapazitaet ist nicht erschoepft.\")\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Konkrete Kaufentscheidung ---------------------------------------\n",
|
||||||
|
" preis_pruefung = schattenpreise[RESSOURCEN.index(\"Qualitätsprüfung\")]\n",
|
||||||
|
" marge = preis_pruefung - ANGEBOTSPREIS_PRUEFSTUNDE\n",
|
||||||
|
" print(\"\\n\" + \"-\" * 74)\n",
|
||||||
|
" print(f\"ENTSCHEIDUNG: Pruefstunden werden fuer \"\n",
|
||||||
|
" f\"{ANGEBOTSPREIS_PRUEFSTUNDE:.2f} EUR/h angeboten.\")\n",
|
||||||
|
" print(f\" Schattenpreis: {preis_pruefung:6.2f} EUR/h\")\n",
|
||||||
|
" print(f\" Angebotspreis: {ANGEBOTSPREIS_PRUEFSTUNDE:6.2f} EUR/h\")\n",
|
||||||
|
" print(f\" Marge: {marge:+6.2f} EUR je zugekaufter Stunde\")\n",
|
||||||
|
" print(f\" >> {'ZUKAUFEN' if marge > 0 else 'NICHT ZUKAUFEN'}\")\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Numerische Gegenprobe: Kapazitaet wirklich um 1 erhoehen --------\n",
|
||||||
|
" kapazitaet_plus = KAPAZITAET.copy()\n",
|
||||||
|
" kapazitaet_plus[RESSOURCEN.index(\"Qualitätsprüfung\")] += 1.0\n",
|
||||||
|
" gegenprobe = linprog(c=-DECKUNGSBEITRAG, A_ub=VERBRAUCH, b_ub=kapazitaet_plus,\n",
|
||||||
|
" bounds=[(0, None)] * 3, method=\"highs\")\n",
|
||||||
|
" tatsaechlicher_zuwachs = -gegenprobe.fun - max_gewinn\n",
|
||||||
|
" print(\"\\n--- Gegenprobe: Modell mit +1 Pruefstunde neu geloest ---\")\n",
|
||||||
|
" print(f\" Vorhergesagt (Schattenpreis): {preis_pruefung:8.4f} EUR\")\n",
|
||||||
|
" print(f\" Tatsaechlich gemessen: {tatsaechlicher_zuwachs:8.4f} EUR\")\n",
|
||||||
|
" assert abs(tatsaechlicher_zuwachs - preis_pruefung) < 1e-6, \\\n",
|
||||||
|
" \"Schattenpreis stimmt nicht mit der Messung ueberein!\"\n",
|
||||||
|
" print(\" -> Der Schattenpreis ist bestaetigt.\")\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Komplementaerer Schlupf pruefen --------------------------------\n",
|
||||||
|
" for s, y in zip(schlupf, schattenpreise):\n",
|
||||||
|
" assert abs(s * y) < 1e-6, \"Komplementaerer Schlupf verletzt!\"\n",
|
||||||
|
" print(\"\\nPruefung: komplementaerer Schlupf fuer alle Ressourcen erfuellt.\")\n",
|
||||||
|
" print(\"=\" * 74)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"if __name__ == \"__main__\":\n",
|
||||||
|
" analysiere()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Die ehrliche Auskunft: Schattenpreis-Spannen\n",
|
||||||
|
"\n",
|
||||||
|
"`Toleranzen_und_Entartung.py`\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"\n",
|
||||||
|
"# Toleranzen_und_Entartung.py\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Kapitel LP: Zwei Faelle, in denen man Schattenpreisen NICHT trauen darf.\n",
|
||||||
|
"\n",
|
||||||
|
" 1. Entartung (Degeneriertheit): Mehr Nebenbedingungen sind aktiv, als das\n",
|
||||||
|
" Problem Variablen hat. Dann ist der Schattenpreis nicht eindeutig - zwei\n",
|
||||||
|
" korrekte Solver liefern voellig verschiedene Werte fuer dasselbe Optimum.\n",
|
||||||
|
" 2. Toleranzen: \"ausgelastet\" heisst nie 'schlupf == 0', sondern immer\n",
|
||||||
|
" 'schlupf < toleranz'. Wer auf exakte Gleichheit prueft, baut Berichte,\n",
|
||||||
|
" die zufaellig mal stimmen und mal nicht.\n",
|
||||||
|
"\n",
|
||||||
|
"Teil 3 zeigt die professionelle Antwort auf Fall 1: Statt EINEN Schattenpreis\n",
|
||||||
|
"zu melden, berechnet man seine SPANNE ueber alle optimalen Dualloesungen.\n",
|
||||||
|
"\n",
|
||||||
|
"Benoetigt: numpy, scipy\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"from __future__ import annotations\n",
|
||||||
|
"\n",
|
||||||
|
"import numpy as np\n",
|
||||||
|
"from scipy.optimize import linprog\n",
|
||||||
|
"\n",
|
||||||
|
"# Ein bewusst entartetes Beispiel: drei Geraden schneiden sich in EINEM Punkt.\n",
|
||||||
|
"# max x1 + x2\n",
|
||||||
|
"# u.d.N. x1 + 2*x2 <= 4 (A)\n",
|
||||||
|
"# 2*x1 + x2 <= 4 (B)\n",
|
||||||
|
"# x1 + x2 <= 8/3 (C) - laeuft genau durch die Ecke (4/3, 4/3)\n",
|
||||||
|
"# In zwei Dimensionen legen schon zwei Geraden eine Ecke fest. Hier sind drei\n",
|
||||||
|
"# aktiv - eine zu viel. Genau das ist Entartung.\n",
|
||||||
|
"C_ZIEL = np.array([-1.0, -1.0]) # linprog minimiert -> negiert\n",
|
||||||
|
"A_UB = np.array([[1.0, 2.0],\n",
|
||||||
|
" [2.0, 1.0],\n",
|
||||||
|
" [1.0, 1.0]])\n",
|
||||||
|
"B_UB = np.array([4.0, 4.0, 8.0 / 3.0])\n",
|
||||||
|
"NAMEN = [\"A: Fraeszeit\", \"B: Schleifzeit\", \"C: Pruefzeit\"]\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def zeige_entartung() -> float:\n",
|
||||||
|
" \"\"\"Loest dasselbe LP mit zwei Verfahren und vergleicht die Dualwerte.\"\"\"\n",
|
||||||
|
" print(\"=\" * 78)\n",
|
||||||
|
" print(\" 1. ENTARTUNG: DERSELBE PLAN, GEGENSAETZLICHE SCHATTENPREISE\")\n",
|
||||||
|
" print(\"=\" * 78)\n",
|
||||||
|
"\n",
|
||||||
|
" print(f\"{'Verfahren':<26} {'x1':>7} {'x2':>7} {'Z*':>9} Schattenpreise\")\n",
|
||||||
|
" print(\"-\" * 78)\n",
|
||||||
|
"\n",
|
||||||
|
" dualwerte = {}\n",
|
||||||
|
" for verfahren, beschreibung in [(\"highs-ds\", \"Dual Simplex\"),\n",
|
||||||
|
" (\"highs-ipm\", \"Innere-Punkte-Verfahren\")]:\n",
|
||||||
|
" ergebnis = linprog(C_ZIEL, A_ub=A_UB, b_ub=B_UB, bounds=(0, None),\n",
|
||||||
|
" method=verfahren)\n",
|
||||||
|
" if not ergebnis.success:\n",
|
||||||
|
" raise RuntimeError(f\"{verfahren}: {ergebnis.message}\")\n",
|
||||||
|
" y = -ergebnis.ineqlin.marginals\n",
|
||||||
|
" dualwerte[verfahren] = y\n",
|
||||||
|
" print(f\"{beschreibung:<26} {ergebnis.x[0]:>7.3f} {ergebnis.x[1]:>7.3f} \"\n",
|
||||||
|
" f\"{-ergebnis.fun:>9.4f} {np.round(y, 4)}\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"-\" * 78)\n",
|
||||||
|
" print(\"Beide Zeilen sind RICHTIG: gleicher Plan, gleicher Zielwert, und beide\")\n",
|
||||||
|
" print(\"Dualvektoren erfuellen die Optimalitaetsbedingungen. Trotzdem sagen sie\")\n",
|
||||||
|
" print(\"das Gegenteil:\")\n",
|
||||||
|
" print(f\" Dual Simplex : {NAMEN[2]} ist wertlos, A und B sind je 0,33 EUR wert.\")\n",
|
||||||
|
" print(f\" Innere Punkte: {NAMEN[0]} und {NAMEN[1]} sind wertlos, C ist 1,00 EUR wert.\")\n",
|
||||||
|
" print()\n",
|
||||||
|
" print(\"Wer auf dieser Grundlage eine Maschine kauft, hat eine 50:50-Chance -\")\n",
|
||||||
|
" print(\"abhaengig davon, welches Verfahren der Solver zufaellig gewaehlt hat.\")\n",
|
||||||
|
"\n",
|
||||||
|
" return float(-linprog(C_ZIEL, A_ub=A_UB, b_ub=B_UB, bounds=(0, None)).fun)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def entartung_erkennen() -> None:\n",
|
||||||
|
" \"\"\"Der Test, der in jedes Auswertungsskript gehoert.\"\"\"\n",
|
||||||
|
" print(\"\\n\" + \"=\" * 78)\n",
|
||||||
|
" print(\" 2. ENTARTUNG ERKENNEN - UND WARUM '== 0' DABEI VERSAGT\")\n",
|
||||||
|
" print(\"=\" * 78)\n",
|
||||||
|
"\n",
|
||||||
|
" ergebnis = linprog(C_ZIEL, A_ub=A_UB, b_ub=B_UB, bounds=(0, None))\n",
|
||||||
|
" schlupf = B_UB - A_UB @ ergebnis.x\n",
|
||||||
|
"\n",
|
||||||
|
" print(f\"{'Nebenbedingung':<18} {'Schlupf':>16} {'== 0 ?':>9} \"\n",
|
||||||
|
" f\"{'< 1e-7 ?':>10}\")\n",
|
||||||
|
" print(\"-\" * 78)\n",
|
||||||
|
" for name, s in zip(NAMEN, schlupf):\n",
|
||||||
|
" print(f\"{name:<18} {s:>16.3e} {str(s == 0.0):>9} {str(abs(s) < 1e-7):>10}\")\n",
|
||||||
|
"\n",
|
||||||
|
" aktiv = int((np.abs(schlupf) < 1e-7).sum())\n",
|
||||||
|
" variablen = A_UB.shape[1]\n",
|
||||||
|
" print(\"-\" * 78)\n",
|
||||||
|
" print(f\"Aktive Nebenbedingungen: {aktiv}, Variablen: {variablen}\")\n",
|
||||||
|
" if aktiv > variablen:\n",
|
||||||
|
" print(f\"=> ENTARTET. {aktiv} aktive Restriktionen bei nur {variablen} \"\n",
|
||||||
|
" \"Variablen bedeuten:\")\n",
|
||||||
|
" print(\" Der Schattenpreis ist nicht eindeutig. Melden Sie eine Spanne,\")\n",
|
||||||
|
" print(\" keinen Einzelwert (siehe Teil 3).\")\n",
|
||||||
|
" else:\n",
|
||||||
|
" print(\"=> nicht entartet, die Dualwerte sind eindeutig.\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\nBeachten Sie die Spalte '== 0': Ein Schlupf von 4.44e-16 ist\")\n",
|
||||||
|
" print(\"rechnerisch null, aber nicht gleich 0.0. Wer mit '==' prueft,\")\n",
|
||||||
|
" print(\"uebersieht genau die Engpaesse, die er sucht.\")\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def schattenpreis_spanne(zielwert: float, toleranz: float = 1e-9\n",
|
||||||
|
" ) -> list[tuple[float, float]]:\n",
|
||||||
|
" \"\"\"Berechnet fuer jede Nebenbedingung die Spanne ihres Schattenpreises\n",
|
||||||
|
" ueber ALLE optimalen Dualloesungen.\n",
|
||||||
|
"\n",
|
||||||
|
" Die Menge der optimalen Dualloesungen ist selbst ein Polyeder:\n",
|
||||||
|
"\n",
|
||||||
|
" A^T y >= c, y >= 0, b^T y = Z*\n",
|
||||||
|
"\n",
|
||||||
|
" (Dualzulaessigkeit plus starker Dualitaetssatz.) Minimiert und maximiert\n",
|
||||||
|
" man darauf y_i, erhaelt man die exakten Grenzen. Das ist die ehrliche\n",
|
||||||
|
" Auskunft an das Management: nicht 'die Stunde ist 0,33 EUR wert', sondern\n",
|
||||||
|
" 'zwischen 0,00 und 0,33 EUR - der Wert ist aus dem Modell nicht bestimmbar'.\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" m = A_UB.shape[0]\n",
|
||||||
|
" # A^T y >= c <=> -A^T y <= -c ; Ziel war max c^T x, in linprog-Notation\n",
|
||||||
|
" # steckt c mit negativem Vorzeichen in C_ZIEL.\n",
|
||||||
|
" c_original = -C_ZIEL\n",
|
||||||
|
" A_dual_ub = -A_UB.T\n",
|
||||||
|
" b_dual_ub = -c_original\n",
|
||||||
|
"\n",
|
||||||
|
" spannen = []\n",
|
||||||
|
" for i in range(m):\n",
|
||||||
|
" richtung = np.zeros(m)\n",
|
||||||
|
" richtung[i] = 1.0\n",
|
||||||
|
" grenzen = []\n",
|
||||||
|
" for vorzeichen in (1.0, -1.0): # 1 = minimieren, -1 = maximieren\n",
|
||||||
|
" ergebnis = linprog(\n",
|
||||||
|
" vorzeichen * richtung,\n",
|
||||||
|
" A_ub=A_dual_ub, b_ub=b_dual_ub,\n",
|
||||||
|
" A_eq=B_UB.reshape(1, -1), b_eq=[zielwert],\n",
|
||||||
|
" bounds=(0, None))\n",
|
||||||
|
" if not ergebnis.success:\n",
|
||||||
|
" raise RuntimeError(f\"Spannenberechnung fehlgeschlagen: \"\n",
|
||||||
|
" f\"{ergebnis.message}\")\n",
|
||||||
|
" grenzen.append(float(ergebnis.x[i]))\n",
|
||||||
|
" spannen.append((min(grenzen), max(grenzen)))\n",
|
||||||
|
" return spannen\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def zeige_spanne(zielwert: float) -> None:\n",
|
||||||
|
" print(\"\\n\" + \"=\" * 78)\n",
|
||||||
|
" print(\" 3. DIE EHRLICHE AUSKUNFT: SCHATTENPREIS-SPANNEN\")\n",
|
||||||
|
" print(\"=\" * 78)\n",
|
||||||
|
"\n",
|
||||||
|
" spannen = schattenpreis_spanne(zielwert)\n",
|
||||||
|
" print(f\"{'Nebenbedingung':<18} {'von':>10} {'bis':>10} Aussage\")\n",
|
||||||
|
" print(\"-\" * 78)\n",
|
||||||
|
" for name, (unten, oben) in zip(NAMEN, spannen):\n",
|
||||||
|
" if oben - unten < 1e-7:\n",
|
||||||
|
" aussage = f\"eindeutig {oben:.2f} EUR\"\n",
|
||||||
|
" elif oben < 1e-7:\n",
|
||||||
|
" aussage = \"sicher wertlos (kein Engpass)\"\n",
|
||||||
|
" else:\n",
|
||||||
|
" aussage = \"NICHT bestimmbar - Spanne melden!\"\n",
|
||||||
|
" print(f\"{name:<18} {unten:>10.4f} {oben:>10.4f} {aussage}\")\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"-\" * 78)\n",
|
||||||
|
" print(\"So berichtet man an Entscheider: 'Eine zusaetzliche Fraesstunde ist\")\n",
|
||||||
|
" print(\"zwischen 0,00 und 0,33 EUR wert - das Modell kann es nicht genauer\")\n",
|
||||||
|
" print(\"sagen, weil drei Engpaesse exakt gleichzeitig binden.' Das ist eine\")\n",
|
||||||
|
" print(\"brauchbare Aussage. Ein erfundener Einzelwert ist es nicht.\")\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"if __name__ == \"__main__\":\n",
|
||||||
|
" zielwert = zeige_entartung()\n",
|
||||||
|
" entartung_erkennen()\n",
|
||||||
|
" zeige_spanne(zielwert)\n",
|
||||||
|
"\n",
|
||||||
|
" print(\"\\n\" + \"=\" * 78)\n",
|
||||||
|
" print(\"Merksatz: Pruefen Sie VOR jeder Sensitivitaetsaussage auf Entartung -\")\n",
|
||||||
|
" print(\"und vergleichen Sie Schlupfwerte nie mit '== 0', sondern mit einer\")\n",
|
||||||
|
" print(\"Toleranz.\")\n",
|
||||||
|
" print(\"=\" * 78)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"name": "python",
|
||||||
|
"version": "3.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue