{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "b34db3ce",
   "metadata": {},
   "source": [
    "# Entscheidungen unter Unsicherheit: Personalplanung im Krankenhaus\n",
    "\n",
    "**Begleitmaterial zur Vorlesungseinheit** — Dr. Celso Gustavo Stall Sikora\n",
    "\n",
    "Dieses Notebook enthält das vollständige Rechenbeispiel aus der Vorlesung:\n",
    "Eine Station plant die Nachtdienste für die Samstagnächte des kommenden Monats.\n",
    "Festgelegt werden **jetzt**: $x_V$ Vollarbeitskräfte und $x_B$ Bereitschaftsdienste.\n",
    "Der Bedarf $D$ zeigt sich **erst in der Nacht**; reagiert wird durch Aktivierung\n",
    "der Bereitschaft und — als teuerste Stufe — durch **Einspringen**.\n",
    "\n",
    "$$\\min_{x_V,\\; x_B \\le 2}\\; \\underbrace{1{,}0\\,x_V + 0{,}75\\,x_B}_{\\text{jetzt, sicher}}\n",
    "\\;+\\; \\underbrace{\\mathbb{E}_D\\big[\\,0{,}3 \\cdot B(D) + 3{,}0 \\cdot S(D)\\,\\big]}_{\\text{später, im Erwartungswert}}$$\n",
    "\n",
    "mit $B(D) = \\min(\\max(D-x_V,0),\\, x_B)$ (aktivierte Bereitschaft) und\n",
    "$S(D) = \\max(D - x_V - x_B,\\, 0)$ (Einspringer).\n",
    "\n",
    "**Kostenstruktur** (stilisiert nach TV-Ärzte/VKA, alle Werte in \"Schichtäquivalenten\"):\n",
    "\n",
    "| Instrument | Kosten |\n",
    "|---|---|\n",
    "| Vollarbeit | 1,0 pro Schicht |\n",
    "| Bereitschaftsdienst (Stufe II, § 12) | 0,75 + 0,3 je Einsatz (Stufendrift; aktiviert 1,05 > 1,0) |\n",
    "| Einspringen / kurzfristiger Ersatz | 3,0 |\n",
    "\n",
    "**Ihre Aufgabe zu Hause:** Ändern Sie die Parameter im nächsten Block und beobachten\n",
    "Sie, wie sich der optimale Dienstplan verschiebt. Die Leitfragen stehen am Ende."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fac0050a",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ============================================================\n",
    "# PARAMETER — hier dürfen (sollen!) Sie ändern und neu rechnen\n",
    "# ============================================================\n",
    "\n",
    "# Kosten (in Schichtäquivalenten)\n",
    "c_V   = 1.0    # Vollarbeit pro Schicht\n",
    "c_B   = 0.75   # Bereitschaftsdienst: Grundkosten (Stufe II = 75 %)\n",
    "c_akt = 0.3    # Zusatzkosten je aktivierter Bereitschaft (Stufendrift; aktiviert: 0,75+0,3 > 1,0)\n",
    "c_E   = 3.0    # Einspringen / kurzfristiger Ersatz\n",
    "\n",
    "# Obergrenze Bereitschaftsdienste (§ 10: keine Regelarbeit)\n",
    "x_B_max = 2\n",
    "\n",
    "# Bedarfsverteilung: D -> Wahrscheinlichkeit\n",
    "verteilung = {2: 0.05, 3: 0.20, 4: 0.30, 5: 0.25, 6: 0.15, 7: 0.05}\n",
    "\n",
    "assert abs(sum(verteilung.values()) - 1.0) < 1e-9, \"Wahrscheinlichkeiten müssen sich zu 1 summieren!\"\n",
    "E_D = sum(d * p for d, p in verteilung.items())\n",
    "print(f\"Erwarteter Bedarf E[D] = {E_D:.2f} Pflegekräfte\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "856c7677",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ============================================================\n",
    "# MODELL: erwartete Gesamtkosten eines Plans (x_V, x_B)\n",
    "# ============================================================\n",
    "\n",
    "def kosten(x_V, x_B, detail=False):\n",
    "    # Erwartete Gesamtkosten des Plans (x_V Vollarbeit, x_B Bereitschaft)\n",
    "    plankosten = c_V * x_V + c_B * x_B\n",
    "    e_akt, e_einspr = 0.0, 0.0\n",
    "    for d, p in verteilung.items():\n",
    "        fehl = max(d - x_V, 0)                 # Unterdeckung nach Vollarbeit\n",
    "        aktiviert = min(fehl, x_B)             # Bereitschaft einspringen lassen\n",
    "        einspringer = max(d - x_V - x_B, 0)    # Rest: Einspringen\n",
    "        e_akt += p * aktiviert\n",
    "        e_einspr += p * einspringer\n",
    "    gesamt = plankosten + c_akt * e_akt + c_E * e_einspr\n",
    "    if detail:\n",
    "        return dict(plan=plankosten, aktivierung=c_akt * e_akt,\n",
    "                    einspringen=c_E * e_einspr, gesamt=gesamt,\n",
    "                    p_einspringen=sum(p for d, p in verteilung.items() if d > x_V + x_B))\n",
    "    return gesamt\n",
    "\n",
    "# Beispiel: der Plan aus der Vorlesung\n",
    "print(kosten(3, 2, detail=True))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "15e5c9b9",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ============================================================\n",
    "# VOLLSTÄNDIGE ENUMERATION: alle Pläne im Gitter\n",
    "# ============================================================\n",
    "import numpy as np\n",
    "\n",
    "D_max = max(verteilung)\n",
    "gitter = {(v, b): kosten(v, b)\n",
    "          for v in range(0, D_max + 1)\n",
    "          for b in range(0, x_B_max + 1)}\n",
    "\n",
    "optimum = min(gitter, key=gitter.get)\n",
    "print(f\"Optimaler Plan: {optimum[0]} Vollarbeit + {optimum[1]} Bereitschaft \"\n",
    "      f\"-> erwartete Kosten {gitter[optimum]:.2f}\")\n",
    "\n",
    "print(\"\\nKostenmatrix (Zeilen: x_V, Spalten: x_B):\")\n",
    "print(\"x_V\\\\x_B \" + \"\".join(f\"{b:>8}\" for b in range(x_B_max + 1)))\n",
    "for v in range(0, D_max + 1):\n",
    "    zeile = \"\".join(f\"{gitter[(v, b)]:8.2f}\" for b in range(x_B_max + 1))\n",
    "    marker = \"  <-- Optimum\" if v == optimum[0] else \"\"\n",
    "    print(f\"{v:>4}   {zeile}{marker}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0cd2c984",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ============================================================\n",
    "# VERGLEICH: die drei Pläne aus der Vorlesung + Optimum\n",
    "# ============================================================\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "plaene = {\n",
    "    f\"Plan A 'Durchschnitt' ({round(E_D)}V)\": (round(E_D), 0),\n",
    "    f\"Plan B 'Auf Nummer sicher' ({D_max - 1}V)\": (D_max - 1, 0),\n",
    "    f\"Plan C 'Portfolio' ({optimum[0]}V+{optimum[1]}B)\": optimum,\n",
    "}\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(9, 3.2))\n",
    "farben = {\"plan\": \"#00547a\", \"aktivierung\": \"#e0a96d\", \"einspringen\": \"#cc5500\"}\n",
    "for i, (name, (v, b)) in enumerate(plaene.items()):\n",
    "    det = kosten(v, b, detail=True)\n",
    "    links = 0\n",
    "    for teil, wert in [(\"plan\", det[\"plan\"]), (\"aktivierung\", det[\"aktivierung\"]),\n",
    "                       (\"einspringen\", det[\"einspringen\"])]:\n",
    "        ax.barh(i, wert, left=links, color=farben[teil], edgecolor=\"white\")\n",
    "        links += wert\n",
    "    ax.text(links + 0.05, i, f\"{det['gesamt']:.2f}\", va=\"center\", fontweight=\"bold\")\n",
    "ax.set_yticks(range(len(plaene)), plaene.keys())\n",
    "ax.invert_yaxis()\n",
    "ax.set_xlabel(\"Erwartete Kosten pro Nacht (Schichtäquivalente)\")\n",
    "ax.legend([\"Plankosten\", \"Bereitschaft aktiviert\", \"Einspringen\"], loc=\"lower right\")\n",
    "ax.spines[[\"top\", \"right\"]].set_visible(False)\n",
    "plt.tight_layout(); plt.show()\n",
    "\n",
    "for name, (v, b) in plaene.items():\n",
    "    det = kosten(v, b, detail=True)\n",
    "    print(f\"{name}: gesamt {det['gesamt']:.2f} | P(Einspringen) = {det['p_einspringen']:.0%}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1669dc5b",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ============================================================\n",
    "# VALUE OF THE STOCHASTIC SOLUTION (VSS)\n",
    "# ============================================================\n",
    "import math\n",
    "\n",
    "# Deterministische Logik: plane für E[D] (aufgerundet), nur Vollarbeit —\n",
    "# unter Sicherheit gäbe es rechtlich gar keinen Bereitschaftsdienst.\n",
    "x_det = math.ceil(E_D)\n",
    "eev = kosten(x_det, 0)\n",
    "vss = eev - gitter[optimum]\n",
    "print(f\"Deterministischer Plan ({x_det} Vollarbeit): {eev:.2f}\")\n",
    "print(f\"Stochastisches Optimum {optimum}:          {gitter[optimum]:.2f}\")\n",
    "print(f\"Value of the Stochastic Solution (VSS):    {vss:.2f}  ({vss/eev:.1%})\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "19b95fd2",
   "metadata": {},
   "source": [
    "## Aufgaben\n",
    "\n",
    "1. **Einspringpreis.** Senken Sie $c_E$ schrittweise von 3,0 auf 1,0.\n",
    "   Ab welchem Wert verschwindet die Bereitschaft aus dem optimalen Plan? Warum?\n",
    "2. **Stufendrift.** Setzen Sie $c_{akt} = 0$. Was passiert mit dem Optimum —\n",
    "   und warum ist das Ergebnis ökonomisch (und tarifrechtlich) unplausibel?\n",
    "3. **Verteilung.** Ersetzen Sie die Verteilung durch eine symmetrische mit gleichem\n",
    "   Erwartungswert (z. B. $\\{3{:}\\,0{,}2;\\ 4{:}\\,0{,}3;\\ 5{:}\\,0{,}3;\\ 6{:}\\,0{,}2\\}$).\n",
    "   Wie ändert sich der Wert des Portfolios? Was lernt man daraus über die Rolle des\n",
    "   *rechten Rands* der Verteilung?\n",
    "4. **Kritisches Verhältnis.** Für $x_B = 0$ ist das Problem ein klassisches\n",
    "   Newsvendor-Problem. Leiten Sie das optimale $x_V$ über das kritische Verhältnis\n",
    "   $c_E / (c_E + c_V)$ her und vergleichen Sie mit der Enumeration.\n",
    "\n",
    "## Weiterführende Literatur\n",
    "\n",
    "- Birge & Louveaux: *Introduction to Stochastic Programming*, Springer (Kap. 1–4).\n",
    "- Erhard, Schoenfelder, Fügener, Brunner (2018): *State of the art in physician\n",
    "  scheduling*, EJOR 265(1), 1–18.\n",
    "- Ban & Rudin (2019): *The Big Data Newsvendor*, Management Science 65(9).\n",
    "- Porrmann & Römer (2021): *Learning to Reduce State-Expanded Networks for\n",
    "  Multi-Activity Shift Scheduling*, CPAIOR 2021 — ML-gestütztes Lösen von\n",
    "  Dienstplanungsmodellen, entwickelt an der Universität Bielefeld.\n",
    "\n",
    "*Kostenwerte stilisiert nach TV-Ärzte/VKA (§§ 10–12); Einspringprämien nach\n",
    "öffentlich dokumentierten Regelungen (z. B. LMU Klinikum München).*"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
