131 lines
5.2 KiB
Python
131 lines
5.2 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
|
||
|
|
# erzeuge_fluch_durchschnitt.py
|
||
|
|
"""
|
||
|
|
Erzeugt das Diagramm zum Fluch des Durchschnitts:
|
||
|
|
|
||
|
|
bilder_04/kap_unsicherheit_durchschnitt.svg
|
||
|
|
|
||
|
|
Die Handrechnung des Abschnitts vergleicht zwei Plaene fuer dieselbe unsichere
|
||
|
|
Nachfrage. Das Bild zeigt, warum der eine schlechter ist - und zwar nicht als
|
||
|
|
Behauptung, sondern als Kurve: die erwarteten Kosten ueber ALLEN moeglichen
|
||
|
|
Bestellmengen.
|
||
|
|
|
||
|
|
Was man daran sieht: Die Kurve hat einen Knick an jedem Szenario, faellt links
|
||
|
|
flach und steigt rechts steil - Fehlmenge kostet 120 EUR, Ueberschuss nur 5. In
|
||
|
|
dieser Schieflage liegt der ganze Effekt. Ihr Minimum liegt deshalb NICHT beim
|
||
|
|
Erwartungswert der Nachfrage, sondern rechts davon.
|
||
|
|
|
||
|
|
Ohne Formel gesagt: Mit dem Mittelwert zu planen unterstellt, dass sich zu viel
|
||
|
|
und zu wenig gegenseitig aufheben. Sie tun es nicht.
|
||
|
|
|
||
|
|
Alle Werte werden gerechnet. pruefe_gegen_handrechnung() vergleicht sie mit den
|
||
|
|
im Kapitel abgedruckten Zwischenergebnissen - weicht eines ab, bricht das
|
||
|
|
Skript ab.
|
||
|
|
|
||
|
|
Aufruf (aus dem Repository-Wurzelverzeichnis):
|
||
|
|
python3 bilder_04/erzeuge_fluch_durchschnitt.py
|
||
|
|
|
||
|
|
Benoetigt: numpy, matplotlib
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import os
|
||
|
|
import sys
|
||
|
|
|
||
|
|
import numpy as np
|
||
|
|
|
||
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||
|
|
from stil_04 import FARBEN, neue_figur, speichere # noqa: E402
|
||
|
|
|
||
|
|
# Die Instanz der Handrechnung: (Bedarf, Wahrscheinlichkeit)
|
||
|
|
SZENARIEN = [(100, 0.50), (250, 0.30), (500, 0.20)]
|
||
|
|
NAMEN = ["Ruhig", "Volatil", "Crash"]
|
||
|
|
|
||
|
|
VORAB = 40.0 # EUR je Einheit im Voraus
|
||
|
|
FEHLT = 120.0 # EUR je fehlender Einheit, kurzfristig
|
||
|
|
UEBRIG = 5.0 # EUR je ueberschuessiger Einheit, Verwaltung
|
||
|
|
|
||
|
|
# Aus dem Kapitel abgedruckt - nur zum Gegenpruefen.
|
||
|
|
ERWARTET_IM_BUCH = {225: 16_812.50, 250: 16_375.00}
|
||
|
|
|
||
|
|
|
||
|
|
def kosten(x: float) -> float:
|
||
|
|
"""Erwartete Gesamtkosten bei Bestellmenge x."""
|
||
|
|
return sum(p * (VORAB * x + FEHLT * max(bedarf - x, 0.0)
|
||
|
|
+ UEBRIG * max(x - bedarf, 0.0))
|
||
|
|
for bedarf, p in SZENARIEN)
|
||
|
|
|
||
|
|
|
||
|
|
def pruefe_gegen_handrechnung() -> None:
|
||
|
|
for x, erwartet in ERWARTET_IM_BUCH.items():
|
||
|
|
ist = kosten(x)
|
||
|
|
if abs(ist - erwartet) > 0.01:
|
||
|
|
raise SystemExit(
|
||
|
|
f"Fluch des Durchschnitts: Bei x={x} ergibt die Rechnung "
|
||
|
|
f"{ist:,.2f} EUR, die Handrechnung im Kapitel nennt "
|
||
|
|
f"{erwartet:,.2f} EUR.")
|
||
|
|
|
||
|
|
|
||
|
|
def zeichne(mittelwert: float, bestes_x: float) -> None:
|
||
|
|
figur, achse = neue_figur(breite=8.0, hoehe=4.6)
|
||
|
|
|
||
|
|
x = np.linspace(0, 560, 1200)
|
||
|
|
y = np.array([kosten(float(k)) for k in x])
|
||
|
|
achse.plot(x, y, "-", color=FARBEN["haupt"], linewidth=2.4, zorder=4,
|
||
|
|
label="erwartete Gesamtkosten")
|
||
|
|
|
||
|
|
# Die Szenarien als senkrechte Marken - dort knickt die Kurve.
|
||
|
|
for (bedarf, p), name in zip(SZENARIEN, NAMEN):
|
||
|
|
achse.axvline(bedarf, color=FARBEN["linie"], linewidth=0.9,
|
||
|
|
linestyle=":", zorder=2)
|
||
|
|
achse.text(bedarf, achse.get_ylim()[0], f" {name}\n {p:.0%}",
|
||
|
|
fontsize=8.5, color=FARBEN["gedaempft"], va="bottom")
|
||
|
|
|
||
|
|
for stelle, farbe, beschriftung in (
|
||
|
|
(mittelwert, FARBEN["fehler"],
|
||
|
|
f"Planung mit dem Mittelwert\n$x = {mittelwert:.0f}$ "
|
||
|
|
f"{kosten(mittelwert):,.0f} €"),
|
||
|
|
(bestes_x, FARBEN["gut"],
|
||
|
|
f"stochastisch optimal\n$x = {bestes_x:.0f}$ "
|
||
|
|
f"{kosten(bestes_x):,.0f} €")):
|
||
|
|
achse.plot([stelle], [kosten(stelle)], "o", color=farbe, markersize=9,
|
||
|
|
zorder=6)
|
||
|
|
achse.annotate(beschriftung.replace(",", "."),
|
||
|
|
xy=(stelle, kosten(stelle)),
|
||
|
|
xytext=(14 if farbe == FARBEN["gut"] else -14, 44),
|
||
|
|
textcoords="offset points", fontsize=9, color=farbe,
|
||
|
|
fontweight="bold",
|
||
|
|
ha="left" if farbe == FARBEN["gut"] else "right",
|
||
|
|
arrowprops=dict(arrowstyle="->", color=farbe,
|
||
|
|
linewidth=1.2))
|
||
|
|
|
||
|
|
mehrkosten = kosten(mittelwert) - kosten(bestes_x)
|
||
|
|
achse.annotate(
|
||
|
|
f"Der Mittelwertplan kostet {mehrkosten:,.0f} € mehr —\n"
|
||
|
|
f"nicht weil er ungenau ist, sondern weil\n"
|
||
|
|
f"Fehlmenge {FEHLT:.0f} € kostet und Überschuss nur "
|
||
|
|
f"{UEBRIG:.0f} €.".replace(",", "."),
|
||
|
|
xy=(0.03, 0.95), xycoords="axes fraction", fontsize=9,
|
||
|
|
color=FARBEN["text"], va="top")
|
||
|
|
|
||
|
|
achse.set_xlabel("vorab gekaufte Kapazität $x$")
|
||
|
|
achse.set_ylabel("erwartete Gesamtkosten in €")
|
||
|
|
achse.set_title("Warum der Mittelwert der falsche Plan ist",
|
||
|
|
fontsize=10.5, color=FARBEN["text"])
|
||
|
|
achse.set_xlim(0, 560)
|
||
|
|
|
||
|
|
speichere(figur, "kap_unsicherheit_durchschnitt")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
pruefe_gegen_handrechnung()
|
||
|
|
mittelwert = sum(bedarf * p for bedarf, p in SZENARIEN)
|
||
|
|
gitter = np.arange(0, 561, 1.0)
|
||
|
|
bestes_x = float(gitter[int(np.argmin([kosten(float(k)) for k in gitter]))])
|
||
|
|
print(f" Erwartungswert der Nachfrage: {mittelwert:.0f}")
|
||
|
|
print(f" Kosten dort: {kosten(mittelwert):,.2f} EUR")
|
||
|
|
print(f" bestes x: {bestes_x:.0f}")
|
||
|
|
print(f" Kosten dort: {kosten(bestes_x):,.2f} EUR")
|
||
|
|
zeichne(mittelwert, bestes_x)
|