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:
dschlueter 2026-09-08 01:20:09 +02:00
commit b7af2f1d9a
468 changed files with 262141 additions and 0 deletions

View file

@ -0,0 +1,91 @@
#!/usr/bin/env python3
# Runden_Gegenbeispiel.py
"""
Kapitel MILP: Wie schlecht ist Runden wirklich?
Ersetzt die unbelegte Behauptung "20-50 % Verlust" durch eine Messung ueber
viele Zufallsinstanzen.
"""
import numpy as np
from scipy.optimize import linprog
def erzeuge_instanz(n, m, rng):
"""Zufaelliges Rucksack-aehnliches MILP mit kleinen Zahlen."""
c = rng.integers(3, 20, size=n).astype(float) # Ertraege
A = rng.integers(1, 9, size=(m, n)).astype(float) # Verbraeuche
b = (A.sum(axis=1) * rng.uniform(0.25, 0.45)).round() # knappe Kapazitaeten
return c, A, b
def loese_lp(c, A, b, ganzzahlig=False):
"""LP-Relaxation oder exaktes MILP ueber HiGHS."""
n = len(c)
ergebnis = linprog(
c=-c, A_ub=A, b_ub=b, bounds=[(0, None)] * n,
integrality=np.ones(n) if ganzzahlig else None,
method="highs")
return (-ergebnis.fun, ergebnis.x) if ergebnis.success else (None, None)
def abrunden_und_reparieren(x_lp, c, A, b):
"""Naive Strategie: abrunden, dann gierig auffuellen, solange zulaessig."""
x = np.floor(x_lp + 1e-9)
verbessert = True
while verbessert: # gierig auffuellen
verbessert = False
for j in np.argsort(-c): # bester Ertrag zuerst
kandidat = x.copy()
kandidat[j] += 1
if np.all(A @ kandidat <= b + 1e-9):
x = kandidat
verbessert = True
break
return c @ x, x
if __name__ == "__main__":
rng = np.random.default_rng(2026)
print("=" * 82)
print(" WIE TEUER IST RUNDEN? (200 Zufallsinstanzen je Groesse)")
print("=" * 82)
print(f"{'n x m':>8} | {'Aufrunden unzul.':>17} | {'Abrunden: mittl.':>17} | "
f"{'schlimmster':>12} | {'gierig':>8}")
print(f"{'':>8} | {'':>17} | {'Verlust':>17} | {'Fall':>12} | {'Verlust':>8}")
print("-" * 82)
for n, m in [(5, 2), (10, 3), (20, 5), (40, 8)]:
unzulaessig = 0
verluste_ab, verluste_gierig = [], []
for _ in range(200):
c, A, b = erzeuge_instanz(n, m, rng)
z_lp, x_lp = loese_lp(c, A, b, ganzzahlig=False)
z_ip, _ = loese_lp(c, A, b, ganzzahlig=True)
if z_lp is None or z_ip is None or z_ip <= 0:
continue
# Variante 1: aufrunden
x_auf = np.ceil(x_lp - 1e-9)
if np.any(A @ x_auf > b + 1e-9):
unzulaessig += 1
# Variante 2: abrunden
x_ab = np.floor(x_lp + 1e-9)
verluste_ab.append(1.0 - (c @ x_ab) / z_ip)
# Variante 3: abrunden + gierig auffuellen
z_gierig, _ = abrunden_und_reparieren(x_lp, c, A, b)
verluste_gierig.append(1.0 - z_gierig / z_ip)
print(f"{n:>3} x {m:<3} | {unzulaessig/2:>15.1f} % | "
f"{np.mean(verluste_ab)*100:>15.1f} % | "
f"{np.max(verluste_ab)*100:>10.1f} % | "
f"{np.mean(verluste_gierig)*100:>6.1f} %")
print("-" * 82)
print("Lesart: 'Aufrunden unzul.' = Anteil der Faelle, in denen die aufgerundete")
print("Loesung eine Nebenbedingung verletzt. 'Verlust' = Abstand zum exakten Optimum.")
print("=" * 82)