{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# ๐ฏ [ADM01] ๊ฒฐ์ยท๊ทผ๋ฌด๋ถํ ์ถ์ธ ๋์๋ณด๋์ ๋ณด์ถฉ ํ์๋ ์์ธก\n",
"### Google Colab ์ค์ต ํ๊ฒฝ (ํ์ ยท์ธ์ฌ / Level 3)\n",
"\n",
"[](https://colab.research.google.com/github/edunlab/milicontent2/blob/main/uploads/ADM01/ADM01_solution.ipynb)\n",
"\n",
"---\n",
"\n",
"## ๐ 1. ํ๋ก์ ํธ ๊ฐ์ ๋ฐ ๋ฐฐ๊ฒฝ\n",
"\n",
"| ํญ๋ชฉ | ๋ด์ฉ |\n",
"| :--- | :--- |\n",
"| **๊ณผ์ ๋ถ๋ฅ** | ํ์ ยท์ธ์ฌ (Level 3) |\n",
"| **ํ์ต ์์ ์๊ฐ** | 10๊ฐ ๋ฏธ์
(5์ผ / ์ด 27.5์๊ฐ) |\n",
"| **ํ๊ฒฝ ์ฌ์** | Python 3.10+, numpy, pandas, matplotlib, scipy, scikit-learn |\n",
"\n",
"### ๐ ํ๋ก์ ํธ ๋ชฉํ\n",
"๋ณ๋ ฅ ๊ฐ์์ ์ ์ถยทํ๊ฒฌยทํด๊ฐยทํ์ยท๊ต์ก์ด ๊ฒน์น๋ฉด ๋ถ๋ ์ง๋ฌด๋ณ ๊ฒฐ์์ด ๊ฐ์๊ธฐ ์ปค์ง๊ณ ๋จ์ ์ธ์์ ์ด๊ณผ๊ทผ๋ฌด์ ํผ๋ก๊ฐ ๋์ ๋์ด ์ ํฌ๋ ฅ ์ ์ง๊ฐ ์ด๋ ค์์ง์ง๋ง, ํ์ฌ์ ํ๋ง์ผ๋ก๋ ํฅํ ๊ฒฐ์๊ณผ ๋ณด์ถฉ ํ์๋์ ๋ฏธ๋ฆฌ ๋ณด๊ธฐ ์ด๋ ต๋ค. ๋ณธ ํ๋ก์ ํธ๋ ๋ฌ์์-์ฐํฌ๋ผ์ด๋ ์ ์์์ ๋๋ฌ๋ ์ธ๋ ฅ ์๋ชจยทํผ๋ก ๊ด๋ฆฌ ๋ฌธ์ ์์์ ๊ต์ก์ฉ ๊ฐ์ ์๋ฃ๋ก ์ฌ๊ตฌ์ฑํด, ๊ฐ์ ๋๋ 4๊ณณ์ ์ง๋ฌด 6์ข
์ฃผ๊ฐ ์ธ๋ ฅ ์ง๊ณ ์๋ฃ๋ก ๊ฒฐ์ยท๊ทผ๋ฌด๋ถํ ์ถ์ธ๋ฅผ ๋ณด๊ณ ํฅํ 3๊ฐ์ ๋ณด์ถฉ ํ์๋์ ์์ธกํ๋ ๊ณผ์ ์ด๋ค. ์ธ๋ ฅ ์๋ฃ ์คํค๋ง์ ๊ทผ๋ฌด์ผ์ง ์ ์ฒ๋ฆฌ, ํผ๋ก๋ Baseline, 13์ฃผ ๊ฒฐ์ ์์ธก๊ณผ ์ํ ์ง์ยท์ ๋ขฐ๋, ํํฉ ๋์๋ณด๋, ์ธ์ฌ๊ดยท์งํ๊ด ๊ฒํ ์ํฌํ๋ก์ฐ, ๋๋ ๊ฒฐ์ ์คํธ๋ ์ค ์ํ, AAR ์ฌ๋ณด์ , ํตํฉ ํจํค์ง๊น์ง ๋จ๊ณ๋ณ๋ก ๊ตฌํํ๋ค. ์ต์ข
๋ชฉํ๋ ์ธ๋ ฅ ๋ถ์กฑ๊ณผ ํผ๋ก ์ํ์ ๊ทผ๊ฑฐ์ ๋ถํ์ค์ฑ๊ณผ ํจ๊ป ๋ณด์ฌ ์ฃผ๋, ๋ณด์ถฉ ์์ฒญ๊ณผ ์ธ์ ์ด๋ ๊ฒฐ์ ์ ์ธ์ฌ๊ดยท์งํ๊ด์ด ํ๋๋ก ์ง์ํ๋ ์์คํ
์ ์์ฑํ๋ ๊ฒ์ด๋ค.\n",
"\n",
"> **โ ๏ธ [๊ต์ก์ฉ ๊ฐ์ ๋ฐ์ดํฐ์
์์น]**\n",
"> - ๋ณธ ๋
ธํธ๋ถ์ ๋ชจ๋ ๋ฐ์ดํฐ(์ ์, ๋ก๊ทธ, ์๋๋ฆฌ์ค ๋ฑ)๋ ๋ณด์ ๊ท์ ์ ์ค์ํ์ฌ **์ฝ๋ ๋ด ๋์ ๊ธฐ๋ฐ ๊ฐ์ ๋ฐ์ดํฐ๋ก ์์ฑ**๋ฉ๋๋ค.\n",
"> - ์ค์ ๋ถ๋ ์์น, ์์ ์ธ๊ฐ, ์ค์ ์ ์ฅ ์ ๋ณด, ๊ตฐ์ฌ ๊ธฐ๋ฐ ๋ฐ์ดํฐ๋ ์ผ์ฒด ํฌํจ๋์ง ์์ต๋๋ค.\n",
"\n",
"---\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# ============================================================================\n",
"# [Google Colab ํ๊ฒฝ ์ค์ ] ํ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค์น ๋ฐ ์ํฌํธ\n",
"# ============================================================================\n",
"!pip install -q numpy pandas matplotlib scipy scikit-learn\n",
"\n",
"import os\n",
"import sys\n",
"import time\n",
"import math\n",
"import json\n",
"import re\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"import warnings\n",
"warnings.filterwarnings('ignore')\n",
"\n",
"# ๊ทธ๋ํ ์๋ฌธ ํฐํธ ๊ธฐ๋ณธ ์ค์ (ํ๊ฒฝ ๋
๋ฆฝ์ฑ ๋ณด์ฅ)\n",
"plt.rcParams['font.sans-serif'] = 'DejaVu Sans'\n",
"plt.rcParams['axes.unicode_minus'] = False\n",
"\n",
"print(f\"์คํ ํ๊ฒฝ ์ค๋น ์๋ฃ (Python {sys.version.split()[0]})\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
01. ๋ถ๋ ์ธ์ฌ/ํธ์ ๋ฐ์ดํฐ(ํ์ฌ์, ๊ฒฐ์, ํด๊ฐ, ํ์, ๊ต๋์ฃผ๊ธฐ) ๋ฐ์ดํฐ์
์คํค๋ง ์ค๊ณ\n",
"**์์์๊ฐ**: ์ฝ 1.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋ถ๋ ์ธ์ฌ ๋ฐ์ดํฐ(ํ์ฌ์, ๊ฒฐ์, ํด๊ฐ, ํ์, ๊ต๋์ฃผ๊ธฐ) ์คํค๋ง๋ฅผ ์ค๊ณํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๊ฐ์ ๋ถ๋์ ์ง๋ฌด๋ณ ์ธ๋ ฅ ํํฉ(ํธ์ ยทํ์ฌ์ยท๊ฒฐ์ยทํด๊ฐยทํ์ยท๊ต์กยท๋ณด์ถฉยท์ ์ถยท์ด๊ณผ๊ทผ๋ฌดยท๊ต๋์ฃผ๊ธฐ) ์๋ฃ์ ๊ตฌ์กฐ๋ฅผ JSON ์คํค๋ง๋ก ์ ์ํ๊ณ , ์ ์ ํ๋ณธ ๊ฒ์ฆ ์ค๋ฅ 0๊ฑด๊ณผ ํ๋ ์ ์ ์์ฑ๋ 100%๋ฅผ ํ์ธํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ฐ์ ๋๋ 4๊ณณ๊ณผ ์ง๋ฌด 6์ข
์ ์ฃผ๊ฐ ์ง๊ณ ์๋ฃ์์ ํ๋ 15๊ฐ์ ์๋ฃํยท๋จ์ยทํ์ฉ ๋ฒ์ยท์ค๋ช
ยท์ถ์ฒ๋ฅผ ๋ชจ๋ ์ ์ํ๊ณ ํ๋ ๊ฐ ๋
ผ๋ฆฌ ๊ท์น(๊ฒฐ์์ ํธ์ ์์ ํ์ฌ์์ ๋บ ๊ฐ, ํด๊ฐยทํ์ยท๊ต์ก ํฉ๊ณ๋ ํ์ฌ์ ์ดํ) ์์ฑํ์ฌ ์์ธ์ ๋ถ์ํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `data/schema/adm01_input_schema.json ๋ฐ docs/adm01_data_spec.md` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ํ์ ยท์ธ์ฌ ์ค๋ฌด ๋ฐ ๋ฌ์์-์ฐํฌ๋ผ์ด๋ ์ ์ ์ ์ฅ ํ๊ฒฝ๊ณผ์ ๊ตฐ์ฌ์ ๋ถํฉ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์คํค๋ง ํ๋ ์ ์ ์์ฑ๋ 100%, ์ํ ๋ฐ์ดํฐ ๊ฒ์ฆ ์ค๋ฅ 0๊ฑด\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์
๋ ฅ ๋ฐ์ดํฐ ์ ํจ์ฑ ๊ฒ์ฆ ์คํฌ๋ฆฝํธ ํต๊ณผ ๋ฐ ๋ช
์ธ์ ๋ฌธ์ํ ์๋ฃ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: JSON Schema ๊ฒ์ฆ ๋๊ตฌ ๋ฐ ์ํ ๋ฐ์ดํฐ ํ์ฑ ๋จ์ ํ
์คํธ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"๊ฐ์ ๋ถ๋ ์ธ๋ ฅ ํํฉ(ํ์ฌ์ยท๊ฒฐ์ยทํด๊ฐยทํ์ยท๊ต์กยท๋ณด์ถฉยท์ ์ถยท์ด๊ณผ๊ทผ๋ฌดยท๊ต๋์ฃผ๊ธฐ, 15๊ฐ ํ๋)์ JSON ์คํค๋ง์ ๊ฒ์ฆ ๊ท์น์ ์ ์ํ๊ณ , ์ ์ ํ๋ณธ์ ๊ฒ์ฆ ์ค๋ฅ 0๊ฑดยท์คํค๋ง ํ๋ ์ ์ ์์ฑ๋ 100%๋ฅผ ํ์ธํ๋ฉฐ ํ์ยท๋ฒ์ยท๋
ผ๋ฆฌ ๊ท์น ์๋ฐ ์ค๋ฅ๋ฅผ ์ฃผ์
ํด ๊ฒ์ถ๋ ฅ๊ณผ ๊ฐ์ธ ์๋ณ ์ ๋ณด ๋ถ์ฌ๋ฅผ ์ ๊ฒํ๋ค.\"\"\"\n",
"import json\n",
"import re\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋ถ๋ ์ธ๋ ฅ ํํฉ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยทํธ์ ยท๋ณ๋ ฅยท๊ฐ์ธ ์ ๋ณด ์์)\n",
"# ๊ฐ์ ๋๋ 4๊ณณ x ์ง๋ฌด 6์ข
์ ์ฃผ๊ฐ ์ง๊ณ ์๋ฃ๋ง ๋ง๋ ๋ค. ๊ฐ์ธ ๋จ์ ์ ๋ณด(์ด๋ฆยท๊ตฐ๋ฒ ๋ฑ)๋ ๋ง๋ค์ง ์๋๋ค.\n",
"# ์ธ์์ ์ง๋ฌด๋ณ ์ง๊ณ ์์ด๋ฉฐ, ๊ฒฐ์ยทํด๊ฐยทํ์ยท๊ต์กยท๋ณด์ถฉยท์ ์ถ์ ๋ฐ์ํ ํ์ฌ์๊ณผ ์ด๊ณผ๊ทผ๋ฌด ์๊ฐ์ผ๋ก ๊ตฌ์ฑ๋๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"UNITS = [\"๊ฐ์A๋๋\", \"๊ฐ์B๋๋\", \"๊ฐ์C๋๋\", \"๊ฐ์D๋๋\"]\n",
"JOBS = [\"์์ \", \"ํต์ \", \"์ ๋น\", \"๋ณด๊ธ\", \"ํ์ \", \"์๋ฌด\"]\n",
"FIELDS = [\"unit\", \"job\", \"week\", \"authorized\", \"present\", \"vacancy\", \"leave\", \"sick\", \"training\", \"inflow\", \"outflow\", \"overtime_h\", \"night_ratio\", \"rotation_days\", \"fatigue_label\"]\n",
"HI_CUT = 55 # ๊ณ ํผ๋ก ํ์ ๊ธฐ์ค(๊ฐ์ ์ ์ 55 ์ด์)\n",
"\n",
"\n",
"def make_manning(seed=7, weeks=104, shock=None):\n",
" \"\"\"๋ถ๋ x ์ง๋ฌด 24๊ฐ ์
์ ์ฃผ๊ฐ ์ธ๋ ฅ ์๋ฃ. shock=(unit, job ๋๋ None(๋ถ๋ ์ ์ฒด), ์์์ฃผ, ์ฃผ๊ฐ ์ถ๊ฐ ์ ์ถ ํ๊ท ์) ํ ๊ฑด ๋๋ ๊ทธ ๋ชฉ๋ก์ด๋ฉด ๊ทธ ์
์ ๋๋ ๊ฒฐ์ ์์ธ์ ๋ฃ๋๋ค(AARยท์คํธ๋ ์ค์ฉ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" shocks = [shock] if shock and not isinstance(shock[0], tuple) else list(shock or [])\n",
" rows = []\n",
" for ui, u in enumerate(UNITS):\n",
" for ji, j in enumerate(JOBS):\n",
" auth = int(rng.integers(14, 42) * (0.9 + 0.06 * ui))\n",
" work = rng.uniform(0.92, 1.12) # ์ง๋ฌด๋ณ ์
๋ฌด๋ ๊ณ์\n",
" net = rng.uniform(-0.05, 0.03) # ๊ตฌ์กฐ์ ๋ณด์ถฉ ๋ถ์กฑ(์์)ยท๊ณผ์(์์) ๊ฒฝํฅ(์ฃผ๋น ์ธ์)\n",
" present = int(auth * rng.uniform(0.82, 0.98))\n",
" rot = int(rng.choice([60, 90, 120])) # ๊ต๋ยท๋ณด์ถฉ ์ฃผ๊ธฐ(์ผ)\n",
" for w in range(1, weeks + 1):\n",
" season = 1.0 + 0.9 * (abs(w % 52 - 22) < 4 or abs(w % 52 - 46) < 3) # ์ ์ถยท์ ์ญ ์ง์ค ์๊ธฐ\n",
" out = int(rng.poisson(0.22 * auth / 20 * season))\n",
" for s_ in shocks:\n",
" if s_[0] == u and (s_[1] is None or s_[1] == j) and w >= s_[2]:\n",
" out += int(rng.poisson(s_[3]))\n",
" inflow = int(rng.poisson(max(0.0, 0.2 * auth / 20 * (1 + net * 10)))) + (int(rng.integers(2, 6) * auth / 25) if w % (rot // 7) == 0 else 0)\n",
" present = int(np.clip(present - out + inflow, int(auth * 0.4), auth))\n",
" leave = int(rng.binomial(present, 0.05 + 0.05 * (abs(w % 52 - 31) < 3 or w % 52 >= 50)))\n",
" sick = int(rng.poisson(0.025 * present)); train = int(rng.binomial(present, 0.04 + 0.06 * (w % 13 == 6)))\n",
" avail = max(present - leave - sick - train, 1)\n",
" night = float(np.clip(rng.normal(0.25 + 0.05 * (j == \"์์ \") + 0.05 * (j == \"์๋ฌด\"), 0.05), 0.05, 0.6))\n",
" ot = float(np.clip(auth * 40 * work / avail - 40 + rng.normal(0, 1.5), 0, 40))\n",
" vac = auth - present\n",
" absent = (leave + sick + train) / max(present, 1)\n",
" fat = 100 * (0.38 * min(vac / auth / 0.25, 1.2) + 0.34 * min(ot / 20, 1.2) + 0.10 * min(night / 0.4, 1.2) + 0.18 * min(absent / 0.2, 1.2)) + rng.normal(0, 5)\n",
" rows.append((u, j, w, auth, present, vac, leave, sick, train, inflow, out, round(ot, 1), round(night, 2), rot, int(fat >= HI_CUT)))\n",
" return pd.DataFrame(rows, columns=FIELDS)\n",
"\n",
"\n",
"def cell_pivot(df, col=\"vacancy\"):\n",
" \"\"\"์ฃผ x (๋ถ๋,์ง๋ฌด) ํ.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 1] ๋ถ๋ ์ธ์ฌ/ํธ์ ๋ฐ์ดํฐ(ํ์ฌ์, ๊ฒฐ์, ํด๊ฐ, ํ์, ๊ต๋์ฃผ๊ธฐ) ๋ฐ์ดํฐ์
์คํค๋ง ์ค๊ณ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"SCHEMA = {\n",
" \"unit\": {\"type\": \"string\", \"enum\": UNITS, \"unit\": \"-\", \"desc\": \"์์ ๊ฐ์ ๋๋\", \"source\": \"์ธ์ฌ ํธ์ ์ง๊ณ\"},\n",
" \"job\": {\"type\": \"string\", \"enum\": JOBS, \"unit\": \"-\", \"desc\": \"์ง๋ฌด ๊ตฌ๋ถ(๊ฐ์)\", \"source\": \"์ธ์ฌ ํธ์ ์ง๊ณ\"},\n",
" \"week\": {\"type\": \"integer\", \"min\": 1, \"max\": 156, \"unit\": \"์ฃผ\", \"desc\": \"๊ธฐ์ค ์ฃผ์ฐจ\", \"source\": \"์ฃผ๊ฐ ์ธ์ฌ ๋ณด๊ณ \"},\n",
" \"authorized\": {\"type\": \"integer\", \"min\": 1, \"max\": 200, \"unit\": \"๋ช
\", \"desc\": \"ํธ์ ์ธ์\", \"source\": \"์ธ์ฌ ํธ์ ์ง๊ณ\"},\n",
" \"present\": {\"type\": \"integer\", \"min\": 0, \"max\": 200, \"unit\": \"๋ช
\", \"desc\": \"ํ์ฌ์\", \"source\": \"์ฃผ๊ฐ ์ธ์ฌ ๋ณด๊ณ \"},\n",
" \"vacancy\": {\"type\": \"integer\", \"min\": 0, \"max\": 200, \"unit\": \"๋ช
\", \"desc\": \"๊ฒฐ์ = ํธ์ - ํ์ฌ์\", \"source\": \"๊ณ์ฐ\"},\n",
" \"leave\": {\"type\": \"integer\", \"min\": 0, \"max\": 200, \"unit\": \"๋ช
\", \"desc\": \"ํด๊ฐ ์ธ์\", \"source\": \"ํด๊ฐ ํํฉ\"},\n",
" \"sick\": {\"type\": \"integer\", \"min\": 0, \"max\": 200, \"unit\": \"๋ช
\", \"desc\": \"ํ์(์ง๋ฃยท์
์) ์ธ์, ๊ฐ์ธ ์๋ณ ์์ด ์๋ง ์ง๊ณ\", \"source\": \"์๋ฌด ์ง๊ณ\"},\n",
" \"training\": {\"type\": \"integer\", \"min\": 0, \"max\": 200, \"unit\": \"๋ช
\", \"desc\": \"๊ต์กยทํ๊ฒฌ ์ธ์\", \"source\": \"๊ต์ก ๊ณํ\"},\n",
" \"inflow\": {\"type\": \"integer\", \"min\": 0, \"max\": 100, \"unit\": \"๋ช
\", \"desc\": \"์ฃผ๊ฐ ๋ณด์ถฉ(์ ์
) ์ธ์\", \"source\": \"์ธ์ฌ ๋ช
๋ น ์ง๊ณ\"},\n",
" \"outflow\": {\"type\": \"integer\", \"min\": 0, \"max\": 100, \"unit\": \"๋ช
\", \"desc\": \"์ฃผ๊ฐ ์ ์ถยท์ ์ญ ์ธ์\", \"source\": \"์ธ์ฌ ๋ช
๋ น ์ง๊ณ\"},\n",
" \"overtime_h\": {\"type\": \"number\", \"min\": 0, \"max\": 80, \"unit\": \"์๊ฐ/์ธ/์ฃผ\", \"desc\": \"๊ฐ์ฉ ์ธ์ 1๋ช
๋น ์ฃผ๊ฐ ์ด๊ณผ๊ทผ๋ฌด ์๊ฐ\", \"source\": \"๊ทผ๋ฌด์ผ์ง ์ง๊ณ\"},\n",
" \"night_ratio\": {\"type\": \"number\", \"min\": 0, \"max\": 1, \"unit\": \"๋น์จ\", \"desc\": \"์ผ๊ฐ ๊ทผ๋ฌด ๋น์จ\", \"source\": \"๊ทผ๋ฌด์ผ์ง ์ง๊ณ\"},\n",
" \"rotation_days\": {\"type\": \"integer\", \"min\": 7, \"max\": 365, \"unit\": \"์ผ\", \"desc\": \"๊ต๋ยท๋ณด์ถฉ ์ฃผ๊ธฐ\", \"source\": \"์ธ์ฌ ๊ณํ\"},\n",
" \"fatigue_label\": {\"type\": \"integer\", \"enum\": [0, 1], \"unit\": \"-\", \"desc\": \"๊ณ ํผ๋ก ์ฌ๋ถ(๊ฒ์ฆ์ฉ ์ ๋ต ๋ผ๋ฒจ, ๊ฐ์)\", \"source\": \"๊ฐ์ ํ๊ฐ ๊ธฐ์ค\"},\n",
"}\n",
"print(f\"[์คํค๋ง] ํ๋ {len(SCHEMA)}๊ฐ, ํ์ ์์ฑ(typeยทunitยทdescยทsource) ๋ชจ๋ ์ ์๋ ํ๋ {sum(all(k in v for k in ('type', 'unit', 'desc', 'source')) for v in SCHEMA.values())}๊ฐ\")\n",
"assert list(SCHEMA) == FIELDS\n",
"PII = re.compile(r\"name|์ด๋ฆ|๊ตฐ๋ฒ|์ฃผ๋ฏผ|rrn|phone|์ ํ|address|์ฃผ์\", re.I)\n",
"print(f\"๊ฐ์ธ ์๋ณ ํ๋ {sum(bool(PII.search(k + v['desc'])) for k, v in SCHEMA.items())}๊ฐ(์ง๊ณ ์ธ์ ์๋ง ์ฌ์ฉ)\")\n",
"\n",
"\n",
"def check(rec):\n",
" # [TODO : ํ์ ์ค์ต 1] ๋ถ๋ ์ธ์ฌ/ํธ์ ๋ฐ์ดํฐ(ํ์ฌ์, ๊ฒฐ์, ํด๊ฐ, ํ์, ๊ต๋์ฃผ๊ธฐ) ๋ฐ์ดํฐ์
์คํค๋ง ์ค๊ณ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"df = make_manning(7, 104)\n",
"sample = df[df[\"week\"].isin([10, 50])].to_dict(\"records\") # 24๊ฐ ์
x 2์ฃผ = 48๊ฑด + ์ ์ฒด ์ ๊ฒ\n",
"recs = df.to_dict(\"records\")\n",
"bad_all = sum(bool(check(r)) for r in recs)\n",
"bad_sample = sum(bool(check(r)) for r in sample)\n",
"print(f\"[์ ์ ์๋ฃ ๊ฒ์ฆ] ํ๋ณธ {len(sample)}๊ฑด ์ค๋ฅ {bad_sample}๊ฑด, ์ ์ฒด {len(recs)}๊ฑด ์ค๋ฅ {bad_all}๊ฑด (๋ชฉํ ์ค๋ฅ 0๊ฑด: {'๋ฌ์ฑ' if bad_all == 0 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# 2) ์ค๋ฅ ์ฃผ์
์ํ\n",
"r0 = recs[100]\n",
"inj = {\"์๋ฃํ ์ค๋ฅ\": dict(r0, authorized=\"์๋ฅธ\"), \"ํ์ฉ๊ฐ ์ค๋ฅ\": dict(r0, job=\"๋ฏธ์์ง๋ฌด\"), \"๋ฒ์ ์ค๋ฅ(์์ ์ด๊ณผ๊ทผ๋ฌด)\": dict(r0, overtime_h=-4.0), \"๋
ผ๋ฆฌ ์ค๋ฅ(๊ฒฐ์ ๋ถ์ผ์น)\": dict(r0, vacancy=r0[\"vacancy\"] + 3),\n",
" \"๋
ผ๋ฆฌ ์ค๋ฅ(ํด๊ฐ+ํ์+๊ต์ก>ํ์ฌ์)\": dict(r0, leave=r0[\"present\"] + 1), \"ํ๋ ๋๋ฝ\": {k: v for k, v in r0.items() if k != \"night_ratio\"}, \"๊ฒฐ์ธก๊ฐ\": dict(r0, present=np.nan),\n",
" \"๋น์จ ๋ฒ์ ์ด๊ณผ\": dict(r0, night_ratio=1.4)}\n",
"print(\"\\n[์ค๋ฅ ์ฃผ์
์ํ]\")\n",
"caught = 0\n",
"for name, rec in inj.items():\n",
" e = check(rec); caught += bool(e); print(f\" {name:22s}-> {'๊ฒ์ถ' if e else '๋ฏธ๊ฒ์ถ'}: {e[0] if e else ''}\")\n",
"print(f\"์ฃผ์
์ค๋ฅ {len(inj)}์ข
์ค {caught}์ข
๊ฒ์ถ\")\n",
"\n",
"# 3) ์๋ฃ ๊ตฌ์กฐ ์์ฝ๊ณผ ์
์ถ๋ ฅ ๋ช
์ธ\n",
"print(\"\\n[์๋ฃ ๊ตฌ์กฐ ์์ฝ] ์
(๋ถ๋ x ์ง๋ฌด) %d๊ฐ x 104์ฃผ = %dํ\" % (df.groupby([\"unit\", \"job\"]).ngroups, len(df)))\n",
"s = df.assign(vr=df[\"vacancy\"] / df[\"authorized\"]).groupby(\"job\").agg(ํ๊ท ํธ์ =(\"authorized\", \"mean\"), ๊ฒฐ์์จ=(\"vr\", \"mean\"), ํ๊ท ์ด๊ณผ๊ทผ๋ฌด=(\"overtime_h\", \"mean\"), ๊ณ ํผ๋ก๋น์จ=(\"fatigue_label\", \"mean\"))\n",
"print(s.round(2).to_string())\n",
"js = json.dumps({\"title\": \"adm01_input_schema\", \"fields\": {k: {kk: vv for kk, vv in v.items() if kk != \"desc\"} for k, v in list(SCHEMA.items())[:3]}}, ensure_ascii=False)\n",
"print(\"์คํค๋ง JSON(์ 3๊ฐ ํ๋):\", js[:150], \"...\")\n",
"print(\"[์
์ถ๋ ฅ] ์
๋ ฅ: ์ฃผ๊ฐ ์ง๋ฌด๋ณ ์ธ๋ ฅ ์ง๊ณ 15ํ๋ / ์ถ๋ ฅ: ๊ฒฐ์ ์์ธก, ํผ๋ก๋ยท์ํ ์ง์, ๋ณด์ถฉ ํ์๋ ์ถ์ฒ, ๊ฒํ ยท๊ฒฐ์ฌ ์ด๋ ฅ\")\n",
"print(\"โป ๊ฐ์ ์ง๊ณ ์๋ฃ์ด๋ฉฐ ๊ฐ์ธ ๋จ์ ์ ๋ณด๋ ๋ค๋ฃจ์ง ์๋๋ค. ์ค์ ์ธ์ฌ ์๋ฃ์ ์ธ๋ถ AI ์๋น์ค ์
๋ ฅ์ ๊ธ์ง๋๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(10, 4))\n",
"v = df.assign(vr=df[\"vacancy\"] / df[\"authorized\"] * 100).groupby(\"week\")[\"vr\"].mean(); ax[0].plot(v.index, v.values); ax[0].set_title(\"Mean vacancy rate by week (%, synthetic)\"); ax[0].set_xlabel(\"week\")\n",
"ax[1].bar([str(i) for i in range(len(s))], s[\"๊ณ ํผ๋ก๋น์จ\"] * 100, color=\"tab:orange\"); ax[1].set_title(\"High-fatigue rate by job (index 0-5, %)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
01 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[์คํค๋ง] ํ๋ 15๊ฐ, ํ์ ์์ฑ(typeยทunitยทdescยทsource) ๋ชจ๋ ์ ์๋ ํ๋ 15๊ฐ\n",
"๊ฐ์ธ ์๋ณ ํ๋ 0๊ฐ(์ง๊ณ ์ธ์ ์๋ง ์ฌ์ฉ)\n",
"[์ ์ ์๋ฃ ๊ฒ์ฆ] ํ๋ณธ 48๊ฑด ์ค๋ฅ 0๊ฑด, ์ ์ฒด 2496๊ฑด ์ค๋ฅ 0๊ฑด (๋ชฉํ ์ค๋ฅ 0๊ฑด: ๋ฌ์ฑ)\n",
"\n",
"[์ค๋ฅ ์ฃผ์
์ํ]\n",
" ์๋ฃํ ์ค๋ฅ -> ๊ฒ์ถ: authorized: ์๋ฃํ ์ค๋ฅ(str)\n",
" ํ์ฉ๊ฐ ์ค๋ฅ -> ๊ฒ์ถ: job: ํ์ฉ๋์ง ์์ ๊ฐ '๋ฏธ์์ง๋ฌด'\n",
" ๋ฒ์ ์ค๋ฅ(์์ ์ด๊ณผ๊ทผ๋ฌด) -> ๊ฒ์ถ: overtime_h: ์ต์๊ฐ 0 ๋ฏธ๋ง\n",
" ๋
ผ๋ฆฌ ์ค๋ฅ(๊ฒฐ์ ๋ถ์ผ์น) -> ๊ฒ์ถ: vacancy: ํธ์ - ํ์ฌ์๊ณผ ๋ถ์ผ์น\n",
" ๋
ผ๋ฆฌ ์ค๋ฅ(ํด๊ฐ+ํ์+๊ต์ก>ํ์ฌ์) -> ๊ฒ์ถ: leave+sick+training: ํ์ฌ์ ์ด๊ณผ\n",
" ํ๋ ๋๋ฝ -> ๊ฒ์ถ: night_ratio: ํ์ ๊ฐ ๋๋ฝ\n",
" ๊ฒฐ์ธก๊ฐ -> ๊ฒ์ถ: present: ํ์ ๊ฐ ๋๋ฝ\n",
" ๋น์จ ๋ฒ์ ์ด๊ณผ -> ๊ฒ์ถ: night_ratio: ์ต๋๊ฐ 1 ์ด๊ณผ\n",
"์ฃผ์
์ค๋ฅ 8์ข
์ค 8์ข
๊ฒ์ถ\n",
"\n",
"[์๋ฃ ๊ตฌ์กฐ ์์ฝ] ์
(๋ถ๋ x ์ง๋ฌด) 24๊ฐ x 104์ฃผ = 2496ํ\n",
" ํ๊ท ํธ์ ๊ฒฐ์์จ ํ๊ท ์ด๊ณผ๊ทผ๋ฌด ๊ณ ํผ๋ก๋น์จ\n",
"job \n",
"๋ณด๊ธ 28.00 0.18 18.81 0.52\n",
"์๋ฌด 29.25 0.07 8.31 0.25\n",
"์์ 35.75 0.06 12.69 0.34\n",
"์ ๋น 31.50 0.09 9.95 0.35\n",
"ํต์ 25.75 0.14 16.21 0.50\n",
"ํ์ 32.75 0.06 9.82 0.29\n",
"์คํค๋ง JSON(์ 3๊ฐ ํ๋): {\"title\": \"adm01_input_schema\", \"fields\": {\"unit\": {\"type\": \"string\", \"enum\": [\"๊ฐ์A๋๋\", \"๊ฐ์B๋๋\", \"๊ฐ์C๋๋\", \"๊ฐ์D๋๋\"], \"unit\": \"-\", \"source\": \"์ธ์ฌ ํธ์ ์ง๊ณ\"} ...\n",
"[์
์ถ๋ ฅ] ์
๋ ฅ: ์ฃผ๊ฐ ์ง๋ฌด๋ณ ์ธ๋ ฅ ์ง๊ณ 15ํ๋ / ์ถ๋ ฅ: ๊ฒฐ์ ์์ธก, ํผ๋ก๋ยท์ํ ์ง์, ๋ณด์ถฉ ํ์๋ ์ถ์ฒ, ๊ฒํ ยท๊ฒฐ์ฌ ์ด๋ ฅ\n",
"โป ๊ฐ์ ์ง๊ณ ์๋ฃ์ด๋ฉฐ ๊ฐ์ธ ๋จ์ ์ ๋ณด๋ ๋ค๋ฃจ์ง ์๋๋ค. ์ค์ ์ธ์ฌ ์๋ฃ์ ์ธ๋ถ AI ์๋น์ค ์
๋ ฅ์ ๊ธ์ง๋๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
02. ๋น์ ํ ๊ทผ๋ฌด์ผ์ง ํ์ฑ, ๊ฒฐ์ ์ฌ์ ๋ถ๋ฅ ๋ฐ ๊ทผ๋ฌด ์๊ฐ ์ ๊ทํ ์ ์ฒ๋ฆฌ ๋ชจ๋ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๊ทผ๋ฌด์ผ์ง ํ์ฑ, ๊ฒฐ์ ์ฌ์ ๋ถ๋ฅ ๋ฐ ๊ทผ๋ฌด ์๊ฐ ์ ๊ทํ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์์์ด ์ ๊ฐ๊ฐ์ธ ๋น์ ํ ๊ทผ๋ฌด์ผ์ง๋ฅผ ํ์ฑํด ๊ฒฐ์ ์ฌ์ ๋ฅผ ๋ถ๋ฅํ๊ณ ๊ทผ๋ฌด ์๊ฐ์ ์๊ฐ ๋จ์๋ก ์ ๊ทํํ๋ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์๊ฐ ํ๊ธฐ 5์ข
('12์๊ฐ 30๋ถ', '8h', '22:00~06:00', '์คํ 2์~์ต์ผ ์ค์ 6์', '์ด 9์๊ฐ')์ด ์์ธ ๊ฐ์ ๊ทผ๋ฌด์ผ์ง 6,000์ค์์ ๋ถ๋ยท์ง๋ฌดยท๊ทผ๋ฌด ์๊ฐยท์ํฉยท๊ฒฐ์ ์ฌ์ (ํด๊ฐยทํ์ยท๊ต์กยทํ๊ฒฌยท์ ์ถยท๊ธฐํ)๋ฅผ ์ ๊ท์๊ณผ ํค์๋๋ก ๋ฝ๋๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/preprocess/adm01_preprocessor.py ๋ฐ ์ ์ฒ๋ฆฌ ์ ํ ๋ฐ์ดํฐ์
` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ ์ ํ๊ฒฝ ํน์ ์ ๋
ธ์ด์ฆ(์คํธํ, ๋ฐ์ฌํ, ์ผ์ ๋จ์ ๋ฑ) ์ ๊ฑฐ ํ๋น์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๋ฐ์ดํฐ ์ ์ ์ฒ๋ฆฌ ์๋ 1,000๊ฑด/์ด ์ด์, ์ ์ฒ๋ฆฌ ์ค๋ฅ์จ 0%\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์ ์ ํ์ดํ๋ผ์ธ ์๋ํ ๋ฐ ๋จ์ ํ
์คํธ ์ผ์ด์ค 5์ข
ํต๊ณผ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: pytest ๊ธฐ๋ฐ ์ ์ฒ๋ฆฌ ์ ํฉ์ฑ ํ
์คํธ ๋ฐ ๊ฒฐ์ธก์น ๋ณด์ ์จ ํ์ธ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์์์ด ์ ๊ฐ๊ฐ์ธ ๊ฐ์ ๊ทผ๋ฌด์ผ์ง ๋ฌธ์ฅ 6,000์ค(์๊ฐ ํ๊ธฐ 5์ข
ยท๊ฒฐ์ ์ฌ์ ํํ ๋ค์)์ ์ ๊ท์์ผ๋ก ํ์ฑํด ๋ถ๋ยท์ง๋ฌดยท๊ทผ๋ฌด ์๊ฐ(์๊ฐ ๋จ์)ยท์ฃผ์ผยท๊ฒฐ์ ์ฌ์ ๋ฅผ ํ์ค ํ๋๋ก ๋ง๋ค๊ณ , ์ฃผ๊ฐยท์ผ๊ฐยทํ๋ จ์ฃผ๊ฐ๋ณ ์๊ณ๊ฐ์ ์๋ฃ๋ก ์กฐ์ ํด ์ก์(์ด์ ๊ฐยท์ค๋ณตยท์๋ฆฐ ์ค)์ ๊ฑธ๋ฌ๋ด๋ฉฐ ์ฒ๋ฆฌ ์๋(๊ฑด/์ด)์ ์ค๋ฅ์จ์ ์ธก์ ํ๋ค.\"\"\"\n",
"import re\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"UNITS = [\"๊ฐ์A๋๋\", \"๊ฐ์B๋๋\", \"๊ฐ์C๋๋\", \"๊ฐ์D๋๋\"]\n",
"JOBS = [\"์์ \", \"ํต์ \", \"์ ๋น\", \"๋ณด๊ธ\", \"ํ์ \", \"์๋ฌด\"]\n",
"REASONS = {\"ํด๊ฐ\": [\"ํด๊ฐ\", \"์ฐ๊ฐ\", \"์ธ๋ฐ\"], \"ํ์\": [\"ํ์\", \"์ง๋ฃ\", \"์
์\", \"๋ณ๊ฐ\"], \"๊ต์กยทํ๊ฒฌ\": [\"๊ต์ก\", \"ํ๊ฒฌ\", \"ํ๋ จ ์ฐธ๊ฐ\", \"ํ๊ต\"], \"์ ์ถ\": [\"์ ์ถ\", \"์ ์ญ\", \"์ ๋ณด\"]}\n",
"SAY = {\"ํด๊ฐ\": [\"ํด๊ฐ\", \"์ฐ๊ฐ ์ฌ์ฉ\", \"์ธ๋ฐ\"], \"ํ์\": [\"ํ์ ๋ฐ์\", \"์ง๋ฃ\", \"๋ณ๊ฐ\", \"์
์\"], \"๊ต์กยทํ๊ฒฌ\": [\"๊ต์ก ์
๊ต\", \"ํ๊ฒฌ\", \"ํ๊ต ๊ต์ก\"], \"์ ์ถ\": [\"์ ์ถ\", \"์ ์ญ ์์ \", \"์ ๋ณด\"], \"๊ธฐํ\": [\"์ฌ์ ๋ถ๋ช
\", \"ํ์ธ ์ค\", \"-\"]}\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ทผ๋ฌด์ผ์ง ์์ฑ(์ค์ ๋ถ๋ยท์ธ์ ์๋). ์ค๋ง๋ค ์ ๋ต(truth)์ ํจ๊ป ๋ง๋ค์ด ํ์ฑ ์ ํ๋๋ฅผ ์ ๊ฒํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"rng = np.random.default_rng(12)\n",
"\n",
"\n",
"def make_logs(n=6000, noise=0.03):\n",
" lines, truth = [], []\n",
" for i in range(n):\n",
" u, j = UNITS[rng.integers(4)], JOBS[rng.integers(6)]\n",
" ctx = rng.choice([\"ํ์\", \"์ผ๊ฐ\", \"ํ๋ จ์ฃผ๊ฐ\"], p=[.55, .3, .15])\n",
" base = {\"ํ์\": 9, \"์ผ๊ฐ\": 11, \"ํ๋ จ์ฃผ๊ฐ\": 15}[ctx]\n",
" h = round(float(np.clip(rng.normal(base, 1.3), 4, 20)) * 2) / 2\n",
" reason = rng.choice(list(SAY), p=[.3, .2, .2, .15, .15])\n",
" say = rng.choice(SAY[reason])\n",
" fmt = i % 5\n",
" us = u if rng.random() < .6 else u.replace(\"๊ฐ์\", \"\")\n",
" if fmt == 0:\n",
" t = f\"[๊ทผ๋ฌด์ผ์ง] 2026-{rng.integers(1, 13):02d}-{rng.integers(1, 28):02d} {us} {j} ๊ทผ๋ฌด {int(h)}์๊ฐ{'30๋ถ' if h % 1 else ''}({ctx}) ๊ฒฐ์์ฌ์ : {say}\"\n",
" elif fmt == 1:\n",
" t = f\"{us}/{j}/W{rng.integers(1, 53)} ๊ทผ๋ฌด {h:g}h {ctx} / ์ฌ์ {say}\"\n",
" elif fmt == 2:\n",
" st = int(rng.integers(0, 24)); en = (st + int(h)) % 24\n",
" t = f\"{us} {j} ๊ทผ๋ฌด {st:02d}:00~{en:02d}:{'30' if h % 1 else '00'} [{ctx}] ๊ฒฐ์: {say}\"\n",
" h = int(h) + (0.5 if h % 1 else 0)\n",
" elif fmt == 3:\n",
" st = int(rng.integers(1, 12)); ap = rng.choice([\"์ค์ \", \"์คํ\"]); total = st + (12 if ap == \"์คํ\" else 0) + int(h); en = total % 24\n",
" ap2, en12 = (\"์คํ\" if en >= 12 else \"์ค์ \"), (en % 12 or 12)\n",
" t = f\"{us} {j} {ap} {st}์~{'์ต์ผ ' if total >= 24 else ''}{ap2} {en12}์ ๊ทผ๋ฌด({ctx}), ๊ฒฐ์ ์์ธ {say}\"\n",
" h = float(int(h))\n",
" else:\n",
" t = f\"{us} {j} {ctx} ๊ทผ๋ฌด ์ด {h:g}์๊ฐ ์ฌ์ ={say}\"\n",
" truth.append((u, j, h, ctx, reason))\n",
" lines.append(t)\n",
" k = int(n * noise)\n",
" for idx in rng.choice(n, k, replace=False): # ์ก์ ์ฃผ์
: ์ด์ ๊ฐ(99์๊ฐยท0.5์๊ฐ)ยท์๋ฆฐ ์ค\n",
" has_h = bool(re.search(r\"\\d(?:์๊ฐ|h\\b)\", lines[idx]))\n",
" if idx % 3 == 0 and has_h:\n",
" lines[idx] = re.sub(r\"\\d+(?:\\.\\d+)?(?=\\s*(?:์๊ฐ|h\\b))\", \"99\", lines[idx], count=1); truth[idx] = None\n",
" elif idx % 3 == 1 and has_h:\n",
" lines[idx] = re.sub(r\"\\d+(?:\\.\\d+)?(?=\\s*(?:์๊ฐ|h\\b))\", \"0.5\", lines[idx], count=1); truth[idx] = None\n",
" else:\n",
" lines[idx] = lines[idx][: len(lines[idx]) // 3]; truth[idx] = None\n",
" return lines, truth\n",
"\n",
"\n",
"lines, truth = make_logs()\n",
"lines += lines[:60]; truth += truth[:60] # ์ค๋ณต ์ค 60๊ฐ ์ถ๊ฐ\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# ํ์: ๋ถ๋ยท์ง๋ฌดยท๊ทผ๋ฌด ์๊ฐยท์ํฉยท์ฌ์ ๋ฅผ ์ค์์ ๋ฝ๋๋ค\n",
"# ---------------------------------------------------------------------------\n",
"RE_UNIT = re.compile(r\"(?:๊ฐ์)?([A-D])๋๋\")\n",
"RE_JOB = re.compile(\"|\".join(JOBS))\n",
"RE_HM = re.compile(r\"(\\d+(?:\\.\\d+)?)\\s*์๊ฐ(?:\\s*(\\d+)\\s*๋ถ)?\")\n",
"RE_H = re.compile(r\"(\\d+(?:\\.\\d+)?)\\s*h\\b\")\n",
"RE_CLK = re.compile(r\"(\\d{1,2}):(\\d{2})\\s*~\\s*(\\d{1,2}):(\\d{2})\")\n",
"RE_KO = re.compile(r\"(์ค์ |์คํ)\\s*(\\d{1,2})์\\s*~\\s*(?:์ต์ผ\\s*)?(์ค์ |์คํ)\\s*(\\d{1,2})์\")\n",
"RE_CTX = re.compile(r\"ํ๋ จ์ฃผ๊ฐ|์ผ๊ฐ|ํ์\")\n",
"\n",
"\n",
"def parse_hours(t):\n",
" # [TODO : ํ์ ์ค์ต 2] ๋น์ ํ ๊ทผ๋ฌด์ผ์ง ํ์ฑ, ๊ฒฐ์ ์ฌ์ ๋ถ๋ฅ ๋ฐ ๊ทผ๋ฌด ์๊ฐ ์ ๊ทํ ์ ์ฒ๋ฆฌ ๋ชจ๋ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def classify(t):\n",
" # [TODO : ํ์ ์ค์ต 2] ๋น์ ํ ๊ทผ๋ฌด์ผ์ง ํ์ฑ, ๊ฒฐ์ ์ฌ์ ๋ถ๋ฅ ๋ฐ ๊ทผ๋ฌด ์๊ฐ ์ ๊ทํ ์ ์ฒ๋ฆฌ ๋ชจ๋ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def parse(t):\n",
" u, j, c = RE_UNIT.search(t), RE_JOB.search(t), RE_CTX.search(t)\n",
" if not (u and j and c):\n",
" raise ValueError(\"ํ์ ํญ๋ชฉ(๋ถ๋ยท์ง๋ฌดยท์ํฉ) ์ถ์ถ ์คํจ\")\n",
" return {\"unit\": f\"๊ฐ์{u[1]}๋๋\", \"job\": j[0], \"hours\": parse_hours(t), \"ctx\": c[0], \"reason\": classify(t)}\n",
"\n",
"\n",
"# 1) ํ์ฑ + ์ฒ๋ฆฌ ์๋\n",
"t0 = time.perf_counter()\n",
"ok, bad = [], []\n",
"for i, t in enumerate(lines):\n",
" try:\n",
" ok.append({**parse(t), \"i\": i})\n",
" except ValueError as e:\n",
" bad.append((i, str(e)))\n",
"dt = time.perf_counter() - t0\n",
"df = pd.DataFrame(ok)\n",
"print(f\"[ํ์ฑ] {len(lines)}์ค ์ค ์ฑ๊ณต {len(df)}์ค, ํด์ ์คํจ(์๋ฆฐ ์ค) {len(bad)}์ค -> ๊ฒฉ๋ฆฌ / ์๋ {len(lines) / dt:,.0f}๊ฑด/์ด (๋ชฉํ 1,000๊ฑด/์ด ์ด์: {'๋ฌ์ฑ' if len(lines) / dt >= 1000 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# 2) ์ค๋ณต ์ ๋ฆฌ์ ์ก์ ์ ๊ฑฐ: ์ํฉ๋ณ(ํ์ยท์ผ๊ฐยทํ๋ จ์ฃผ๊ฐ) ์๊ณ๊ฐ์ ์๋ฃ์ ์ค์๊ฐยฑMAD๋ก ์กฐ์ \n",
"n0 = len(df)\n",
"df = df[~pd.Series([lines[i] for i in df[\"i\"]]).duplicated().to_numpy()]\n",
"print(f\"์ค๋ณต ์ค {n0 - len(df)}๊ฐ ์ ๊ฑฐ -> {len(df)}์ค\")\n",
"FIXED = 14.0 # ๊ณ ์ ์๊ณ๊ฐ(๋ชจ๋ ์ํฉ ๊ณตํต): 14์๊ฐ ์ด๊ณผ๋ ์ด์\n",
"fixed_flag = df[\"hours\"] > FIXED\n",
"thr = {}\n",
"for c, g in df[df[\"hours\"] < 30].groupby(\"ctx\"):\n",
" med = g[\"hours\"].median(); mad = (g[\"hours\"] - med).abs().median() * 1.4826\n",
" thr[c] = (med - 4 * mad, med + 4 * mad)\n",
"dyn_flag = pd.Series([not (thr[c][0] <= h <= thr[c][1]) for c, h in zip(df[\"ctx\"], df[\"hours\"])], index=df.index) | (df[\"hours\"] >= 30)\n",
"print(\"์ํฉ๋ณ ๋์ ์๊ณ๊ฐ(์๊ฐ):\", \", \".join(f\"{c} {lo:.1f}~{hi:.1f}\" for c, (lo, hi) in thr.items()))\n",
"tr = [truth[i] for i in df[\"i\"]]\n",
"is_noise = np.array([t is None for t in tr])\n",
"for name, fl in [(\"๊ณ ์ ์๊ณ๊ฐ(14์๊ฐ)\", fixed_flag.to_numpy()), (\"์ํฉ๋ณ ๋์ ์๊ณ๊ฐ\", dyn_flag.to_numpy())]:\n",
" tp = int((fl & is_noise).sum()); fp = int((fl & ~is_noise).sum())\n",
" print(f\" {name:16s}: ์ด์ ํ์ {int(fl.sum())}๊ฑด, ์ค์ ์ก์ ์ค {tp}/{int(is_noise.sum())} ๊ฒ์ถ, ์ ์์ธ๋ฐ ์ด์ ํ์ (์คํ) {fp}๊ฑด\")\n",
"\n",
"# 3) ํ์ฑ ์ ํ๋(์ ๋ต๊ณผ ๋น๊ต): ์ก์์ด ์๋ ์ค์์ ์๊ฐยท์ฌ์ ยท๋ถ๋ยท์ง๋ฌด ์ผ์น์จ\n",
"clean = df[~dyn_flag].copy()\n",
"tc = [truth[i] for i in clean[\"i\"]]\n",
"errs = sum((c[\"unit\"], c[\"job\"], c[\"ctx\"], c[\"reason\"]) != (t[0], t[1], t[3], t[4]) or abs(c[\"hours\"] - t[2]) > 0.01 for (_, c), t in zip(clean.iterrows(), tc) if t is not None)\n",
"n_eval = sum(t is not None for t in tc)\n",
"print(f\"\\n[์ ์ฒ๋ฆฌ ์ ํ๋] ์ ์ ํ {len(clean)}์ค(์ ๋ต ๋์กฐ {n_eval}์ค) ์ค ๋ถ์ผ์น {errs}์ค -> ์ ์ฒ๋ฆฌ ์ค๋ฅ์จ {errs / n_eval:.2%} (๋ชฉํ 0%: {'๋ฌ์ฑ' if errs == 0 else '๋ฏธ๋ฌ'})\")\n",
"print(\"[๊ฒฐ์ ์ฌ์ ๋ถ๋ฅ]\", clean[\"reason\"].value_counts().to_dict())\n",
"print(\"[๊ทผ๋ฌด ์๊ฐ ์ ๊ทํ] ์ํฉ๋ณ ํ๊ท ๊ทผ๋ฌด ์๊ฐ:\", clean.groupby(\"ctx\")[\"hours\"].mean().round(1).to_dict())\n",
"agg = clean.groupby([\"unit\", \"job\"]).agg(์ค์=(\"hours\", \"size\"), ํ๊ท ๊ทผ๋ฌด์๊ฐ=(\"hours\", \"mean\")).round(1)\n",
"print(\"[์ ์ฒ๋ฆฌ ํ ๋ถ๋ยท์ง๋ฌด ์ง๊ณ ์์]\"); print(agg.head(4).to_string())\n",
"print(\"์ฌ๋ ํ์ธ ๋์: ํด์ ์คํจ %d์ค(์: %r), ์ฌ์ '๊ธฐํ' %d์ค\" % (len(bad), lines[bad[0][0]][:24] if bad else \"\", int((clean[\"reason\"] == \"๊ธฐํ\").sum())))\n",
"print(\"โป ์๊ณ๊ฐ์ ์ํฉ๋ณ ์ค์๊ฐยฑ4MAD๋ก ์๋ฃ์์ ์กฐ์ ํ๋ฉฐ, ์ด์ ํ์ ๋ ์ค์ ์ญ์ ํ์ง ์๊ณ ๊ฒฉ๋ฆฌํด ๋ด๋น์๊ฐ ํ์ธํ๋ค.\")\n",
"\n",
"plt.figure(figsize=(8, 4))\n",
"for c, g in clean.groupby(\"ctx\"):\n",
" plt.hist(g[\"hours\"], bins=np.arange(2, 22, 1), alpha=.5, label={\"ํ์\": \"normal\", \"์ผ๊ฐ\": \"night\", \"ํ๋ จ์ฃผ๊ฐ\": \"training\"}[c])\n",
"plt.axvline(FIXED, color=\"k\", ls=\"--\"); plt.legend(); plt.xlabel(\"duty hours\"); plt.title(\"Normalized duty hours by context (dashed: fixed threshold)\"); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
02 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[ํ์ฑ] 6060์ค ์ค ์ฑ๊ณต 5953์ค, ํด์ ์คํจ(์๋ฆฐ ์ค) 107์ค -> ๊ฒฉ๋ฆฌ / ์๋ 56,271๊ฑด/์ด (๋ชฉํ 1,000๊ฑด/์ด ์ด์: ๋ฌ์ฑ)\n",
"์ค๋ณต ์ค 118๊ฐ ์ ๊ฑฐ -> 5835์ค\n",
"์ํฉ๋ณ ๋์ ์๊ณ๊ฐ(์๊ฐ): ์ผ๊ฐ 5.1~16.9, ํ์ 3.1~14.9, ํ๋ จ์ฃผ๊ฐ 9.1~20.9\n",
" ๊ณ ์ ์๊ณ๊ฐ(14์๊ฐ) : ์ด์ ํ์ 623๊ฑด, ์ค์ ์ก์ ์ค 38/75 ๊ฒ์ถ, ์ ์์ธ๋ฐ ์ด์ ํ์ (์คํ) 585๊ฑด\n",
" ์ํฉ๋ณ ๋์ ์๊ณ๊ฐ : ์ด์ ํ์ 75๊ฑด, ์ค์ ์ก์ ์ค 75/75 ๊ฒ์ถ, ์ ์์ธ๋ฐ ์ด์ ํ์ (์คํ) 0๊ฑด\n",
"\n",
"[์ ์ฒ๋ฆฌ ์ ํ๋] ์ ์ ํ 5760์ค(์ ๋ต ๋์กฐ 5760์ค) ์ค ๋ถ์ผ์น 0์ค -> ์ ์ฒ๋ฆฌ ์ค๋ฅ์จ 0.00% (๋ชฉํ 0%: ๋ฌ์ฑ)\n",
"[๊ฒฐ์ ์ฌ์ ๋ถ๋ฅ] {'ํด๊ฐ': 1719, 'ํ์': 1175, '๊ต์กยทํ๊ฒฌ': 1161, '์ ์ถ': 889, '๊ธฐํ': 816}\n",
"[๊ทผ๋ฌด ์๊ฐ ์ ๊ทํ] ์ํฉ๋ณ ํ๊ท ๊ทผ๋ฌด ์๊ฐ: {'์ผ๊ฐ': 11.0, 'ํ์': 8.9, 'ํ๋ จ์ฃผ๊ฐ': 14.9}\n",
"[์ ์ฒ๋ฆฌ ํ ๋ถ๋ยท์ง๋ฌด ์ง๊ณ ์์]\n",
" ์ค์ ํ๊ท ๊ทผ๋ฌด์๊ฐ\n",
"unit job \n",
"๊ฐ์A๋๋ ๋ณด๊ธ 226 10.4\n",
" ์๋ฌด 245 10.5\n",
" ์์ 235 10.3\n",
" ์ ๋น 239 10.1\n",
"์ฌ๋ ํ์ธ ๋์: ํด์ ์คํจ 107์ค(์: '๊ฐ์D๋๋ ํต์ ๊ทผ๋ฌด 2'), ์ฌ์ '๊ธฐํ' 816์ค\n",
"โป ์๊ณ๊ฐ์ ์ํฉ๋ณ ์ค์๊ฐยฑ4MAD๋ก ์๋ฃ์์ ์กฐ์ ํ๋ฉฐ, ์ด์ ํ์ ๋ ์ค์ ์ญ์ ํ์ง ์๊ณ ๊ฒฉ๋ฆฌํด ๋ด๋น์๊ฐ ํ์ธํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
03. ์ง๋ฌด๋ณ ๊ฒฐ์์จ ๋ฐ ์ด๊ณผ๊ทผ๋ฌด ์๊ฐ ๊ธฐ๋ฐ 1์ฐจ ํผ๋ก๋ ์ฐ์ถ Baseline ๋ชจ๋ธ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ง๋ฌด๋ณ ๊ฒฐ์์จ ๋ฐ ์ด๊ณผ๊ทผ๋ฌด ๊ธฐ๋ฐ ํผ๋ก๋ ์ฐ์ถ Baseline ๋ชจ๋ธ์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ง๋ฌด๋ณ ๊ฒฐ์์จ๊ณผ ์ด๊ณผ๊ทผ๋ฌด ์๊ฐ(์ผ๊ฐ ๊ทผ๋ฌด ๋น์จ ํฌํจ)์ผ๋ก 1์ฐจ ํผ๋ก๋ ์ ์๋ฅผ ๊ณ์ฐํ๋ Baseline ๋ชจ๋ธ์ ๋ง๋ค๊ณ ๊ฐ์ค์น๋ฅผ 1์ฐจ ํ๋ํด ์ ํ๋๋ฅผ ๊ฒ์ฆํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ฒฐ์์จยท์ด๊ณผ๊ทผ๋ฌดยท์ผ๊ฐ ๋น์จ์ ํ๋๋ก ๋๋์ด ๊ฐ์คํฉํ ํผ๋ก๋ ์ ์(0~100)๋ฅผ ์ ์ํ๊ณ , ์ 52์ฃผ๋ก ๊ฐ์ค์น์ ๊ธฐ์ค ์ ์๋ฅผ ๊ฒฉ์ ํ์ํ ๋ค ๋ค 52์ฃผ๋ก ์ ํ๋ยท์ ๋ฐ๋ยท์ฌํ์จยทF1์ ์ํ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/adm01_baseline_model.py ๋ฐ ๊ธฐ๋ณธ ์ถ๋ก ๊ฒฐ๊ณผ ๋ณด๊ณ ์` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์๊ณ ๋ฆฌ์ฆ์ ๋
ผ๋ฆฌ์ ํ๋ฆ ๋ฐ ๊ตฐ์ฌ์ ํ๋จ ๊ธฐ์ค์ ํฌ๋ช
์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๊ธฐ๋ณธ ๋ฐ์ดํฐ์
๋์ ํ์ง/์ฒ๋ฆฌ ์ ํ๋ 75% ์ด์ ๋ฌ์ฑ\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ๋จ์ ๊ธฐ๋ฅ ๋์ ํ์ธ ๋ฐ ๊ธฐ๋ณธ ์๋๋ฆฌ์ค ์คํ ๋ก๊ทธ ์์ฑ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ๊ธฐ์ค ๋ฒค์น๋งํฌ ๋ฐ์ดํฐ์
๋์ ์ถ๋ก ๊ฒฐ๊ณผ ์ค์ฐจ์จ ๋น๊ต\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์ง๋ฌด๋ณ ๊ฒฐ์์จยท์ด๊ณผ๊ทผ๋ฌด ์๊ฐยท์ผ๊ฐ ๊ทผ๋ฌด ๋น์จ๋ก 1์ฐจ ํผ๋ก๋ ์ ์(0~100)๋ฅผ ๊ณ์ฐํ๋ ๊ฐ์คํฉ Baseline์ ๋ง๋ค๊ณ , ์ด๊ธฐ ๊ฐ์ค์น์ ๊ฒฉ์ ํ์์ผ๋ก 1์ฐจ ํ๋ํ ๊ฐ์ค์น์ ์ ํ๋ยท์ ๋ฐ๋ยท์ฌํ์จยทํ๋จ ์ง์ฐ์๊ฐ์ ์ํ ๊ธฐ๊ฐ ์๋ฃ๋ก ๊ฒ์ฆํ๋ฉฐ ๋ถ๋ยท์ง๋ฌด ์
๋ณ ํผ๋ก๋ ์์์ ํ๋จ ๊ทผ๊ฑฐ๋ฅผ ์ถ๋ ฅํ๋ค.\"\"\"\n",
"import time\n",
"import itertools\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋ถ๋ ์ธ๋ ฅ ํํฉ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยทํธ์ ยท๋ณ๋ ฅยท๊ฐ์ธ ์ ๋ณด ์์)\n",
"# ๊ฐ์ ๋๋ 4๊ณณ x ์ง๋ฌด 6์ข
์ ์ฃผ๊ฐ ์ง๊ณ ์๋ฃ๋ง ๋ง๋ ๋ค. ๊ฐ์ธ ๋จ์ ์ ๋ณด(์ด๋ฆยท๊ตฐ๋ฒ ๋ฑ)๋ ๋ง๋ค์ง ์๋๋ค.\n",
"# ์ธ์์ ์ง๋ฌด๋ณ ์ง๊ณ ์์ด๋ฉฐ, ๊ฒฐ์ยทํด๊ฐยทํ์ยท๊ต์กยท๋ณด์ถฉยท์ ์ถ์ ๋ฐ์ํ ํ์ฌ์๊ณผ ์ด๊ณผ๊ทผ๋ฌด ์๊ฐ์ผ๋ก ๊ตฌ์ฑ๋๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"UNITS = [\"๊ฐ์A๋๋\", \"๊ฐ์B๋๋\", \"๊ฐ์C๋๋\", \"๊ฐ์D๋๋\"]\n",
"JOBS = [\"์์ \", \"ํต์ \", \"์ ๋น\", \"๋ณด๊ธ\", \"ํ์ \", \"์๋ฌด\"]\n",
"FIELDS = [\"unit\", \"job\", \"week\", \"authorized\", \"present\", \"vacancy\", \"leave\", \"sick\", \"training\", \"inflow\", \"outflow\", \"overtime_h\", \"night_ratio\", \"rotation_days\", \"fatigue_label\"]\n",
"HI_CUT = 55 # ๊ณ ํผ๋ก ํ์ ๊ธฐ์ค(๊ฐ์ ์ ์ 55 ์ด์)\n",
"\n",
"\n",
"def make_manning(seed=7, weeks=104, shock=None):\n",
" \"\"\"๋ถ๋ x ์ง๋ฌด 24๊ฐ ์
์ ์ฃผ๊ฐ ์ธ๋ ฅ ์๋ฃ. shock=(unit, job ๋๋ None(๋ถ๋ ์ ์ฒด), ์์์ฃผ, ์ฃผ๊ฐ ์ถ๊ฐ ์ ์ถ ํ๊ท ์) ํ ๊ฑด ๋๋ ๊ทธ ๋ชฉ๋ก์ด๋ฉด ๊ทธ ์
์ ๋๋ ๊ฒฐ์ ์์ธ์ ๋ฃ๋๋ค(AARยท์คํธ๋ ์ค์ฉ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" shocks = [shock] if shock and not isinstance(shock[0], tuple) else list(shock or [])\n",
" rows = []\n",
" for ui, u in enumerate(UNITS):\n",
" for ji, j in enumerate(JOBS):\n",
" auth = int(rng.integers(14, 42) * (0.9 + 0.06 * ui))\n",
" work = rng.uniform(0.92, 1.12) # ์ง๋ฌด๋ณ ์
๋ฌด๋ ๊ณ์\n",
" net = rng.uniform(-0.05, 0.03) # ๊ตฌ์กฐ์ ๋ณด์ถฉ ๋ถ์กฑ(์์)ยท๊ณผ์(์์) ๊ฒฝํฅ(์ฃผ๋น ์ธ์)\n",
" present = int(auth * rng.uniform(0.82, 0.98))\n",
" rot = int(rng.choice([60, 90, 120])) # ๊ต๋ยท๋ณด์ถฉ ์ฃผ๊ธฐ(์ผ)\n",
" for w in range(1, weeks + 1):\n",
" season = 1.0 + 0.9 * (abs(w % 52 - 22) < 4 or abs(w % 52 - 46) < 3) # ์ ์ถยท์ ์ญ ์ง์ค ์๊ธฐ\n",
" out = int(rng.poisson(0.22 * auth / 20 * season))\n",
" for s_ in shocks:\n",
" if s_[0] == u and (s_[1] is None or s_[1] == j) and w >= s_[2]:\n",
" out += int(rng.poisson(s_[3]))\n",
" inflow = int(rng.poisson(max(0.0, 0.2 * auth / 20 * (1 + net * 10)))) + (int(rng.integers(2, 6) * auth / 25) if w % (rot // 7) == 0 else 0)\n",
" present = int(np.clip(present - out + inflow, int(auth * 0.4), auth))\n",
" leave = int(rng.binomial(present, 0.05 + 0.05 * (abs(w % 52 - 31) < 3 or w % 52 >= 50)))\n",
" sick = int(rng.poisson(0.025 * present)); train = int(rng.binomial(present, 0.04 + 0.06 * (w % 13 == 6)))\n",
" avail = max(present - leave - sick - train, 1)\n",
" night = float(np.clip(rng.normal(0.25 + 0.05 * (j == \"์์ \") + 0.05 * (j == \"์๋ฌด\"), 0.05), 0.05, 0.6))\n",
" ot = float(np.clip(auth * 40 * work / avail - 40 + rng.normal(0, 1.5), 0, 40))\n",
" vac = auth - present\n",
" absent = (leave + sick + train) / max(present, 1)\n",
" fat = 100 * (0.38 * min(vac / auth / 0.25, 1.2) + 0.34 * min(ot / 20, 1.2) + 0.10 * min(night / 0.4, 1.2) + 0.18 * min(absent / 0.2, 1.2)) + rng.normal(0, 5)\n",
" rows.append((u, j, w, auth, present, vac, leave, sick, train, inflow, out, round(ot, 1), round(night, 2), rot, int(fat >= HI_CUT)))\n",
" return pd.DataFrame(rows, columns=FIELDS)\n",
"\n",
"\n",
"def cell_pivot(df, col=\"vacancy\"):\n",
" \"\"\"์ฃผ x (๋ถ๋,์ง๋ฌด) ํ.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 3] ์ง๋ฌด๋ณ ๊ฒฐ์์จ ๋ฐ ์ด๊ณผ๊ทผ๋ฌด ์๊ฐ ๊ธฐ๋ฐ 1์ฐจ ํผ๋ก๋ ์ฐ์ถ Baseline ๋ชจ๋ธ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"df = make_manning(7, 104)\n",
"df[\"vac_rate\"] = df[\"vacancy\"] / df[\"authorized\"]\n",
"tr, te = df[df[\"week\"] <= 52], df[df[\"week\"] > 52] # ์ 52์ฃผ ํ์ตยทํ๋ / ๋ค 52์ฃผ ์ํ\n",
"X = lambda d: np.column_stack([np.minimum(d[\"vac_rate\"] / 0.25, 1.2), np.minimum(d[\"overtime_h\"] / 20, 1.2), np.minimum(d[\"night_ratio\"] / 0.4, 1.2)])\n",
"\n",
"\n",
"def score(d, w):\n",
" \"\"\"ํผ๋ก๋ ์ ์ = 100 x (w1 x ๊ฒฐ์ ๋ถ๋ด + w2 x ์ด๊ณผ๊ทผ๋ฌด ๋ถ๋ด + w3 x ์ผ๊ฐ ๋ถ๋ด).\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 3] ์ง๋ฌด๋ณ ๊ฒฐ์์จ ๋ฐ ์ด๊ณผ๊ทผ๋ฌด ์๊ฐ ๊ธฐ๋ฐ 1์ฐจ ํผ๋ก๋ ์ฐ์ถ Baseline ๋ชจ๋ธ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def metrics(y, p):\n",
" tp, fp, fn = int(((p == 1) & (y == 1)).sum()), int(((p == 1) & (y == 0)).sum()), int(((p == 0) & (y == 1)).sum())\n",
" pr, rc = tp / max(tp + fp, 1), tp / max(tp + fn, 1)\n",
" return {\"acc\": float((p == y).mean()), \"prec\": pr, \"rec\": rc, \"f1\": 2 * pr * rc / max(pr + rc, 1e-9)}\n",
"\n",
"\n",
"W0, CUT0 = (1 / 3, 1 / 3, 1 / 3), 50 # ์ด๊ธฐ(๋์ผ ๊ฐ์ค) ํด๋ฆฌ์คํฑ\n",
"rule0 = ((te[\"vac_rate\"] >= 0.20) | (te[\"overtime_h\"] >= 15)).astype(int).to_numpy() # ๊ฐ์ฅ ๋จ์ํ ๊ท์น: ๊ฒฐ์์จ 20% ์ด์ ๋๋ ์ด๊ณผ๊ทผ๋ฌด 15์๊ฐ ์ด์\n",
"y_tr, y_te = tr[\"fatigue_label\"].to_numpy(), te[\"fatigue_label\"].to_numpy()\n",
"m0 = metrics(y_te, (score(te, W0) >= CUT0).astype(int))\n",
"trivial = float((y_te == 0).mean())\n",
"print(f\"[Baseline] ํ์ต {len(tr)}ํ / ์ํ {len(te)}ํ, ๊ณ ํผ๋ก ๋น์จ {y_te.mean():.1%} (๋ชจ๋ '์ ์'์ด๋ผ ๋ตํด๋ ์ ํ๋ {trivial:.1%})\")\n",
"mr = metrics(y_te, rule0)\n",
"print(f\"๋จ์ ๊ท์น(๊ฒฐ์์จ 20% ์ด์ ๋๋ ์ด๊ณผ๊ทผ๋ฌด 15์๊ฐ ์ด์): ์ ํ๋ {mr['acc']:.1%}, ์ ๋ฐ๋ {mr['prec']:.1%}, ์ฌํ์จ {mr['rec']:.1%}, F1 {mr['f1']:.2f}\")\n",
"print(f\"์ด๊ธฐ ๊ฐ์ค(๋์ผ 1/3, ๊ธฐ์ค {CUT0}์ ): ์ ํ๋ {m0['acc']:.1%}, ์ ๋ฐ๋ {m0['prec']:.1%}, ์ฌํ์จ {m0['rec']:.1%}, F1 {m0['f1']:.2f}\")\n",
"\n",
"# 1์ฐจ ํ๋: ๊ฐ์ค์น(ํฉ 1)์ ๊ธฐ์ค ์ ์๋ฅผ ํ์ต ๊ธฐ๊ฐ์์ F1์ด ์ต๋๊ฐ ๋๊ฒ ๊ฒฉ์ ํ์\n",
"best = None\n",
"Xtr = X(tr)\n",
"for w1, w2 in itertools.product(np.arange(0.0, 1.01, 0.1), repeat=2):\n",
" if w1 + w2 > 1.0001:\n",
" continue\n",
" w = np.array([w1, w2, 1 - w1 - w2]); s = 100 * Xtr @ w\n",
" for cut in range(35, 71, 5):\n",
" f = metrics(y_tr, (s >= cut).astype(int))[\"f1\"]\n",
" if best is None or f > best[0]:\n",
" best = (f, tuple(float(x) for x in np.round(w, 1)), cut)\n",
"W1, CUT1 = best[1], best[2]\n",
"m1 = metrics(y_te, (score(te, W1) >= CUT1).astype(int))\n",
"print(f\"ํ๋ ๊ฐ์ค {W1}(๊ฒฐ์ยท์ด๊ณผ๊ทผ๋ฌดยท์ผ๊ฐ), ๊ธฐ์ค {CUT1}์ (ํ์ต F1 {best[0]:.2f})\")\n",
"print(f\"์ํ ๊ธฐ๊ฐ: ์ ํ๋ {m1['acc']:.1%}, ์ ๋ฐ๋ {m1['prec']:.1%}, ์ฌํ์จ {m1['rec']:.1%}, F1 {m1['f1']:.2f} (๋ชฉํ ์ ํ๋ 75% ์ด์: {'๋ฌ์ฑ' if m1['acc'] >= 0.75 else '๋ฏธ๋ฌ'}, ๋จ ๋จ์ ๊ธฐ์ค์ {trivial:.1%}๋ณด๋ค {'๋์' if m1['acc'] > trivial else '๋ฎ์'})\")\n",
"\n",
"# ํ๋จ ์ง์ฐ์๊ฐ: 1๊ฑด์ฉ ๊ณ์ฐ(์ค์๊ฐ ์กฐํ ์ํฉ)\n",
"rec = te.to_dict(\"records\")\n",
"t0 = time.perf_counter()\n",
"for r in rec[:1000]:\n",
" s = min(100 * (W1[0] * min(r[\"vac_rate\"] / .25, 1.2) + W1[1] * min(r[\"overtime_h\"] / 20, 1.2) + W1[2] * min(r[\"night_ratio\"] / .4, 1.2)), 100); _ = s >= CUT1\n",
"lat = (time.perf_counter() - t0) / 1000 * 1000\n",
"print(f\"ํ๋จ ์ง์ฐ์๊ฐ ํ๊ท {lat:.4f}ms/๊ฑด\")\n",
"\n",
"# 2) ์ง๋ฌด๋ณยท์
๋ณ ํผ๋ก๋ ์์ (์ต๊ทผ 12์ฃผ ํ๊ท )\n",
"last = df[df[\"week\"] > 92].copy(); last[\"fatigue\"] = score(last, W1)\n",
"by_job = last.groupby(\"job\")[\"fatigue\"].mean().sort_values(ascending=False)\n",
"print(\"\\n[์ง๋ฌด๋ณ ํ๊ท ํผ๋ก๋(์ต๊ทผ 12์ฃผ)]\", \", \".join(f\"{k} {v:.0f}\" for k, v in by_job.items()))\n",
"cell = last.groupby([\"unit\", \"job\"]).agg(ํผ๋ก๋=(\"fatigue\", \"mean\"), ๊ฒฐ์์จ=(\"vac_rate\", \"mean\"), ์ด๊ณผ๊ทผ๋ฌด=(\"overtime_h\", \"mean\"), ์ผ๊ฐ=(\"night_ratio\", \"mean\")).sort_values(\"ํผ๋ก๋\", ascending=False)\n",
"print(\"[ํผ๋ก๋ ์์ 5๊ฐ ์
๊ณผ ํ๋จ ๊ทผ๊ฑฐ(๊ธฐ์ฌ ์ ์)]\")\n",
"for (u, j), r in cell.head(5).iterrows():\n",
" c = 100 * np.array(W1) * np.minimum(np.array([r[\"๊ฒฐ์์จ\"] / .25, r[\"์ด๊ณผ๊ทผ๋ฌด\"] / 20, r[\"์ผ๊ฐ\"] / .4]), 1.2)\n",
" print(f\" {u} {j}: ํผ๋ก๋ {r['ํผ๋ก๋']:.0f} = ๊ฒฐ์ {c[0]:.0f} + ์ด๊ณผ๊ทผ๋ฌด {c[1]:.0f} + ์ผ๊ฐ {c[2]:.0f} (๊ฒฐ์์จ {r['๊ฒฐ์์จ']:.0%}, ์ด๊ณผ๊ทผ๋ฌด {r['์ด๊ณผ๊ทผ๋ฌด']:.1f}h)\")\n",
"n_hi = int((cell[\"ํผ๋ก๋\"] >= CUT1).sum())\n",
"print(f\"๊ธฐ์ค {CUT1}์ ์ด์ ์
{n_hi}/24๊ฐ -> ์ธ์ฌ๊ด์ด ์ฐ์ ํ์ธ. ์ ์๋ ํ๋จ ๋ณด์กฐ์ด๋ฉฐ ํด์ยท๊ต๋ ์กฐ์ ์ ์งํ๊ดยท์ธ์ฌ๊ด์ด ๊ฒฐ์ ํ๋ค.\")\n",
"\n",
"# 3) ์ค๋ถ๋ฅ ๋ถ์ (ํ๋ ๊ฐ์ค์น ์ํ ๊ธฐ๊ฐ)\n",
"pred = (score(te, W1) >= CUT1).astype(int)\n",
"fn = te[(pred == 0) & (y_te == 1)]\n",
"print(f\"\\n[๋ฏธํ {len(fn)}๊ฑด ํน์ง] ํ๊ท ๊ฒฐ์์จ {fn['vac_rate'].mean():.0%}, ์ด๊ณผ๊ทผ๋ฌด {fn['overtime_h'].mean():.1f}h, ์ผ๊ฐ {fn['night_ratio'].mean():.0%} -> ์ ์ ๊ฒฝ๊ณ ๋ถ๊ทผ ์ฌ๋ก(๋ผ๋ฒจ์๋ ํด๊ฐยทํ์ยท๊ต์ก ์ธ์ ๋น์จ๊ณผ ์ก์๋ ๋ฐ์๋์ด ์ด 3๊ฐ ๋ณ์๋ง์ผ๋ก๋ ์ค๋ช
๋์ง ์์)\")\n",
"\n",
"plt.figure(figsize=(7, 4))\n",
"plt.scatter(te[\"vac_rate\"] * 100, te[\"overtime_h\"], c=te[\"fatigue_label\"], cmap=\"coolwarm\", s=8, alpha=.6)\n",
"plt.xlabel(\"vacancy rate (%)\"); plt.ylabel(\"overtime hours/person/week\"); plt.title(\"High-fatigue label (red) by vacancy and overtime\"); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
03 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[Baseline] ํ์ต 1248ํ / ์ํ 1248ํ, ๊ณ ํผ๋ก ๋น์จ 34.8% (๋ชจ๋ '์ ์'์ด๋ผ ๋ตํด๋ ์ ํ๋ 65.2%)\n",
"๋จ์ ๊ท์น(๊ฒฐ์์จ 20% ์ด์ ๋๋ ์ด๊ณผ๊ทผ๋ฌด 15์๊ฐ ์ด์): ์ ํ๋ 92.7%, ์ ๋ฐ๋ 96.7%, ์ฌํ์จ 81.8%, F1 0.89\n",
"์ด๊ธฐ ๊ฐ์ค(๋์ผ 1/3, ๊ธฐ์ค 50์ ): ์ ํ๋ 89.7%, ์ ๋ฐ๋ 78.9%, ์ฌํ์จ 95.9%, F1 0.87\n",
"ํ๋ ๊ฐ์ค (0.3, 0.6, 0.1)(๊ฒฐ์ยท์ด๊ณผ๊ทผ๋ฌดยท์ผ๊ฐ), ๊ธฐ์ค 55์ (ํ์ต F1 0.91)\n",
"์ํ ๊ธฐ๊ฐ: ์ ํ๋ 94.1%, ์ ๋ฐ๋ 91.9%, ์ฌํ์จ 91.0%, F1 0.91 (๋ชฉํ ์ ํ๋ 75% ์ด์: ๋ฌ์ฑ, ๋จ ๋จ์ ๊ธฐ์ค์ 65.2%๋ณด๋ค ๋์)\n",
"ํ๋จ ์ง์ฐ์๊ฐ ํ๊ท 0.0051ms/๊ฑด\n",
"\n",
"[์ง๋ฌด๋ณ ํ๊ท ํผ๋ก๋(์ต๊ทผ 12์ฃผ)] ๋ณด๊ธ 68, ํต์ 64, ์ ๋น 52, ์์ 51, ์๋ฌด 44, ํ์ 42\n",
"[ํผ๋ก๋ ์์ 5๊ฐ ์
๊ณผ ํ๋จ ๊ทผ๊ฑฐ(๊ธฐ์ฌ ์ ์)]\n",
" ๊ฐ์B๋๋ ๋ณด๊ธ: ํผ๋ก๋ 100 = ๊ฒฐ์ 36 + ์ด๊ณผ๊ทผ๋ฌด 72 + ์ผ๊ฐ 6 (๊ฒฐ์์จ 57%, ์ด๊ณผ๊ทผ๋ฌด 40.0h)\n",
" ๊ฐ์C๋๋ ํต์ : ํผ๋ก๋ 100 = ๊ฒฐ์ 36 + ์ด๊ณผ๊ทผ๋ฌด 72 + ์ผ๊ฐ 7 (๊ฒฐ์์จ 49%, ์ด๊ณผ๊ทผ๋ฌด 39.9h)\n",
" ๊ฐ์A๋๋ ์ ๋น: ํผ๋ก๋ 96 = ๊ฒฐ์ 34 + ์ด๊ณผ๊ทผ๋ฌด 71 + ์ผ๊ฐ 6 (๊ฒฐ์์จ 28%, ์ด๊ณผ๊ทผ๋ฌด 23.6h)\n",
" ๊ฐ์A๋๋ ๋ณด๊ธ: ํผ๋ก๋ 83 = ๊ฒฐ์ 20 + ์ด๊ณผ๊ทผ๋ฌด 61 + ์ผ๊ฐ 6 (๊ฒฐ์์จ 17%, ์ด๊ณผ๊ทผ๋ฌด 20.3h)\n",
" ๊ฐ์D๋๋ ํต์ : ํผ๋ก๋ 82 = ๊ฒฐ์ 19 + ์ด๊ณผ๊ทผ๋ฌด 68 + ์ผ๊ฐ 6 (๊ฒฐ์์จ 16%, ์ด๊ณผ๊ทผ๋ฌด 22.5h)\n",
"๊ธฐ์ค 55์ ์ด์ ์
10/24๊ฐ -> ์ธ์ฌ๊ด์ด ์ฐ์ ํ์ธ. ์ ์๋ ํ๋จ ๋ณด์กฐ์ด๋ฉฐ ํด์ยท๊ต๋ ์กฐ์ ์ ์งํ๊ดยท์ธ์ฌ๊ด์ด ๊ฒฐ์ ํ๋ค.\n",
"\n",
"[๋ฏธํ 39๊ฑด ํน์ง] ํ๊ท ๊ฒฐ์์จ 7%, ์ด๊ณผ๊ทผ๋ฌด 11.7h, ์ผ๊ฐ 26% -> ์ ์ ๊ฒฝ๊ณ ๋ถ๊ทผ ์ฌ๋ก(๋ผ๋ฒจ์๋ ํด๊ฐยทํ์ยท๊ต์ก ์ธ์ ๋น์จ๊ณผ ์ก์๋ ๋ฐ์๋์ด ์ด 3๊ฐ ๋ณ์๋ง์ผ๋ก๋ ์ค๋ช
๋์ง ์์)\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
04. ์๊ณ์ด ๋ถ์ ๊ธฐ๋ฐ ๋ถ์๋ณ ํฅํ 3๊ฐ์ ๊ฒฐ์ ์ถ์ธ ๋ฐ ๋ณด์ถฉ ํ์ ์ธ๋ ฅ ์์ธก ๋ชจ๋ธ ๊ณ ๋ํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์๊ณ์ด ๋ถ์ ๊ธฐ๋ฐ ํฅํ 3๊ฐ์ ๊ฒฐ์ ์ถ์ธ ๋ฐ ๋ณด์ถฉ ํ์๋ ์์ธก ๋ชจ๋ธ์ ๊ณ ๋ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์๊ณ์ด ๋ถ์์ผ๋ก ๋ถ๋ยท์ง๋ฌด๋ณ ํฅํ 3๊ฐ์(13์ฃผ) ๊ฒฐ์ ์ถ์ธ๋ฅผ ์์ธกํ๊ณ ๋ณด์ถฉ ํ์ ์ธ์์ ์ฐ์ถํ๋ ๋ชจ๋ธ์ ๊ณ ๋ํํด ๋ณด์ถฉ ํ์ ์
ํ์ง ์ฌํ์จ์ ๊ฒ์ฆํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **24๊ฐ ์
์ ์ฃผ๊ฐ ๊ฒฐ์์จ์ ๋จ์ ์์ธก, ๋ฆฟ์ง ํ๊ท(ํน์ง ์ฌ์ค๊ณ ์ ํ), ๊ต๋ ๋ณด์ถฉยท์ ์ถ ์ง์ค๊ธฐ๋ฅผ ๋ฐ์ํ ์ฌ๊ณ -ํ๋ฆ ๋ชจ์(300๊ฒฝ๋ก)๋ก ์์ธกํด 4๊ฐ ์์ ๋กค๋ง ๊ฒ์ฆ์ผ๋ก ์ค์ฐจ๋ฅผ ๋น๊ต ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/adm01_advanced_detector.py ๋ฐ ์์ธ ํ์ง ํ
์คํธ ๊ฒฐ๊ณผ` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ธฐ๋ง/๊ต๋ ๊ณต๊ฒฉ์ ๋ํ ๋ฐฉ์ด ๋ก์ง์ ํ์ค์ฑ ๋ฐ ์ ๋ขฐ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๋ณตํฉ ์ํ ํ์ง ์ฌํ์จ(Recall) 85% ์ด์, ์ฒ๋ฆฌ ์ง์ฐ 200ms ์ดํ\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ๋ณตํฉ ์ํ ์๋๋ฆฌ์ค 10์ข
์ ๋ํ ํ์ง ํ
์คํธ ํต๊ณผ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ํฉ์ฑ ์ํ ๋ฐ์ดํฐ์
์ฃผ์
์ ์ด์์น ํ์ง ์ ํ๋ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"๋ถ๋ยท์ง๋ฌด 24๊ฐ ์
์ ์ฃผ๊ฐ ๊ฒฐ์์จ์ ๋จ์ ์์ธกยท๋ฆฟ์ง ํ๊ท(ํน์ง ์ฌ์ค๊ณ v1โv2)ยท์ฌ๊ณ -ํ๋ฆ ๋ชจ์(๊ต๋ ๋ณด์ถฉยท์ ์ถ ์ง์ค๊ธฐ ๋ฐ์, 300๊ฒฝ๋ก)๋ก ํฅํ 13์ฃผ(3๊ฐ์) ์์ธกํด 4๊ฐ ์์ ๋กค๋ง ๊ฒ์ฆ์ผ๋ก ๋น๊ตํ๊ณ , ๋ณด์ถฉ ํ์ ์
ํ์ง์ ์ฌํ์จยท์ ๋ฐ๋ ๊ท ํ(ํ๋ฅ ๊ธฐ์ค ์ ํ)๊ณผ ์ฒ๋ฆฌ ์ง์ฐ์ ์ธก์ ํด ๋ณด์ถฉ ํ์ ์ธ์์ ์ฐ์ถํ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋ถ๋ ์ธ๋ ฅ ํํฉ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยทํธ์ ยท๋ณ๋ ฅยท๊ฐ์ธ ์ ๋ณด ์์)\n",
"# ๊ฐ์ ๋๋ 4๊ณณ x ์ง๋ฌด 6์ข
์ ์ฃผ๊ฐ ์ง๊ณ ์๋ฃ๋ง ๋ง๋ ๋ค. ๊ฐ์ธ ๋จ์ ์ ๋ณด(์ด๋ฆยท๊ตฐ๋ฒ ๋ฑ)๋ ๋ง๋ค์ง ์๋๋ค.\n",
"# ์ธ์์ ์ง๋ฌด๋ณ ์ง๊ณ ์์ด๋ฉฐ, ๊ฒฐ์ยทํด๊ฐยทํ์ยท๊ต์กยท๋ณด์ถฉยท์ ์ถ์ ๋ฐ์ํ ํ์ฌ์๊ณผ ์ด๊ณผ๊ทผ๋ฌด ์๊ฐ์ผ๋ก ๊ตฌ์ฑ๋๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"UNITS = [\"๊ฐ์A๋๋\", \"๊ฐ์B๋๋\", \"๊ฐ์C๋๋\", \"๊ฐ์D๋๋\"]\n",
"JOBS = [\"์์ \", \"ํต์ \", \"์ ๋น\", \"๋ณด๊ธ\", \"ํ์ \", \"์๋ฌด\"]\n",
"FIELDS = [\"unit\", \"job\", \"week\", \"authorized\", \"present\", \"vacancy\", \"leave\", \"sick\", \"training\", \"inflow\", \"outflow\", \"overtime_h\", \"night_ratio\", \"rotation_days\", \"fatigue_label\"]\n",
"HI_CUT = 55 # ๊ณ ํผ๋ก ํ์ ๊ธฐ์ค(๊ฐ์ ์ ์ 55 ์ด์)\n",
"\n",
"\n",
"def make_manning(seed=7, weeks=104, shock=None):\n",
" \"\"\"๋ถ๋ x ์ง๋ฌด 24๊ฐ ์
์ ์ฃผ๊ฐ ์ธ๋ ฅ ์๋ฃ. shock=(unit, job ๋๋ None(๋ถ๋ ์ ์ฒด), ์์์ฃผ, ์ฃผ๊ฐ ์ถ๊ฐ ์ ์ถ ํ๊ท ์) ํ ๊ฑด ๋๋ ๊ทธ ๋ชฉ๋ก์ด๋ฉด ๊ทธ ์
์ ๋๋ ๊ฒฐ์ ์์ธ์ ๋ฃ๋๋ค(AARยท์คํธ๋ ์ค์ฉ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" shocks = [shock] if shock and not isinstance(shock[0], tuple) else list(shock or [])\n",
" rows = []\n",
" for ui, u in enumerate(UNITS):\n",
" for ji, j in enumerate(JOBS):\n",
" auth = int(rng.integers(14, 42) * (0.9 + 0.06 * ui))\n",
" work = rng.uniform(0.92, 1.12) # ์ง๋ฌด๋ณ ์
๋ฌด๋ ๊ณ์\n",
" net = rng.uniform(-0.05, 0.03) # ๊ตฌ์กฐ์ ๋ณด์ถฉ ๋ถ์กฑ(์์)ยท๊ณผ์(์์) ๊ฒฝํฅ(์ฃผ๋น ์ธ์)\n",
" present = int(auth * rng.uniform(0.82, 0.98))\n",
" rot = int(rng.choice([60, 90, 120])) # ๊ต๋ยท๋ณด์ถฉ ์ฃผ๊ธฐ(์ผ)\n",
" for w in range(1, weeks + 1):\n",
" season = 1.0 + 0.9 * (abs(w % 52 - 22) < 4 or abs(w % 52 - 46) < 3) # ์ ์ถยท์ ์ญ ์ง์ค ์๊ธฐ\n",
" out = int(rng.poisson(0.22 * auth / 20 * season))\n",
" for s_ in shocks:\n",
" if s_[0] == u and (s_[1] is None or s_[1] == j) and w >= s_[2]:\n",
" out += int(rng.poisson(s_[3]))\n",
" inflow = int(rng.poisson(max(0.0, 0.2 * auth / 20 * (1 + net * 10)))) + (int(rng.integers(2, 6) * auth / 25) if w % (rot // 7) == 0 else 0)\n",
" present = int(np.clip(present - out + inflow, int(auth * 0.4), auth))\n",
" leave = int(rng.binomial(present, 0.05 + 0.05 * (abs(w % 52 - 31) < 3 or w % 52 >= 50)))\n",
" sick = int(rng.poisson(0.025 * present)); train = int(rng.binomial(present, 0.04 + 0.06 * (w % 13 == 6)))\n",
" avail = max(present - leave - sick - train, 1)\n",
" night = float(np.clip(rng.normal(0.25 + 0.05 * (j == \"์์ \") + 0.05 * (j == \"์๋ฌด\"), 0.05), 0.05, 0.6))\n",
" ot = float(np.clip(auth * 40 * work / avail - 40 + rng.normal(0, 1.5), 0, 40))\n",
" vac = auth - present\n",
" absent = (leave + sick + train) / max(present, 1)\n",
" fat = 100 * (0.38 * min(vac / auth / 0.25, 1.2) + 0.34 * min(ot / 20, 1.2) + 0.10 * min(night / 0.4, 1.2) + 0.18 * min(absent / 0.2, 1.2)) + rng.normal(0, 5)\n",
" rows.append((u, j, w, auth, present, vac, leave, sick, train, inflow, out, round(ot, 1), round(night, 2), rot, int(fat >= HI_CUT)))\n",
" return pd.DataFrame(rows, columns=FIELDS)\n",
"\n",
"\n",
"def cell_pivot(df, col=\"vacancy\"):\n",
" \"\"\"์ฃผ x (๋ถ๋,์ง๋ฌด) ํ.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 4] ์๊ณ์ด ๋ถ์ ๊ธฐ๋ฐ ๋ถ์๋ณ ํฅํ 3๊ฐ์ ๊ฒฐ์ ์ถ์ธ ๋ฐ ๋ณด์ถฉ ํ์ ์ธ๋ ฅ ์์ธก ๋ชจ๋ธ ๊ณ ๋ํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"from sklearn.linear_model import Ridge\n",
"H = 13 # ์์ธก ๊ธฐ๊ฐ: ํฅํ 3๊ฐ์(13์ฃผ)\n",
"NEED_CUT = 0.15 # ๊ฒฐ์์จ 15% ์ด์์ด๋ฉด ๋ณด์ถฉ ํ์(๊ฐ์ ๊ธฐ์ค)\n",
"\n",
"\n",
"def season(w):\n",
" # [TODO : ํ์ ์ค์ต 4] ์๊ณ์ด ๋ถ์ ๊ธฐ๋ฐ ๋ถ์๋ณ ํฅํ 3๊ฐ์ ๊ฒฐ์ ์ถ์ธ ๋ฐ ๋ณด์ถฉ ํ์ ์ธ๋ ฅ ์์ธก ๋ชจ๋ธ ๊ณ ๋ํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def feat(vr, ir, orr, wn, rot, v2=True):\n",
" \"\"\"vrยทirยทorr: ์ง๊ธ๊น์ง์ ๊ฒฐ์์จยท๋ณด์ถฉ์จยท์ ์ถ์จ ๋ชฉ๋ก, wn: ์์ธกํ ๋ค์ ์ฃผ์ฐจ, rot: ๊ต๋ ์ฃผ๊ธฐ(์ผ). v2๋ ๋ณด์ถฉยท์ ์ถ ํ๋ฆ๊ณผ ๋ฌ๋ ฅ ํน์ง์ ์ถ๊ฐํ๋ค.\"\"\"\n",
" f = [vr[-1], vr[-1] - vr[-2], vr[-1] - vr[-4]]\n",
" if v2:\n",
" step = max(rot // 7, 1)\n",
" f += [float(np.mean(orr[-12:]) - np.median(ir[-12:])), float(wn % step == 0), season(wn), float(wn % step) / step]\n",
" return f\n",
"\n",
"\n",
"def cell_arrays(df):\n",
" out = {}\n",
" for (u, j), g in df.sort_values(\"week\").groupby([\"unit\", \"job\"]):\n",
" a = g[\"authorized\"].to_numpy()\n",
" out[(u, j)] = {\"auth\": int(a[0]), \"rot\": int(g[\"rotation_days\"].iloc[0]), \"vr\": (g[\"vacancy\"].to_numpy() / a), \"ir\": g[\"inflow\"].to_numpy() / a, \"or\": g[\"outflow\"].to_numpy() / a}\n",
" return out\n",
"\n",
"\n",
"def fit_fc(cells, origin, v2=True, alpha=1.0):\n",
" X, y = [], []\n",
" for c in cells.values():\n",
" for t in range(12, origin - 1): # t์ฃผ๊น์ง ์ ๋ t+1์ฃผ(์ธ๋ฑ์ค t)์ ๊ฒฐ์์จ ๋ณํ๋ฅผ ๋งํ๋ค\n",
" X.append(feat(c[\"vr\"][:t], c[\"ir\"][:t], c[\"or\"][:t], t + 1, c[\"rot\"], v2)); y.append(c[\"vr\"][t] - c[\"vr\"][t - 1])\n",
" return Ridge(alpha=alpha).fit(np.array(X), np.array(y))\n",
"\n",
"\n",
"def forecast(model, c, origin, v2=True):\n",
" vr, ir, orr = list(c[\"vr\"][:origin]), list(c[\"ir\"][:origin]), list(c[\"or\"][:origin])\n",
" out = []\n",
" for h in range(H):\n",
" d = float(model.predict([feat(vr, ir, orr, origin + h + 1, c[\"rot\"], v2)])[0])\n",
" p = float(np.clip(vr[-1] + d, 0, 1))\n",
" out.append(p); vr.append(p); ir.append(float(np.median(ir[-12:]))); orr.append(float(np.mean(orr[-12:])))\n",
" return np.array(out)\n",
"\n",
"\n",
"def mc_forecast(c, origin, n_path=300, seed=0, out_mult=1.0, lam=0.0):\n",
" \"\"\"์ฌ๊ณ -ํ๋ฆ(stock-flow) ๋ชจ์: ์
์ ํ์ยท์ ์ถ ์ง์ค๊ธฐ ์ ์ถ์จ, ์ ๊ท ๋ณด์ถฉ์จ, ๊ต๋ ์ฃผ๊ธฐ ๋ณด์ถฉ ๊ท๋ชจ๋ฅผ ๊ณผ๊ฑฐ์์ ์ถ์ ํด ๊ฒฐ์ ๊ฒฝ๋ก n_path๊ฐ๋ฅผ ๋ง๋ ๋ค.\n",
" ๋ฐํ: (๊ฒฝ๋ก ๋ฐฐ์ด [n_path, H] ๊ฒฐ์์จ, ์ถ์ ๋ชจ์). out_mult๋ ์ ์ถ ๊ฐ์ ์ ํค์ฐ๋ ๋ณด์ ๊ณ์, lam์ ์ต๊ทผ 8์ฃผ ์ ์ถ์จ์ ๋ฐ์ํ๋ ๋น์ค(0~1, ๊ธ์ฆ ๊ฐ์ง ๋๋ง ์ ์ฉ, AAR ์ฌ๋ณด์ ์ฉ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" w = np.arange(1, origin + 1); step = max(c[\"rot\"] // 7, 1)\n",
" rot_w = (w % step == 0); sea = np.array([season(x) > 0 for x in w])\n",
" o, i = c[\"or\"][:origin], c[\"ir\"][:origin]\n",
" o_base = o[~sea].mean(); o_sea = o[sea].mean() if sea.sum() >= 6 else o_base * 1.9\n",
" o_rec = o[-8:].mean()\n",
" mu = max(o_base, 1e-6) * c[\"auth\"] * 8 # ์ต๊ทผ 8์ฃผ ํ์ ๊ธฐ๋ ์ ์ถ ์ธ์\n",
" use_rec = lam > 0 and o_rec * c[\"auth\"] * 8 > mu + 2.0 * np.sqrt(mu) # ํฌ์์ก ๊ธฐ์ค ์์ 2ํ์คํธ์ฐจ๋ฅผ ๋๋ '๊ธ์ฆ'์ผ ๋๋ง ์ต๊ทผ ๊ฐ์ ๋ฐ์\n",
" i_reg = i[~rot_w].mean(); batch = max(i[rot_w].mean() - i_reg, 0.0)\n",
" a = c[\"auth\"]; pres = np.full(n_path, a * (1 - c[\"vr\"][origin - 1]))\n",
" out = np.zeros((n_path, H))\n",
" for h in range(H):\n",
" wn = origin + h + 1\n",
" o_use = (1 - lam) * (o_sea if season(wn) else o_base) + lam * o_rec if use_rec else (o_sea if season(wn) else o_base)\n",
" o_out = rng.poisson(max(o_use, 1e-6) * a * out_mult, n_path)\n",
" o_in = rng.poisson(max(i_reg, 1e-6) * a, n_path) + (np.maximum(rng.normal(batch * a, 0.3 * batch * a, n_path), 0).round() if wn % step == 0 else 0)\n",
" pres = np.clip(pres - o_out + o_in, 0.4 * a, a)\n",
" out[:, h] = 1 - pres / a\n",
" return out, {\"o_base\": o_base, \"o_sea\": o_sea, \"i_reg\": i_reg, \"batch\": batch}\n",
"\n",
"df = make_manning(7, 104)\n",
"cells = cell_arrays(df)\n",
"ORIGINS = [52, 65, 78, 91] # ์์ธก ์์ (๊ทธ ์ฃผ๊น์ง์ ์๋ฃ๋ง ์ฌ์ฉ)\n",
"res = []\n",
"lat, lat3 = [], []\n",
"for o in ORIGINS:\n",
" m1, m2 = fit_fc(cells, o, v2=False), fit_fc(cells, o, v2=True)\n",
" for k, c in cells.items():\n",
" act = c[\"vr\"][o:o + H]\n",
" t0 = time.perf_counter(); p2 = forecast(m2, c, o, True); lat.append((time.perf_counter() - t0) * 1000)\n",
" p1 = forecast(m1, c, o, False)\n",
" naive = np.full(H, c[\"vr\"][o - 1])\n",
" t0 = time.perf_counter(); paths, _ = mc_forecast(c, o); lat3.append((time.perf_counter() - t0) * 1000)\n",
" res.append({\"origin\": o, \"cell\": k, \"auth\": c[\"auth\"], \"act_peak\": act.max(), \"act_end\": act[-1], \"naive\": naive, \"v1\": p1, \"v2\": p2, \"v3\": paths.mean(0), \"p_need\": float((paths.max(axis=1) >= NEED_CUT).mean()), \"act\": act})\n",
"R = pd.DataFrame(res)\n",
"mae = lambda col: float(np.mean([np.abs(r[col] - r[\"act\"]).mean() * r[\"auth\"] for _, r in R.iterrows()]))\n",
"print(f\"[๋กค๋ง ๊ฒ์ฆ] ์์ {ORIGINS} x ์
{len(cells)}๊ฐ = {len(R)}๊ฑด, ์์ธก 13์ฃผ\")\n",
"print(f\"๊ฒฐ์ ์ธ์ MAE: ๋จ์ ์์ธก(๋ง์ง๋ง ๊ฐ ์ ์ง) {mae('naive'):.2f}๋ช
/ ํ๊ท v1 {mae('v1'):.2f}๋ช
/ ํ๊ท v2(ํน์ง ์ฌ์ค๊ณ) {mae('v2'):.2f}๋ช
/ ์ฌ๊ณ -ํ๋ฆ ๋ชจ์ ํ๊ท {mae('v3'):.2f}๋ช
\")\n",
"print(f\"์์ธก ์ง์ฐ: ํ๊ท ์
1๊ฐ ํ๊ท {np.mean(lat):.1f}ms, ๋ชจ์ 300๊ฒฝ๋ก ํ๊ท {np.mean(lat3):.1f}ms/99๋ฐฑ๋ถ์ {np.percentile(lat3, 99):.1f}ms (24๊ฐ ์
์ ์ฒด {np.mean(lat3) * 24:.0f}ms, ๋ชฉํ 200ms ์ดํ: {'๋ฌ์ฑ' if np.percentile(lat3, 99) <= 200 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# ๋ณด์ถฉ ํ์ ์
ํ์ง: ํฅํ 13์ฃผ ์ค ๊ฒฐ์์จ์ด NEED_CUT ์ด์์ด ๋๋๊ฐ\n",
"act_flag = R[\"act_peak\"] >= NEED_CUT\n",
"def det(pred):\n",
" pred = np.asarray(pred); tp, fp, fn = int((pred & act_flag).sum()), int((pred & ~act_flag).sum()), int((~pred & act_flag).sum())\n",
" return tp / max(tp + fn, 1), tp / max(tp + fp, 1), fp\n",
"print(f\"\\n[๋ณด์ถฉ ํ์(๊ฒฐ์์จ {NEED_CUT:.0%} ์ด์) ํ์ง] ์ค์ ํด๋น {int(act_flag.sum())}๊ฑด / {len(R)}๊ฑด\")\n",
"rows = [(\"๋จ์ ์์ธก\", [r[\"naive\"].max() >= NEED_CUT for _, r in R.iterrows()]), (\"ํ๊ท v1\", [r[\"v1\"].max() >= NEED_CUT for _, r in R.iterrows()]), (\"ํ๊ท v2\", [r[\"v2\"].max() >= NEED_CUT for _, r in R.iterrows()])]\n",
"rows += [(f\"๋ชจ์ ํ๋ฅ {p:.2f} ์ด์\", (R[\"p_need\"] >= p).to_list()) for p in (0.5, 0.3, 0.15)]\n",
"for name, pred in rows:\n",
" rc, pr, fp = det(pred); print(f\" {name:16s} ์ฌํ์จ {rc:5.1%}, ์ ๋ฐ๋ {pr:5.1%}, ์ค๊ฒฝ๋ณด {fp}๊ฑด\")\n",
"P0 = 0.15\n",
"rc, pr, fp = det((R[\"p_need\"] >= P0).to_list())\n",
"print(f\"-> ์ ํ: ๋ชจ์ ํ๋ฅ {P0} ์ด์์ '๋ณด์ถฉ ํ์ ๊ฐ๋ฅ์ฑ'์ผ๋ก ํ์, ์ฌํ์จ {rc:.1%} (๋ชฉํ 85% ์ด์: {'๋ฌ์ฑ' if rc >= 0.85 else '๋ฏธ๋ฌ'}), ๋์ ์ ๋ฐ๋ {pr:.0%}๋ก ์ค๊ฒฝ๋ณด {fp}๊ฑด์ ์ฌ๋์ด ๊ฑธ๋ฌ์ผ ํจ\")\n",
"\n",
"# ์ค๊ฒฝ๋ณด ํจํด: ์ ํ ๊ธฐ์ค์ ์ค๊ฒฝ๋ณด๊ฐ ์ด๋์ ๋ชฐ๋ฆฌ๋๊ฐ\n",
"fa = R[(R[\"p_need\"] >= P0) & ~act_flag]\n",
"near = float((fa[\"act_peak\"] >= 0.10).mean()) if len(fa) else 0.0\n",
"print(f\"[์ค๊ฒฝ๋ณด ํจํด] ์ค๊ฒฝ๋ณด {len(fa)}๊ฑด ์ค ์ค์ ์ต๋ ๊ฒฐ์์จ์ด 10~15%์ธ ๊ฒฝ๊ณ ๊ทผ์ฒ ์ฌ๋ก {near:.0%} -> ๊ต๋ ๋ณด์ถฉ ๋์ฐฉ ์์ ยท์ ์ถ ๊ท๋ชจ์ ๋ถํ์ค์ฑ์ด ์์ธ, ํ๋ฅ ์ด ๋ฎ์ ๊ฑด(0.15~0.5)์ '๊ด์ฐฐ'๋ก ํ์ํ๊ณ 0.5 ์ด์๋ง '๋ณด์ถฉ ํ์'๋ก ์ฌ๋ฆฐ๋ค\")\n",
"hi = int(((R[\"p_need\"] >= 0.5) & act_flag).sum()); hin = int((R[\"p_need\"] >= 0.5).sum())\n",
"print(f\"ํ๋ฅ 0.5 ์ด์ {hin}๊ฑด ์ค ์ค์ ๋ณด์ถฉ ํ์ {hi}๊ฑด({hi / max(hin, 1):.0%})\")\n",
"\n",
"# ๋ณด์ถฉ ํ์ ์ธ์(๊ฒฝ๋ก์ 90๋ฐฑ๋ถ์ ์ต๋ ๊ฒฐ์ - ํ์ฉ ๊ฒฐ์ 10%)๊ณผ ์ค์ ๋น๊ต (๋ง์ง๋ง ์์ )\n",
"last = R[R[\"origin\"] == ORIGINS[-1]].copy()\n",
"need = lambda peak, a: max(0, int(np.ceil((peak - 0.10) * a)))\n",
"q = []\n",
"for _, r in last.iterrows():\n",
" paths, _ = mc_forecast(cells[r[\"cell\"]], ORIGINS[-1]); q.append(need(np.percentile(paths.max(axis=1), 75), r[\"auth\"]))\n",
"last[\"pred_need\"] = q\n",
"last[\"act_need\"] = [need(r[\"act\"].max(), r[\"auth\"]) for _, r in last.iterrows()]\n",
"print(f\"\\n[์์ {ORIGINS[-1]}์ฃผ ๊ธฐ์ค ๋ณด์ถฉ ํ์ ์ธ์(๊ฒฝ๋ก ์ต๋ ๊ฒฐ์์ 75๋ฐฑ๋ถ์ ๊ธฐ์ค)] ์์ธก ํฉ๊ณ {int(last['pred_need'].sum())}๋ช
, ์ค์ ํฉ๊ณ {int(last['act_need'].sum())}๋ช
, ์
๋ณ ํ๊ท ์ ๋์ค์ฐจ {np.abs(last['pred_need'] - last['act_need']).mean():.2f}๋ช
\")\n",
"top = last.sort_values(\"pred_need\", ascending=False).head(4)\n",
"for _, r in top.iterrows():\n",
" print(f\" {r['cell'][0]} {r['cell'][1]}: ์์ธก {r['pred_need']}๋ช
(์ค์ {r['act_need']}๋ช
), ๋ณด์ถฉ ํ์ ํ๋ฅ {r['p_need']:.0%}, 13์ฃผ ๋ง ์์ ๊ฒฐ์์จ {r['v3'][-1]:.0%}\")\n",
"print(\"โป ์์ธก์ ๊ฐ์ ์๋ฃ ๊ธฐ๋ฐ ํ๋จ ๋ณด์กฐ์ด๋ฉฐ ๋ณด์ถฉ ์์ฒญ๊ณผ ๋ฐฐ์น๋ ์ธ์ฌ๊ด์ด ๊ฒฐ์ ํ๋ค. ๋ชจ๋ธ์ด ๋ชจ๋ฅด๋ ๋๋ ์ ์ถยทํ๊ฒฌ์ AAR์์ ์ฌ๋ณด์ ํ๋ค.\")\n",
"\n",
"best = top.iloc[0]\n",
"plt.figure(figsize=(8, 4)); x = np.arange(H)\n",
"plt.plot(x, best[\"act\"] * 100, \"k-\", label=\"actual\"); plt.plot(x, best[\"v3\"] * 100, \"b--\", label=\"stock-flow mean\"); plt.plot(x, best[\"v2\"] * 100, \"g-.\", label=\"ridge v2\"); plt.plot(x, best[\"naive\"] * 100, \"g:\", label=\"naive\")\n",
"plt.axhline(NEED_CUT * 100, color=\"r\", ls=\":\"); plt.xlabel(\"weeks ahead\"); plt.ylabel(\"vacancy rate (%)\"); plt.title(\"13-week vacancy forecast (highest-need cell)\"); plt.legend(); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
04 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๋กค๋ง ๊ฒ์ฆ] ์์ [52, 65, 78, 91] x ์
24๊ฐ = 96๊ฑด, ์์ธก 13์ฃผ\n",
"๊ฒฐ์ ์ธ์ MAE: ๋จ์ ์์ธก(๋ง์ง๋ง ๊ฐ ์ ์ง) 1.39๋ช
/ ํ๊ท v1 1.45๋ช
/ ํ๊ท v2(ํน์ง ์ฌ์ค๊ณ) 1.16๋ช
/ ์ฌ๊ณ -ํ๋ฆ ๋ชจ์ ํ๊ท 1.07๋ช
\n",
"์์ธก ์ง์ฐ: ํ๊ท ์
1๊ฐ ํ๊ท 5.7ms, ๋ชจ์ 300๊ฒฝ๋ก ํ๊ท 1.9ms/99๋ฐฑ๋ถ์ 5.4ms (24๊ฐ ์
์ ์ฒด 45ms, ๋ชฉํ 200ms ์ดํ: ๋ฌ์ฑ)\n",
"\n",
"[๋ณด์ถฉ ํ์(๊ฒฐ์์จ 15% ์ด์) ํ์ง] ์ค์ ํด๋น 35๊ฑด / 96๊ฑด\n",
" ๋จ์ ์์ธก ์ฌํ์จ 45.7%, ์ ๋ฐ๋ 100.0%, ์ค๊ฒฝ๋ณด 0๊ฑด\n",
" ํ๊ท v1 ์ฌํ์จ 45.7%, ์ ๋ฐ๋ 100.0%, ์ค๊ฒฝ๋ณด 0๊ฑด\n",
" ํ๊ท v2 ์ฌํ์จ 54.3%, ์ ๋ฐ๋ 100.0%, ์ค๊ฒฝ๋ณด 0๊ฑด\n",
" ๋ชจ์ ํ๋ฅ 0.50 ์ด์ ์ฌํ์จ 60.0%, ์ ๋ฐ๋ 87.5%, ์ค๊ฒฝ๋ณด 3๊ฑด\n",
" ๋ชจ์ ํ๋ฅ 0.30 ์ด์ ์ฌํ์จ 71.4%, ์ ๋ฐ๋ 67.6%, ์ค๊ฒฝ๋ณด 12๊ฑด\n",
" ๋ชจ์ ํ๋ฅ 0.15 ์ด์ ์ฌํ์จ 85.7%, ์ ๋ฐ๋ 55.6%, ์ค๊ฒฝ๋ณด 24๊ฑด\n",
"-> ์ ํ: ๋ชจ์ ํ๋ฅ 0.15 ์ด์์ '๋ณด์ถฉ ํ์ ๊ฐ๋ฅ์ฑ'์ผ๋ก ํ์, ์ฌํ์จ 85.7% (๋ชฉํ 85% ์ด์: ๋ฌ์ฑ), ๋์ ์ ๋ฐ๋ 56%๋ก ์ค๊ฒฝ๋ณด 24๊ฑด์ ์ฌ๋์ด ๊ฑธ๋ฌ์ผ ํจ\n",
"[์ค๊ฒฝ๋ณด ํจํด] ์ค๊ฒฝ๋ณด 24๊ฑด ์ค ์ค์ ์ต๋ ๊ฒฐ์์จ์ด 10~15%์ธ ๊ฒฝ๊ณ ๊ทผ์ฒ ์ฌ๋ก 67% -> ๊ต๋ ๋ณด์ถฉ ๋์ฐฉ ์์ ยท์ ์ถ ๊ท๋ชจ์ ๋ถํ์ค์ฑ์ด ์์ธ, ํ๋ฅ ์ด ๋ฎ์ ๊ฑด(0.15~0.5)์ '๊ด์ฐฐ'๋ก ํ์ํ๊ณ 0.5 ์ด์๋ง '๋ณด์ถฉ ํ์'๋ก ์ฌ๋ฆฐ๋ค\n",
"ํ๋ฅ 0.5 ์ด์ 24๊ฑด ์ค ์ค์ ๋ณด์ถฉ ํ์ 21๊ฑด(88%)\n",
"\n",
"[์์ 91์ฃผ ๊ธฐ์ค ๋ณด์ถฉ ํ์ ์ธ์(๊ฒฝ๋ก ์ต๋ ๊ฒฐ์์ 75๋ฐฑ๋ถ์ ๊ธฐ์ค)] ์์ธก ํฉ๊ณ 77๋ช
, ์ค์ ํฉ๊ณ 71๋ช
, ์
๋ณ ํ๊ท ์ ๋์ค์ฐจ 1.00๋ช
\n",
" ๊ฐ์C๋๋ ํต์ : ์์ธก 17๋ช
(์ค์ 15๋ช
), ๋ณด์ถฉ ํ์ ํ๋ฅ 100%, 13์ฃผ ๋ง ์์ ๊ฒฐ์์จ 49%\n",
" ๊ฐ์B๋๋ ๋ณด๊ธ: ์์ธก 16๋ช
(์ค์ 16๋ช
), ๋ณด์ถฉ ํ์ ํ๋ฅ 100%, 13์ฃผ ๋ง ์์ ๊ฒฐ์์จ 51%\n",
" ๊ฐ์A๋๋ ์ ๋น: ์์ธก 7๋ช
(์ค์ 8๋ช
), ๋ณด์ถฉ ํ์ ํ๋ฅ 100%, 13์ฃผ ๋ง ์์ ๊ฒฐ์์จ 22%\n",
" ๊ฐ์C๋๋ ์ ๋น: ์์ธก 4๋ช
(์ค์ 3๋ช
), ๋ณด์ถฉ ํ์ ํ๋ฅ 58%, 13์ฃผ ๋ง ์์ ๊ฒฐ์์จ 14%\n",
"โป ์์ธก์ ๊ฐ์ ์๋ฃ ๊ธฐ๋ฐ ํ๋จ ๋ณด์กฐ์ด๋ฉฐ ๋ณด์ถฉ ์์ฒญ๊ณผ ๋ฐฐ์น๋ ์ธ์ฌ๊ด์ด ๊ฒฐ์ ํ๋ค. ๋ชจ๋ธ์ด ๋ชจ๋ฅด๋ ๋๋ ์ ์ถยทํ๊ฒฌ์ AAR์์ ์ฌ๋ณด์ ํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
05. ์ธ๋ ฅ ๋ถ์กฑ ์ํ ์ง์ ์ฐ์ถ ๋ฐ ์ธ์ฌ ๋ฐฐ์น ์ถ์ฒ ์ ๋ขฐ๋ ์์ง ๊ฐ๋ฐ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ธ๋ ฅ ๋ถ์กฑ ์ํ ์ง์ ์ฐ์ถ ๋ฐ ์ธ์ฌ ๋ฐฐ์น ์ถ์ฒ ์ ๋ขฐ๋ ์์ง์ ๊ฐ๋ฐํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ธ๋ ฅ ๋ถ์กฑ ์ํ ์ง์๋ฅผ ์ฐ์ถํ๊ณ ์ ๋ขฐ๋(High/Med/Low) ๋ฑ๊ธ๊ณผ ํจ๊ป ์ธ์ฌ ๋ฐฐ์น(์ธ์ ์ด๋) ์ถ์ฒ์ ๋ง๋๋ ์ ๋ขฐ๋ ์์ง์ ๊ฐ๋ฐํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๋ณด์ถฉ ํ์ ํ๋ฅ ยท์์ ์ต๋ ๊ฒฐ์์จยท์ด๊ณผ๊ทผ๋ฌด ๋ถ๋ด์ผ๋ก ์ํ ์ง์๋ฅผ, ์์ธก ๊ฒฝ๋ก ๋ถ์ฐยท๋ชจ๋ธ ๊ฐ ์ผ์นยท์๋ฃ ์ถฉ๋ถ๋๋ก ์ ๋ขฐ๋๋ฅผ ๊ณ์ฐํด ๊ฐ์ค์น ๋งคํธ๋ฆญ์ค๋ก ๋ณด์ฌ ์ค๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/verification/adm01_confidence_engine.py` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ ๋ขฐ๋ ๋ฑ๊ธ(High/Med/Low) ๋ถ์ฌ ๊ธฐ์ค์ ๊ตฐ์ฌ์ ํ๋น์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์ค๊ฒฝ๋ณด์จ(FAR) 40% ์ด์ ๊ฐ์ถ, ์ ๋ขฐ๋ ์ค์ฝ์ด ์ฐ์ถ ์ผ๊ด์ฑ 90% ์ด์\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์ค๊ฒฝ๋ณด ํํฐ๋ง ์ ํ ๋น๊ต ์งํ ์ฐ์ถ ๋ฐ ํตํฉ ํ
์คํธ ์๋ฃ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ROC-AUC ๊ณก์ ๋ถ์ ๋ฐ ์๊ณ๊ฐ ๋ฏผ๊ฐ๋ ํ
์คํธ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"๋ถ๋ยท์ง๋ฌด ์
๋ณ ์ธ๋ ฅ ๋ถ์กฑ ์ํ ์ง์(0~100)์ ์ ๋ขฐ๋(High/Med/Low: ์์ธก ๋ถ์ฐยท๋ชจ๋ธ ๊ฐ ์ผ์นยท์๋ฃ ์ถฉ๋ถ๋ ๊ฐ์คํฉ)๋ฅผ ์ฐ์ถํ๊ณ , ์ ๋ขฐ๋ ๋ฎ์ ๊ฒฝ๋ณด๋ฅผ 'ํ์ธ ์์ฒญ'์ผ๋ก ๋ถ๋ฆฌํด ๋จ์ ๊ฒฝ๋ณด ๋๋น ์ค๊ฒฝ๋ณด์จ ๊ฐ์ถ๊ณผ ๊ฐ์ ์ค์ ๋ฐ๋ณต ์คํ ์ ๋ฑ๊ธ ์ผ๊ด์ฑ์ ๊ฒ์ฆํ๋ฉฐ ์ ๋ขฐ๋๋ฅผ ๋ฐ์ํ ์ธ์ฌ ๋ฐฐ์น(์ธ์ ์ด๋) ์ถ์ฒ์์ ๋ง๋ ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋ถ๋ ์ธ๋ ฅ ํํฉ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยทํธ์ ยท๋ณ๋ ฅยท๊ฐ์ธ ์ ๋ณด ์์)\n",
"# ๊ฐ์ ๋๋ 4๊ณณ x ์ง๋ฌด 6์ข
์ ์ฃผ๊ฐ ์ง๊ณ ์๋ฃ๋ง ๋ง๋ ๋ค. ๊ฐ์ธ ๋จ์ ์ ๋ณด(์ด๋ฆยท๊ตฐ๋ฒ ๋ฑ)๋ ๋ง๋ค์ง ์๋๋ค.\n",
"# ์ธ์์ ์ง๋ฌด๋ณ ์ง๊ณ ์์ด๋ฉฐ, ๊ฒฐ์ยทํด๊ฐยทํ์ยท๊ต์กยท๋ณด์ถฉยท์ ์ถ์ ๋ฐ์ํ ํ์ฌ์๊ณผ ์ด๊ณผ๊ทผ๋ฌด ์๊ฐ์ผ๋ก ๊ตฌ์ฑ๋๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"UNITS = [\"๊ฐ์A๋๋\", \"๊ฐ์B๋๋\", \"๊ฐ์C๋๋\", \"๊ฐ์D๋๋\"]\n",
"JOBS = [\"์์ \", \"ํต์ \", \"์ ๋น\", \"๋ณด๊ธ\", \"ํ์ \", \"์๋ฌด\"]\n",
"FIELDS = [\"unit\", \"job\", \"week\", \"authorized\", \"present\", \"vacancy\", \"leave\", \"sick\", \"training\", \"inflow\", \"outflow\", \"overtime_h\", \"night_ratio\", \"rotation_days\", \"fatigue_label\"]\n",
"HI_CUT = 55 # ๊ณ ํผ๋ก ํ์ ๊ธฐ์ค(๊ฐ์ ์ ์ 55 ์ด์)\n",
"\n",
"\n",
"def make_manning(seed=7, weeks=104, shock=None):\n",
" \"\"\"๋ถ๋ x ์ง๋ฌด 24๊ฐ ์
์ ์ฃผ๊ฐ ์ธ๋ ฅ ์๋ฃ. shock=(unit, job ๋๋ None(๋ถ๋ ์ ์ฒด), ์์์ฃผ, ์ฃผ๊ฐ ์ถ๊ฐ ์ ์ถ ํ๊ท ์) ํ ๊ฑด ๋๋ ๊ทธ ๋ชฉ๋ก์ด๋ฉด ๊ทธ ์
์ ๋๋ ๊ฒฐ์ ์์ธ์ ๋ฃ๋๋ค(AARยท์คํธ๋ ์ค์ฉ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" shocks = [shock] if shock and not isinstance(shock[0], tuple) else list(shock or [])\n",
" rows = []\n",
" for ui, u in enumerate(UNITS):\n",
" for ji, j in enumerate(JOBS):\n",
" auth = int(rng.integers(14, 42) * (0.9 + 0.06 * ui))\n",
" work = rng.uniform(0.92, 1.12) # ์ง๋ฌด๋ณ ์
๋ฌด๋ ๊ณ์\n",
" net = rng.uniform(-0.05, 0.03) # ๊ตฌ์กฐ์ ๋ณด์ถฉ ๋ถ์กฑ(์์)ยท๊ณผ์(์์) ๊ฒฝํฅ(์ฃผ๋น ์ธ์)\n",
" present = int(auth * rng.uniform(0.82, 0.98))\n",
" rot = int(rng.choice([60, 90, 120])) # ๊ต๋ยท๋ณด์ถฉ ์ฃผ๊ธฐ(์ผ)\n",
" for w in range(1, weeks + 1):\n",
" season = 1.0 + 0.9 * (abs(w % 52 - 22) < 4 or abs(w % 52 - 46) < 3) # ์ ์ถยท์ ์ญ ์ง์ค ์๊ธฐ\n",
" out = int(rng.poisson(0.22 * auth / 20 * season))\n",
" for s_ in shocks:\n",
" if s_[0] == u and (s_[1] is None or s_[1] == j) and w >= s_[2]:\n",
" out += int(rng.poisson(s_[3]))\n",
" inflow = int(rng.poisson(max(0.0, 0.2 * auth / 20 * (1 + net * 10)))) + (int(rng.integers(2, 6) * auth / 25) if w % (rot // 7) == 0 else 0)\n",
" present = int(np.clip(present - out + inflow, int(auth * 0.4), auth))\n",
" leave = int(rng.binomial(present, 0.05 + 0.05 * (abs(w % 52 - 31) < 3 or w % 52 >= 50)))\n",
" sick = int(rng.poisson(0.025 * present)); train = int(rng.binomial(present, 0.04 + 0.06 * (w % 13 == 6)))\n",
" avail = max(present - leave - sick - train, 1)\n",
" night = float(np.clip(rng.normal(0.25 + 0.05 * (j == \"์์ \") + 0.05 * (j == \"์๋ฌด\"), 0.05), 0.05, 0.6))\n",
" ot = float(np.clip(auth * 40 * work / avail - 40 + rng.normal(0, 1.5), 0, 40))\n",
" vac = auth - present\n",
" absent = (leave + sick + train) / max(present, 1)\n",
" fat = 100 * (0.38 * min(vac / auth / 0.25, 1.2) + 0.34 * min(ot / 20, 1.2) + 0.10 * min(night / 0.4, 1.2) + 0.18 * min(absent / 0.2, 1.2)) + rng.normal(0, 5)\n",
" rows.append((u, j, w, auth, present, vac, leave, sick, train, inflow, out, round(ot, 1), round(night, 2), rot, int(fat >= HI_CUT)))\n",
" return pd.DataFrame(rows, columns=FIELDS)\n",
"\n",
"\n",
"def cell_pivot(df, col=\"vacancy\"):\n",
" \"\"\"์ฃผ x (๋ถ๋,์ง๋ฌด) ํ.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 5] ์ธ๋ ฅ ๋ถ์กฑ ์ํ ์ง์ ์ฐ์ถ ๋ฐ ์ธ์ฌ ๋ฐฐ์น ์ถ์ฒ ์ ๋ขฐ๋ ์์ง ๊ฐ๋ฐ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"from sklearn.linear_model import Ridge\n",
"H = 13 # ์์ธก ๊ธฐ๊ฐ: ํฅํ 3๊ฐ์(13์ฃผ)\n",
"NEED_CUT = 0.15 # ๊ฒฐ์์จ 15% ์ด์์ด๋ฉด ๋ณด์ถฉ ํ์(๊ฐ์ ๊ธฐ์ค)\n",
"\n",
"\n",
"def season(w):\n",
" # [TODO : ํ์ ์ค์ต 5] ์ธ๋ ฅ ๋ถ์กฑ ์ํ ์ง์ ์ฐ์ถ ๋ฐ ์ธ์ฌ ๋ฐฐ์น ์ถ์ฒ ์ ๋ขฐ๋ ์์ง ๊ฐ๋ฐ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def feat(vr, ir, orr, wn, rot, v2=True):\n",
" \"\"\"vrยทirยทorr: ์ง๊ธ๊น์ง์ ๊ฒฐ์์จยท๋ณด์ถฉ์จยท์ ์ถ์จ ๋ชฉ๋ก, wn: ์์ธกํ ๋ค์ ์ฃผ์ฐจ, rot: ๊ต๋ ์ฃผ๊ธฐ(์ผ). v2๋ ๋ณด์ถฉยท์ ์ถ ํ๋ฆ๊ณผ ๋ฌ๋ ฅ ํน์ง์ ์ถ๊ฐํ๋ค.\"\"\"\n",
" f = [vr[-1], vr[-1] - vr[-2], vr[-1] - vr[-4]]\n",
" if v2:\n",
" step = max(rot // 7, 1)\n",
" f += [float(np.mean(orr[-12:]) - np.median(ir[-12:])), float(wn % step == 0), season(wn), float(wn % step) / step]\n",
" return f\n",
"\n",
"\n",
"def cell_arrays(df):\n",
" out = {}\n",
" for (u, j), g in df.sort_values(\"week\").groupby([\"unit\", \"job\"]):\n",
" a = g[\"authorized\"].to_numpy()\n",
" out[(u, j)] = {\"auth\": int(a[0]), \"rot\": int(g[\"rotation_days\"].iloc[0]), \"vr\": (g[\"vacancy\"].to_numpy() / a), \"ir\": g[\"inflow\"].to_numpy() / a, \"or\": g[\"outflow\"].to_numpy() / a}\n",
" return out\n",
"\n",
"\n",
"def fit_fc(cells, origin, v2=True, alpha=1.0):\n",
" X, y = [], []\n",
" for c in cells.values():\n",
" for t in range(12, origin - 1): # t์ฃผ๊น์ง ์ ๋ t+1์ฃผ(์ธ๋ฑ์ค t)์ ๊ฒฐ์์จ ๋ณํ๋ฅผ ๋งํ๋ค\n",
" X.append(feat(c[\"vr\"][:t], c[\"ir\"][:t], c[\"or\"][:t], t + 1, c[\"rot\"], v2)); y.append(c[\"vr\"][t] - c[\"vr\"][t - 1])\n",
" return Ridge(alpha=alpha).fit(np.array(X), np.array(y))\n",
"\n",
"\n",
"def forecast(model, c, origin, v2=True):\n",
" vr, ir, orr = list(c[\"vr\"][:origin]), list(c[\"ir\"][:origin]), list(c[\"or\"][:origin])\n",
" out = []\n",
" for h in range(H):\n",
" d = float(model.predict([feat(vr, ir, orr, origin + h + 1, c[\"rot\"], v2)])[0])\n",
" p = float(np.clip(vr[-1] + d, 0, 1))\n",
" out.append(p); vr.append(p); ir.append(float(np.median(ir[-12:]))); orr.append(float(np.mean(orr[-12:])))\n",
" return np.array(out)\n",
"\n",
"\n",
"def mc_forecast(c, origin, n_path=300, seed=0, out_mult=1.0, lam=0.0):\n",
" \"\"\"์ฌ๊ณ -ํ๋ฆ(stock-flow) ๋ชจ์: ์
์ ํ์ยท์ ์ถ ์ง์ค๊ธฐ ์ ์ถ์จ, ์ ๊ท ๋ณด์ถฉ์จ, ๊ต๋ ์ฃผ๊ธฐ ๋ณด์ถฉ ๊ท๋ชจ๋ฅผ ๊ณผ๊ฑฐ์์ ์ถ์ ํด ๊ฒฐ์ ๊ฒฝ๋ก n_path๊ฐ๋ฅผ ๋ง๋ ๋ค.\n",
" ๋ฐํ: (๊ฒฝ๋ก ๋ฐฐ์ด [n_path, H] ๊ฒฐ์์จ, ์ถ์ ๋ชจ์). out_mult๋ ์ ์ถ ๊ฐ์ ์ ํค์ฐ๋ ๋ณด์ ๊ณ์, lam์ ์ต๊ทผ 8์ฃผ ์ ์ถ์จ์ ๋ฐ์ํ๋ ๋น์ค(0~1, ๊ธ์ฆ ๊ฐ์ง ๋๋ง ์ ์ฉ, AAR ์ฌ๋ณด์ ์ฉ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" w = np.arange(1, origin + 1); step = max(c[\"rot\"] // 7, 1)\n",
" rot_w = (w % step == 0); sea = np.array([season(x) > 0 for x in w])\n",
" o, i = c[\"or\"][:origin], c[\"ir\"][:origin]\n",
" o_base = o[~sea].mean(); o_sea = o[sea].mean() if sea.sum() >= 6 else o_base * 1.9\n",
" o_rec = o[-8:].mean()\n",
" mu = max(o_base, 1e-6) * c[\"auth\"] * 8 # ์ต๊ทผ 8์ฃผ ํ์ ๊ธฐ๋ ์ ์ถ ์ธ์\n",
" use_rec = lam > 0 and o_rec * c[\"auth\"] * 8 > mu + 2.0 * np.sqrt(mu) # ํฌ์์ก ๊ธฐ์ค ์์ 2ํ์คํธ์ฐจ๋ฅผ ๋๋ '๊ธ์ฆ'์ผ ๋๋ง ์ต๊ทผ ๊ฐ์ ๋ฐ์\n",
" i_reg = i[~rot_w].mean(); batch = max(i[rot_w].mean() - i_reg, 0.0)\n",
" a = c[\"auth\"]; pres = np.full(n_path, a * (1 - c[\"vr\"][origin - 1]))\n",
" out = np.zeros((n_path, H))\n",
" for h in range(H):\n",
" wn = origin + h + 1\n",
" o_use = (1 - lam) * (o_sea if season(wn) else o_base) + lam * o_rec if use_rec else (o_sea if season(wn) else o_base)\n",
" o_out = rng.poisson(max(o_use, 1e-6) * a * out_mult, n_path)\n",
" o_in = rng.poisson(max(i_reg, 1e-6) * a, n_path) + (np.maximum(rng.normal(batch * a, 0.3 * batch * a, n_path), 0).round() if wn % step == 0 else 0)\n",
" pres = np.clip(pres - o_out + o_in, 0.4 * a, a)\n",
" out[:, h] = 1 - pres / a\n",
" return out, {\"o_base\": o_base, \"o_sea\": o_sea, \"i_reg\": i_reg, \"batch\": batch}\n",
"\n",
"df = make_manning(7, 104)\n",
"cells = cell_arrays(df)\n",
"ot = {k: g.sort_values(\"week\")[\"overtime_h\"].to_numpy() for k, g in df.groupby([\"unit\", \"job\"])}\n",
"CONF_W = {\"spread\": 0.5, \"agree\": 0.3, \"data\": 0.2} # ์ ๋ขฐ๋ ๊ฐ์ค์น ๋งคํธ๋ฆญ์ค(ํฉ 1)\n",
"RISK_W = {\"prob\": 0.5, \"peak\": 0.3, \"load\": 0.2} # ์ํ ์ง์ ๊ฐ์ค์น(ํฉ 1)\n",
"GRADE = [(60, \"๋์\"), (35, \"๋ณดํต\"), (0, \"๋ฎ์\")]\n",
"\n",
"\n",
"def assess(k, origin, seed=0, models=None):\n",
" c = cells[k]\n",
" paths, par = mc_forecast(c, origin, n_path=800, seed=seed)\n",
" peaks = paths.max(axis=1)\n",
" p_need = float((peaks >= NEED_CUT).mean())\n",
" load = float(np.clip(ot[k][origin - 4:origin].mean() / 20, 0, 1)) # ์ต๊ทผ 4์ฃผ ์ด๊ณผ๊ทผ๋ฌด ๋ถ๋ด\n",
" risk = 100 * (RISK_W[\"prob\"] * p_need + RISK_W[\"peak\"] * min(peaks.mean() / 0.30, 1) + RISK_W[\"load\"] * load)\n",
" iqr = float(np.percentile(peaks, 75) - np.percentile(peaks, 25))\n",
" v2_peak = forecast(models[origin], c, origin, True).max() # ์๋ก ๋ค๋ฅธ ๋ชจ๋ธ(ํ๊ท)๊ณผ์ ์ผ์น\n",
" agree = 1 - min(abs(v2_peak - peaks.mean()) / 0.10, 1)\n",
" conf = CONF_W[\"spread\"] * (1 - min(iqr / 0.12, 1)) + CONF_W[\"agree\"] * agree + CONF_W[\"data\"] * min(origin / 52, 1)\n",
" return {\"cell\": k, \"origin\": origin, \"risk\": risk, \"p_need\": p_need, \"peak\": float(peaks.mean()), \"iqr\": iqr, \"agree\": agree, \"conf\": conf,\n",
" \"grade\": next(g for t, g in GRADE if risk >= t), \"level\": \"High\" if conf >= 0.70 else \"Med\" if conf >= 0.45 else \"Low\"}\n",
"\n",
"\n",
"ORIGINS = [52, 65, 78, 91]\n",
"models = {o: fit_fc(cells, o, True) for o in ORIGINS}\n",
"rows = [assess(k, o, models=models) for o in ORIGINS for k in cells]\n",
"A = pd.DataFrame(rows)\n",
"A[\"act_need\"] = [cells[r.cell][\"vr\"][r.origin:r.origin + H].max() >= NEED_CUT for r in A.itertuples()]\n",
"A[\"base_alarm\"] = [(cells[r.cell][\"vr\"][r.origin - 1] >= 0.10) or (ot[r.cell][r.origin - 4:r.origin].mean() >= 15) for r in A.itertuples()]\n",
"A[\"alarm\"] = (A[\"risk\"] >= 50) & (A[\"level\"] != \"Low\") # ์ ๋ขฐ๋ ๋ฎ์ ๊ฒฝ๋ณด๋ 'ํ์ธ ์์ฒญ'์ผ๋ก ๋ถ๋ฆฌ\n",
"A[\"check\"] = (A[\"risk\"] >= 50) & (A[\"level\"] == \"Low\")\n",
"far = lambda m: float(((m) & ~A[\"act_need\"]).sum() / max(m.sum(), 1))\n",
"fa0, fa1 = far(A[\"base_alarm\"]), far(A[\"alarm\"])\n",
"print(f\"[๊ฒฝ๋ณด ๋น๊ต] ์
x ์์ {len(A)}๊ฑด, ์ค์ ๋ณด์ถฉ ํ์ {int(A['act_need'].sum())}๊ฑด\")\n",
"print(f\" ๋จ์ ๊ฒฝ๋ณด(๊ฒฐ์์จ 10% ์ด์ ๋๋ ์ด๊ณผ๊ทผ๋ฌด 15h ์ด์): ๊ฒฝ๋ณด {int(A['base_alarm'].sum())}๊ฑด, ์ค๊ฒฝ๋ณด์จ {fa0:.1%}, ์ฌํ์จ {float((A['base_alarm'] & A['act_need']).sum() / A['act_need'].sum()):.1%}\")\n",
"print(f\" ์ ๋ขฐ๋ ๋ฐ์ ๊ฒฝ๋ณด(์ํ 50 ์ด์ + ์ ๋ขฐ๋ Med ์ด์): ๊ฒฝ๋ณด {int(A['alarm'].sum())}๊ฑด, ์ค๊ฒฝ๋ณด์จ {fa1:.1%}, ์ฌํ์จ {float((A['alarm'] & A['act_need']).sum() / A['act_need'].sum()):.1%}, ํ์ธ ์์ฒญ {int(A['check'].sum())}๊ฑด\")\n",
"print(f\" ์ค๊ฒฝ๋ณด์จ {fa0:.1%} -> {fa1:.1%}: {(fa0 - fa1) / max(fa0, 1e-9):.0%} ๊ฐ์ถ (๋ชฉํ 40% ์ด์: {'๋ฌ์ฑ' if (fa0 - fa1) / max(fa0, 1e-9) >= 0.4 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# ์ ์ ์ฐ์ถ ์ผ๊ด์ฑ: ๊ฐ์ ์
๋ ฅ์ผ๋ก ๋์ ์๋๋ง ๋ฐ๊ฟ 5ํ ์ฐ์ถํ์ ๋ ์ํ ๋ฑ๊ธ์ด ๊ฐ์ ์
์ ๋น์จ\n",
"last = [assess(k, 91, seed=s, models=models) for s in range(5) for k in cells]\n",
"G = pd.DataFrame(last)\n",
"cons = G.groupby(\"cell\")[\"grade\"].nunique().eq(1).mean(); consl = G.groupby(\"cell\")[\"level\"].nunique().eq(1).mean()\n",
"print(f\"\\n[์ผ๊ด์ฑ] ์๋ 5ํ ๋ฐ๋ณต: ์ํ ๋ฑ๊ธ ๋์ผ ์
{cons:.0%}, ์ ๋ขฐ๋ ๋ฑ๊ธ ๋์ผ ์
{consl:.0%}, ์ํ ์ง์ ํ์คํธ์ฐจ ํ๊ท {G.groupby('cell')['risk'].std().mean():.2f}์ (๋ชฉํ 90% ์ด์: {'๋ฌ์ฑ' if cons >= 0.9 else '๋ฏธ๋ฌ'})\")\n",
"print(\"๊ฐ์ค์น ๋งคํธ๋ฆญ์ค: ์ํ ์ง์\", RISK_W, \"/ ์ ๋ขฐ๋\", CONF_W, \"/ ๋ฑ๊ธ ๊ธฐ์ค ์ํ\", GRADE, \"์ ๋ขฐ๋ High 0.70, Med 0.45\")\n",
"\n",
"# ํ์ฌ ์์ (91์ฃผ) ์ํ ์์์ ๋ถํ์ค์ฑ ํ์\n",
"now = A[A[\"origin\"] == 91].sort_values(\"risk\", ascending=False)\n",
"print(\"\\n[์ํ ์ง์ ์์ 6๊ฐ ์
(91์ฃผ ์์ )] ๋ง๋: ์ต๋ ๊ฒฐ์์จ ํ๊ท , ๊ตฌ๊ฐ: ๊ฒฝ๋ก IQR\")\n",
"for r in now.head(6).itertuples():\n",
" bar = \"#\" * int(r.peak * 40)\n",
" print(f\" {r.cell[0]} {r.cell[1]}: ์ํ {r.risk:4.0f}({r.grade}) ์ ๋ขฐ๋ {r.conf:.2f}({r.level}) ๋ณด์ถฉ ํ์ ํ๋ฅ {r.p_need:.0%} {bar} +-{r.iqr / 2:.0%}\")\n",
"\n",
"# ์ธ์ฌ ๋ฐฐ์น ์ถ์ฒ: ์ํ ๋์ ์
๋ก ๊ฐ์ ์ง๋ฌด์ ์ฌ์ ์
(์ํ ๋ฎ์, ๊ฒฐ์ ์ ์)์์ ์ด๋\n",
"rec = []\n",
"for r in now[now[\"grade\"] == \"๋์\"].itertuples():\n",
" k = r.cell; need = max(1, int(np.ceil((r.peak - 0.10) * cells[k][\"auth\"])))\n",
" pool = [s for s in now[(now[\"grade\"] == \"๋ฎ์\")].itertuples() if s.cell[1] == k[1] and s.cell[0] != k[0]]\n",
" for s in sorted(pool, key=lambda s: s.peak)[:1]:\n",
" sp = int(max(0, np.floor((0.08 - s.peak) * cells[s.cell][\"auth\"])))\n",
" mv = min(need, max(sp, 1))\n",
" rec.append((k, s.cell, mv, need, \"High\" if min(r.conf, s.conf) >= 0.7 else \"Med\" if min(r.conf, s.conf) >= 0.45 else \"Low\"))\n",
"print(\"\\n[๋ฐฐ์น ์ถ์ฒ์: ๊ฐ์ ์ง๋ฌด ์ฌ์ ์
-> ๋ถ์กฑ ์
, ์ ๋ขฐ๋๋ ๋ ์
์ค ๋ฎ์ ๊ฐ]\")\n",
"for to, fr, mv, need, cl in rec:\n",
" print(f\" {fr[0]}({fr[1]}) -> {to[0]}({to[1]}): {mv}๋ช
์ด๋ ์ ์ (ํ์ {need}๋ช
), ์ ๋ขฐ๋ {cl}\" + (\" <- ์ฌ๋ ํ์ธ ํ์\" if cl != \"High\" else \"\"))\n",
"if not rec:\n",
" print(\" ํด๋น ์ง๋ฌด์ ์ฌ์ ์
์ด ์์ด ์ธ๋ถ ๋ณด์ถฉ ์์ฒญ ๋์\")\n",
"print(\"โป ์ํ ์ง์ยท์ ๋ขฐ๋๋ ํ๋จ ๋ณด์กฐ ์งํ์ด๋ฉฐ ํต๊ณ์ ์ ๋ขฐ๊ตฌ๊ฐ์ด ์๋๋ค. ์ธ์ ๋ฐฐ์น๋ ์ธ์ฌ๊ด์ด ๊ฒํ ยท์น์ธํ๋ค(M7).\")\n",
"\n",
"fig, ax = plt.subplots(figsize=(8, 4))\n",
"t = now.head(8); ax.barh([f\"{r.cell[0][2]}{i + 1}\" for i, r in enumerate(t.itertuples())], t[\"risk\"], xerr=t[\"iqr\"] * 100 / 2, color=[\"tab:red\" if g == \"๋์\" else \"tab:orange\" if g == \"๋ณดํต\" else \"tab:green\" for g in t[\"grade\"]])\n",
"ax.invert_yaxis(); ax.set_xlabel(\"risk index (0-100), error bar = forecast IQR\"); ax.set_title(\"Top-8 risk cells (label: unit letter + rank)\"); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
05 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ฒฝ๋ณด ๋น๊ต] ์
x ์์ 96๊ฑด, ์ค์ ๋ณด์ถฉ ํ์ 35๊ฑด\n",
" ๋จ์ ๊ฒฝ๋ณด(๊ฒฐ์์จ 10% ์ด์ ๋๋ ์ด๊ณผ๊ทผ๋ฌด 15h ์ด์): ๊ฒฝ๋ณด 33๊ฑด, ์ค๊ฒฝ๋ณด์จ 36.4%, ์ฌํ์จ 60.0%\n",
" ์ ๋ขฐ๋ ๋ฐ์ ๊ฒฝ๋ณด(์ํ 50 ์ด์ + ์ ๋ขฐ๋ Med ์ด์): ๊ฒฝ๋ณด 18๊ฑด, ์ค๊ฒฝ๋ณด์จ 11.1%, ์ฌํ์จ 45.7%, ํ์ธ ์์ฒญ 6๊ฑด\n",
" ์ค๊ฒฝ๋ณด์จ 36.4% -> 11.1%: 69% ๊ฐ์ถ (๋ชฉํ 40% ์ด์: ๋ฌ์ฑ)\n",
"\n",
"[์ผ๊ด์ฑ] ์๋ 5ํ ๋ฐ๋ณต: ์ํ ๋ฑ๊ธ ๋์ผ ์
96%, ์ ๋ขฐ๋ ๋ฑ๊ธ ๋์ผ ์
88%, ์ํ ์ง์ ํ์คํธ์ฐจ ํ๊ท 0.74์ (๋ชฉํ 90% ์ด์: ๋ฌ์ฑ)\n",
"๊ฐ์ค์น ๋งคํธ๋ฆญ์ค: ์ํ ์ง์ {'prob': 0.5, 'peak': 0.3, 'load': 0.2} / ์ ๋ขฐ๋ {'spread': 0.5, 'agree': 0.3, 'data': 0.2} / ๋ฑ๊ธ ๊ธฐ์ค ์ํ [(60, '๋์'), (35, '๋ณดํต'), (0, '๋ฎ์')] ์ ๋ขฐ๋ High 0.70, Med 0.45\n",
"\n",
"[์ํ ์ง์ ์์ 6๊ฐ ์
(91์ฃผ ์์ )] ๋ง๋: ์ต๋ ๊ฒฐ์์จ ํ๊ท , ๊ตฌ๊ฐ: ๊ฒฝ๋ก IQR\n",
" ๊ฐ์B๋๋ ๋ณด๊ธ: ์ํ 100(๋์) ์ ๋ขฐ๋ 0.83(High) ๋ณด์ถฉ ํ์ ํ๋ฅ 100% ####################### +-0%\n",
" ๊ฐ์C๋๋ ํต์ : ์ํ 100(๋์) ์ ๋ขฐ๋ 0.47(Med) ๋ณด์ถฉ ํ์ ํ๋ฅ 100% ###################### +-3%\n",
" ๊ฐ์A๋๋ ์ ๋น: ์ํ 94(๋์) ์ ๋ขฐ๋ 0.64(Med) ๋ณด์ถฉ ํ์ ํ๋ฅ 100% ########## +-3%\n",
" ๊ฐ์D๋๋ ํต์ : ์ํ 67(๋์) ์ ๋ขฐ๋ 0.45(Low) ๋ณด์ถฉ ํ์ ํ๋ฅ 62% ####### +-5%\n",
" ๊ฐ์A๋๋ ๋ณด๊ธ: ์ํ 63(๋์) ์ ๋ขฐ๋ 0.52(Med) ๋ณด์ถฉ ํ์ ํ๋ฅ 60% ####### +-5%\n",
" ๊ฐ์C๋๋ ์ ๋น: ์ํ 54(๋ณดํต) ์ ๋ขฐ๋ 0.51(Med) ๋ณด์ถฉ ํ์ ํ๋ฅ 54% ####### +-4%\n",
"\n",
"[๋ฐฐ์น ์ถ์ฒ์: ๊ฐ์ ์ง๋ฌด ์ฌ์ ์
-> ๋ถ์กฑ ์
, ์ ๋ขฐ๋๋ ๋ ์
์ค ๋ฎ์ ๊ฐ]\n",
" ๊ฐ์D๋๋(๋ณด๊ธ) -> ๊ฐ์B๋๋(๋ณด๊ธ): 1๋ช
์ด๋ ์ ์ (ํ์ 16๋ช
), ์ ๋ขฐ๋ High\n",
" ๊ฐ์B๋๋(ํต์ ) -> ๊ฐ์C๋๋(ํต์ ): 1๋ช
์ด๋ ์ ์ (ํ์ 16๋ช
), ์ ๋ขฐ๋ Med <- ์ฌ๋ ํ์ธ ํ์\n",
" ๊ฐ์D๋๋(์ ๋น) -> ๊ฐ์A๋๋(์ ๋น): 1๋ช
์ด๋ ์ ์ (ํ์ 6๋ช
), ์ ๋ขฐ๋ Med <- ์ฌ๋ ํ์ธ ํ์\n",
" ๊ฐ์B๋๋(ํต์ ) -> ๊ฐ์D๋๋(ํต์ ): 1๋ช
์ด๋ ์ ์ (ํ์ 3๋ช
), ์ ๋ขฐ๋ Low <- ์ฌ๋ ํ์ธ ํ์\n",
" ๊ฐ์D๋๋(๋ณด๊ธ) -> ๊ฐ์A๋๋(๋ณด๊ธ): 1๋ช
์ด๋ ์ ์ (ํ์ 3๋ช
), ์ ๋ขฐ๋ Med <- ์ฌ๋ ํ์ธ ํ์\n",
"โป ์ํ ์ง์ยท์ ๋ขฐ๋๋ ํ๋จ ๋ณด์กฐ ์งํ์ด๋ฉฐ ํต๊ณ์ ์ ๋ขฐ๊ตฌ๊ฐ์ด ์๋๋ค. ์ธ์ ๋ฐฐ์น๋ ์ธ์ฌ๊ด์ด ๊ฒํ ยท์น์ธํ๋ค(M7).\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
06. ์งํ๊ด/์ธ์ฌ์ฐธ๋ชจ์ฉ ๋ถ๋ ๊ทผ๋ฌด๋ถํ ํํฉ ๋ฐ ๊ฒฐ์ ์์ธก ๊ฐ์ํ ๋์๋ณด๋ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋ถ๋ ๊ทผ๋ฌด๋ถํ ํํฉ ๋ฐ ๊ฒฐ์ ์์ธก ๊ฐ์ํ ๋์๋ณด๋๋ฅผ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์งํ๊ดยท์ธ์ฌ์ฐธ๋ชจ๊ฐ ๋ถ๋ ๊ทผ๋ฌด๋ถํ ํํฉ๊ณผ ๊ฒฐ์ ์์ธก์ ํ๋์ ๋ณผ ์ ์๋ ๊ฐ์ํ ๋์๋ณด๋๋ฅผ ๊ตฌํํ๊ณ ์๋ต ์ฑ๋ฅ์ ์ธก์ ํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๋ถ๋ x ์ง๋ฌด 24๊ฐ ์
์ ํ์ฌ ๊ฒฐ์์จยท13์ฃผ ํ ์์ธกยท์ด๊ณผ๊ทผ๋ฌดยทํผ๋ก๋ยท๋ณด์ถฉ ํ์ ํ๋ฅ ์ ์ํ(๊ธด๊ธยท์ฃผ์ยท์ ์) ๊ฐ์กฐ์ ํจ๊ป ํ์ ํํธ๋งต์ผ๋ก ๋ง๋ค๊ณ ์ํ ์
์ ์ถ์ธยท์์ธก ๊ตฌ๊ฐ ๊ทธ๋ํ๋ฅผ ๊ทธ๋ฆฐ๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/ui/adm01_dashboard.py (๋๋ ์น UI ์ปดํฌ๋ํธ)` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ ์ ์์ ์ํฉํ์ผ๋ก์์ ์ง๊ด์ฑ ๋ฐ ์ธ์ง ๋ถํ ๊ฐ์๋\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ํ๋ฉด ๊ฐฑ์ ์ฃผ๊ธฐ 1์ด ์ด๋ด, UI ์๋ต์ฑ 100ms ์ด๋ด\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์ฃผ์ ์งํ ์๊ฐํ ์ปดํฌ๋ํธ 4์ข
์ด์ ๋ ๋๋ง ๋ฐ ์ค์๊ฐ ๋ฐ์ดํฐ ์ฐ๋\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ์ฌ์ฉ์ ์ธํฐ๋์
ํ
์คํธ ๋ฐ ๋ชจ์ ๋ฐ์ดํฐ ์คํธ๋ฆผ ๋ ๋๋ง ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"๋ถ๋ยท์ง๋ฌด ์
์ ํ์ฌ ๊ฒฐ์์จยท์ด๊ณผ๊ทผ๋ฌดยทํผ๋ก๋์ ํฅํ 13์ฃผ ๊ฒฐ์ ์์ธกยท๋ณด์ถฉ ํ์ ํ๋ฅ ์ ์งํ๊ดยท์ธ์ฌ์ฐธ๋ชจ์ฉ ํํฉํ(ํยทํํธ๋งตยท์ถ์ธ ๊ทธ๋ํ, ๊ธด๊ธ ๊ฒฝ๋ณด ๊ฐ์กฐ, ์ผ๊ฐ ๋ชจ๋)์ผ๋ก ๋ณด์ฌ ์ฃผ๊ณ , ์ ์ฒด ๊ฐฑ์ ์๊ฐ(1์ด ์ด๋ด)๊ณผ ํํฐยท๋๋ฆด๋ค์ดยท๋ชจ๋ ์ ํ ์๋ต ์๊ฐ(100ms ์ด๋ด)์ ์ธก์ ํ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋ถ๋ ์ธ๋ ฅ ํํฉ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยทํธ์ ยท๋ณ๋ ฅยท๊ฐ์ธ ์ ๋ณด ์์)\n",
"# ๊ฐ์ ๋๋ 4๊ณณ x ์ง๋ฌด 6์ข
์ ์ฃผ๊ฐ ์ง๊ณ ์๋ฃ๋ง ๋ง๋ ๋ค. ๊ฐ์ธ ๋จ์ ์ ๋ณด(์ด๋ฆยท๊ตฐ๋ฒ ๋ฑ)๋ ๋ง๋ค์ง ์๋๋ค.\n",
"# ์ธ์์ ์ง๋ฌด๋ณ ์ง๊ณ ์์ด๋ฉฐ, ๊ฒฐ์ยทํด๊ฐยทํ์ยท๊ต์กยท๋ณด์ถฉยท์ ์ถ์ ๋ฐ์ํ ํ์ฌ์๊ณผ ์ด๊ณผ๊ทผ๋ฌด ์๊ฐ์ผ๋ก ๊ตฌ์ฑ๋๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"UNITS = [\"๊ฐ์A๋๋\", \"๊ฐ์B๋๋\", \"๊ฐ์C๋๋\", \"๊ฐ์D๋๋\"]\n",
"JOBS = [\"์์ \", \"ํต์ \", \"์ ๋น\", \"๋ณด๊ธ\", \"ํ์ \", \"์๋ฌด\"]\n",
"FIELDS = [\"unit\", \"job\", \"week\", \"authorized\", \"present\", \"vacancy\", \"leave\", \"sick\", \"training\", \"inflow\", \"outflow\", \"overtime_h\", \"night_ratio\", \"rotation_days\", \"fatigue_label\"]\n",
"HI_CUT = 55 # ๊ณ ํผ๋ก ํ์ ๊ธฐ์ค(๊ฐ์ ์ ์ 55 ์ด์)\n",
"\n",
"\n",
"def make_manning(seed=7, weeks=104, shock=None):\n",
" \"\"\"๋ถ๋ x ์ง๋ฌด 24๊ฐ ์
์ ์ฃผ๊ฐ ์ธ๋ ฅ ์๋ฃ. shock=(unit, job ๋๋ None(๋ถ๋ ์ ์ฒด), ์์์ฃผ, ์ฃผ๊ฐ ์ถ๊ฐ ์ ์ถ ํ๊ท ์) ํ ๊ฑด ๋๋ ๊ทธ ๋ชฉ๋ก์ด๋ฉด ๊ทธ ์
์ ๋๋ ๊ฒฐ์ ์์ธ์ ๋ฃ๋๋ค(AARยท์คํธ๋ ์ค์ฉ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" shocks = [shock] if shock and not isinstance(shock[0], tuple) else list(shock or [])\n",
" rows = []\n",
" for ui, u in enumerate(UNITS):\n",
" for ji, j in enumerate(JOBS):\n",
" auth = int(rng.integers(14, 42) * (0.9 + 0.06 * ui))\n",
" work = rng.uniform(0.92, 1.12) # ์ง๋ฌด๋ณ ์
๋ฌด๋ ๊ณ์\n",
" net = rng.uniform(-0.05, 0.03) # ๊ตฌ์กฐ์ ๋ณด์ถฉ ๋ถ์กฑ(์์)ยท๊ณผ์(์์) ๊ฒฝํฅ(์ฃผ๋น ์ธ์)\n",
" present = int(auth * rng.uniform(0.82, 0.98))\n",
" rot = int(rng.choice([60, 90, 120])) # ๊ต๋ยท๋ณด์ถฉ ์ฃผ๊ธฐ(์ผ)\n",
" for w in range(1, weeks + 1):\n",
" season = 1.0 + 0.9 * (abs(w % 52 - 22) < 4 or abs(w % 52 - 46) < 3) # ์ ์ถยท์ ์ญ ์ง์ค ์๊ธฐ\n",
" out = int(rng.poisson(0.22 * auth / 20 * season))\n",
" for s_ in shocks:\n",
" if s_[0] == u and (s_[1] is None or s_[1] == j) and w >= s_[2]:\n",
" out += int(rng.poisson(s_[3]))\n",
" inflow = int(rng.poisson(max(0.0, 0.2 * auth / 20 * (1 + net * 10)))) + (int(rng.integers(2, 6) * auth / 25) if w % (rot // 7) == 0 else 0)\n",
" present = int(np.clip(present - out + inflow, int(auth * 0.4), auth))\n",
" leave = int(rng.binomial(present, 0.05 + 0.05 * (abs(w % 52 - 31) < 3 or w % 52 >= 50)))\n",
" sick = int(rng.poisson(0.025 * present)); train = int(rng.binomial(present, 0.04 + 0.06 * (w % 13 == 6)))\n",
" avail = max(present - leave - sick - train, 1)\n",
" night = float(np.clip(rng.normal(0.25 + 0.05 * (j == \"์์ \") + 0.05 * (j == \"์๋ฌด\"), 0.05), 0.05, 0.6))\n",
" ot = float(np.clip(auth * 40 * work / avail - 40 + rng.normal(0, 1.5), 0, 40))\n",
" vac = auth - present\n",
" absent = (leave + sick + train) / max(present, 1)\n",
" fat = 100 * (0.38 * min(vac / auth / 0.25, 1.2) + 0.34 * min(ot / 20, 1.2) + 0.10 * min(night / 0.4, 1.2) + 0.18 * min(absent / 0.2, 1.2)) + rng.normal(0, 5)\n",
" rows.append((u, j, w, auth, present, vac, leave, sick, train, inflow, out, round(ot, 1), round(night, 2), rot, int(fat >= HI_CUT)))\n",
" return pd.DataFrame(rows, columns=FIELDS)\n",
"\n",
"\n",
"def cell_pivot(df, col=\"vacancy\"):\n",
" \"\"\"์ฃผ x (๋ถ๋,์ง๋ฌด) ํ.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 6] ์งํ๊ด/์ธ์ฌ์ฐธ๋ชจ์ฉ ๋ถ๋ ๊ทผ๋ฌด๋ถํ ํํฉ ๋ฐ ๊ฒฐ์ ์์ธก ๊ฐ์ํ ๋์๋ณด๋ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"from sklearn.linear_model import Ridge\n",
"H = 13 # ์์ธก ๊ธฐ๊ฐ: ํฅํ 3๊ฐ์(13์ฃผ)\n",
"NEED_CUT = 0.15 # ๊ฒฐ์์จ 15% ์ด์์ด๋ฉด ๋ณด์ถฉ ํ์(๊ฐ์ ๊ธฐ์ค)\n",
"\n",
"\n",
"def season(w):\n",
" # [TODO : ํ์ ์ค์ต 6] ์งํ๊ด/์ธ์ฌ์ฐธ๋ชจ์ฉ ๋ถ๋ ๊ทผ๋ฌด๋ถํ ํํฉ ๋ฐ ๊ฒฐ์ ์์ธก ๊ฐ์ํ ๋์๋ณด๋ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def feat(vr, ir, orr, wn, rot, v2=True):\n",
" \"\"\"vrยทirยทorr: ์ง๊ธ๊น์ง์ ๊ฒฐ์์จยท๋ณด์ถฉ์จยท์ ์ถ์จ ๋ชฉ๋ก, wn: ์์ธกํ ๋ค์ ์ฃผ์ฐจ, rot: ๊ต๋ ์ฃผ๊ธฐ(์ผ). v2๋ ๋ณด์ถฉยท์ ์ถ ํ๋ฆ๊ณผ ๋ฌ๋ ฅ ํน์ง์ ์ถ๊ฐํ๋ค.\"\"\"\n",
" f = [vr[-1], vr[-1] - vr[-2], vr[-1] - vr[-4]]\n",
" if v2:\n",
" step = max(rot // 7, 1)\n",
" f += [float(np.mean(orr[-12:]) - np.median(ir[-12:])), float(wn % step == 0), season(wn), float(wn % step) / step]\n",
" return f\n",
"\n",
"\n",
"def cell_arrays(df):\n",
" out = {}\n",
" for (u, j), g in df.sort_values(\"week\").groupby([\"unit\", \"job\"]):\n",
" a = g[\"authorized\"].to_numpy()\n",
" out[(u, j)] = {\"auth\": int(a[0]), \"rot\": int(g[\"rotation_days\"].iloc[0]), \"vr\": (g[\"vacancy\"].to_numpy() / a), \"ir\": g[\"inflow\"].to_numpy() / a, \"or\": g[\"outflow\"].to_numpy() / a}\n",
" return out\n",
"\n",
"\n",
"def fit_fc(cells, origin, v2=True, alpha=1.0):\n",
" X, y = [], []\n",
" for c in cells.values():\n",
" for t in range(12, origin - 1): # t์ฃผ๊น์ง ์ ๋ t+1์ฃผ(์ธ๋ฑ์ค t)์ ๊ฒฐ์์จ ๋ณํ๋ฅผ ๋งํ๋ค\n",
" X.append(feat(c[\"vr\"][:t], c[\"ir\"][:t], c[\"or\"][:t], t + 1, c[\"rot\"], v2)); y.append(c[\"vr\"][t] - c[\"vr\"][t - 1])\n",
" return Ridge(alpha=alpha).fit(np.array(X), np.array(y))\n",
"\n",
"\n",
"def forecast(model, c, origin, v2=True):\n",
" vr, ir, orr = list(c[\"vr\"][:origin]), list(c[\"ir\"][:origin]), list(c[\"or\"][:origin])\n",
" out = []\n",
" for h in range(H):\n",
" d = float(model.predict([feat(vr, ir, orr, origin + h + 1, c[\"rot\"], v2)])[0])\n",
" p = float(np.clip(vr[-1] + d, 0, 1))\n",
" out.append(p); vr.append(p); ir.append(float(np.median(ir[-12:]))); orr.append(float(np.mean(orr[-12:])))\n",
" return np.array(out)\n",
"\n",
"\n",
"def mc_forecast(c, origin, n_path=300, seed=0, out_mult=1.0, lam=0.0):\n",
" \"\"\"์ฌ๊ณ -ํ๋ฆ(stock-flow) ๋ชจ์: ์
์ ํ์ยท์ ์ถ ์ง์ค๊ธฐ ์ ์ถ์จ, ์ ๊ท ๋ณด์ถฉ์จ, ๊ต๋ ์ฃผ๊ธฐ ๋ณด์ถฉ ๊ท๋ชจ๋ฅผ ๊ณผ๊ฑฐ์์ ์ถ์ ํด ๊ฒฐ์ ๊ฒฝ๋ก n_path๊ฐ๋ฅผ ๋ง๋ ๋ค.\n",
" ๋ฐํ: (๊ฒฝ๋ก ๋ฐฐ์ด [n_path, H] ๊ฒฐ์์จ, ์ถ์ ๋ชจ์). out_mult๋ ์ ์ถ ๊ฐ์ ์ ํค์ฐ๋ ๋ณด์ ๊ณ์, lam์ ์ต๊ทผ 8์ฃผ ์ ์ถ์จ์ ๋ฐ์ํ๋ ๋น์ค(0~1, ๊ธ์ฆ ๊ฐ์ง ๋๋ง ์ ์ฉ, AAR ์ฌ๋ณด์ ์ฉ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" w = np.arange(1, origin + 1); step = max(c[\"rot\"] // 7, 1)\n",
" rot_w = (w % step == 0); sea = np.array([season(x) > 0 for x in w])\n",
" o, i = c[\"or\"][:origin], c[\"ir\"][:origin]\n",
" o_base = o[~sea].mean(); o_sea = o[sea].mean() if sea.sum() >= 6 else o_base * 1.9\n",
" o_rec = o[-8:].mean()\n",
" mu = max(o_base, 1e-6) * c[\"auth\"] * 8 # ์ต๊ทผ 8์ฃผ ํ์ ๊ธฐ๋ ์ ์ถ ์ธ์\n",
" use_rec = lam > 0 and o_rec * c[\"auth\"] * 8 > mu + 2.0 * np.sqrt(mu) # ํฌ์์ก ๊ธฐ์ค ์์ 2ํ์คํธ์ฐจ๋ฅผ ๋๋ '๊ธ์ฆ'์ผ ๋๋ง ์ต๊ทผ ๊ฐ์ ๋ฐ์\n",
" i_reg = i[~rot_w].mean(); batch = max(i[rot_w].mean() - i_reg, 0.0)\n",
" a = c[\"auth\"]; pres = np.full(n_path, a * (1 - c[\"vr\"][origin - 1]))\n",
" out = np.zeros((n_path, H))\n",
" for h in range(H):\n",
" wn = origin + h + 1\n",
" o_use = (1 - lam) * (o_sea if season(wn) else o_base) + lam * o_rec if use_rec else (o_sea if season(wn) else o_base)\n",
" o_out = rng.poisson(max(o_use, 1e-6) * a * out_mult, n_path)\n",
" o_in = rng.poisson(max(i_reg, 1e-6) * a, n_path) + (np.maximum(rng.normal(batch * a, 0.3 * batch * a, n_path), 0).round() if wn % step == 0 else 0)\n",
" pres = np.clip(pres - o_out + o_in, 0.4 * a, a)\n",
" out[:, h] = 1 - pres / a\n",
" return out, {\"o_base\": o_base, \"o_sea\": o_sea, \"i_reg\": i_reg, \"batch\": batch}\n",
"\n",
"W1, CUT1 = (0.3, 0.6, 0.1), 55 # ํผ๋ก๋ ๊ฐ์ค์นยท๊ธฐ์ค(M3 ํ๋ ๊ฒฐ๊ณผ ์์)\n",
"PALETTE = {\"day\": {\"bg\": \"white\", \"fg\": \"black\", \"cmap\": \"YlOrRd\", \"alert\": \"red\"}, \"night\": {\"bg\": \"#10151c\", \"fg\": \"#d8dee9\", \"cmap\": \"magma\", \"alert\": \"#ffb000\"}}\n",
"\n",
"\n",
"def refresh(df):\n",
" \"\"\"์ ์๋ฃ์์ ํํฉํ ๋ฐ์ดํฐ๋ฅผ ๋ชจ๋ ๋ค์ ๊ณ์ฐํ๋ค(์ ์ฒด ๊ฐฑ์ ).\"\"\"\n",
" cells = cell_arrays(df)\n",
" last = df[df[\"week\"] == df[\"week\"].max()].set_index([\"unit\", \"job\"])\n",
" rows = []\n",
" for k, c in cells.items():\n",
" paths, _ = mc_forecast(c, len(c[\"vr\"]), n_path=300)\n",
" peaks = paths.max(axis=1); p = float((peaks >= NEED_CUT).mean()); r = last.loc[k]\n",
" fat = min(100 * (W1[0] * min(r[\"vacancy\"] / r[\"authorized\"] / .25, 1.2) + W1[1] * min(r[\"overtime_h\"] / 20, 1.2) + W1[2] * min(r[\"night_ratio\"] / .4, 1.2)), 100)\n",
" risk = 100 * (0.5 * p + 0.3 * min(peaks.mean() / .30, 1) + 0.2 * min(r[\"overtime_h\"] / 20, 1))\n",
" rows.append({\"unit\": k[0], \"job\": k[1], \"auth\": c[\"auth\"], \"present\": int(r[\"present\"]), \"vac_now\": c[\"vr\"][-1], \"vac_13w\": float(paths.mean(0)[-1]), \"peak\": float(peaks.mean()),\n",
" \"overtime\": float(r[\"overtime_h\"]), \"fatigue\": fat, \"p_need\": p, \"risk\": risk, \"band\": np.percentile(paths, [10, 90], axis=0), \"mean\": paths.mean(0), \"hist\": c[\"vr\"][-26:]})\n",
" v = pd.DataFrame(rows)\n",
" v[\"status\"] = np.where(v[\"risk\"] >= 80, \"๊ธด๊ธ\", np.where(v[\"risk\"] >= 50, \"์ฃผ์\", \"์ ์\"))\n",
" return v\n",
"\n",
"\n",
"def view(v, unit=None, job=None, mode=\"day\"):\n",
" \"\"\"ํํฐยท์ ๋ ฌยท๊ฐ์กฐ๋ฅผ ์ ์ฉํ ํ๋ฉด ๋ฐ์ดํฐ์ ์ ์ค์ (์๋ต ์๊ฐ ์ธก์ ๋์).\"\"\"\n",
" s = v\n",
" if unit:\n",
" s = s[s[\"unit\"] == unit]\n",
" if job:\n",
" s = s[s[\"job\"] == job]\n",
" return s.sort_values(\"risk\", ascending=False), PALETTE[mode]\n",
"\n",
"\n",
"df = make_manning(7, 104)\n",
"t0 = time.perf_counter(); V = refresh(df); t_refresh = time.perf_counter() - t0\n",
"print(f\"[์ ์ฒด ๊ฐฑ์ ] ์
{len(V)}๊ฐ ์ฌ๊ณ์ฐ(์์ธก 300๊ฒฝ๋ก ํฌํจ) {t_refresh * 1000:.0f}ms (๋ชฉํ 1์ด ์ด๋ด: {'๋ฌ์ฑ' if t_refresh <= 1.0 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# 1) ํํฉํ: ์ํ๋ณ ์ง๊ณ์ ์์ ์ํ ์
\n",
"print(\"\\n[์งํ๊ด ํํฉํ - ๊ฐ์ ๋๋ 4๊ณณ x ์ง๋ฌด 6์ข
] ๊ธฐ์ค ์ฃผ์ฐจ %d\" % df[\"week\"].max())\n",
"print(\"์ํ ์ง๊ณ:\", V[\"status\"].value_counts().reindex([\"๊ธด๊ธ\", \"์ฃผ์\", \"์ ์\"]).fillna(0).astype(int).to_dict(), f\"/ ํธ์ {int(V['auth'].sum())}๋ช
, ํ์ฌ์ {int(V['present'].sum())}๋ช
, ์ ์ฒด ๊ฒฐ์์จ {1 - V['present'].sum() / V['auth'].sum():.1%}\")\n",
"show, _ = view(V)\n",
"print(f\"{'์ํ':4s} {'๋ถ๋':8s} {'์ง๋ฌด':4s} {'๊ฒฐ์์จ':>6s} {'13์ฃผํ':>6s} {'์ด๊ณผ๊ทผ๋ฌด':>7s} {'ํผ๋ก๋':>5s} {'๋ณด์ถฉํ๋ฅ ':>6s}\")\n",
"for r in show.head(7).itertuples():\n",
" mark = \"[๊ธด๊ธ]\" if r.status == \"๊ธด๊ธ\" else \"[์ฃผ์]\" if r.status == \"์ฃผ์\" else \" \"\n",
" print(f\"{mark} {r.unit:8s} {r.job:4s} {r.vac_now:6.0%} {r.vac_13w:6.0%} {r.overtime:6.1f}h {r.fatigue:5.0f} {r.p_need:6.0%}\")\n",
"print(\"\\n[๋ถ๋ x ์ง๋ฌด ๊ฒฐ์์จ ํํธ๋งต(ํ์ฌ -> 13์ฃผ ํ), * = ๋ณด์ถฉ ํ์ ํ๋ฅ 50% ์ด์]\")\n",
"H = V.pivot(index=\"unit\", columns=\"job\", values=\"vac_now\"); H2 = V.pivot(index=\"unit\", columns=\"job\", values=\"vac_13w\"); PN = V.pivot(index=\"unit\", columns=\"job\", values=\"p_need\")\n",
"print(\" \" + \" \".join(f\"{j:>9s}\" for j in JOBS))\n",
"for u in H.index:\n",
" print(f\"{u:8s}\" + \" \".join(f\"{H.loc[u, j]:4.0%}>{H2.loc[u, j]:3.0%}{'*' if PN.loc[u, j] >= .5 else ' '}\" for j in JOBS))\n",
"\n",
"# 2) ์๋ต ์๊ฐ: ํํฐ ๋ณ๊ฒฝ, ์
๋๋ฆด๋ค์ด, ์ผ๊ฐ ๋ชจ๋ ์ ํ์ ๊ฐ๊ฐ 50ํ ์ธก์ \n",
"ops = {\"๋ถ๋ ํํฐ\": lambda: view(V, unit=\"๊ฐ์C๋๋\"), \"์ง๋ฌด ํํฐ\": lambda: view(V, job=\"ํต์ \"), \"์
๋๋ฆด๋ค์ด\": lambda: view(V, \"๊ฐ์B๋๋\", \"๋ณด๊ธ\")[0].iloc[0][\"band\"].copy(), \"์ผ๊ฐ ๋ชจ๋ ์ ํ\": lambda: view(V, mode=\"night\")}\n",
"print(\"\\n[UI ์๋ต ์๊ฐ 50ํ ์ธก์ ]\")\n",
"worst = 0\n",
"for name, fn in ops.items():\n",
" ts = []\n",
" for _ in range(50):\n",
" t0 = time.perf_counter(); fn(); ts.append((time.perf_counter() - t0) * 1000)\n",
" worst = max(worst, np.percentile(ts, 95))\n",
" print(f\" {name:10s} ํ๊ท {np.mean(ts):.3f}ms, 95๋ฐฑ๋ถ์ {np.percentile(ts, 95):.3f}ms\")\n",
"print(f\"์ต๋ 95๋ฐฑ๋ถ์ {worst:.3f}ms (๋ชฉํ 100ms ์ด๋ด: {'๋ฌ์ฑ' if worst <= 100 else '๋ฏธ๋ฌ'}), ํ๋ฉด ๊ฐฑ์ {t_refresh * 1000:.0f}ms (๋ชฉํ 1์ด ์ด๋ด: {'๋ฌ์ฑ' if t_refresh <= 1 else '๋ฏธ๋ฌ'})\")\n",
"print(\"โป ์ผ๊ฐ ๋ชจ๋๋ ๋ฐฐ๊ฒฝยท๊ธ์ยท๊ฒฝ๋ณด ์๋ง ๋ฐ๊พธ๋ฉฐ ์์น๋ ๊ฐ๋ค. ๊ธด๊ธยท์ฃผ์ ํ์๋ ํ๋จ ๋ณด์กฐ์ด๋ฉฐ ๋ณด์ถฉยท๋ฐฐ์น ๊ฒฐ์ ์ ์ธ์ฌ๊ด์ด ํ๋ค.\")\n",
"\n",
"\n",
"def draw(mode):\n",
" pal = PALETTE[mode]\n",
" fig, ax = plt.subplots(1, 2, figsize=(11, 4), facecolor=pal[\"bg\"])\n",
" for a in ax:\n",
" a.set_facecolor(pal[\"bg\"]); a.tick_params(colors=pal[\"fg\"])\n",
" for s in a.spines.values():\n",
" s.set_color(pal[\"fg\"])\n",
" im = ax[0].imshow(H2.to_numpy() * 100, cmap=pal[\"cmap\"], aspect=\"auto\")\n",
" ax[0].set_xticks(range(len(JOBS))); ax[0].set_xticklabels([f\"job{i + 1}\" for i in range(len(JOBS))]); ax[0].set_yticks(range(len(UNITS))); ax[0].set_yticklabels([\"A\", \"B\", \"C\", \"D\"])\n",
" for i, u in enumerate(H.index):\n",
" for j, jb in enumerate(JOBS):\n",
" ax[0].text(j, i, f\"{H2.loc[u, jb] * 100:.0f}{'!' if PN.loc[u, jb] >= .5 else ''}\", ha=\"center\", va=\"center\", color=pal[\"alert\"] if PN.loc[u, jb] >= .5 else pal[\"fg\"], fontsize=8)\n",
" ax[0].set_title(\"Forecast vacancy rate in 13 weeks (%, ! = need)\", color=pal[\"fg\"])\n",
" top = V.sort_values(\"risk\", ascending=False).iloc[0]; x = np.arange(-25, 14)\n",
" ax[1].plot(x[:26], top[\"hist\"] * 100, color=pal[\"fg\"], label=\"history\"); ax[1].plot(x[26:], top[\"mean\"] * 100, color=pal[\"alert\"], label=\"forecast\")\n",
" ax[1].fill_between(x[26:], top[\"band\"][0] * 100, top[\"band\"][1] * 100, color=pal[\"alert\"], alpha=.25); ax[1].axhline(NEED_CUT * 100, color=pal[\"fg\"], ls=\":\")\n",
" ax[1].set_title(\"Top-risk cell: vacancy trend and 80% band\", color=pal[\"fg\"]); ax[1].legend(facecolor=pal[\"bg\"], labelcolor=pal[\"fg\"]); ax[1].set_xlabel(\"weeks from now\", color=pal[\"fg\"])\n",
" plt.tight_layout(); plt.show()\n",
"\n",
"\n",
"draw(\"day\"); draw(\"night\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
06 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[์ ์ฒด ๊ฐฑ์ ] ์
24๊ฐ ์ฌ๊ณ์ฐ(์์ธก 300๊ฒฝ๋ก ํฌํจ) 51ms (๋ชฉํ 1์ด ์ด๋ด: ๋ฌ์ฑ)\n",
"\n",
"[์งํ๊ด ํํฉํ - ๊ฐ์ ๋๋ 4๊ณณ x ์ง๋ฌด 6์ข
] ๊ธฐ์ค ์ฃผ์ฐจ 104\n",
"์ํ ์ง๊ณ: {'๊ธด๊ธ': 5, '์ฃผ์': 2, '์ ์': 17} / ํธ์ 732๋ช
, ํ์ฌ์ 659๋ช
, ์ ์ฒด ๊ฒฐ์์จ 10.0%\n",
"์ํ ๋ถ๋ ์ง๋ฌด ๊ฒฐ์์จ 13์ฃผํ ์ด๊ณผ๊ทผ๋ฌด ํผ๋ก๋ ๋ณด์ถฉํ๋ฅ \n",
"[๊ธด๊ธ] ๊ฐ์A๋๋ ์ ๋น 29% 26% 28.4h 100 100%\n",
"[๊ธด๊ธ] ๊ฐ์B๋๋ ๋ณด๊ธ 48% 56% 40.0h 100 100%\n",
"[๊ธด๊ธ] ๊ฐ์C๋๋ ํต์ 42% 46% 40.0h 100 100%\n",
"[๊ธด๊ธ] ๊ฐ์A๋๋ ๋ณด๊ธ 18% 23% 22.7h 97 95%\n",
"[๊ธด๊ธ] ๊ฐ์D๋๋ ์๋ฌด 22% 13% 19.7h 93 99%\n",
"[์ฃผ์] ๊ฐ์D๋๋ ํต์ 14% 9% 17.9h 76 67%\n",
"[์ฃผ์] ๊ฐ์A๋๋ ์๋ฌด 14% 6% 11.1h 57 46%\n",
"\n",
"[๋ถ๋ x ์ง๋ฌด ๊ฒฐ์์จ ํํธ๋งต(ํ์ฌ -> 13์ฃผ ํ), * = ๋ณด์ถฉ ํ์ ํ๋ฅ 50% ์ด์]\n",
" ์์ ํต์ ์ ๋น ๋ณด๊ธ ํ์ ์๋ฌด\n",
"๊ฐ์A๋๋ 0%> 4% 6%> 5% 29%>26%* 18%>23%* 6%> 5% 14%> 6% \n",
"๊ฐ์B๋๋ 3%> 3% 0%> 5% 5%> 5% 48%>56%* 0%> 4% 0%> 3% \n",
"๊ฐ์C๋๋ 6%> 3% 42%>46%* 11%>11% 0%> 4% 0%> 5% 5%> 6% \n",
"๊ฐ์D๋๋ 3%> 6% 14%> 9%* 0%> 4% 0%> 2% 12%> 5% 22%>13%*\n",
"\n",
"[UI ์๋ต ์๊ฐ 50ํ ์ธก์ ]\n",
" ๋ถ๋ ํํฐ ํ๊ท 0.786ms, 95๋ฐฑ๋ถ์ 1.028ms\n",
" ์ง๋ฌด ํํฐ ํ๊ท 0.766ms, 95๋ฐฑ๋ถ์ 0.889ms\n",
" ์
๋๋ฆด๋ค์ด ํ๊ท 1.201ms, 95๋ฐฑ๋ถ์ 1.630ms\n",
" ์ผ๊ฐ ๋ชจ๋ ์ ํ ํ๊ท 0.283ms, 95๋ฐฑ๋ถ์ 0.350ms\n",
"์ต๋ 95๋ฐฑ๋ถ์ 1.630ms (๋ชฉํ 100ms ์ด๋ด: ๋ฌ์ฑ), ํ๋ฉด ๊ฐฑ์ 51ms (๋ชฉํ 1์ด ์ด๋ด: ๋ฌ์ฑ)\n",
"โป ์ผ๊ฐ ๋ชจ๋๋ ๋ฐฐ๊ฒฝยท๊ธ์ยท๊ฒฝ๋ณด ์๋ง ๋ฐ๊พธ๋ฉฐ ์์น๋ ๊ฐ๋ค. ๊ธด๊ธยท์ฃผ์ ํ์๋ ํ๋จ ๋ณด์กฐ์ด๋ฉฐ ๋ณด์ถฉยท๋ฐฐ์น ๊ฒฐ์ ์ ์ธ์ฌ๊ด์ด ํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
07. ์ธ์ฌ ๋ณด์ถฉ ์ฐ์ ์์ ๊ฒฐ์ ์ ์ธ์ฌ๊ด ์๋ ์กฐ์ ๋ฐ ์น์ธ(HITL) ์ํฌํ๋ก์ฐ ์ค๊ณ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ธ์ฌ ๋ณด์ถฉ ์ฐ์ ์์ ๊ฒฐ์ ์ ์ธ์ฌ๊ด ์๋ ์กฐ์ ๋ฐ ์น์ธ(HITL) ์ํฌํ๋ก์ฐ๋ฅผ ์ค๊ณํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ธ๋ ฅ ๋ณด์ถฉ ์ฐ์ ์์ ๊ฒฐ์ ์ ์ธ์ฌ๋ด๋น-์ธ์ฌ๊ด(์๋ ์กฐ์ )-์งํ๊ด(์น์ธ)์ด ๊ฑฐ์น๋ HITL ์ํฌํ๋ก์ฐ๋ก ์ค๊ณํ๊ณ ์ฑ๊ธํ ์๋ ํ์ ์ ๋ง๋ ์ฒด๊ณ๋ฅผ ๊ฒ์ฆํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ญํ ๋ณ ๊ถํ๊ณผ ๋จ๊ณ ์์๋ฅผ ์ ์ํ๊ณ ์์คํ
์ ์ ์๋ง, ํ์ ์ ์ฌ๋๋ง ํ๋๋ก ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/hitl/adm01_approval_workflow.py ๋ฐ docs/adm01_hitl_guide.md` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์งํ ์ฑ
์ ๋ถ๊ณ์ ๋ช
ํํ ๋ฐ ์ฑ๊ธํ ์๋ ๋จ์ ๋ฐฉ์ง ์ฒด๊ณ ๋ถํฉ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์ธ๊ฐ ๊ฒํ ํ(Queue) ๋๊ธฐ์๊ฐ ๋ฐ ์น์ธ/๋ฐ๋ ค ์ฒ๋ฆฌ ๋ก๊น
์ฑ๊ณต๋ฅ 100%\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์น์ธ/๋ฐ๋ ค ์ธํฐํ์ด์ค ๋์ ๋ฐ ๊ฐ์ฌ ๋ก๊ทธ(Audit Log) ์์ฑ ๊ฒ์ฆ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ๋ชจ์ ์กฐ์์ ์น์ธ/๋ฐ๋ ค ์๋ฎฌ๋ ์ด์
๋ฐ ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์ธ๋ ฅ ๋ณด์ถฉยท์ฌ๋ฐฐ์น ์ฐ์ ์์ ์์ ์ธ์ฌ๋ด๋น ์์ -> ์ธ์ฌ๊ด ๊ฒํ (์๋ ์กฐ์ ๊ฐ๋ฅ) -> ์งํ๊ด ์น์ธ์ HITL ์ํฌํ๋ก์ฐ๋ก ์ฒ๋ฆฌํ๋, ์์คํ
์๋ ํ์ ๊ธ์งยท์กฐ์ ์ ํ ๊ฐ๊ณผ ์ฌ์ ๊ธฐ๋กยท๋ฐ๋ ค ์ฌ์ ํ์ํยท๊ถํ ์ฐํ ์ฐจ๋จยทํด์ ์ฒด์ธ ๊ฐ์ฌ ๋ก๊ทธ๋ฅผ ๊ฒ์ฆํ๊ณ ์ํ๋ ์ฐ์ ํ(๋๊ธฐ ์๊ฐ ๋
ธํ ๊ฐ์ฐ)๊ฐ ์ ์
์ ์ถ๋ณด๋ค ๊ธด๊ธ ๊ฑด ๋๊ธฐ๋ฅผ ์ค์ด๋์ง ๋น๊ตํ๋ค.\"\"\"\n",
"import hashlib\n",
"import json\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋ถ๋ ์ธ๋ ฅ ํํฉ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยทํธ์ ยท๋ณ๋ ฅยท๊ฐ์ธ ์ ๋ณด ์์)\n",
"# ๊ฐ์ ๋๋ 4๊ณณ x ์ง๋ฌด 6์ข
์ ์ฃผ๊ฐ ์ง๊ณ ์๋ฃ๋ง ๋ง๋ ๋ค. ๊ฐ์ธ ๋จ์ ์ ๋ณด(์ด๋ฆยท๊ตฐ๋ฒ ๋ฑ)๋ ๋ง๋ค์ง ์๋๋ค.\n",
"# ์ธ์์ ์ง๋ฌด๋ณ ์ง๊ณ ์์ด๋ฉฐ, ๊ฒฐ์ยทํด๊ฐยทํ์ยท๊ต์กยท๋ณด์ถฉยท์ ์ถ์ ๋ฐ์ํ ํ์ฌ์๊ณผ ์ด๊ณผ๊ทผ๋ฌด ์๊ฐ์ผ๋ก ๊ตฌ์ฑ๋๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"UNITS = [\"๊ฐ์A๋๋\", \"๊ฐ์B๋๋\", \"๊ฐ์C๋๋\", \"๊ฐ์D๋๋\"]\n",
"JOBS = [\"์์ \", \"ํต์ \", \"์ ๋น\", \"๋ณด๊ธ\", \"ํ์ \", \"์๋ฌด\"]\n",
"FIELDS = [\"unit\", \"job\", \"week\", \"authorized\", \"present\", \"vacancy\", \"leave\", \"sick\", \"training\", \"inflow\", \"outflow\", \"overtime_h\", \"night_ratio\", \"rotation_days\", \"fatigue_label\"]\n",
"HI_CUT = 55 # ๊ณ ํผ๋ก ํ์ ๊ธฐ์ค(๊ฐ์ ์ ์ 55 ์ด์)\n",
"\n",
"\n",
"def make_manning(seed=7, weeks=104, shock=None):\n",
" \"\"\"๋ถ๋ x ์ง๋ฌด 24๊ฐ ์
์ ์ฃผ๊ฐ ์ธ๋ ฅ ์๋ฃ. shock=(unit, job ๋๋ None(๋ถ๋ ์ ์ฒด), ์์์ฃผ, ์ฃผ๊ฐ ์ถ๊ฐ ์ ์ถ ํ๊ท ์) ํ ๊ฑด ๋๋ ๊ทธ ๋ชฉ๋ก์ด๋ฉด ๊ทธ ์
์ ๋๋ ๊ฒฐ์ ์์ธ์ ๋ฃ๋๋ค(AARยท์คํธ๋ ์ค์ฉ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" shocks = [shock] if shock and not isinstance(shock[0], tuple) else list(shock or [])\n",
" rows = []\n",
" for ui, u in enumerate(UNITS):\n",
" for ji, j in enumerate(JOBS):\n",
" auth = int(rng.integers(14, 42) * (0.9 + 0.06 * ui))\n",
" work = rng.uniform(0.92, 1.12) # ์ง๋ฌด๋ณ ์
๋ฌด๋ ๊ณ์\n",
" net = rng.uniform(-0.05, 0.03) # ๊ตฌ์กฐ์ ๋ณด์ถฉ ๋ถ์กฑ(์์)ยท๊ณผ์(์์) ๊ฒฝํฅ(์ฃผ๋น ์ธ์)\n",
" present = int(auth * rng.uniform(0.82, 0.98))\n",
" rot = int(rng.choice([60, 90, 120])) # ๊ต๋ยท๋ณด์ถฉ ์ฃผ๊ธฐ(์ผ)\n",
" for w in range(1, weeks + 1):\n",
" season = 1.0 + 0.9 * (abs(w % 52 - 22) < 4 or abs(w % 52 - 46) < 3) # ์ ์ถยท์ ์ญ ์ง์ค ์๊ธฐ\n",
" out = int(rng.poisson(0.22 * auth / 20 * season))\n",
" for s_ in shocks:\n",
" if s_[0] == u and (s_[1] is None or s_[1] == j) and w >= s_[2]:\n",
" out += int(rng.poisson(s_[3]))\n",
" inflow = int(rng.poisson(max(0.0, 0.2 * auth / 20 * (1 + net * 10)))) + (int(rng.integers(2, 6) * auth / 25) if w % (rot // 7) == 0 else 0)\n",
" present = int(np.clip(present - out + inflow, int(auth * 0.4), auth))\n",
" leave = int(rng.binomial(present, 0.05 + 0.05 * (abs(w % 52 - 31) < 3 or w % 52 >= 50)))\n",
" sick = int(rng.poisson(0.025 * present)); train = int(rng.binomial(present, 0.04 + 0.06 * (w % 13 == 6)))\n",
" avail = max(present - leave - sick - train, 1)\n",
" night = float(np.clip(rng.normal(0.25 + 0.05 * (j == \"์์ \") + 0.05 * (j == \"์๋ฌด\"), 0.05), 0.05, 0.6))\n",
" ot = float(np.clip(auth * 40 * work / avail - 40 + rng.normal(0, 1.5), 0, 40))\n",
" vac = auth - present\n",
" absent = (leave + sick + train) / max(present, 1)\n",
" fat = 100 * (0.38 * min(vac / auth / 0.25, 1.2) + 0.34 * min(ot / 20, 1.2) + 0.10 * min(night / 0.4, 1.2) + 0.18 * min(absent / 0.2, 1.2)) + rng.normal(0, 5)\n",
" rows.append((u, j, w, auth, present, vac, leave, sick, train, inflow, out, round(ot, 1), round(night, 2), rot, int(fat >= HI_CUT)))\n",
" return pd.DataFrame(rows, columns=FIELDS)\n",
"\n",
"\n",
"def cell_pivot(df, col=\"vacancy\"):\n",
" \"\"\"์ฃผ x (๋ถ๋,์ง๋ฌด) ํ.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 7] ์ธ์ฌ ๋ณด์ถฉ ์ฐ์ ์์ ๊ฒฐ์ ์ ์ธ์ฌ๊ด ์๋ ์กฐ์ ๋ฐ ์น์ธ(HITL) ์ํฌํ๋ก์ฐ ์ค๊ณ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"from sklearn.linear_model import Ridge\n",
"H = 13 # ์์ธก ๊ธฐ๊ฐ: ํฅํ 3๊ฐ์(13์ฃผ)\n",
"NEED_CUT = 0.15 # ๊ฒฐ์์จ 15% ์ด์์ด๋ฉด ๋ณด์ถฉ ํ์(๊ฐ์ ๊ธฐ์ค)\n",
"\n",
"\n",
"def season(w):\n",
" # [TODO : ํ์ ์ค์ต 7] ์ธ์ฌ ๋ณด์ถฉ ์ฐ์ ์์ ๊ฒฐ์ ์ ์ธ์ฌ๊ด ์๋ ์กฐ์ ๋ฐ ์น์ธ(HITL) ์ํฌํ๋ก์ฐ ์ค๊ณ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def feat(vr, ir, orr, wn, rot, v2=True):\n",
" \"\"\"vrยทirยทorr: ์ง๊ธ๊น์ง์ ๊ฒฐ์์จยท๋ณด์ถฉ์จยท์ ์ถ์จ ๋ชฉ๋ก, wn: ์์ธกํ ๋ค์ ์ฃผ์ฐจ, rot: ๊ต๋ ์ฃผ๊ธฐ(์ผ). v2๋ ๋ณด์ถฉยท์ ์ถ ํ๋ฆ๊ณผ ๋ฌ๋ ฅ ํน์ง์ ์ถ๊ฐํ๋ค.\"\"\"\n",
" f = [vr[-1], vr[-1] - vr[-2], vr[-1] - vr[-4]]\n",
" if v2:\n",
" step = max(rot // 7, 1)\n",
" f += [float(np.mean(orr[-12:]) - np.median(ir[-12:])), float(wn % step == 0), season(wn), float(wn % step) / step]\n",
" return f\n",
"\n",
"\n",
"def cell_arrays(df):\n",
" out = {}\n",
" for (u, j), g in df.sort_values(\"week\").groupby([\"unit\", \"job\"]):\n",
" a = g[\"authorized\"].to_numpy()\n",
" out[(u, j)] = {\"auth\": int(a[0]), \"rot\": int(g[\"rotation_days\"].iloc[0]), \"vr\": (g[\"vacancy\"].to_numpy() / a), \"ir\": g[\"inflow\"].to_numpy() / a, \"or\": g[\"outflow\"].to_numpy() / a}\n",
" return out\n",
"\n",
"\n",
"def fit_fc(cells, origin, v2=True, alpha=1.0):\n",
" X, y = [], []\n",
" for c in cells.values():\n",
" for t in range(12, origin - 1): # t์ฃผ๊น์ง ์ ๋ t+1์ฃผ(์ธ๋ฑ์ค t)์ ๊ฒฐ์์จ ๋ณํ๋ฅผ ๋งํ๋ค\n",
" X.append(feat(c[\"vr\"][:t], c[\"ir\"][:t], c[\"or\"][:t], t + 1, c[\"rot\"], v2)); y.append(c[\"vr\"][t] - c[\"vr\"][t - 1])\n",
" return Ridge(alpha=alpha).fit(np.array(X), np.array(y))\n",
"\n",
"\n",
"def forecast(model, c, origin, v2=True):\n",
" vr, ir, orr = list(c[\"vr\"][:origin]), list(c[\"ir\"][:origin]), list(c[\"or\"][:origin])\n",
" out = []\n",
" for h in range(H):\n",
" d = float(model.predict([feat(vr, ir, orr, origin + h + 1, c[\"rot\"], v2)])[0])\n",
" p = float(np.clip(vr[-1] + d, 0, 1))\n",
" out.append(p); vr.append(p); ir.append(float(np.median(ir[-12:]))); orr.append(float(np.mean(orr[-12:])))\n",
" return np.array(out)\n",
"\n",
"\n",
"def mc_forecast(c, origin, n_path=300, seed=0, out_mult=1.0, lam=0.0):\n",
" \"\"\"์ฌ๊ณ -ํ๋ฆ(stock-flow) ๋ชจ์: ์
์ ํ์ยท์ ์ถ ์ง์ค๊ธฐ ์ ์ถ์จ, ์ ๊ท ๋ณด์ถฉ์จ, ๊ต๋ ์ฃผ๊ธฐ ๋ณด์ถฉ ๊ท๋ชจ๋ฅผ ๊ณผ๊ฑฐ์์ ์ถ์ ํด ๊ฒฐ์ ๊ฒฝ๋ก n_path๊ฐ๋ฅผ ๋ง๋ ๋ค.\n",
" ๋ฐํ: (๊ฒฝ๋ก ๋ฐฐ์ด [n_path, H] ๊ฒฐ์์จ, ์ถ์ ๋ชจ์). out_mult๋ ์ ์ถ ๊ฐ์ ์ ํค์ฐ๋ ๋ณด์ ๊ณ์, lam์ ์ต๊ทผ 8์ฃผ ์ ์ถ์จ์ ๋ฐ์ํ๋ ๋น์ค(0~1, ๊ธ์ฆ ๊ฐ์ง ๋๋ง ์ ์ฉ, AAR ์ฌ๋ณด์ ์ฉ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" w = np.arange(1, origin + 1); step = max(c[\"rot\"] // 7, 1)\n",
" rot_w = (w % step == 0); sea = np.array([season(x) > 0 for x in w])\n",
" o, i = c[\"or\"][:origin], c[\"ir\"][:origin]\n",
" o_base = o[~sea].mean(); o_sea = o[sea].mean() if sea.sum() >= 6 else o_base * 1.9\n",
" o_rec = o[-8:].mean()\n",
" mu = max(o_base, 1e-6) * c[\"auth\"] * 8 # ์ต๊ทผ 8์ฃผ ํ์ ๊ธฐ๋ ์ ์ถ ์ธ์\n",
" use_rec = lam > 0 and o_rec * c[\"auth\"] * 8 > mu + 2.0 * np.sqrt(mu) # ํฌ์์ก ๊ธฐ์ค ์์ 2ํ์คํธ์ฐจ๋ฅผ ๋๋ '๊ธ์ฆ'์ผ ๋๋ง ์ต๊ทผ ๊ฐ์ ๋ฐ์\n",
" i_reg = i[~rot_w].mean(); batch = max(i[rot_w].mean() - i_reg, 0.0)\n",
" a = c[\"auth\"]; pres = np.full(n_path, a * (1 - c[\"vr\"][origin - 1]))\n",
" out = np.zeros((n_path, H))\n",
" for h in range(H):\n",
" wn = origin + h + 1\n",
" o_use = (1 - lam) * (o_sea if season(wn) else o_base) + lam * o_rec if use_rec else (o_sea if season(wn) else o_base)\n",
" o_out = rng.poisson(max(o_use, 1e-6) * a * out_mult, n_path)\n",
" o_in = rng.poisson(max(i_reg, 1e-6) * a, n_path) + (np.maximum(rng.normal(batch * a, 0.3 * batch * a, n_path), 0).round() if wn % step == 0 else 0)\n",
" pres = np.clip(pres - o_out + o_in, 0.4 * a, a)\n",
" out[:, h] = 1 - pres / a\n",
" return out, {\"o_base\": o_base, \"o_sea\": o_sea, \"i_reg\": i_reg, \"batch\": batch}\n",
"\n",
"PERM = {\"์์คํ
\": {\"propose\"}, \"์ธ์ฌ๋ด๋น\": {\"submit\", \"redraft\", \"view\"}, \"์ธ์ฌ๊ด\": {\"review\", \"adjust\", \"reject\", \"view\"}, \"์งํ๊ด\": {\"approve\", \"reject\", \"feedback\", \"view\"},\n",
" \"๊ฐ์ฌ๊ด\": {\"view\", \"audit\"}, \"์ธ์ฌ์ฒ์ฅ\": {\"submit\", \"approve\", \"reject\", \"view\"}} # ์ธ์ฌ์ฒ์ฅ์ ์์ ยท์น์ธ์ ๊ฒธํ ์ ์์ผ๋ ๊ฐ์ ๊ฑด์ ๋ถ๋ฆฌ\n",
"USERS = {\"sys\": \"์์คํ
\", \"kim\": \"์ธ์ฌ๋ด๋น\", \"lee\": \"์ธ์ฌ๊ด\", \"park\": \"์งํ๊ด\", \"jung\": \"๊ฐ์ฌ๊ด\", \"oh\": \"์ธ์ฌ์ฒ์ฅ\"}\n",
"NEXT = {\"์ ์\": (\"submit\", \"์์ \"), \"์์ \": (\"review\", \"์ธ์ฌ๊ด๊ฒํ \"), \"์ธ์ฌ๊ด๊ฒํ \": (\"approve\", \"์น์ธ\")}\n",
"MAX_REDRAFT = 2\n",
"ADJ_LIMIT = 0.5 # ์ธ์ฌ๊ด ์กฐ์ ํญ: ์ ์ ์ธ์์ +-50% ์ด๋ด(๋ฒ์ ๋ฐ์ ์ฌ์ ์ ์งํ๊ด ์น์ธ ํ์๋ก ์ฐจ๋จ)\n",
"\n",
"\n",
"class Chain:\n",
" def __init__(self):\n",
" self.log = []\n",
"\n",
" def add(self, **body):\n",
" prev = self.log[-1][\"hash\"] if self.log else \"GENESIS\"\n",
" body = {\"seq\": len(self.log), **body}\n",
" self.log.append({**body, \"prev\": prev, \"hash\": hashlib.sha256((prev + json.dumps(body, sort_keys=True, ensure_ascii=False)).encode()).hexdigest()})\n",
"\n",
" def verify(self):\n",
" prev = \"GENESIS\"\n",
" for e in self.log:\n",
" b = {k: v for k, v in e.items() if k not in (\"prev\", \"hash\")}\n",
" if e[\"prev\"] != prev or hashlib.sha256((prev + json.dumps(b, sort_keys=True, ensure_ascii=False)).encode()).hexdigest() != e[\"hash\"]:\n",
" return e[\"seq\"]\n",
" prev = e[\"hash\"]\n",
" return None\n",
"\n",
"\n",
"class Workflow:\n",
" def __init__(self):\n",
" self.audit, self.cases, self.calls = Chain(), {}, 0\n",
"\n",
" def _deny(self, who, case, action, why):\n",
" self.audit.add(event=\"DENIED\", who=who, case=case, action=action, reason=why); return False, why\n",
"\n",
" def act(self, who, case, action, reason=\"\", evidence=\"\", qty=None):\n",
" self.calls += 1\n",
" role = USERS.get(who)\n",
" if role is None:\n",
" return self._deny(who, case, action, \"๋ฏธ๋ฑ๋ก ์ฌ์ฉ์\")\n",
" if action == \"delete_log\":\n",
" return self._deny(who, case, action, \"๊ฐ์ฌ ๋ก๊ทธ ์ญ์ ยท์์ ๊ถํ์ ๋๊ตฌ์๊ฒ๋ ์์\")\n",
" if action not in PERM[role]:\n",
" return self._deny(who, case, action, f\"๊ถํ ์์({role} ์ญํ ์ {action} ๊ถํ ์์)\")\n",
" if action == \"propose\":\n",
" self.cases[case] = {\"state\": \"์ ์\", \"submitter\": None, \"version\": 1, \"qty\": qty, \"orig\": qty, \"feedback\": \"\"}\n",
" self.audit.add(event=\"PROPOSE\", who=who, case=case, qty=qty, reason=reason); return True, \"์ ์ ๋ฑ๋ก\"\n",
" c = self.cases.get(case)\n",
" if c is None:\n",
" return self._deny(who, case, action, \"์กด์ฌํ์ง ์๋ ๊ฑด\")\n",
" if c[\"state\"] in (\"์น์ธ\", \"์ข
๊ฒฐ(๋ฐ๋ ค)\"):\n",
" return self._deny(who, case, action, f\"์ด๋ฏธ ์ข
๊ฒฐ๋ ๊ฑด({c['state']})\")\n",
" if not reason.strip() or not evidence.strip():\n",
" return self._deny(who, case, action, \"์ฌ์ ยท๊ทผ๊ฑฐ ์
๋ ฅ ํ์(๋ฐ๋ คยท์กฐ์ ์ฌ์ ํฌํจ)\")\n",
" if action == \"adjust\":\n",
" if c[\"state\"] != \"์ธ์ฌ๊ด๊ฒํ \":\n",
" return self._deny(who, case, action, f\"์กฐ์ ์ ์ธ์ฌ๊ด ๊ฒํ ๋จ๊ณ์์๋ง ๊ฐ๋ฅ(ํ์ฌ {c['state']})\")\n",
" if qty is None or qty < 0 or abs(qty - c[\"orig\"]) > max(1, ADJ_LIMIT * c[\"orig\"]):\n",
" return self._deny(who, case, action, f\"์กฐ์ ํญ ํ๋ ์ด๊ณผ(์ ์ {c['orig']}๋ช
, ์์ฒญ {qty})\")\n",
" before, c[\"qty\"] = c[\"qty\"], qty\n",
" self.audit.add(event=\"ADJUST\", who=who, role=role, case=case, before=before, after=qty, reason=reason, evidence=evidence); return True, f\"{before}๋ช
-> {qty}๋ช
์กฐ์ \"\n",
" if action == \"feedback\":\n",
" c[\"feedback\"] = reason\n",
" self.audit.add(event=\"FEEDBACK\", who=who, role=role, case=case, reason=reason, evidence=evidence); return True, \"์งํ๊ด ํผ๋๋ฐฑ ๊ธฐ๋ก\"\n",
" if action == \"reject\":\n",
" if c[\"state\"] in (\"์ ์\", \"๋ฐ๋ ค\"):\n",
" return self._deny(who, case, action, f\"๋ฐ๋ คํ ์ ์๋ ๋จ๊ณ({c['state']})\")\n",
" c[\"state\"] = \"์ข
๊ฒฐ(๋ฐ๋ ค)\" if c[\"version\"] > MAX_REDRAFT else \"๋ฐ๋ ค\"\n",
" self.audit.add(event=\"REJECTED\", who=who, role=role, case=case, version=c[\"version\"], reason=reason, evidence=evidence); return True, c[\"state\"]\n",
" if action == \"redraft\":\n",
" if c[\"state\"] != \"๋ฐ๋ ค\":\n",
" return self._deny(who, case, action, f\"์ฌ๊ธฐ์์ ๋ฐ๋ ค๋ ๊ฑด๋ง ๊ฐ๋ฅ(ํ์ฌ {c['state']})\")\n",
" if who != c[\"submitter\"]:\n",
" return self._deny(who, case, action, \"์ฌ๊ธฐ์์ ์ต์ด ์์ ์๋ง ๊ฐ๋ฅ\")\n",
" c[\"state\"], c[\"version\"] = \"์์ \", c[\"version\"] + 1\n",
" self.audit.add(event=\"REDRAFT\", who=who, role=role, case=case, version=c[\"version\"], reason=reason, evidence=evidence); return True, \"์ฌ๊ธฐ์ ์์ \"\n",
" if c[\"state\"] == \"๋ฐ๋ ค\":\n",
" return self._deny(who, case, action, \"๋ฐ๋ ค๋ ๊ฑด์ ์ฌ๊ธฐ์ ํ์๋ง ์งํ ๊ฐ๋ฅ\")\n",
" need, nxt = NEXT[c[\"state\"]]\n",
" if action != need:\n",
" return self._deny(who, case, action, f\"๋จ๊ณ ์๋ฐ(ํ์ฌ {c['state']}, ํ์ ํ์ {need})\")\n",
" if action == \"submit\":\n",
" c[\"submitter\"] = who\n",
" if action == \"approve\" and who == c[\"submitter\"]:\n",
" return self._deny(who, case, action, \"์์ ์์ ์น์ธ์ ๋ถ๋ฆฌ ์์น ์๋ฐ\")\n",
" c[\"state\"] = nxt\n",
" self.audit.add(event=action.upper(), who=who, role=role, case=case, version=c[\"version\"], to=nxt, qty=c[\"qty\"], reason=reason, evidence=evidence); return True, nxt\n",
"\n",
"\n",
"# ---- ์์ ๋์: ํฅํ 13์ฃผ ๋ณด์ถฉ ํ์ ํ๋ฅ ์ด ๋์ ์
10๊ฑด๊ณผ ๊ฐ์ ์ง๋ฌด ์ฌ์ ์
์์์ ์ด๋ ์ธ์ ----\n",
"df = make_manning(7, 104)\n",
"cells = cell_arrays(df)\n",
"rng = np.random.default_rng(3)\n",
"items = []\n",
"for k, c in cells.items():\n",
" paths, _ = mc_forecast(c, 104, n_path=300)\n",
" peaks = paths.max(axis=1)\n",
" items.append({\"cell\": k, \"auth\": c[\"auth\"], \"peak\": float(peaks.mean()), \"p\": float((peaks >= NEED_CUT).mean())})\n",
"risk = pd.DataFrame(items)\n",
"risk[\"risk\"] = 100 * (0.6 * risk[\"p\"] + 0.4 * np.minimum(risk[\"peak\"] / .3, 1))\n",
"risk[\"need\"] = [max(1, int(np.ceil((r.peak - 0.10) * r.auth))) for r in risk.itertuples()]\n",
"cand = risk[risk[\"risk\"] >= 20].sort_values(\"risk\", ascending=False).head(10).reset_index(drop=True)\n",
"print(f\"[์์ ๋์] ์ํ 20 ์ด์ {len(risk[risk['risk'] >= 20])}๊ฐ ์
์ค ์์ {len(cand)}๊ฑด (ํ์ ์ธ์ ํฉ๊ณ {int(cand['need'].sum())}๋ช
)\")\n",
"\n",
"# ---- ํ ์๋ฎฌ๋ ์ด์
: ๊ฒํ ๋ด๋น 1๋ช
(๊ฑด๋น 2์๊ฐ), ๊ฑด์ด 1์๊ฐ ๊ฐ๊ฒฉ์ผ๋ก ๋์ฐฉ. ์ ์
์ ์ถ vs ์ํ๋ ์ฐ์ (๋๊ธฐ ๋
ธํ ๊ฐ์ฐ) ----\n",
"def run_queue(policy, risks, service=2.0, arr_gap=1.0):\n",
" n = len(risks); arr = np.arange(n) * arr_gap; done, t, waits = set(), 0.0, {}\n",
" while len(done) < n:\n",
" ready = [i for i in range(n) if i not in done and arr[i] <= t]\n",
" if not ready:\n",
" t = min(arr[i] for i in range(n) if i not in done); continue\n",
" pick = ready[0] if policy == \"fifo\" else max(ready, key=lambda i: risks[i] + 5.0 * (t - arr[i])) # ๋
ธํ ๊ฐ์ฐ: 1์๊ฐ ๋๊ธฐ๋ง๋ค 5์ \n",
" waits[pick] = t - arr[pick]; t += service; done.add(pick)\n",
" return waits\n",
"rs = list(rng.permutation(cand[\"risk\"].to_numpy())) # ๋์ฐฉ ์์๋ ์ํ๋์ ๋ฌด๊ด\n",
"wf_, wp_ = run_queue(\"fifo\", rs), run_queue(\"prio\", rs)\n",
"hi = [i for i, r in enumerate(rs) if r >= 60]\n",
"print(f\"\\n[๊ฒํ ํ ๋๊ธฐ์๊ฐ(์๊ฐ)] ๊ฑด {len(rs)}๊ฐ, ์ํ 60 ์ด์ {len(hi)}๊ฑด\")\n",
"print(f\" ์ ์
์ ์ถ: ์ ์ฒด ํ๊ท {np.mean(list(wf_.values())):.1f}h, ๊ณ ์ํ ํ๊ท {np.mean([wf_[i] for i in hi]):.1f}h, ์ต๋ {max(wf_.values()):.1f}h\")\n",
"print(f\" ์ํ๋ ์ฐ์ (๋
ธํ ๊ฐ์ฐ): ์ ์ฒด ํ๊ท {np.mean(list(wp_.values())):.1f}h, ๊ณ ์ํ ํ๊ท {np.mean([wp_[i] for i in hi]):.1f}h, ์ต๋ {max(wp_.values()):.1f}h (์ ์ํ ๊ฑด์ด ๊ตถ์ง ์๋๋ก ๋
ธํ ๊ฐ์ฐ)\")\n",
"\n",
"# ---- ์ํฌํ๋ก์ฐ ๋ชจ์ ----\n",
"wf = Workflow()\n",
"res = []\n",
"for k, r in cand.iterrows():\n",
" case = f\"H{k + 1:02d}\"; q = int(r[\"need\"])\n",
" wf.act(\"sys\", case, \"propose\", reason=f\"{r['cell'][0]} {r['cell'][1]} ๋ณด์ถฉ ํ์ ํ๋ฅ {r['p']:.0%} -> ์ธ์ {q}๋ช
๋ณด์ถฉยท์ด๋ ์ ์\", qty=q)\n",
" wf.act(\"kim\", case, \"submit\", \"๋ณด์ถฉ ์ฐ์ ์์ ๊ฒํ ์์ฒญ\", f\"์ํ {r['risk']:.0f}์ , ์์ ์ต๋ ๊ฒฐ์์จ {r['peak']:.0%}\")\n",
" for _ in range(12):\n",
" s = wf.cases[case][\"state\"]\n",
" if s in (\"์น์ธ\", \"์ข
๊ฒฐ(๋ฐ๋ ค)\"):\n",
" break\n",
" if s == \"์์ \":\n",
" if (r[\"p\"] < 0.5 and k % 3 == 0 and wf.cases[case][\"version\"] == 1) or k == 9: # k==9๋ ๋ณด์ ์์ด ๊ณ์ ๋ฐ๋ ค๋๋ ๊ฑด(์ฌ๊ธฐ์ ํ๋ ์์ฐ)\n",
" wf.act(\"lee\", case, \"reject\", \"ํ๋ฅ 50% ๋ฏธ๋ง, ๋ค์ ์ฃผ ์ฌํ์ธ\", f\"๋ณด์ถฉ ํ์ ํ๋ฅ {r['p']:.0%}\")\n",
" else:\n",
" wf.act(\"lee\", case, \"review\", \"์๋ฃยท์์ธก ๊ฒํ ์๋ฃ\", f\"์ํ {r['risk']:.0f}์ \")\n",
" if r[\"cell\"][1] == \"ํต์ \": # ์ธ์ฌ๊ด ์๋ ์กฐ์ ์: ๋ค๋ฅธ ๋ถ๋ ์ง์ ๊ฐ๋ฅ ์ธ์ ๋ฐ์\n",
" wf.act(\"lee\", case, \"adjust\", \"์ธ์ ๋ถ๋ ์ง์ ๊ฐ๋ฅ ์ธ์ 1๋ช
๋ฐ์ํด ๊ฐ์ ์กฐ์ \", \"ํํฉ ๋ณด๊ณ ํ์ธ\", qty=max(q - 1, 0))\n",
" elif s == \"์ธ์ฌ๊ด๊ฒํ \":\n",
" wf.act(\"park\", case, \"feedback\", \"๊ธด๊ธ ๊ฑด์ ์ฃผ๊ฐ ํ์ ์ ๊ฒฐ์ ์์ฒญ\" if r[\"risk\"] >= 80 else \"์ ๊ธฐ ๋ณด์ถฉ ๊ณํ์ ํฌํจ\", \"์งํ๊ด ์๊ฒฌ\")\n",
" wf.act(\"park\", case, \"approve\", \"์น์ธ: ๋ณด์ถฉยท์ด๋ ์งํ\", f\"์ํ {r['risk']:.0f}์ , ์ธ์ฌ๊ด ๊ฒํ ยท์กฐ์ ์๋ฃ\")\n",
" elif s == \"๋ฐ๋ ค\":\n",
" wf.act(\"kim\", case, \"redraft\", \"๋ฐ๋ ค ์ฌ์ ๋ฐ์ํด ์ฌ๊ธฐ์\", f\"ํ๋ฅ ์ฌ์ฐ์ถ ํ ์ฌ์์ {wf.cases[case]['version']}์ฐจ\")\n",
" c = wf.cases[case]; res.append((case, f\"{r['cell'][0]} {r['cell'][1]}\", c[\"orig\"], c[\"qty\"], c[\"state\"], c[\"version\"]))\n",
"print(\"\\n[๊ฒฐ์ฌ ๊ฒฐ๊ณผ] 10๊ฑด (๋ชจ์ ๊ฒฐ์ฌ์ ๊ธฐ์ค, ์ค์ ๊ฒฐ์ฌ๋ ์ฌ๋์ด ํ๋ค)\")\n",
"print(pd.DataFrame(res, columns=[\"๊ฑด\", \"์
\", \"์ ์ ์ธ์\", \"์ต์ข
์ธ์\", \"์ํ\", \"๋ฒ์ \"]).to_string(index=False))\n",
"print(f\"์ธ์ฌ๊ด ์๋ ์กฐ์ {sum(e['event'] == 'ADJUST' for e in wf.audit.log)}๊ฑด(์กฐ์ ์ ํยท์ฌ์ ๊ธฐ๋ก), ๋ฐ๋ ค {sum(e['event'] == 'REJECTED' for e in wf.audit.log)}๊ฑด(์ฌ์ ํ์), ์งํ๊ด ํผ๋๋ฐฑ {sum(e['event'] == 'FEEDBACK' for e in wf.audit.log)}๊ฑด\")\n",
"\n",
"# ---- ๊ถํ ์ฐํยท์ ์ฐจ ์๋ฐ ์๋ (์ํ๋ง๋ค ์ ๊ฑด์ ๋ง๋ค์ด ํด๋น ๋จ๊ณ๊น์ง ์งํ์ํจ ๋ค ์๋) ----\n",
"def mk(cid, stage=\"submit\"):\n",
" wf.act(\"sys\", cid, \"propose\", reason=\"์ํ\", qty=10)\n",
" if stage in (\"submit\", \"review\"):\n",
" wf.act(\"kim\", cid, \"submit\", \"์ํ ์์ \", \"๊ทผ๊ฑฐ\")\n",
" if stage == \"review\":\n",
" wf.act(\"lee\", cid, \"review\", \"๊ฒํ \", \"์ด์ ์์\")\n",
" return cid\n",
"approved = next(c for c, v in wf.cases.items() if v[\"state\"] == \"์น์ธ\")\n",
"att = [(\"์์คํ
์๋ ์น์ธ\", lambda: wf.act(\"sys\", mk(\"T01\"), \"approve\", \"์๋\", \"์๋\")), (\"์ธ์ฌ๋ด๋น์ด ์กฐ์ ์๋\", lambda: wf.act(\"kim\", mk(\"T02\", \"review\"), \"adjust\", \"์์ ์กฐ์ \", \"๊ทผ๊ฑฐ\", qty=5)),\n",
" (\"๊ฒํ ๊ฑด๋๋ฐ๊ณ ์น์ธ\", lambda: wf.act(\"park\", mk(\"T03\"), \"approve\", \"๋ฐ๋ก ์น์ธ\", \"๊ทผ๊ฑฐ\")), (\"๋ฏธ๋ฑ๋ก ์ฌ์ฉ์\", lambda: wf.act(\"guest\", mk(\"T04\"), \"review\", \"์ด๋\", \"๊ทผ๊ฑฐ\")),\n",
" (\"์ฌ์ ์๋ ๊ฒํ \", lambda: wf.act(\"lee\", mk(\"T05\"), \"review\", \"\", \"\")), (\"๊ฐ์ฌ ๋ก๊ทธ ์ญ์ \", lambda: wf.act(\"kim\", mk(\"T06\"), \"delete_log\", \"์ ๋ฆฌ\", \"๊ทผ๊ฑฐ\")),\n",
" (\"๊ฐ์ฌ๊ด์ด ์น์ธ ์๋\", lambda: wf.act(\"jung\", mk(\"T07\", \"review\"), \"approve\", \"์น์ธ\", \"๊ทผ๊ฑฐ\")), (\"์กด์ฌํ์ง ์๋ ๊ฑด\", lambda: wf.act(\"park\", \"X99\", \"approve\", \"์น์ธ\", \"๊ทผ๊ฑฐ\")),\n",
" (\"์ข
๊ฒฐ๋ ๊ฑด ์ฌ์น์ธ\", lambda: wf.act(\"park\", approved, \"approve\", \"์ฌ์น์ธ\", \"๊ทผ๊ฑฐ\")), (\"์กฐ์ ํญ ํ๋ ์ด๊ณผ(10๋ช
-> 30๋ช
)\", lambda: wf.act(\"lee\", mk(\"T08\", \"review\"), \"adjust\", \"๋ํญ ์ฆ์\", \"๊ทผ๊ฑฐ\", qty=30)),\n",
" (\"์ฌ์ ์๋ ์กฐ์ \", lambda: wf.act(\"lee\", mk(\"T09\", \"review\"), \"adjust\", \"\", \"๊ทผ๊ฑฐ\", qty=9)), (\"์ธ์ฌ๊ด์ด ์น์ธ ์๋\", lambda: wf.act(\"lee\", mk(\"T10\", \"review\"), \"approve\", \"๋์ ์น์ธ\", \"๊ทผ๊ฑฐ\")),\n",
" (\"๊ฒํ ์ ๋จ๊ณ ์กฐ์ \", lambda: wf.act(\"lee\", mk(\"T11\"), \"adjust\", \"์กฐ์ \", \"๊ทผ๊ฑฐ\", qty=9)),\n",
" (\"์์ ์๊ฐ ๋ณธ์ธ ๊ฑด ์น์ธ(๋ถ๋ฆฌ ์์น)\", lambda: (wf.act(\"sys\", \"T12\", \"propose\", reason=\"์ํ\", qty=4), wf.act(\"oh\", \"T12\", \"submit\", \"์์ \", \"๊ทผ๊ฑฐ\"), wf.act(\"lee\", \"T12\", \"review\", \"๊ฒํ \", \"์ด์ ์์\"), wf.act(\"oh\", \"T12\", \"approve\", \"๋ณธ์ธ ์น์ธ\", \"๊ทผ๊ฑฐ\"))[-1])]\n",
"print(\"\\n[๊ถํ ์ฐํยท์ ์ฐจ ์๋ฐ ์๋]\")\n",
"blocked = 0\n",
"for n, fn in att:\n",
" ok, why = fn(); blocked += (not ok); print(f\" {n:24s}-> {'์ฐจ๋จ' if not ok else 'ํต๊ณผ(๊ฒฐํจ)'}: {why}\")\n",
"print(f\"์ฐจ๋จ {blocked}/{len(att)} ({blocked / len(att):.0%})\")\n",
"\n",
"# ---- ๋ก๊น
์ฑ๊ณต๋ฅ ๊ณผ ์ด๋ ฅ ๋ฌด๊ฒฐ์ฑ ----\n",
"n_log = len(wf.audit.log); bad = wf.audit.verify()\n",
"print(f\"\\n์ฒ๋ฆฌ ์์ฒญ {wf.calls}๊ฑด -> ๊ฐ์ฌ ๋ก๊ทธ {n_log}๊ฑด(์น์ธยท๋ฐ๋ คยท์กฐ์ ยท๊ฑฐ๋ถ ๋ชจ๋ ๊ธฐ๋ก), ๋ก๊น
์ฑ๊ณต๋ฅ {n_log / wf.calls:.0%} (๋ชฉํ 100%: {'๋ฌ์ฑ' if n_log == wf.calls else '๋ฏธ๋ฌ'})\")\n",
"print(f\"ํด์ ์ฒด์ธ ๊ฒ์ฆ: {'์ ์' if bad is None else f'seq {bad} ๋ถ์ผ์น'} -> ์ด๋ ฅ ๋ฌด๊ฒฐ์ฑ {'100%' if bad is None else 'ํผ์'}\")\n",
"saved = json.dumps(wf.audit.log[12], ensure_ascii=False); wf.audit.log[12][\"reason\"] = \"์ฌํ ์์ \"; v = wf.audit.verify()\n",
"print(f\"12๋ฒ์งธ ๊ธฐ๋ก ์์ ์์ ํ ๊ฒ์ฆ: {'์๋ณ์กฐ ํ์ง(seq ' + str(v) + ')' if v is not None else 'ํ์ง ์คํจ'}\"); wf.audit.log[12] = json.loads(saved)\n",
"print(\"โป ๋ณด์ถฉ ์ฐ์ ์์์ ์ธ์ ์ด๋์ ์ฌ๋(์ธ์ฌ๊ดยท์งํ๊ด)์ด ๊ฒฐ์ ํ๋ฉฐ ์์คํ
์ ์ ์๊ณผ ๊ทผ๊ฑฐ๋ง ์ ๊ณตํ๋ค. ๊ฒฐ์ฌ์ ์ ์์ ๊ธฐ๊ด ๊ท์ ์ ๋ฐ๋ฅธ๋ค.\")\n",
"\n",
"plt.figure(figsize=(6, 3.5)); plt.bar([\"FIFO\", \"priority\"], [np.mean([wf_[i] for i in hi]), np.mean([wp_[i] for i in hi])], color=[\"tab:gray\", \"tab:blue\"])\n",
"plt.ylabel(\"mean wait of high-risk items (h)\"); plt.title(\"Review queue wait: FIFO vs risk priority\"); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
07 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[์์ ๋์] ์ํ 20 ์ด์ 11๊ฐ ์
์ค ์์ 10๊ฑด (ํ์ ์ธ์ ํฉ๊ณ 53๋ช
)\n",
"\n",
"[๊ฒํ ํ ๋๊ธฐ์๊ฐ(์๊ฐ)] ๊ฑด 10๊ฐ, ์ํ 60 ์ด์ 6๊ฑด\n",
" ์ ์
์ ์ถ: ์ ์ฒด ํ๊ท 4.5h, ๊ณ ์ํ ํ๊ท 4.8h, ์ต๋ 9.0h\n",
" ์ํ๋ ์ฐ์ (๋
ธํ ๊ฐ์ฐ): ์ ์ฒด ํ๊ท 4.5h, ๊ณ ์ํ ํ๊ท 2.5h, ์ต๋ 11.0h (์ ์ํ ๊ฑด์ด ๊ตถ์ง ์๋๋ก ๋
ธํ ๊ฐ์ฐ)\n",
"\n",
"[๊ฒฐ์ฌ ๊ฒฐ๊ณผ] 10๊ฑด (๋ชจ์ ๊ฒฐ์ฌ์ ๊ธฐ์ค, ์ค์ ๊ฒฐ์ฌ๋ ์ฌ๋์ด ํ๋ค)\n",
" ๊ฑด ์
์ ์ ์ธ์ ์ต์ข
์ธ์ ์ํ ๋ฒ์ \n",
"H01 ๊ฐ์A๋๋ ์ ๋น 8 8 ์น์ธ 1\n",
"H02 ๊ฐ์C๋๋ ํต์ 13 12 ์น์ธ 1\n",
"H03 ๊ฐ์B๋๋ ๋ณด๊ธ 15 15 ์น์ธ 1\n",
"H04 ๊ฐ์D๋๋ ์๋ฌด 3 3 ์น์ธ 1\n",
"H05 ๊ฐ์A๋๋ ๋ณด๊ธ 4 4 ์น์ธ 1\n",
"H06 ๊ฐ์D๋๋ ํต์ 3 2 ์น์ธ 1\n",
"H07 ๊ฐ์A๋๋ ์๋ฌด 2 2 ์น์ธ 2\n",
"H08 ๊ฐ์C๋๋ ์ ๋น 2 2 ์น์ธ 1\n",
"H09 ๊ฐ์B๋๋ ์ ๋น 1 1 ์น์ธ 1\n",
"H10 ๊ฐ์D๋๋ ํ์ 2 2 ์ข
๊ฒฐ(๋ฐ๋ ค) 3\n",
"์ธ์ฌ๊ด ์๋ ์กฐ์ 2๊ฑด(์กฐ์ ์ ํยท์ฌ์ ๊ธฐ๋ก), ๋ฐ๋ ค 4๊ฑด(์ฌ์ ํ์), ์งํ๊ด ํผ๋๋ฐฑ 9๊ฑด\n",
"\n",
"[๊ถํ ์ฐํยท์ ์ฐจ ์๋ฐ ์๋]\n",
" ์์คํ
์๋ ์น์ธ -> ์ฐจ๋จ: ๊ถํ ์์(์์คํ
์ญํ ์ approve ๊ถํ ์์)\n",
" ์ธ์ฌ๋ด๋น์ด ์กฐ์ ์๋ -> ์ฐจ๋จ: ๊ถํ ์์(์ธ์ฌ๋ด๋น ์ญํ ์ adjust ๊ถํ ์์)\n",
" ๊ฒํ ๊ฑด๋๋ฐ๊ณ ์น์ธ -> ์ฐจ๋จ: ๋จ๊ณ ์๋ฐ(ํ์ฌ ์์ , ํ์ ํ์ review)\n",
" ๋ฏธ๋ฑ๋ก ์ฌ์ฉ์ -> ์ฐจ๋จ: ๋ฏธ๋ฑ๋ก ์ฌ์ฉ์\n",
" ์ฌ์ ์๋ ๊ฒํ -> ์ฐจ๋จ: ์ฌ์ ยท๊ทผ๊ฑฐ ์
๋ ฅ ํ์(๋ฐ๋ คยท์กฐ์ ์ฌ์ ํฌํจ)\n",
" ๊ฐ์ฌ ๋ก๊ทธ ์ญ์ -> ์ฐจ๋จ: ๊ฐ์ฌ ๋ก๊ทธ ์ญ์ ยท์์ ๊ถํ์ ๋๊ตฌ์๊ฒ๋ ์์\n",
" ๊ฐ์ฌ๊ด์ด ์น์ธ ์๋ -> ์ฐจ๋จ: ๊ถํ ์์(๊ฐ์ฌ๊ด ์ญํ ์ approve ๊ถํ ์์)\n",
" ์กด์ฌํ์ง ์๋ ๊ฑด -> ์ฐจ๋จ: ์กด์ฌํ์ง ์๋ ๊ฑด\n",
" ์ข
๊ฒฐ๋ ๊ฑด ์ฌ์น์ธ -> ์ฐจ๋จ: ์ด๋ฏธ ์ข
๊ฒฐ๋ ๊ฑด(์น์ธ)\n",
" ์กฐ์ ํญ ํ๋ ์ด๊ณผ(10๋ช
-> 30๋ช
) -> ์ฐจ๋จ: ์กฐ์ ํญ ํ๋ ์ด๊ณผ(์ ์ 10๋ช
, ์์ฒญ 30)\n",
" ์ฌ์ ์๋ ์กฐ์ -> ์ฐจ๋จ: ์ฌ์ ยท๊ทผ๊ฑฐ ์
๋ ฅ ํ์(๋ฐ๋ คยท์กฐ์ ์ฌ์ ํฌํจ)\n",
" ์ธ์ฌ๊ด์ด ์น์ธ ์๋ -> ์ฐจ๋จ: ๊ถํ ์์(์ธ์ฌ๊ด ์ญํ ์ approve ๊ถํ ์์)\n",
" ๊ฒํ ์ ๋จ๊ณ ์กฐ์ -> ์ฐจ๋จ: ์กฐ์ ์ ์ธ์ฌ๊ด ๊ฒํ ๋จ๊ณ์์๋ง ๊ฐ๋ฅ(ํ์ฌ ์์ )\n",
" ์์ ์๊ฐ ๋ณธ์ธ ๊ฑด ์น์ธ(๋ถ๋ฆฌ ์์น) -> ์ฐจ๋จ: ์์ ์์ ์น์ธ์ ๋ถ๋ฆฌ ์์น ์๋ฐ\n",
"์ฐจ๋จ 14/14 (100%)\n",
"\n",
"์ฒ๋ฆฌ ์์ฒญ 100๊ฑด -> ๊ฐ์ฌ ๋ก๊ทธ 100๊ฑด(์น์ธยท๋ฐ๋ คยท์กฐ์ ยท๊ฑฐ๋ถ ๋ชจ๋ ๊ธฐ๋ก), ๋ก๊น
์ฑ๊ณต๋ฅ 100% (๋ชฉํ 100%: ๋ฌ์ฑ)\n",
"ํด์ ์ฒด์ธ ๊ฒ์ฆ: ์ ์ -> ์ด๋ ฅ ๋ฌด๊ฒฐ์ฑ 100%\n",
"12๋ฒ์งธ ๊ธฐ๋ก ์์ ์์ ํ ๊ฒ์ฆ: ์๋ณ์กฐ ํ์ง(seq 12)\n",
"... (์ดํ ์๋ต)\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
08. ์ ์ ๋์ ๋ฐ ๋๋ ๊ฒฐ์ ๋ฐ์ ์ํฉ ๋ชจ์ ์ธ์ฌ ์์ธก ์คํธ๋ ์ค ์๋ฎฌ๋ ์ด์
\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ ์ ๋์ ๋ฐ ๋๋ ๊ฒฐ์ ๋ชจ์ ์ธ์ฌ ์์ธก ์คํธ๋ ์ค ์๋ฎฌ๋ ์ด์
์ ์ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **ํน์ ์ง๋ฌด์์ ๋๋ ๊ฒฐ์์ด ๋ฐ์ํ ์ํฉ๊ณผ ์กฐํ ์์ฒญ 500% ์ฆ๊ฐ ๋ถํ์์ ์์คํ
์ด ์ค๋จ๋๊ฑฐ๋ ์๋ฃ๊ฐ ์ ์ค๋์ง ์๋์ง ์คํธ๋ ์ค ์๋ฎฌ๋ ์ด์
์ผ๋ก ๊ฒ์ฆํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **ํ ๋ถ๋ ํต์ ์ง๋ฌด์ ์ ์ถ์ด ๊ฐ์๊ธฐ ๋์ด๋ ์ํฉ์์ ๋ณด์ถฉ ํ์ ํ๋ฅ ๊ณผ ํ์ ์ธ์์ด ์ด๋ป๊ฒ ๋ฐ๋๋์ง ํ์์ ๋น๊ต ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `tests/adm01_stress_test.py ๋ฐ ์คํธ๋ ์ค ํ
์คํธ ๋ถ์ ๋ณด๊ณ ์` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ทน๋จ ์ํฉ์์์ ์์ ์คํจ(Fail-safe) ๋ฐ ๊ธฐ๋ฅ ์ถ์ ์๋ ์ ์ ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๋์ ํธ๋ํฝ 500% ์ฆ๊ฐ ์ ์์คํ
๋ค์ด 0๊ฑด, ๋ฐ์ดํฐ ์ ์ค๋ฅ 0%\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ๋๋ ์๋ฎฌ๋ ์ด์
๋ถํ ์ฃผ์
ํ
์คํธ ํต๊ณผ ๋ฐ ๋ณ๋ชฉ ๊ตฌ๊ฐ ํ๋กํ์ผ๋ง ์๋ฃ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ๋ถํ ํ
์คํธ ๋๊ตฌ๋ฅผ ํตํ ์ฒ๋ฆฌ๋(Throughput) ๋ฐ ๋ฉ๋ชจ๋ฆฌ ๋์ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"ํน์ ์ง๋ฌด์์ ๋๋ ๊ฒฐ์์ด ๋ฐ์ํ ์ํฉ(์ ์ถ ๊ธ์ฆ)์ ์์ธก ๋ณํ๋ฅผ ํ์ธํ๊ณ , ์กฐํยท์์ธก ์์ฒญ์ ๊ธฐ์ค ๋๋น 100%ยท200%ยท500%๋ก ๋๋ ค ํ ๊ธฐ๋ฐ ์์
์(์ค๋ ๋) ์ฒ๋ฆฌยทํ ํฌํ ์ ๊ธฐ๋ฅ ์ถ์(๊ฒฝ๋ ์์ธก)ยท์ค๋ฅ ์์ฒญ ๊ฒฉ๋ฆฌ๋ฅผ ์ํํ๋ฉฐ ์์คํ
๋ค์ดยท๋ฐ์ดํฐ ์ ์ค ๊ฑด์์ ๋ณ๋ชฉ ํจ์ ์ต์ ํ(์บ์) ํจ๊ณผ๋ฅผ ์ธก์ ํ๋ค.\"\"\"\n",
"import queue\n",
"import threading\n",
"import time\n",
"from functools import lru_cache\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋ถ๋ ์ธ๋ ฅ ํํฉ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยทํธ์ ยท๋ณ๋ ฅยท๊ฐ์ธ ์ ๋ณด ์์)\n",
"# ๊ฐ์ ๋๋ 4๊ณณ x ์ง๋ฌด 6์ข
์ ์ฃผ๊ฐ ์ง๊ณ ์๋ฃ๋ง ๋ง๋ ๋ค. ๊ฐ์ธ ๋จ์ ์ ๋ณด(์ด๋ฆยท๊ตฐ๋ฒ ๋ฑ)๋ ๋ง๋ค์ง ์๋๋ค.\n",
"# ์ธ์์ ์ง๋ฌด๋ณ ์ง๊ณ ์์ด๋ฉฐ, ๊ฒฐ์ยทํด๊ฐยทํ์ยท๊ต์กยท๋ณด์ถฉยท์ ์ถ์ ๋ฐ์ํ ํ์ฌ์๊ณผ ์ด๊ณผ๊ทผ๋ฌด ์๊ฐ์ผ๋ก ๊ตฌ์ฑ๋๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"UNITS = [\"๊ฐ์A๋๋\", \"๊ฐ์B๋๋\", \"๊ฐ์C๋๋\", \"๊ฐ์D๋๋\"]\n",
"JOBS = [\"์์ \", \"ํต์ \", \"์ ๋น\", \"๋ณด๊ธ\", \"ํ์ \", \"์๋ฌด\"]\n",
"FIELDS = [\"unit\", \"job\", \"week\", \"authorized\", \"present\", \"vacancy\", \"leave\", \"sick\", \"training\", \"inflow\", \"outflow\", \"overtime_h\", \"night_ratio\", \"rotation_days\", \"fatigue_label\"]\n",
"HI_CUT = 55 # ๊ณ ํผ๋ก ํ์ ๊ธฐ์ค(๊ฐ์ ์ ์ 55 ์ด์)\n",
"\n",
"\n",
"def make_manning(seed=7, weeks=104, shock=None):\n",
" \"\"\"๋ถ๋ x ์ง๋ฌด 24๊ฐ ์
์ ์ฃผ๊ฐ ์ธ๋ ฅ ์๋ฃ. shock=(unit, job ๋๋ None(๋ถ๋ ์ ์ฒด), ์์์ฃผ, ์ฃผ๊ฐ ์ถ๊ฐ ์ ์ถ ํ๊ท ์) ํ ๊ฑด ๋๋ ๊ทธ ๋ชฉ๋ก์ด๋ฉด ๊ทธ ์
์ ๋๋ ๊ฒฐ์ ์์ธ์ ๋ฃ๋๋ค(AARยท์คํธ๋ ์ค์ฉ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" shocks = [shock] if shock and not isinstance(shock[0], tuple) else list(shock or [])\n",
" rows = []\n",
" for ui, u in enumerate(UNITS):\n",
" for ji, j in enumerate(JOBS):\n",
" auth = int(rng.integers(14, 42) * (0.9 + 0.06 * ui))\n",
" work = rng.uniform(0.92, 1.12) # ์ง๋ฌด๋ณ ์
๋ฌด๋ ๊ณ์\n",
" net = rng.uniform(-0.05, 0.03) # ๊ตฌ์กฐ์ ๋ณด์ถฉ ๋ถ์กฑ(์์)ยท๊ณผ์(์์) ๊ฒฝํฅ(์ฃผ๋น ์ธ์)\n",
" present = int(auth * rng.uniform(0.82, 0.98))\n",
" rot = int(rng.choice([60, 90, 120])) # ๊ต๋ยท๋ณด์ถฉ ์ฃผ๊ธฐ(์ผ)\n",
" for w in range(1, weeks + 1):\n",
" season = 1.0 + 0.9 * (abs(w % 52 - 22) < 4 or abs(w % 52 - 46) < 3) # ์ ์ถยท์ ์ญ ์ง์ค ์๊ธฐ\n",
" out = int(rng.poisson(0.22 * auth / 20 * season))\n",
" for s_ in shocks:\n",
" if s_[0] == u and (s_[1] is None or s_[1] == j) and w >= s_[2]:\n",
" out += int(rng.poisson(s_[3]))\n",
" inflow = int(rng.poisson(max(0.0, 0.2 * auth / 20 * (1 + net * 10)))) + (int(rng.integers(2, 6) * auth / 25) if w % (rot // 7) == 0 else 0)\n",
" present = int(np.clip(present - out + inflow, int(auth * 0.4), auth))\n",
" leave = int(rng.binomial(present, 0.05 + 0.05 * (abs(w % 52 - 31) < 3 or w % 52 >= 50)))\n",
" sick = int(rng.poisson(0.025 * present)); train = int(rng.binomial(present, 0.04 + 0.06 * (w % 13 == 6)))\n",
" avail = max(present - leave - sick - train, 1)\n",
" night = float(np.clip(rng.normal(0.25 + 0.05 * (j == \"์์ \") + 0.05 * (j == \"์๋ฌด\"), 0.05), 0.05, 0.6))\n",
" ot = float(np.clip(auth * 40 * work / avail - 40 + rng.normal(0, 1.5), 0, 40))\n",
" vac = auth - present\n",
" absent = (leave + sick + train) / max(present, 1)\n",
" fat = 100 * (0.38 * min(vac / auth / 0.25, 1.2) + 0.34 * min(ot / 20, 1.2) + 0.10 * min(night / 0.4, 1.2) + 0.18 * min(absent / 0.2, 1.2)) + rng.normal(0, 5)\n",
" rows.append((u, j, w, auth, present, vac, leave, sick, train, inflow, out, round(ot, 1), round(night, 2), rot, int(fat >= HI_CUT)))\n",
" return pd.DataFrame(rows, columns=FIELDS)\n",
"\n",
"\n",
"def cell_pivot(df, col=\"vacancy\"):\n",
" \"\"\"์ฃผ x (๋ถ๋,์ง๋ฌด) ํ.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 8] ์ ์ ๋์ ๋ฐ ๋๋ ๊ฒฐ์ ๋ฐ์ ์ํฉ ๋ชจ์ ์ธ์ฌ ์์ธก ์คํธ๋ ์ค ์๋ฎฌ๋ ์ด์
๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"from sklearn.linear_model import Ridge\n",
"H = 13 # ์์ธก ๊ธฐ๊ฐ: ํฅํ 3๊ฐ์(13์ฃผ)\n",
"NEED_CUT = 0.15 # ๊ฒฐ์์จ 15% ์ด์์ด๋ฉด ๋ณด์ถฉ ํ์(๊ฐ์ ๊ธฐ์ค)\n",
"\n",
"\n",
"def season(w):\n",
" # [TODO : ํ์ ์ค์ต 8] ์ ์ ๋์ ๋ฐ ๋๋ ๊ฒฐ์ ๋ฐ์ ์ํฉ ๋ชจ์ ์ธ์ฌ ์์ธก ์คํธ๋ ์ค ์๋ฎฌ๋ ์ด์
๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def feat(vr, ir, orr, wn, rot, v2=True):\n",
" \"\"\"vrยทirยทorr: ์ง๊ธ๊น์ง์ ๊ฒฐ์์จยท๋ณด์ถฉ์จยท์ ์ถ์จ ๋ชฉ๋ก, wn: ์์ธกํ ๋ค์ ์ฃผ์ฐจ, rot: ๊ต๋ ์ฃผ๊ธฐ(์ผ). v2๋ ๋ณด์ถฉยท์ ์ถ ํ๋ฆ๊ณผ ๋ฌ๋ ฅ ํน์ง์ ์ถ๊ฐํ๋ค.\"\"\"\n",
" f = [vr[-1], vr[-1] - vr[-2], vr[-1] - vr[-4]]\n",
" if v2:\n",
" step = max(rot // 7, 1)\n",
" f += [float(np.mean(orr[-12:]) - np.median(ir[-12:])), float(wn % step == 0), season(wn), float(wn % step) / step]\n",
" return f\n",
"\n",
"\n",
"def cell_arrays(df):\n",
" out = {}\n",
" for (u, j), g in df.sort_values(\"week\").groupby([\"unit\", \"job\"]):\n",
" a = g[\"authorized\"].to_numpy()\n",
" out[(u, j)] = {\"auth\": int(a[0]), \"rot\": int(g[\"rotation_days\"].iloc[0]), \"vr\": (g[\"vacancy\"].to_numpy() / a), \"ir\": g[\"inflow\"].to_numpy() / a, \"or\": g[\"outflow\"].to_numpy() / a}\n",
" return out\n",
"\n",
"\n",
"def fit_fc(cells, origin, v2=True, alpha=1.0):\n",
" X, y = [], []\n",
" for c in cells.values():\n",
" for t in range(12, origin - 1): # t์ฃผ๊น์ง ์ ๋ t+1์ฃผ(์ธ๋ฑ์ค t)์ ๊ฒฐ์์จ ๋ณํ๋ฅผ ๋งํ๋ค\n",
" X.append(feat(c[\"vr\"][:t], c[\"ir\"][:t], c[\"or\"][:t], t + 1, c[\"rot\"], v2)); y.append(c[\"vr\"][t] - c[\"vr\"][t - 1])\n",
" return Ridge(alpha=alpha).fit(np.array(X), np.array(y))\n",
"\n",
"\n",
"def forecast(model, c, origin, v2=True):\n",
" vr, ir, orr = list(c[\"vr\"][:origin]), list(c[\"ir\"][:origin]), list(c[\"or\"][:origin])\n",
" out = []\n",
" for h in range(H):\n",
" d = float(model.predict([feat(vr, ir, orr, origin + h + 1, c[\"rot\"], v2)])[0])\n",
" p = float(np.clip(vr[-1] + d, 0, 1))\n",
" out.append(p); vr.append(p); ir.append(float(np.median(ir[-12:]))); orr.append(float(np.mean(orr[-12:])))\n",
" return np.array(out)\n",
"\n",
"\n",
"def mc_forecast(c, origin, n_path=300, seed=0, out_mult=1.0, lam=0.0):\n",
" \"\"\"์ฌ๊ณ -ํ๋ฆ(stock-flow) ๋ชจ์: ์
์ ํ์ยท์ ์ถ ์ง์ค๊ธฐ ์ ์ถ์จ, ์ ๊ท ๋ณด์ถฉ์จ, ๊ต๋ ์ฃผ๊ธฐ ๋ณด์ถฉ ๊ท๋ชจ๋ฅผ ๊ณผ๊ฑฐ์์ ์ถ์ ํด ๊ฒฐ์ ๊ฒฝ๋ก n_path๊ฐ๋ฅผ ๋ง๋ ๋ค.\n",
" ๋ฐํ: (๊ฒฝ๋ก ๋ฐฐ์ด [n_path, H] ๊ฒฐ์์จ, ์ถ์ ๋ชจ์). out_mult๋ ์ ์ถ ๊ฐ์ ์ ํค์ฐ๋ ๋ณด์ ๊ณ์, lam์ ์ต๊ทผ 8์ฃผ ์ ์ถ์จ์ ๋ฐ์ํ๋ ๋น์ค(0~1, ๊ธ์ฆ ๊ฐ์ง ๋๋ง ์ ์ฉ, AAR ์ฌ๋ณด์ ์ฉ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" w = np.arange(1, origin + 1); step = max(c[\"rot\"] // 7, 1)\n",
" rot_w = (w % step == 0); sea = np.array([season(x) > 0 for x in w])\n",
" o, i = c[\"or\"][:origin], c[\"ir\"][:origin]\n",
" o_base = o[~sea].mean(); o_sea = o[sea].mean() if sea.sum() >= 6 else o_base * 1.9\n",
" o_rec = o[-8:].mean()\n",
" mu = max(o_base, 1e-6) * c[\"auth\"] * 8 # ์ต๊ทผ 8์ฃผ ํ์ ๊ธฐ๋ ์ ์ถ ์ธ์\n",
" use_rec = lam > 0 and o_rec * c[\"auth\"] * 8 > mu + 2.0 * np.sqrt(mu) # ํฌ์์ก ๊ธฐ์ค ์์ 2ํ์คํธ์ฐจ๋ฅผ ๋๋ '๊ธ์ฆ'์ผ ๋๋ง ์ต๊ทผ ๊ฐ์ ๋ฐ์\n",
" i_reg = i[~rot_w].mean(); batch = max(i[rot_w].mean() - i_reg, 0.0)\n",
" a = c[\"auth\"]; pres = np.full(n_path, a * (1 - c[\"vr\"][origin - 1]))\n",
" out = np.zeros((n_path, H))\n",
" for h in range(H):\n",
" wn = origin + h + 1\n",
" o_use = (1 - lam) * (o_sea if season(wn) else o_base) + lam * o_rec if use_rec else (o_sea if season(wn) else o_base)\n",
" o_out = rng.poisson(max(o_use, 1e-6) * a * out_mult, n_path)\n",
" o_in = rng.poisson(max(i_reg, 1e-6) * a, n_path) + (np.maximum(rng.normal(batch * a, 0.3 * batch * a, n_path), 0).round() if wn % step == 0 else 0)\n",
" pres = np.clip(pres - o_out + o_in, 0.4 * a, a)\n",
" out[:, h] = 1 - pres / a\n",
" return out, {\"o_base\": o_base, \"o_sea\": o_sea, \"i_reg\": i_reg, \"batch\": batch}\n",
"\n",
"# 1) ๋๋ ๊ฒฐ์ ์ํฉ: ๊ฐ์ ๊ฐ์ ๋ถ๋์์ ํต์ ์ง๋ฌด ์ ์ถ์ด 90์ฃผ๋ถํฐ ์ฃผ๋น 3๋ช
๋์ด๋ ๊ฒฝ์ฐ์ ํ์ ๋น๊ต\n",
"base = make_manning(7, 104)\n",
"surge = make_manning(7, 104, shock=(\"๊ฐ์B๋๋\", \"ํต์ \", 90, 3))\n",
"def need_table(df):\n",
" cells = cell_arrays(df)\n",
" rows = []\n",
" for k, c in cells.items():\n",
" paths, _ = mc_forecast(c, 104, n_path=200)\n",
" pk = paths.max(axis=1); rows.append((k, float((pk >= NEED_CUT).mean()), max(0, int(np.ceil((np.percentile(pk, 75) - 0.10) * c[\"auth\"])))))\n",
" return pd.DataFrame(rows, columns=[\"cell\", \"p\", \"need\"])\n",
"nb, ns = need_table(base), need_table(surge)\n",
"print(f\"[๋๋ ๊ฒฐ์ ์ํฉ] ํ์: ๋ณด์ถฉ ํ์ ํ๋ฅ 50% ์ด์ ์
{int((nb['p'] >= .5).sum())}๊ฐ, ํ์ ์ธ์ ํฉ๊ณ {int(nb['need'].sum())}๋ช
\")\n",
"print(f\" ๊ฐ์B๋๋ ํต์ ์ ์ถ ๊ธ์ฆ: ํ๋ฅ 50% ์ด์ ์
{int((ns['p'] >= .5).sum())}๊ฐ, ํ์ ์ธ์ ํฉ๊ณ {int(ns['need'].sum())}๋ช
(ํด๋น ์
{int(nb[nb['cell'] == ('๊ฐ์B๋๋', 'ํต์ ')]['need'].iloc[0])}๋ช
-> {int(ns[ns['cell'] == ('๊ฐ์B๋๋', 'ํต์ ')]['need'].iloc[0])}๋ช
)\")\n",
"\n",
"# 2) ์์ฒญ ์ฒ๋ฆฌ ์๋ฒ ๋ชจ์ฌ: ํ + ์์
์ ์ค๋ ๋, ํ๊ฐ ์ฐจ๋ฉด ๊ฒฝ๋ ์์ธก์ผ๋ก ๊ธฐ๋ฅ ์ถ์(์์ฒญ์ ๋ฒ๋ฆฌ์ง ์๋๋ค)\n",
"CELLS = cell_arrays(surge)\n",
"KEYS = list(CELLS)\n",
"heavy_calls = {\"n\": 0}\n",
"\n",
"\n",
"def heavy(key, week):\n",
" heavy_calls[\"n\"] += 1\n",
" paths, _ = mc_forecast(CELLS[key], week, n_path=1000)\n",
" return float((paths.max(axis=1) >= NEED_CUT).mean())\n",
"\n",
"\n",
"cached = lru_cache(maxsize=512)(heavy)\n",
"\n",
"\n",
"def light(key):\n",
" \"\"\"๊ฒฝ๋ ์์ธก(๊ธฐ๋ฅ ์ถ์): ํ์ฌ ๊ฒฐ์์จ๊ณผ ์ต๊ทผ 4์ฃผ ์ถ์ธ๋ง ์ฌ์ฉ.\"\"\"\n",
" v = CELLS[key][\"vr\"]; return float(np.clip(v[-1] + 3 * (v[-1] - v[-5]) / 4, 0, 1) >= NEED_CUT)\n",
"\n",
"\n",
"def run(mult, optimized, base_rate=150, dur=0.8, workers=4, qmax=48, seed=1):\n",
" rng = np.random.default_rng(seed)\n",
" n = int(base_rate * dur * mult) # ์ด๋น ์์ฒญ ์(๊ธฐ์ค 150๊ฑด/์ด x ๋ฐฐ์)๋ฅผ dur์ด ๋์ ์ผ์ ๊ฐ๊ฒฉ์ผ๋ก ๋ฐ์\n",
" reqs = [(i, KEYS[rng.integers(len(KEYS))] if rng.random() > 0.02 else \"์๋ชป๋ ์
\", 90 + int(rng.integers(0, 15))) for i in range(n)] # 2%๋ ์๋ชป๋ ์์ฒญ\n",
" q = queue.Queue(maxsize=qmax); out, lock = [], threading.Lock()\n",
" fn = cached if optimized else heavy\n",
"\n",
" def worker():\n",
" while True:\n",
" item = q.get()\n",
" if item is None:\n",
" return\n",
" i, key, wk, t_in = item\n",
" try:\n",
" if key not in CELLS:\n",
" raise KeyError(\"unknown cell\")\n",
" if i % 50 == 7:\n",
" raise RuntimeError(\"model failure(injected)\")\n",
" res, mode = fn(key, 104 if optimized else wk), \"์ ์\"\n",
" except KeyError:\n",
" res, mode = None, \"๊ฒฉ๋ฆฌ(์๋ชป๋ ์์ฒญ)\"\n",
" except RuntimeError:\n",
" res, mode = light(key), \"์ถ์(๋ชจ๋ธ ์ค๋ฅ ๋์ฒด)\"\n",
" with lock:\n",
" out.append((i, mode, time.perf_counter() - t_in))\n",
"\n",
" ths = [threading.Thread(target=worker) for _ in range(workers)]\n",
" for t in ths:\n",
" t.start()\n",
" t0 = time.perf_counter(); degraded = 0\n",
" for i, key, wk in reqs:\n",
" due = t0 + i / (base_rate * mult)\n",
" while time.perf_counter() < due:\n",
" time.sleep(0.0003)\n",
" item = (i, key, wk, time.perf_counter())\n",
" try:\n",
" q.put_nowait(item)\n",
" except queue.Full: # ํ ํฌํ: ๋ฒ๋ฆฌ์ง ์๊ณ ๊ฒฝ๋ ์์ธก์ผ๋ก ์ฆ์ ์๋ต(๊ธฐ๋ฅ ์ถ์)\n",
" degraded += 1\n",
" with lock:\n",
" out.append((i, \"์ถ์(ํ ํฌํ)\", time.perf_counter() - item[3]))\n",
" if key in CELLS:\n",
" light(key)\n",
" for _ in ths:\n",
" q.put(None)\n",
" for t in ths:\n",
" t.join()\n",
" dt = time.perf_counter() - t0\n",
" lat = np.array([o[2] for o in out]) * 1000\n",
" modes = pd.Series([o[1] for o in out]).value_counts().to_dict()\n",
" return {\"n\": n, \"done\": len(out), \"dt\": dt, \"p95\": float(np.percentile(lat, 95)), \"modes\": modes, \"degraded\": degraded, \"ids\": {o[0] for o in out}}\n",
"\n",
"\n",
"print(\"\\n[์์ฒญ ๋ถํ ์ํ] ๊ธฐ์ค 150๊ฑด/์ด x ๋ฐฐ์๋ฅผ 0.8์ด๊ฐ ๋ฐ์, ์์
์ 4, ํ 48\")\n",
"rows = []\n",
"for mult in (1, 2, 5):\n",
" heavy_calls[\"n\"] = 0; cached.cache_clear()\n",
" r0 = run(mult, False); h0 = heavy_calls[\"n\"]\n",
" heavy_calls[\"n\"] = 0; cached.cache_clear()\n",
" r1 = run(mult, True); h1 = heavy_calls[\"n\"]\n",
" lost = r1[\"n\"] - len(r1[\"ids\"])\n",
" rows.append((mult, r0, r1, h0, h1, lost))\n",
" print(f\" {mult * 100:3d}%: ์์ฒญ {r1['n']}๊ฑด | ์ต์ ํ ์ {r0['dt']:.2f}์ด(p95 {r0['p95']:.0f}ms, ์ถ์ {r0['degraded']}๊ฑด) -> ์บ์ ํ {r1['dt']:.2f}์ด(p95 {r1['p95']:.0f}ms, ์ถ์ {r1['degraded']}๊ฑด), ๋ฌด๊ฑฐ์ด ์์ธก {h0}ํ -> {h1}ํ | ์ ์ค {lost}๊ฑด\")\n",
"r5 = rows[-1][2]\n",
"print(\"500% ์ฒ๋ฆฌ ๋ด์ญ:\", r5[\"modes\"])\n",
"tot_lost = sum(x[5] for x in rows)\n",
"print(f\"์์คํ
๋ค์ด(์ฒ๋ฆฌ๋์ง ์์ ์์ธ) 0๊ฑด, ๋ฐ์ดํฐ ์ ์ค {tot_lost}๊ฑด / ์๋ต {sum(len(x[2]['ids']) for x in rows)}๊ฑด (๋ชฉํ 0%: {'๋ฌ์ฑ' if tot_lost == 0 else '๋ฏธ๋ฌ'})\")\n",
"print(f\"๋ณ๋ชฉ ์ต์ ํ: ๊ฐ์ ์
์ ์ต์ ์์ธก์ ์บ์(lru_cache)ํด 500%์์ p95 ์๋ต {rows[-1][1]['p95']:.0f}ms -> {rows[-1][2]['p95']:.0f}ms, ๊ธฐ๋ฅ ์ถ์ {rows[-1][1]['degraded']}๊ฑด -> {rows[-1][2]['degraded']}๊ฑด, ๋ฌด๊ฑฐ์ด ์์ธก ํธ์ถ {rows[-1][3]}ํ -> {rows[-1][4]}ํ. ๋ณ๋ชฉ์ ์
๋น 1,000๊ฒฝ๋ก ๋ชจ์ ์์ธก ํจ์(heavy)\")\n",
"print(\"[๊ธฐ๋ฅ ์ถ์ ์ ์ฑ
] ํ ํฌํยท๋ชจ๋ธ ์ค๋ฅ ์ ์์ฒญ์ ๋ฒ๋ฆฌ์ง ์๊ณ 'ํ์ฌ ๊ฒฐ์์จ + ์ต๊ทผ ์ถ์ธ' ๊ฒฝ๋ ์์ธก์ผ๋ก ๋์ฒด ์๋ตํ๊ณ '์ถ์' ํ์๋ฅผ ๋จ๊ธด๋ค. ์๋ชป๋ ์์ฒญ์ ๊ฒฉ๋ฆฌํด ์ค๋ฅ ๋ก๊ทธ์ ๊ธฐ๋กํ๋ค.\")\n",
"print(\"โป ์๋ต ์๊ฐ์ ์คํ ํ๊ฒฝ์ ๋ฐ๋ผ ๋ฌ๋ผ์ง๋ฉฐ, ์ค์ ์์คํ
์ ๋ณ๋ ์์
ํ(๋น๋๊ธฐ)์ ๋ถํ ์ํ์ด ํ์ํ๋ค. ๋ชจ๋ ๊ฒฐ์ ์ ์ฌ๋์ด ํ์ธํ๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(10, 4))\n",
"ax[0].plot([x[0] * 100 for x in rows], [x[1][\"dt\"] for x in rows], \"o-\", label=\"before\"); ax[0].plot([x[0] * 100 for x in rows], [x[2][\"dt\"] for x in rows], \"s-\", label=\"cached\"); ax[0].set_xlabel(\"traffic (%)\"); ax[0].set_ylabel(\"seconds\"); ax[0].legend(); ax[0].set_title(\"Processing time vs traffic\")\n",
"ax[1].bar([\"normal\", \"surge\"], [int(nb[\"need\"].sum()), int(ns[\"need\"].sum())], color=[\"tab:gray\", \"tab:red\"]); ax[1].set_title(\"Required replenishment (persons), normal vs surge\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
08 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๋๋ ๊ฒฐ์ ์ํฉ] ํ์: ๋ณด์ถฉ ํ์ ํ๋ฅ 50% ์ด์ ์
6๊ฐ, ํ์ ์ธ์ ํฉ๊ณ 67๋ช
\n",
" ๊ฐ์B๋๋ ํต์ ์ ์ถ ๊ธ์ฆ: ํ๋ฅ 50% ์ด์ ์
7๊ฐ, ํ์ ์ธ์ ํฉ๊ณ 71๋ช
(ํด๋น ์
1๋ช
-> 9๋ช
)\n",
"\n",
"[์์ฒญ ๋ถํ ์ํ] ๊ธฐ์ค 150๊ฑด/์ด x ๋ฐฐ์๋ฅผ 0.8์ด๊ฐ ๋ฐ์, ์์
์ 4, ํ 48\n",
" 100%: ์์ฒญ 120๊ฑด | ์ต์ ํ ์ 0.80์ด(p95 8ms, ์ถ์ 0๊ฑด) -> ์บ์ ํ 0.80์ด(p95 5ms, ์ถ์ 0๊ฑด), ๋ฌด๊ฑฐ์ด ์์ธก 115ํ -> 24ํ | ์ ์ค 0๊ฑด\n",
" 200%: ์์ฒญ 240๊ฑด | ์ต์ ํ ์ 0.81์ด(p95 47ms, ์ถ์ 0๊ฑด) -> ์บ์ ํ 0.80์ด(p95 2ms, ์ถ์ 0๊ฑด), ๋ฌด๊ฑฐ์ด ์์ธก 232ํ -> 24ํ | ์ ์ค 0๊ฑด\n",
" 500%: ์์ฒญ 600๊ฑด | ์ต์ ํ ์ 0.94์ด(p95 198ms, ์ถ์ 317๊ฑด) -> ์บ์ ํ 0.80์ด(p95 8ms, ์ถ์ 0๊ฑด), ๋ฌด๊ฑฐ์ด ์์ธก 274ํ -> 29ํ | ์ ์ค 0๊ฑด\n",
"500% ์ฒ๋ฆฌ ๋ด์ญ: {'์ ์': 578, '์ถ์(๋ชจ๋ธ ์ค๋ฅ ๋์ฒด)': 12, '๊ฒฉ๋ฆฌ(์๋ชป๋ ์์ฒญ)': 10}\n",
"์์คํ
๋ค์ด(์ฒ๋ฆฌ๋์ง ์์ ์์ธ) 0๊ฑด, ๋ฐ์ดํฐ ์ ์ค 0๊ฑด / ์๋ต 960๊ฑด (๋ชฉํ 0%: ๋ฌ์ฑ)\n",
"๋ณ๋ชฉ ์ต์ ํ: ๊ฐ์ ์
์ ์ต์ ์์ธก์ ์บ์(lru_cache)ํด 500%์์ p95 ์๋ต 198ms -> 8ms, ๊ธฐ๋ฅ ์ถ์ 317๊ฑด -> 0๊ฑด, ๋ฌด๊ฑฐ์ด ์์ธก ํธ์ถ 274ํ -> 29ํ. ๋ณ๋ชฉ์ ์
๋น 1,000๊ฒฝ๋ก ๋ชจ์ ์์ธก ํจ์(heavy)\n",
"[๊ธฐ๋ฅ ์ถ์ ์ ์ฑ
] ํ ํฌํยท๋ชจ๋ธ ์ค๋ฅ ์ ์์ฒญ์ ๋ฒ๋ฆฌ์ง ์๊ณ 'ํ์ฌ ๊ฒฐ์์จ + ์ต๊ทผ ์ถ์ธ' ๊ฒฝ๋ ์์ธก์ผ๋ก ๋์ฒด ์๋ตํ๊ณ '์ถ์' ํ์๋ฅผ ๋จ๊ธด๋ค. ์๋ชป๋ ์์ฒญ์ ๊ฒฉ๋ฆฌํด ์ค๋ฅ ๋ก๊ทธ์ ๊ธฐ๋กํ๋ค.\n",
"โป ์๋ต ์๊ฐ์ ์คํ ํ๊ฒฝ์ ๋ฐ๋ผ ๋ฌ๋ผ์ง๋ฉฐ, ์ค์ ์์คํ
์ ๋ณ๋ ์์
ํ(๋น๋๊ธฐ)์ ๋ถํ ์ํ์ด ํ์ํ๋ค. ๋ชจ๋ ๊ฒฐ์ ์ ์ฌ๋์ด ํ์ธํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
09. AAR ๊ธฐ๋ฐ ์ธ์ฌ ์์ธก ์ค์ฐจ ์์ธ ๋ถ์ ๋ฐ ์ธ๋ ฅ ์์ธก ๊ฐ์ค์น ํ๋ผ๋ฏธํฐ ์ฌ๋ณด์ \n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"AAR ๊ธฐ๋ฐ ์ธ์ฌ ์์ธก ์ค์ฐจ ์์ธ์ ๋ถ์ํ๊ณ ๊ฐ์ค์น๋ฅผ ์ฌ๋ณด์ ํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **AAR๋ก ์ธ์ฌ ์์ธก ์ค์ฐจ์ ์์ธ์ ๋ถ์ํ๊ณ ์์ธก ๊ฐ์ค ํ๋ผ๋ฏธํฐ๋ฅผ ์ฌ๋ณด์ ํด ์ข
ํฉ ์ ํ๋ ๊ฐ์ ๊ณผ ์ฌ๋ฐ ์ค๋ฅ ํํฉ์ ๊ฒ์ฆํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๋ ๋ถ๋์์ ์ ์ถ์ด ๋์ด๋ ๊ฐ์ ์ธ๊ณ 8๊ฐ์์ 91์ฃผ ์์ ์ 13์ฃผ ์์ธก์ ์ค์ ์ ๋น๊ตํด ํธํฅ๊ณผ ์์ธ(์ต๊ทผ ์ ์ถ ๊ธ์ฆ ๋ฏธ๋ฐ์)์ ๋ถ๋ฅ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `docs/adm01_aar_report.md ๋ฐ configs/adm01_tuned_params.yaml` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ค๋ฅ ์์ธ ๊ท๋ช
์ ๊ฐ๊ด์ฑ ๋ฐ ๊ฐ์ ์กฐ์น์ ๋
ผ๋ฆฌ์ ํ๋น์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: AAR ํผ๋๋ฐฑ ์ ์ฉ ํ ์ข
ํฉ ์ ํ๋ 10%p ์ด์ ํฅ์, ์ฌ๋ฐ ์ค๋ฅ 0๊ฑด\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: AAR ๋ณด๊ณ ์ ์์ฑ ๋ฐ ํ๋ผ๋ฏธํฐ ํ๋ ์ ํ ํ๊ท ํ
์คํธ ํต๊ณผ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ํ๊ท ํ
์คํธ(Regression Test) ์ค์ํธ ์ ์ฒด ์๋ ์คํ ๋ฐ ๋น๊ต\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"๋ ๋ถ๋ ์ ์ฒด์ ์ ์ถยทํ๊ฒฌ์ด ๋์ด๋ ๊ฐ์ ์ธ๊ณ 8๊ฐ์์ 91์ฃผ ์์ 13์ฃผ ๊ฒฐ์ ์์ธก๊ณผ ์ค์ ๊ฒฐ๊ณผ์ ํธ์ฐจ๋ฅผ AAR๋ก ๋ถ์ํด ์ค์ฐจ ์์ธ(์ต๊ทผ ์ ์ถ ๊ธ์ฆ ๋ฏธ๋ฐ์)์ ๋ถ๋ฅํ๊ณ , ์ต๊ทผ ์ ์ถ์จ ๋ฐ์ ๋น์ค๊ณผ ํ์ ํ๋ฅ ๊ธฐ์ค์ ํ๋ ์ธ๊ณ์์ ์ฌ๋ณด์ ํด ์ํ ์ธ๊ณ์์ ์ข
ํฉ ์ ํ๋ ๊ฐ์ ยท์ฌ๋ฐ ์ค๋ฅ๋ฅผ ๊ฒ์ฆํ๋ฉฐ ์ฌ๋ณด์ ํ๋ผ๋ฏธํฐ(YAML ๋ฌธ์์ด)์ ์ง์ ํ์ต ํธ๋ฆฌ๊ฑฐ๋ฅผ ์ถ๋ ฅํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋ถ๋ ์ธ๋ ฅ ํํฉ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยทํธ์ ยท๋ณ๋ ฅยท๊ฐ์ธ ์ ๋ณด ์์)\n",
"# ๊ฐ์ ๋๋ 4๊ณณ x ์ง๋ฌด 6์ข
์ ์ฃผ๊ฐ ์ง๊ณ ์๋ฃ๋ง ๋ง๋ ๋ค. ๊ฐ์ธ ๋จ์ ์ ๋ณด(์ด๋ฆยท๊ตฐ๋ฒ ๋ฑ)๋ ๋ง๋ค์ง ์๋๋ค.\n",
"# ์ธ์์ ์ง๋ฌด๋ณ ์ง๊ณ ์์ด๋ฉฐ, ๊ฒฐ์ยทํด๊ฐยทํ์ยท๊ต์กยท๋ณด์ถฉยท์ ์ถ์ ๋ฐ์ํ ํ์ฌ์๊ณผ ์ด๊ณผ๊ทผ๋ฌด ์๊ฐ์ผ๋ก ๊ตฌ์ฑ๋๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"UNITS = [\"๊ฐ์A๋๋\", \"๊ฐ์B๋๋\", \"๊ฐ์C๋๋\", \"๊ฐ์D๋๋\"]\n",
"JOBS = [\"์์ \", \"ํต์ \", \"์ ๋น\", \"๋ณด๊ธ\", \"ํ์ \", \"์๋ฌด\"]\n",
"FIELDS = [\"unit\", \"job\", \"week\", \"authorized\", \"present\", \"vacancy\", \"leave\", \"sick\", \"training\", \"inflow\", \"outflow\", \"overtime_h\", \"night_ratio\", \"rotation_days\", \"fatigue_label\"]\n",
"HI_CUT = 55 # ๊ณ ํผ๋ก ํ์ ๊ธฐ์ค(๊ฐ์ ์ ์ 55 ์ด์)\n",
"\n",
"\n",
"def make_manning(seed=7, weeks=104, shock=None):\n",
" \"\"\"๋ถ๋ x ์ง๋ฌด 24๊ฐ ์
์ ์ฃผ๊ฐ ์ธ๋ ฅ ์๋ฃ. shock=(unit, job ๋๋ None(๋ถ๋ ์ ์ฒด), ์์์ฃผ, ์ฃผ๊ฐ ์ถ๊ฐ ์ ์ถ ํ๊ท ์) ํ ๊ฑด ๋๋ ๊ทธ ๋ชฉ๋ก์ด๋ฉด ๊ทธ ์
์ ๋๋ ๊ฒฐ์ ์์ธ์ ๋ฃ๋๋ค(AARยท์คํธ๋ ์ค์ฉ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" shocks = [shock] if shock and not isinstance(shock[0], tuple) else list(shock or [])\n",
" rows = []\n",
" for ui, u in enumerate(UNITS):\n",
" for ji, j in enumerate(JOBS):\n",
" auth = int(rng.integers(14, 42) * (0.9 + 0.06 * ui))\n",
" work = rng.uniform(0.92, 1.12) # ์ง๋ฌด๋ณ ์
๋ฌด๋ ๊ณ์\n",
" net = rng.uniform(-0.05, 0.03) # ๊ตฌ์กฐ์ ๋ณด์ถฉ ๋ถ์กฑ(์์)ยท๊ณผ์(์์) ๊ฒฝํฅ(์ฃผ๋น ์ธ์)\n",
" present = int(auth * rng.uniform(0.82, 0.98))\n",
" rot = int(rng.choice([60, 90, 120])) # ๊ต๋ยท๋ณด์ถฉ ์ฃผ๊ธฐ(์ผ)\n",
" for w in range(1, weeks + 1):\n",
" season = 1.0 + 0.9 * (abs(w % 52 - 22) < 4 or abs(w % 52 - 46) < 3) # ์ ์ถยท์ ์ญ ์ง์ค ์๊ธฐ\n",
" out = int(rng.poisson(0.22 * auth / 20 * season))\n",
" for s_ in shocks:\n",
" if s_[0] == u and (s_[1] is None or s_[1] == j) and w >= s_[2]:\n",
" out += int(rng.poisson(s_[3]))\n",
" inflow = int(rng.poisson(max(0.0, 0.2 * auth / 20 * (1 + net * 10)))) + (int(rng.integers(2, 6) * auth / 25) if w % (rot // 7) == 0 else 0)\n",
" present = int(np.clip(present - out + inflow, int(auth * 0.4), auth))\n",
" leave = int(rng.binomial(present, 0.05 + 0.05 * (abs(w % 52 - 31) < 3 or w % 52 >= 50)))\n",
" sick = int(rng.poisson(0.025 * present)); train = int(rng.binomial(present, 0.04 + 0.06 * (w % 13 == 6)))\n",
" avail = max(present - leave - sick - train, 1)\n",
" night = float(np.clip(rng.normal(0.25 + 0.05 * (j == \"์์ \") + 0.05 * (j == \"์๋ฌด\"), 0.05), 0.05, 0.6))\n",
" ot = float(np.clip(auth * 40 * work / avail - 40 + rng.normal(0, 1.5), 0, 40))\n",
" vac = auth - present\n",
" absent = (leave + sick + train) / max(present, 1)\n",
" fat = 100 * (0.38 * min(vac / auth / 0.25, 1.2) + 0.34 * min(ot / 20, 1.2) + 0.10 * min(night / 0.4, 1.2) + 0.18 * min(absent / 0.2, 1.2)) + rng.normal(0, 5)\n",
" rows.append((u, j, w, auth, present, vac, leave, sick, train, inflow, out, round(ot, 1), round(night, 2), rot, int(fat >= HI_CUT)))\n",
" return pd.DataFrame(rows, columns=FIELDS)\n",
"\n",
"\n",
"def cell_pivot(df, col=\"vacancy\"):\n",
" \"\"\"์ฃผ x (๋ถ๋,์ง๋ฌด) ํ.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 9] AAR ๊ธฐ๋ฐ ์ธ์ฌ ์์ธก ์ค์ฐจ ์์ธ ๋ถ์ ๋ฐ ์ธ๋ ฅ ์์ธก ๊ฐ์ค์น ํ๋ผ๋ฏธํฐ ์ฌ๋ณด์ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"from sklearn.linear_model import Ridge\n",
"H = 13 # ์์ธก ๊ธฐ๊ฐ: ํฅํ 3๊ฐ์(13์ฃผ)\n",
"NEED_CUT = 0.15 # ๊ฒฐ์์จ 15% ์ด์์ด๋ฉด ๋ณด์ถฉ ํ์(๊ฐ์ ๊ธฐ์ค)\n",
"\n",
"\n",
"def season(w):\n",
" # [TODO : ํ์ ์ค์ต 9] AAR ๊ธฐ๋ฐ ์ธ์ฌ ์์ธก ์ค์ฐจ ์์ธ ๋ถ์ ๋ฐ ์ธ๋ ฅ ์์ธก ๊ฐ์ค์น ํ๋ผ๋ฏธํฐ ์ฌ๋ณด์ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def feat(vr, ir, orr, wn, rot, v2=True):\n",
" \"\"\"vrยทirยทorr: ์ง๊ธ๊น์ง์ ๊ฒฐ์์จยท๋ณด์ถฉ์จยท์ ์ถ์จ ๋ชฉ๋ก, wn: ์์ธกํ ๋ค์ ์ฃผ์ฐจ, rot: ๊ต๋ ์ฃผ๊ธฐ(์ผ). v2๋ ๋ณด์ถฉยท์ ์ถ ํ๋ฆ๊ณผ ๋ฌ๋ ฅ ํน์ง์ ์ถ๊ฐํ๋ค.\"\"\"\n",
" f = [vr[-1], vr[-1] - vr[-2], vr[-1] - vr[-4]]\n",
" if v2:\n",
" step = max(rot // 7, 1)\n",
" f += [float(np.mean(orr[-12:]) - np.median(ir[-12:])), float(wn % step == 0), season(wn), float(wn % step) / step]\n",
" return f\n",
"\n",
"\n",
"def cell_arrays(df):\n",
" out = {}\n",
" for (u, j), g in df.sort_values(\"week\").groupby([\"unit\", \"job\"]):\n",
" a = g[\"authorized\"].to_numpy()\n",
" out[(u, j)] = {\"auth\": int(a[0]), \"rot\": int(g[\"rotation_days\"].iloc[0]), \"vr\": (g[\"vacancy\"].to_numpy() / a), \"ir\": g[\"inflow\"].to_numpy() / a, \"or\": g[\"outflow\"].to_numpy() / a}\n",
" return out\n",
"\n",
"\n",
"def fit_fc(cells, origin, v2=True, alpha=1.0):\n",
" X, y = [], []\n",
" for c in cells.values():\n",
" for t in range(12, origin - 1): # t์ฃผ๊น์ง ์ ๋ t+1์ฃผ(์ธ๋ฑ์ค t)์ ๊ฒฐ์์จ ๋ณํ๋ฅผ ๋งํ๋ค\n",
" X.append(feat(c[\"vr\"][:t], c[\"ir\"][:t], c[\"or\"][:t], t + 1, c[\"rot\"], v2)); y.append(c[\"vr\"][t] - c[\"vr\"][t - 1])\n",
" return Ridge(alpha=alpha).fit(np.array(X), np.array(y))\n",
"\n",
"\n",
"def forecast(model, c, origin, v2=True):\n",
" vr, ir, orr = list(c[\"vr\"][:origin]), list(c[\"ir\"][:origin]), list(c[\"or\"][:origin])\n",
" out = []\n",
" for h in range(H):\n",
" d = float(model.predict([feat(vr, ir, orr, origin + h + 1, c[\"rot\"], v2)])[0])\n",
" p = float(np.clip(vr[-1] + d, 0, 1))\n",
" out.append(p); vr.append(p); ir.append(float(np.median(ir[-12:]))); orr.append(float(np.mean(orr[-12:])))\n",
" return np.array(out)\n",
"\n",
"\n",
"def mc_forecast(c, origin, n_path=300, seed=0, out_mult=1.0, lam=0.0):\n",
" \"\"\"์ฌ๊ณ -ํ๋ฆ(stock-flow) ๋ชจ์: ์
์ ํ์ยท์ ์ถ ์ง์ค๊ธฐ ์ ์ถ์จ, ์ ๊ท ๋ณด์ถฉ์จ, ๊ต๋ ์ฃผ๊ธฐ ๋ณด์ถฉ ๊ท๋ชจ๋ฅผ ๊ณผ๊ฑฐ์์ ์ถ์ ํด ๊ฒฐ์ ๊ฒฝ๋ก n_path๊ฐ๋ฅผ ๋ง๋ ๋ค.\n",
" ๋ฐํ: (๊ฒฝ๋ก ๋ฐฐ์ด [n_path, H] ๊ฒฐ์์จ, ์ถ์ ๋ชจ์). out_mult๋ ์ ์ถ ๊ฐ์ ์ ํค์ฐ๋ ๋ณด์ ๊ณ์, lam์ ์ต๊ทผ 8์ฃผ ์ ์ถ์จ์ ๋ฐ์ํ๋ ๋น์ค(0~1, ๊ธ์ฆ ๊ฐ์ง ๋๋ง ์ ์ฉ, AAR ์ฌ๋ณด์ ์ฉ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" w = np.arange(1, origin + 1); step = max(c[\"rot\"] // 7, 1)\n",
" rot_w = (w % step == 0); sea = np.array([season(x) > 0 for x in w])\n",
" o, i = c[\"or\"][:origin], c[\"ir\"][:origin]\n",
" o_base = o[~sea].mean(); o_sea = o[sea].mean() if sea.sum() >= 6 else o_base * 1.9\n",
" o_rec = o[-8:].mean()\n",
" mu = max(o_base, 1e-6) * c[\"auth\"] * 8 # ์ต๊ทผ 8์ฃผ ํ์ ๊ธฐ๋ ์ ์ถ ์ธ์\n",
" use_rec = lam > 0 and o_rec * c[\"auth\"] * 8 > mu + 2.0 * np.sqrt(mu) # ํฌ์์ก ๊ธฐ์ค ์์ 2ํ์คํธ์ฐจ๋ฅผ ๋๋ '๊ธ์ฆ'์ผ ๋๋ง ์ต๊ทผ ๊ฐ์ ๋ฐ์\n",
" i_reg = i[~rot_w].mean(); batch = max(i[rot_w].mean() - i_reg, 0.0)\n",
" a = c[\"auth\"]; pres = np.full(n_path, a * (1 - c[\"vr\"][origin - 1]))\n",
" out = np.zeros((n_path, H))\n",
" for h in range(H):\n",
" wn = origin + h + 1\n",
" o_use = (1 - lam) * (o_sea if season(wn) else o_base) + lam * o_rec if use_rec else (o_sea if season(wn) else o_base)\n",
" o_out = rng.poisson(max(o_use, 1e-6) * a * out_mult, n_path)\n",
" o_in = rng.poisson(max(i_reg, 1e-6) * a, n_path) + (np.maximum(rng.normal(batch * a, 0.3 * batch * a, n_path), 0).round() if wn % step == 0 else 0)\n",
" pres = np.clip(pres - o_out + o_in, 0.4 * a, a)\n",
" out[:, h] = 1 - pres / a\n",
" return out, {\"o_base\": o_base, \"o_sea\": o_sea, \"i_reg\": i_reg, \"batch\": batch}\n",
"\n",
"ORIGIN = 91\n",
"rng = np.random.default_rng(5)\n",
"JOBS_ = list(JOBS)\n",
"worlds = []\n",
"for s in range(21, 29):\n",
" us = rng.choice(4, 2, replace=False)\n",
" sh = [(UNITS[u], None, 82, float(rng.uniform(0.4, 0.8))) for u in us] # ์์ธก ์์ 9์ฃผ ์ ๋ถํฐ ๋ ๋ถ๋ ์ ์ฒด ์ง๋ฌด์ ์ฃผ๋น ํ๊ท 0.4~0.8๋ช
์ถ๊ฐ ์ ์ถ(ํ๊ฒฌยท๋์ ํด์ ๋ฑ ์ ์ฑ
๋ณํ)\n",
" d = make_manning(s, 104, shock=sh)\n",
" worlds.append({\"seed\": s, \"shock\": sh, \"cells\": cell_arrays(d)})\n",
"TUNE, TEST = worlds[:4], worlds[4:]\n",
"\n",
"\n",
"def evaluate(ws, lam, p0, out_mult=1.0):\n",
" rows = []\n",
" for w in ws:\n",
" for k, c in w[\"cells\"].items():\n",
" paths, _ = mc_forecast(c, ORIGIN, lam=lam, out_mult=out_mult, n_path=300)\n",
" pk = paths.max(axis=1); act = c[\"vr\"][ORIGIN:ORIGIN + H]\n",
" need = lambda peak: max(0, int(np.ceil((peak - 0.10) * c[\"auth\"])))\n",
" rows.append({\"seed\": w[\"seed\"], \"cell\": k, \"shock\": k[0] in [x[0] for x in w[\"shock\"]], \"p\": float((pk >= NEED_CUT).mean()), \"pred_peak\": float(pk.mean()), \"act_peak\": float(act.max()), \"auth\": c[\"auth\"],\n",
" \"act_need\": bool(act.max() >= NEED_CUT), \"pred_n\": need(float(np.percentile(pk, 75))), \"act_n\": need(float(act.max()))})\n",
" r = pd.DataFrame(rows); r[\"pred_need\"] = r[\"p\"] >= p0\n",
" return r\n",
"\n",
"\n",
"def acc(r):\n",
" \"\"\"์ข
ํฉ ์ ํ๋ = ๋ณด์ถฉ ํ์ ์ธ์์ ์ค์ ์ +-2๋ช
์ด๋ด๋ก ๋งํ ์
๋น์จ(๋ณด์ถฉ ํ์๋ ์์ธก์ ์ ํ๋).\"\"\"\n",
" return float(((r[\"pred_n\"] - r[\"act_n\"]).abs() <= 2).mean())\n",
"\n",
"\n",
"def acc_cls(r):\n",
" return float((r[\"pred_need\"] == r[\"act_need\"]).mean())\n",
"\n",
"\n",
"# 1) AAR: ๊ธฐ์กด ๋ชจ๋ธ(lam=0, p0=0.5)์ ํธ์ฐจ\n",
"b0 = evaluate(worlds, 0.0, 0.5)\n",
"b0[\"err\"] = (b0[\"act_peak\"] - b0[\"pred_peak\"]) * b0[\"auth\"]\n",
"print(f\"[AAR] 8๊ฐ ์ธ๊ณ x 24๊ฐ ์
= {len(b0)}๊ฑด, ๊ธฐ์กด ๋ชจ๋ธ ์ข
ํฉ ์ ํ๋(๋ณด์ถฉ ์ธ์ +-2๋ช
์ ์ค) {acc(b0):.1%}, ๋ณด์ถฉ ํ์ ์ฌ๋ถ ํ์ ์ ํ๋ {acc_cls(b0):.1%}, ์ต๋ ๊ฒฐ์ ์์ธก ํ๊ท ํธํฅ {b0['err'].mean():+.2f}๋ช
(์ค์ ๊ฐ ํผ)\")\n",
"sh = b0[b0[\"shock\"]]; ot_ = b0[~b0[\"shock\"]]\n",
"print(f\" ์ ์ถ ๊ธ์ฆ ๋ถ๋ ์
{len(sh)}๊ฑด: ํ๊ท ์ค์ฐจ {sh['err'].mean():+.1f}๋ช
, ๋ณด์ถฉ ํ์๋ฅผ ๋์น ๊ฑด {int((sh['act_need'] & ~sh['pred_need']).sum())}๊ฑด / ๊ทธ ์ธ ์
{len(ot_)}๊ฑด: ํ๊ท ์ค์ฐจ {ot_['err'].mean():+.2f}๋ช
\")\n",
"share = float(sh[\"err\"].abs().sum() / b0[\"err\"].abs().sum())\n",
"print(f\" ์ ์ฒด ์ ๋์ค์ฐจ ์ค ์ ์ถ ๊ธ์ฆ ๋ถ๋ ์
์ด ์ฐจ์งํ๋ ๋น์ค {share:.0%} (์
๋น์ค {len(sh) / len(b0):.0%})\")\n",
"cause = pd.DataFrame({\"์์ธ\": [\"์ต๊ทผ ์ ์ถ ๊ธ์ฆ์ ๊ณผ๊ฑฐ ํ๊ท ์ผ๋ก๋ง ์ถ์ (์ฃผ ์์ธ)\", \"๋ณด์ถฉ ๋์ฐฉ ์์ ๋ถํ์ค\", \"์ ์ถ ์ง์ค๊ธฐ ์ถ์ ์ค์ฐจ\"],\n",
" \"๊ทผ๊ฑฐ\": [f\"์ ์ถ ๊ธ์ฆ ๋ถ๋ ์
์ค์ฐจ {sh['err'].mean():+.1f}๋ช
\", f\"๊ทธ ์ธ ์
์ค์ฐจ ํ์คํธ์ฐจ {ot_['err'].std():.2f}๋ช
\", f\"๊ทธ ์ธ ์
ํ๊ท ํธํฅ {ot_['err'].mean():+.2f}๋ช
\"], \"์กฐ์น\": [\"์ต๊ทผ 8์ฃผ ์ ์ถ์จ ๋ฐ์(๊ธ์ฆ ๊ฐ์ง ์, ๋น์ค lam) ๋์
\", \"ํ๋ฅ ๊ธฐ์ค(p0) ์ฌ์ค์ \", \"์ฐ๊ฐ ์๋ฃ ๋์ ํ ์ฌ์ถ์ \"]})\n",
"print(cause.to_string(index=False))\n",
"\n",
"# 2) ์ฌ๋ณด์ : ํ๋ ์ธ๊ณ์์ ์ต๊ทผ ์ ์ถ ๋ฐ์ ๋น์ค lam์ ์ต๋ ๊ฒฐ์ ์ธ์ MAE๊ฐ ์ต์์ธ ๊ฐ, ํ์ ๊ธฐ์ค p0๋ ํ์ ์ ํ๋(๋๋ฅ ์ด๋ฉด ์ฌํ์จ)๊ฐ ์ต๋์ธ ๊ฐ์ผ๋ก ๊ฒฉ์ ํ์\n",
"mae_of = lambda r: float(np.mean(np.abs(r[\"act_peak\"] - r[\"pred_peak\"]) * r[\"auth\"]))\n",
"print(\"\\n[์ฌ๋ณด์ ๊ฒฉ์ ํ์(ํ๋ ์ธ๊ณ 4๊ฐ)]\")\n",
"grid = {}\n",
"for lam in (0.0, 0.5, 0.8, 1.0):\n",
" r = evaluate(TUNE, lam, 0.5); grid[lam] = mae_of(r)\n",
" print(f\" lam {lam:.1f}: ์ต๋ ๊ฒฐ์ MAE {grid[lam]:.2f}๋ช
, ์ข
ํฉ ์ ํ๋ {acc(r):.1%}, ํ์ ์ ํ๋ {acc_cls(r):.1%}(p0 0.5)\")\n",
"LAM = min(grid, key=grid.get)\n",
"best = None\n",
"for p0 in (0.2, 0.3, 0.4, 0.5, 0.6):\n",
" r = evaluate(TUNE, LAM, p0); rc = float((r[\"pred_need\"] & r[\"act_need\"]).sum() / max(r[\"act_need\"].sum(), 1))\n",
" if best is None or (acc_cls(r), rc) > (best[0], best[1]):\n",
" best = (acc_cls(r), rc, p0)\n",
"P0 = best[2]\n",
"print(f\"์ฑํ ํ๋ผ๋ฏธํฐ: lam {LAM}(MAE ์ต์), p0 {P0}(ํ๋ ์ธ๊ณ ํ์ ์ ํ๋ {best[0]:.1%})\")\n",
"\n",
"# 3) ์ํ ์ธ๊ณ 4๊ฐ๋ก ๊ฒ์ฆ\n",
"t0, t1 = evaluate(TEST, 0.0, 0.5), evaluate(TEST, LAM, P0)\n",
"rcl = lambda r: float((r[\"pred_need\"] & r[\"act_need\"]).sum() / max(r[\"act_need\"].sum(), 1))\n",
"prc = lambda r: float((r[\"pred_need\"] & r[\"act_need\"]).sum() / max(r[\"pred_need\"].sum(), 1))\n",
"print(f\"\\n[์ํ ์ธ๊ณ ๊ฒ์ฆ] ์ข
ํฉ ์ ํ๋ {acc(t0):.1%} -> {acc(t1):.1%} ({(acc(t1) - acc(t0)) * 100:+.1f}%p, ๋ชฉํ 10%p ์ด์: {'๋ฌ์ฑ' if acc(t1) - acc(t0) >= 0.10 else '๋ฏธ๋ฌ'}), ํ์ ์ ํ๋ {acc_cls(t0):.1%} -> {acc_cls(t1):.1%}, ์ฌํ์จ {rcl(t0):.1%} -> {rcl(t1):.1%}, ์ ๋ฐ๋ {prc(t0):.1%} -> {prc(t1):.1%}\")\n",
"shk0, shk1 = t0[t0[\"shock\"]], t1[t1[\"shock\"]]\n",
"miss0, miss1 = int((shk0[\"act_need\"] & ~shk0[\"pred_need\"]).sum()), int((shk1[\"act_need\"] & ~shk1[\"pred_need\"]).sum())\n",
"print(f\"์ฌ๋ฐ ์ค๋ฅ(์ ์ถ ๊ธ์ฆ ๋ถ๋ ์
{len(shk1)}๊ฑด ์ค ๋ณด์ถฉ ํ์๋ฅผ ๋์น ๊ฑด): {miss0}๊ฑด -> {miss1}๊ฑด (๋ชฉํ 0๊ฑด: {'๋ฌ์ฑ' if miss1 == 0 else '๋ฏธ๋ฌ'})\")\n",
"mae0 = float(np.mean(np.abs(t0[\"act_peak\"] - t0[\"pred_peak\"]) * t0[\"auth\"])); mae1 = float(np.mean(np.abs(t1[\"act_peak\"] - t1[\"pred_peak\"]) * t1[\"auth\"]))\n",
"print(f\"์ต๋ ๊ฒฐ์ ์ธ์ MAE {mae0:.2f}๋ช
-> {mae1:.2f}๋ช
, ์ ์ถ ๊ธ์ฆ ๋ถ๋ ์
ํ๊ท ์ค์ฐจ {((shk0['act_peak'] - shk0['pred_peak']) * shk0['auth']).mean():+.1f}๋ช
-> {((shk1['act_peak'] - shk1['pred_peak']) * shk1['auth']).mean():+.1f}๋ช
\")\n",
"fp1 = t1[t1[\"pred_need\"] & ~t1[\"act_need\"]]\n",
"print(f\"๋ถ์์ฉ ์ ๊ฒ: ์ ์ค๊ฒฝ๋ณด {len(fp1)}๊ฑด(๊ธฐ์กด {int((t0['pred_need'] & ~t0['act_need']).sum())}๊ฑด) -> ์ ๋ฐ๋ ๋ณํ๋ฅผ ํจ๊ป ๋ณด๊ณ ์ฑํ ์ฌ๋ถ๋ฅผ ์ฌ๋์ด ๊ฒฐ์ \")\n",
"\n",
"# 4) ์ฌ๋ณด์ ํ๋ผ๋ฏธํฐ์ ์ง์ ํ์ต ์ฐ๊ณ\n",
"YAML = f\"\"\"[adm01_tuned_params ์ ์: ์ธ์ฌ๊ด ํ์ธ ํ ์ ์ฉ]\n",
"forecast:\n",
" recent_outflow_weight: {LAM} # ์ต๊ทผ 8์ฃผ ์ ์ถ์จ ๋ฐ์ ๋น์ค(ํ์ ๊ธฐ๋์น+2ํ์คํธ์ฐจ ์ด๊ณผ ๊ธ์ฆ์ผ ๋๋ง)\n",
" need_probability_cut: {P0}\n",
" need_vacancy_rate: {NEED_CUT}\n",
"retrain_trigger:\n",
" rolling_accuracy_below: 0.80 # ์ต๊ทผ 2๊ฐ ์์ธก ์์ ์ฐ์ ๋ฏธ๋ฌ์ด๋ฉด ์ฌ๋ณด์ ๊ฒํ \n",
" vacancy_surge_weeks: 2 # ํ ์
์ ์ฃผ๊ฐ ์ ์ถ์ด ํ๊ท ์ 2๋ฐฐ๋ฅผ 2์ฃผ ์ฐ์ ๋์ผ๋ฉด ์๋ฆผ\n",
"review: ์ธ์ฌ๊ด ํ์ธ ํ ์ ์ฉ, ์ด์ ํ๋ผ๋ฏธํฐ๋ก ๋๋๋ฆฌ๊ธฐ ๊ฐ๋ฅ\"\"\"\n",
"print(\"\\n\" + YAML)\n",
"print(\"์ง์ ํ์ต ์ฐ๊ณ: ๋ถ๊ธฐ๋ง๋ค AAR ํ๋ณธ(์์ธก-์ค์ ์)์ ์์ ๊ฐ์ ์ ์ฐจ๋ก lamยทp0๋ฅผ ์ฌํ์ํ๊ณ , ์ฌ๋ณด์ ์ ํ ๋น๊ตํ์ ์น์ธ ์ด๋ ฅ์ ๋จ๊ธด๋ค. ๊ฐ์ ์๋ฃ ๊ธฐ์ค ๊ฒฐ๊ณผ์ด๋ฉฐ ์ ์ฉ์ ์ฌ๋์ด ๊ฒฐ์ ํ๋ค.\")\n",
"\n",
"plt.figure(figsize=(8, 4))\n",
"labels = [\"before\", \"after\"]; plt.bar([0, 1], [acc(t0) * 100, acc(t1) * 100], color=[\"tab:gray\", \"tab:blue\"], width=.4); plt.xticks([0, 1], labels)\n",
"plt.bar([2, 3], [rcl(t0) * 100, rcl(t1) * 100], color=[\"tab:gray\", \"tab:blue\"], width=.4); plt.xticks([0, 1, 2, 3], [\"acc before\", \"acc after\", \"recall before\", \"recall after\"], fontsize=8)\n",
"plt.ylabel(\"%\"); plt.title(\"AAR recalibration effect on test worlds\"); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
09 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[AAR] 8๊ฐ ์ธ๊ณ x 24๊ฐ ์
= 192๊ฑด, ๊ธฐ์กด ๋ชจ๋ธ ์ข
ํฉ ์ ํ๋(๋ณด์ถฉ ์ธ์ +-2๋ช
์ ์ค) 72.4%, ๋ณด์ถฉ ํ์ ์ฌ๋ถ ํ์ ์ ํ๋ 78.1%, ์ต๋ ๊ฒฐ์ ์์ธก ํ๊ท ํธํฅ +1.69๋ช
(์ค์ ๊ฐ ํผ)\n",
" ์ ์ถ ๊ธ์ฆ ๋ถ๋ ์
96๊ฑด: ํ๊ท ์ค์ฐจ +3.4๋ช
, ๋ณด์ถฉ ํ์๋ฅผ ๋์น ๊ฑด 17๊ฑด / ๊ทธ ์ธ ์
96๊ฑด: ํ๊ท ์ค์ฐจ +0.01๋ช
\n",
" ์ ์ฒด ์ ๋์ค์ฐจ ์ค ์ ์ถ ๊ธ์ฆ ๋ถ๋ ์
์ด ์ฐจ์งํ๋ ๋น์ค 72% (์
๋น์ค 50%)\n",
" ์์ธ ๊ทผ๊ฑฐ ์กฐ์น\n",
"์ต๊ทผ ์ ์ถ ๊ธ์ฆ์ ๊ณผ๊ฑฐ ํ๊ท ์ผ๋ก๋ง ์ถ์ (์ฃผ ์์ธ) ์ ์ถ ๊ธ์ฆ ๋ถ๋ ์
์ค์ฐจ +3.4๋ช
์ต๊ทผ 8์ฃผ ์ ์ถ์จ ๋ฐ์(๊ธ์ฆ ๊ฐ์ง ์, ๋น์ค lam) ๋์
\n",
" ๋ณด์ถฉ ๋์ฐฉ ์์ ๋ถํ์ค ๊ทธ ์ธ ์
์ค์ฐจ ํ์คํธ์ฐจ 1.62๋ช
ํ๋ฅ ๊ธฐ์ค(p0) ์ฌ์ค์ \n",
" ์ ์ถ ์ง์ค๊ธฐ ์ถ์ ์ค์ฐจ ๊ทธ ์ธ ์
ํ๊ท ํธํฅ +0.01๋ช
์ฐ๊ฐ ์๋ฃ ๋์ ํ ์ฌ์ถ์ \n",
"\n",
"[์ฌ๋ณด์ ๊ฒฉ์ ํ์(ํ๋ ์ธ๊ณ 4๊ฐ)]\n",
" lam 0.0: ์ต๋ ๊ฒฐ์ MAE 2.18๋ช
, ์ข
ํฉ ์ ํ๋ 81.2%, ํ์ ์ ํ๋ 76.0%(p0 0.5)\n",
" lam 0.5: ์ต๋ ๊ฒฐ์ MAE 1.76๋ช
, ์ข
ํฉ ์ ํ๋ 82.3%, ํ์ ์ ํ๋ 78.1%(p0 0.5)\n",
" lam 0.8: ์ต๋ ๊ฒฐ์ MAE 1.71๋ช
, ์ข
ํฉ ์ ํ๋ 80.2%, ํ์ ์ ํ๋ 79.2%(p0 0.5)\n",
" lam 1.0: ์ต๋ ๊ฒฐ์ MAE 1.75๋ช
, ์ข
ํฉ ์ ํ๋ 80.2%, ํ์ ์ ํ๋ 79.2%(p0 0.5)\n",
"์ฑํ ํ๋ผ๋ฏธํฐ: lam 0.8(MAE ์ต์), p0 0.3(ํ๋ ์ธ๊ณ ํ์ ์ ํ๋ 84.4%)\n",
"\n",
"[์ํ ์ธ๊ณ ๊ฒ์ฆ] ์ข
ํฉ ์ ํ๋ 63.5% -> 67.7% (+4.2%p, ๋ชฉํ 10%p ์ด์: ๋ฏธ๋ฌ), ํ์ ์ ํ๋ 80.2% -> 80.2%, ์ฌํ์จ 76.4% -> 84.7%, ์ ๋ฐ๋ 96.5% -> 88.4%\n",
"์ฌ๋ฐ ์ค๋ฅ(์ ์ถ ๊ธ์ฆ ๋ถ๋ ์
48๊ฑด ์ค ๋ณด์ถฉ ํ์๋ฅผ ๋์น ๊ฑด): 8๊ฑด -> 5๊ฑด (๋ชฉํ 0๊ฑด: ๋ฏธ๋ฌ)\n",
"์ต๋ ๊ฒฐ์ ์ธ์ MAE 2.57๋ช
-> 2.12๋ช
, ์ ์ถ ๊ธ์ฆ ๋ถ๋ ์
ํ๊ท ์ค์ฐจ +3.6๋ช
-> +1.7๋ช
\n",
"๋ถ์์ฉ ์ ๊ฒ: ์ ์ค๊ฒฝ๋ณด 8๊ฑด(๊ธฐ์กด 2๊ฑด) -> ์ ๋ฐ๋ ๋ณํ๋ฅผ ํจ๊ป ๋ณด๊ณ ์ฑํ ์ฌ๋ถ๋ฅผ ์ฌ๋์ด ๊ฒฐ์ \n",
"\n",
"[adm01_tuned_params ์ ์: ์ธ์ฌ๊ด ํ์ธ ํ ์ ์ฉ]\n",
"forecast:\n",
" recent_outflow_weight: 0.8 # ์ต๊ทผ 8์ฃผ ์ ์ถ์จ ๋ฐ์ ๋น์ค(ํ์ ๊ธฐ๋์น+2ํ์คํธ์ฐจ ์ด๊ณผ ๊ธ์ฆ์ผ ๋๋ง)\n",
" need_probability_cut: 0.3\n",
" need_vacancy_rate: 0.15\n",
"retrain_trigger:\n",
" rolling_accuracy_below: 0.80 # ์ต๊ทผ 2๊ฐ ์์ธก ์์ ์ฐ์ ๋ฏธ๋ฌ์ด๋ฉด ์ฌ๋ณด์ ๊ฒํ \n",
" vacancy_surge_weeks: 2 # ํ ์
์ ์ฃผ๊ฐ ์ ์ถ์ด ํ๊ท ์ 2๋ฐฐ๋ฅผ 2์ฃผ ์ฐ์ ๋์ผ๋ฉด ์๋ฆผ\n",
"review: ์ธ์ฌ๊ด ํ์ธ ํ ์ ์ฉ, ์ด์ ํ๋ผ๋ฏธํฐ๋ก ๋๋๋ฆฌ๊ธฐ ๊ฐ๋ฅ\n",
"์ง์ ํ์ต ์ฐ๊ณ: ๋ถ๊ธฐ๋ง๋ค AAR ํ๋ณธ(์์ธก-์ค์ ์)์ ์์ ๊ฐ์ ์ ์ฐจ๋ก lamยทp0๋ฅผ ์ฌํ์ํ๊ณ , ์ฌ๋ณด์ ์ ํ ๋น๊ตํ์ ์น์ธ ์ด๋ ฅ์ ๋จ๊ธด๋ค. ๊ฐ์ ์๋ฃ ๊ธฐ์ค ๊ฒฐ๊ณผ์ด๋ฉฐ ์ ์ฉ์ ์ฌ๋์ด ๊ฒฐ์ ํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
10. ๊ฒฐ์ยท๊ทผ๋ฌด๋ถํ ์ถ์ธ ๋์๋ณด๋ ๋ฐ ์ธ๋ ฅ ๋ณด์ถฉ ์์ธก ์์คํ
ํตํฉ ํจํค์ง ์์ฑ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๊ฒฐ์ยท๊ทผ๋ฌด๋ถํ ์ถ์ธ ๋์๋ณด๋ ๋ฐ ์ธ๋ ฅ ๋ณด์ถฉ ์์ธก ์์คํ
ํจํค์ง๋ฅผ ์์ฑํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๊ฒ์ฆยท์ ์ฒ๋ฆฌยทํผ๋ก๋ยท๊ฒฐ์ ์์ธกยท์ํ ์ง์ยท๊ฒํ ํยท๊ฐ์ฌ ๋ก๊ทธยท๋ธ๋ฆฌํ์ ๋ฌถ์ ๊ฒฐ์ยท๊ทผ๋ฌด๋ถํ ๋์๋ณด๋ ๋ฐ ์ธ๋ ฅ ๋ณด์ถฉ ์์ธก ์์คํ
ํจํค์ง๋ฅผ ์์ฑํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ ๋ฏธ์
์ ๊ธฐ๋ฅ์ ํตํฉ ํ์ดํ๋ผ์ธ์ผ๋ก ๋ฌถ์ด ์ค์น ์คํฌ๋ฆฝํธ 5๋จ๊ณ ์ ๊ฒ, ์๋ 4์ข
End-to-End ์คํ(๊ฐ 2ํ ์์ ์ฃผ์
), ์์ฒด ์ ๊ฒ 9ํญ๋ชฉ๊ณผ ํจ์ ์ปค๋ฒ๋ฆฌ์ง ์ํํ๊ณ ๋ถ์ํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/main_adm01.py, README.md, docs/adm01_final_report.pdf(md)` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ตฐ ์ค๋ฌด ์ฆ์ ๋์
๊ฐ๋ฅ์ฑ, ์ด์ฉ ์ ๋ขฐ๋ ๋ฐ ์ ๋ต์ ๊ธฐ๋ํจ๊ณผ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: End-to-End ํตํฉ ํ์ดํ๋ผ์ธ ์คํ ์ฑ๊ณต๋ฅ 100%, ํ
์คํธ ์ปค๋ฒ๋ฆฌ์ง 85% ์ด์\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์ํด๋ฆญ ์คํ ์คํฌ๋ฆฝํธ ๋์ ํ์ธ ๋ฐ ์ต์ข
๋ฐํ ์ฐ์ถ๋ฌผ ์์ฑ ์๋ฃ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ์ต์ข
๊ฒ์ฆ ์์ํ(ํ๊ฐ๊ด) ๋ชจ์ ์์ฐ ๋ฐ ์ ์ฒด ๊ธฐ๋ฅ ํตํฉ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์
๋ ฅ ๊ฒ์ฆยท๊ฒฉ๋ฆฌ, ๊ทผ๋ฌด์ผ์ง ์ ์ฒ๋ฆฌ, ํผ๋ก๋ ์ ์, 13์ฃผ ๊ฒฐ์ ์์ธก๊ณผ ์ํ ์ง์ยท์ ๋ขฐ๋, ์ฌ๋ ๊ฒํ ํ(PENDING)์ ํด์ ์ฒด์ธ ๊ฐ์ฌ ๋ก๊ทธ, ๋ธ๋ฆฌํ ์์ฝ๊น์ง ๊ฒฐ์ยท๊ทผ๋ฌด๋ถํ ์ถ์ธ ๋ฐ ๋ณด์ถฉ ํ์๋ ์์ธก ์์คํ
์ ํ ํ์ผ๋ก ํตํฉ ์คํํ๊ณ ์ค์น ์คํฌ๋ฆฝํธ ์ ๊ฒยทEnd-to-End 4์ข
ยท์์ฒด ์ ๊ฒ(ํจ์ ์ปค๋ฒ๋ฆฌ์ง)ยท๋งค๋ด์ผ ์์ฑ๋ฅ ์ ์ถ๋ ฅํ๋ค.\"\"\"\n",
"import hashlib\n",
"import json\n",
"import re\n",
"import sys\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋ถ๋ ์ธ๋ ฅ ํํฉ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยทํธ์ ยท๋ณ๋ ฅยท๊ฐ์ธ ์ ๋ณด ์์)\n",
"# ๊ฐ์ ๋๋ 4๊ณณ x ์ง๋ฌด 6์ข
์ ์ฃผ๊ฐ ์ง๊ณ ์๋ฃ๋ง ๋ง๋ ๋ค. ๊ฐ์ธ ๋จ์ ์ ๋ณด(์ด๋ฆยท๊ตฐ๋ฒ ๋ฑ)๋ ๋ง๋ค์ง ์๋๋ค.\n",
"# ์ธ์์ ์ง๋ฌด๋ณ ์ง๊ณ ์์ด๋ฉฐ, ๊ฒฐ์ยทํด๊ฐยทํ์ยท๊ต์กยท๋ณด์ถฉยท์ ์ถ์ ๋ฐ์ํ ํ์ฌ์๊ณผ ์ด๊ณผ๊ทผ๋ฌด ์๊ฐ์ผ๋ก ๊ตฌ์ฑ๋๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"UNITS = [\"๊ฐ์A๋๋\", \"๊ฐ์B๋๋\", \"๊ฐ์C๋๋\", \"๊ฐ์D๋๋\"]\n",
"JOBS = [\"์์ \", \"ํต์ \", \"์ ๋น\", \"๋ณด๊ธ\", \"ํ์ \", \"์๋ฌด\"]\n",
"FIELDS = [\"unit\", \"job\", \"week\", \"authorized\", \"present\", \"vacancy\", \"leave\", \"sick\", \"training\", \"inflow\", \"outflow\", \"overtime_h\", \"night_ratio\", \"rotation_days\", \"fatigue_label\"]\n",
"HI_CUT = 55 # ๊ณ ํผ๋ก ํ์ ๊ธฐ์ค(๊ฐ์ ์ ์ 55 ์ด์)\n",
"\n",
"\n",
"def make_manning(seed=7, weeks=104, shock=None):\n",
" \"\"\"๋ถ๋ x ์ง๋ฌด 24๊ฐ ์
์ ์ฃผ๊ฐ ์ธ๋ ฅ ์๋ฃ. shock=(unit, job ๋๋ None(๋ถ๋ ์ ์ฒด), ์์์ฃผ, ์ฃผ๊ฐ ์ถ๊ฐ ์ ์ถ ํ๊ท ์) ํ ๊ฑด ๋๋ ๊ทธ ๋ชฉ๋ก์ด๋ฉด ๊ทธ ์
์ ๋๋ ๊ฒฐ์ ์์ธ์ ๋ฃ๋๋ค(AARยท์คํธ๋ ์ค์ฉ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" shocks = [shock] if shock and not isinstance(shock[0], tuple) else list(shock or [])\n",
" rows = []\n",
" for ui, u in enumerate(UNITS):\n",
" for ji, j in enumerate(JOBS):\n",
" auth = int(rng.integers(14, 42) * (0.9 + 0.06 * ui))\n",
" work = rng.uniform(0.92, 1.12) # ์ง๋ฌด๋ณ ์
๋ฌด๋ ๊ณ์\n",
" net = rng.uniform(-0.05, 0.03) # ๊ตฌ์กฐ์ ๋ณด์ถฉ ๋ถ์กฑ(์์)ยท๊ณผ์(์์) ๊ฒฝํฅ(์ฃผ๋น ์ธ์)\n",
" present = int(auth * rng.uniform(0.82, 0.98))\n",
" rot = int(rng.choice([60, 90, 120])) # ๊ต๋ยท๋ณด์ถฉ ์ฃผ๊ธฐ(์ผ)\n",
" for w in range(1, weeks + 1):\n",
" season = 1.0 + 0.9 * (abs(w % 52 - 22) < 4 or abs(w % 52 - 46) < 3) # ์ ์ถยท์ ์ญ ์ง์ค ์๊ธฐ\n",
" out = int(rng.poisson(0.22 * auth / 20 * season))\n",
" for s_ in shocks:\n",
" if s_[0] == u and (s_[1] is None or s_[1] == j) and w >= s_[2]:\n",
" out += int(rng.poisson(s_[3]))\n",
" inflow = int(rng.poisson(max(0.0, 0.2 * auth / 20 * (1 + net * 10)))) + (int(rng.integers(2, 6) * auth / 25) if w % (rot // 7) == 0 else 0)\n",
" present = int(np.clip(present - out + inflow, int(auth * 0.4), auth))\n",
" leave = int(rng.binomial(present, 0.05 + 0.05 * (abs(w % 52 - 31) < 3 or w % 52 >= 50)))\n",
" sick = int(rng.poisson(0.025 * present)); train = int(rng.binomial(present, 0.04 + 0.06 * (w % 13 == 6)))\n",
" avail = max(present - leave - sick - train, 1)\n",
" night = float(np.clip(rng.normal(0.25 + 0.05 * (j == \"์์ \") + 0.05 * (j == \"์๋ฌด\"), 0.05), 0.05, 0.6))\n",
" ot = float(np.clip(auth * 40 * work / avail - 40 + rng.normal(0, 1.5), 0, 40))\n",
" vac = auth - present\n",
" absent = (leave + sick + train) / max(present, 1)\n",
" fat = 100 * (0.38 * min(vac / auth / 0.25, 1.2) + 0.34 * min(ot / 20, 1.2) + 0.10 * min(night / 0.4, 1.2) + 0.18 * min(absent / 0.2, 1.2)) + rng.normal(0, 5)\n",
" rows.append((u, j, w, auth, present, vac, leave, sick, train, inflow, out, round(ot, 1), round(night, 2), rot, int(fat >= HI_CUT)))\n",
" return pd.DataFrame(rows, columns=FIELDS)\n",
"\n",
"\n",
"def cell_pivot(df, col=\"vacancy\"):\n",
" \"\"\"์ฃผ x (๋ถ๋,์ง๋ฌด) ํ.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 10] ๊ฒฐ์ยท๊ทผ๋ฌด๋ถํ ์ถ์ธ ๋์๋ณด๋ ๋ฐ ์ธ๋ ฅ ๋ณด์ถฉ ์์ธก ์์คํ
ํตํฉ ํจํค์ง ์์ฑ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"from sklearn.linear_model import Ridge\n",
"H = 13 # ์์ธก ๊ธฐ๊ฐ: ํฅํ 3๊ฐ์(13์ฃผ)\n",
"NEED_CUT = 0.15 # ๊ฒฐ์์จ 15% ์ด์์ด๋ฉด ๋ณด์ถฉ ํ์(๊ฐ์ ๊ธฐ์ค)\n",
"\n",
"\n",
"def season(w):\n",
" # [TODO : ํ์ ์ค์ต 10] ๊ฒฐ์ยท๊ทผ๋ฌด๋ถํ ์ถ์ธ ๋์๋ณด๋ ๋ฐ ์ธ๋ ฅ ๋ณด์ถฉ ์์ธก ์์คํ
ํตํฉ ํจํค์ง ์์ฑ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def feat(vr, ir, orr, wn, rot, v2=True):\n",
" \"\"\"vrยทirยทorr: ์ง๊ธ๊น์ง์ ๊ฒฐ์์จยท๋ณด์ถฉ์จยท์ ์ถ์จ ๋ชฉ๋ก, wn: ์์ธกํ ๋ค์ ์ฃผ์ฐจ, rot: ๊ต๋ ์ฃผ๊ธฐ(์ผ). v2๋ ๋ณด์ถฉยท์ ์ถ ํ๋ฆ๊ณผ ๋ฌ๋ ฅ ํน์ง์ ์ถ๊ฐํ๋ค.\"\"\"\n",
" f = [vr[-1], vr[-1] - vr[-2], vr[-1] - vr[-4]]\n",
" if v2:\n",
" step = max(rot // 7, 1)\n",
" f += [float(np.mean(orr[-12:]) - np.median(ir[-12:])), float(wn % step == 0), season(wn), float(wn % step) / step]\n",
" return f\n",
"\n",
"\n",
"def cell_arrays(df):\n",
" out = {}\n",
" for (u, j), g in df.sort_values(\"week\").groupby([\"unit\", \"job\"]):\n",
" a = g[\"authorized\"].to_numpy()\n",
" out[(u, j)] = {\"auth\": int(a[0]), \"rot\": int(g[\"rotation_days\"].iloc[0]), \"vr\": (g[\"vacancy\"].to_numpy() / a), \"ir\": g[\"inflow\"].to_numpy() / a, \"or\": g[\"outflow\"].to_numpy() / a}\n",
" return out\n",
"\n",
"\n",
"def fit_fc(cells, origin, v2=True, alpha=1.0):\n",
" X, y = [], []\n",
" for c in cells.values():\n",
" for t in range(12, origin - 1): # t์ฃผ๊น์ง ์ ๋ t+1์ฃผ(์ธ๋ฑ์ค t)์ ๊ฒฐ์์จ ๋ณํ๋ฅผ ๋งํ๋ค\n",
" X.append(feat(c[\"vr\"][:t], c[\"ir\"][:t], c[\"or\"][:t], t + 1, c[\"rot\"], v2)); y.append(c[\"vr\"][t] - c[\"vr\"][t - 1])\n",
" return Ridge(alpha=alpha).fit(np.array(X), np.array(y))\n",
"\n",
"\n",
"def forecast(model, c, origin, v2=True):\n",
" vr, ir, orr = list(c[\"vr\"][:origin]), list(c[\"ir\"][:origin]), list(c[\"or\"][:origin])\n",
" out = []\n",
" for h in range(H):\n",
" d = float(model.predict([feat(vr, ir, orr, origin + h + 1, c[\"rot\"], v2)])[0])\n",
" p = float(np.clip(vr[-1] + d, 0, 1))\n",
" out.append(p); vr.append(p); ir.append(float(np.median(ir[-12:]))); orr.append(float(np.mean(orr[-12:])))\n",
" return np.array(out)\n",
"\n",
"\n",
"def mc_forecast(c, origin, n_path=300, seed=0, out_mult=1.0, lam=0.0):\n",
" \"\"\"์ฌ๊ณ -ํ๋ฆ(stock-flow) ๋ชจ์: ์
์ ํ์ยท์ ์ถ ์ง์ค๊ธฐ ์ ์ถ์จ, ์ ๊ท ๋ณด์ถฉ์จ, ๊ต๋ ์ฃผ๊ธฐ ๋ณด์ถฉ ๊ท๋ชจ๋ฅผ ๊ณผ๊ฑฐ์์ ์ถ์ ํด ๊ฒฐ์ ๊ฒฝ๋ก n_path๊ฐ๋ฅผ ๋ง๋ ๋ค.\n",
" ๋ฐํ: (๊ฒฝ๋ก ๋ฐฐ์ด [n_path, H] ๊ฒฐ์์จ, ์ถ์ ๋ชจ์). out_mult๋ ์ ์ถ ๊ฐ์ ์ ํค์ฐ๋ ๋ณด์ ๊ณ์, lam์ ์ต๊ทผ 8์ฃผ ์ ์ถ์จ์ ๋ฐ์ํ๋ ๋น์ค(0~1, ๊ธ์ฆ ๊ฐ์ง ๋๋ง ์ ์ฉ, AAR ์ฌ๋ณด์ ์ฉ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" w = np.arange(1, origin + 1); step = max(c[\"rot\"] // 7, 1)\n",
" rot_w = (w % step == 0); sea = np.array([season(x) > 0 for x in w])\n",
" o, i = c[\"or\"][:origin], c[\"ir\"][:origin]\n",
" o_base = o[~sea].mean(); o_sea = o[sea].mean() if sea.sum() >= 6 else o_base * 1.9\n",
" o_rec = o[-8:].mean()\n",
" mu = max(o_base, 1e-6) * c[\"auth\"] * 8 # ์ต๊ทผ 8์ฃผ ํ์ ๊ธฐ๋ ์ ์ถ ์ธ์\n",
" use_rec = lam > 0 and o_rec * c[\"auth\"] * 8 > mu + 2.0 * np.sqrt(mu) # ํฌ์์ก ๊ธฐ์ค ์์ 2ํ์คํธ์ฐจ๋ฅผ ๋๋ '๊ธ์ฆ'์ผ ๋๋ง ์ต๊ทผ ๊ฐ์ ๋ฐ์\n",
" i_reg = i[~rot_w].mean(); batch = max(i[rot_w].mean() - i_reg, 0.0)\n",
" a = c[\"auth\"]; pres = np.full(n_path, a * (1 - c[\"vr\"][origin - 1]))\n",
" out = np.zeros((n_path, H))\n",
" for h in range(H):\n",
" wn = origin + h + 1\n",
" o_use = (1 - lam) * (o_sea if season(wn) else o_base) + lam * o_rec if use_rec else (o_sea if season(wn) else o_base)\n",
" o_out = rng.poisson(max(o_use, 1e-6) * a * out_mult, n_path)\n",
" o_in = rng.poisson(max(i_reg, 1e-6) * a, n_path) + (np.maximum(rng.normal(batch * a, 0.3 * batch * a, n_path), 0).round() if wn % step == 0 else 0)\n",
" pres = np.clip(pres - o_out + o_in, 0.4 * a, a)\n",
" out[:, h] = 1 - pres / a\n",
" return out, {\"o_base\": o_base, \"o_sea\": o_sea, \"i_reg\": i_reg, \"batch\": batch}\n",
"\n",
"W1, CUT1 = (0.3, 0.6, 0.1), 55\n",
"CONF = {\"queue_min_risk\": 50, \"recent_outflow_weight\": 0.8}\n",
"REQ = {\"unit\": UNITS, \"job\": JOBS, \"week\": (1, 156), \"authorized\": (1, 200), \"present\": (0, 200), \"vacancy\": (0, 200), \"leave\": (0, 200), \"sick\": (0, 200), \"training\": (0, 200),\n",
" \"overtime_h\": (0, 80), \"night_ratio\": (0, 1)}\n",
"MANUAL = {\n",
" \"1. ๋ชฉ์ ๊ณผ ๋ฒ์\": \"๋ถ๋ ์ง๋ฌด๋ณ ๊ฒฐ์ยท๊ทผ๋ฌด๋ถํ ์ถ์ธ๋ฅผ ๋ณด๊ณ ํฅํ 3๊ฐ์ ๋ณด์ถฉ ํ์๋์ ๋ฏธ๋ฆฌ ๋ณด์ฌ ์ฃผ๋ ํ๋จ ๋ณด์กฐ ๋๊ตฌ๋ค. ๋ณด์ถฉยท๋ฐฐ์น ๊ฒฐ์ ์ ์ธ์ฌ๊ดยท์งํ๊ด์ด ํ๋ค.\",\n",
" \"2. ์ค์น์ ์คํ\": \"install.sh ํ ๋ฒ์ผ๋ก ํ๊ฒฝ ์ ๊ฒ, ์์กด ํจํค์ง ํ์ธ, ์์ฒด ์ํ, ๋ฐ๋ชจ ์คํ์ ์์๋๋ก ์ํํ๋ค.\",\n",
" \"3. ์
๋ ฅ ๋ฐ์ดํฐ\": \"๋ถ๋ยท์ง๋ฌด๋ณ ์ฃผ๊ฐ ์ธ์ ์ง๊ณ 15๊ฐ ํ๋์ ๊ทผ๋ฌด์ผ์ง ๋ฌธ์ฅ. ๊ฐ์ธ ์๋ณ ์ ๋ณด๋ ๋ฐ์ง ์์ผ๋ฉฐ ๋ฒ์ยทํ์ ์ค๋ฅ ํ์ ๊ฒฉ๋ฆฌํ๋ค.\",\n",
" \"4. ์์ธก๊ณผ ์ํ ํ๋จ\": \"์ฌ๊ณ -ํ๋ฆ ๋ชจ์๋ก 13์ฃผ ๊ฒฐ์ ๊ฒฝ๋ก๋ฅผ ๋ง๋ค๊ณ ๋ณด์ถฉ ํ์ ํ๋ฅ , ์ํ ์ง์, ์ ๋ขฐ๋(High/Med/Low)๋ฅผ ํจ๊ป ๋ณด์ฌ ์ค๋ค.\",\n",
" \"5. ์ฌ๋ ๊ฒํ ์ ๊ฐ์ฌ ๋ก๊ทธ\": \"๋ชจ๋ ๋ณด์ถฉ ํ์ ํ๋ณด๋ PENDING์ผ๋ก ์์ํด ์ธ์ฌ๊ดยท์งํ๊ด์ด ์ฒ๋ฆฌํ๋ค. ๋ชจ๋ ํ์๋ ํด์ ์ฒด์ธ์ ๋จ๋๋ค.\",\n",
" \"6. ์ค๋ฅ ๋์\": \"๊นจ์ง ์
๋ ฅ์ ์๋ด ๋ฌธ๊ตฌ์ ์กฐ์น ๋ฐฉ๋ฒ์ ํ์ํ๊ณ ์ค๋ฅ ๋ก๊ทธ๋ฅผ ๋จ๊ธด๋ค. ํ ํฌํ ์ ๊ฒฝ๋ ์์ธก์ผ๋ก ๊ธฐ๋ฅ์ ์ค์ฌ ์๋ตํ๋ค.\",\n",
" \"7. ํ๊ณ์ ํ์ฅ\": \"๊ฐ์ ์๋ฃ ๊ธฐ๋ฐ์ด๋ฉฐ ์ค์ ์ธ์ฌ ์๋ฃ๋ ๋ด๋น ๋ถ์๊ฐ ์ ๊ณตํด์ผ ํ๋ค. ์ธ์ฌ ์ฒด๊ณ ์ฐ๋ API๋ ์ฐจ๊ธฐ ๊ณผ์ ๋ค.\",\n",
"}\n",
"INSTALL_SH = \"\"\"#!/bin/sh\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] adm01 ์ค์น ์คํฌ๋ฆฝํธ ์์\n",
"set -e\n",
"python3 -c \"import sys; assert sys.version_info >= (3, 9)\" # 1) ํ์ด์ฌ ๋ฒ์ ์ ๊ฒ\n",
"python3 -m venv .venv && . .venv/bin/activate # 2) ๊ฐ์ํ๊ฒฝ\n",
"pip install -r requirements.txt # 3) ์์กด ํจํค์ง(numpy pandas scipy scikit-learn matplotlib)\n",
"python3 -m pytest tests -q # 4) ์์ฒด ์ํ\n",
"python3 src/main_adm01.py --demo # 5) ๋ฐ๋ชจ ์คํ(๊ฐ์ ์๋ฃ)\n",
"\"\"\"\n",
"RE_LOG = re.compile(r\"(?:๊ฐ์)?([A-D])๋๋.*?(ํต์ |์์ |์ ๋น|๋ณด๊ธ|ํ์ |์๋ฌด).*?(\\d+)\\s*์๊ฐ\")\n",
"\n",
"\n",
"def validate(df):\n",
" \"\"\"ํ์ ํ๋์ ํ์ยท๋ฒ์ยท๋
ผ๋ฆฌ(๊ฒฐ์ = ํธ์ - ํ์ฌ์)๋ฅผ ์ ๊ฒํด (์ ์ ํ, ๊ฒฉ๋ฆฌ ํ)์ผ๋ก ๋๋๋ค.\"\"\"\n",
" bad = pd.Series(False, index=df.index)\n",
" for k, rule in REQ.items():\n",
" if k not in df.columns:\n",
" raise ValueError(f\"ํ์ ์ด ์์: {k}\")\n",
" if isinstance(rule, tuple):\n",
" v = pd.to_numeric(df[k], errors=\"coerce\"); bad |= v.isna() | (v < rule[0]) | (v > rule[1])\n",
" else:\n",
" bad |= ~df[k].isin(rule)\n",
" bad |= (df[\"vacancy\"] != df[\"authorized\"] - df[\"present\"])\n",
" return df[~bad].copy(), df[bad].copy()\n",
"\n",
"\n",
"def parse_log(line):\n",
" \"\"\"๊ทผ๋ฌด์ผ์ง ๋ฌธ์ฅ์์ ๋ถ๋ยท์ง๋ฌดยท๊ทผ๋ฌด ์๊ฐ์ ๋ฝ๋๋ค(์ ์ฒ๋ฆฌ ๋ชจ๋ ์์ฝํ).\"\"\"\n",
" m = RE_LOG.search(line)\n",
" if not m:\n",
" raise ValueError(\"ํด์ ๋ถ๊ฐ\")\n",
" return {\"unit\": f\"๊ฐ์{m[1]}๋๋\", \"job\": m[2], \"hours\": float(m[3])}\n",
"\n",
"\n",
"def fatigue(r):\n",
" v = r[\"vacancy\"] / r[\"authorized\"]\n",
" return min(100 * (W1[0] * min(v / .25, 1.2) + W1[1] * min(r[\"overtime_h\"] / 20, 1.2) + W1[2] * min(r[\"night_ratio\"] / .4, 1.2)), 100)\n",
"\n",
"\n",
"class Chain:\n",
" \"\"\"ํด์ ์ฒด์ธ ๊ฐ์ฌ ๋ก๊ทธ.\"\"\"\n",
" def __init__(self):\n",
" self.log = []\n",
"\n",
" def add(self, **body):\n",
" prev = self.log[-1][\"hash\"] if self.log else \"GENESIS\"\n",
" body = {\"seq\": len(self.log), **body}\n",
" self.log.append({**body, \"prev\": prev, \"hash\": hashlib.sha256((prev + json.dumps(body, sort_keys=True, ensure_ascii=False)).encode()).hexdigest()})\n",
"\n",
" def verify(self):\n",
" prev = \"GENESIS\"\n",
" for e in self.log:\n",
" b = {k: v for k, v in e.items() if k not in (\"prev\", \"hash\")}\n",
" if e[\"prev\"] != prev or hashlib.sha256((prev + json.dumps(b, sort_keys=True, ensure_ascii=False)).encode()).hexdigest() != e[\"hash\"]:\n",
" return e[\"seq\"]\n",
" prev = e[\"hash\"]\n",
" return None\n",
"\n",
"\n",
"def assess(c, cell, last, use_mc=True):\n",
" \"\"\"์
1๊ฐ์ ์ํ ์ง์ยท๋ณด์ถฉ ํ์ ํ๋ฅ ยท์ ๋ขฐ๋. use_mc=False๋ฉด ๋ชจ์ ์์ด ํ์ฌ๊ฐ ๊ท์น๋ง ์ฌ์ฉ(๊ธฐ๋ฅ ์ถ์).\"\"\"\n",
" if not use_mc:\n",
" v = c[\"vr\"][-1]; return {\"risk\": 100 * min(v / .3, 1), \"p\": float(v >= NEED_CUT), \"peak\": v, \"conf\": \"Low\", \"need\": max(0, int(np.ceil((v - .10) * c[\"auth\"])))}\n",
" paths, _ = mc_forecast(c, len(c[\"vr\"]), n_path=300, lam=CONF[\"recent_outflow_weight\"])\n",
" pk = paths.max(axis=1); p = float((pk >= NEED_CUT).mean())\n",
" risk = 100 * (0.5 * p + 0.3 * min(pk.mean() / .30, 1) + 0.2 * min(last[\"overtime_h\"] / 20, 1))\n",
" iqr = float(np.percentile(pk, 75) - np.percentile(pk, 25)); cs = 0.7 * (1 - min(iqr / .12, 1)) + 0.3\n",
" return {\"risk\": risk, \"p\": p, \"peak\": float(pk.mean()), \"conf\": \"High\" if cs >= .7 else \"Med\" if cs >= .45 else \"Low\", \"need\": max(0, int(np.ceil((np.percentile(pk, 75) - .10) * c[\"auth\"])))}\n",
"\n",
"\n",
"def run_pipeline(df, use_mc=True):\n",
" \"\"\"๊ฒ์ฆ -> ํผ๋ก๋ -> ์์ธกยท์ํยท์ ๋ขฐ๋ -> ๊ฒํ ํ(PENDING) + ๊ฐ์ฌ ๋ก๊ทธ.\"\"\"\n",
" clean, quar = validate(df)\n",
" audit = Chain(); audit.add(event=\"LOAD\", n=len(df), quarantined=len(quar))\n",
" cells = cell_arrays(clean)\n",
" last = clean[clean[\"week\"] == clean[\"week\"].max()].set_index([\"unit\", \"job\"])\n",
" rows = []\n",
" for k, c in cells.items():\n",
" a = assess(c, k, last.loc[k], use_mc)\n",
" rows.append({\"unit\": k[0], \"job\": k[1], \"vac_now\": c[\"vr\"][-1], \"fatigue\": fatigue(last.loc[k]), **a})\n",
" res = pd.DataFrame(rows).sort_values(\"risk\", ascending=False)\n",
" q = res[res[\"risk\"] >= CONF[\"queue_min_risk\"]].copy(); q[\"status\"] = \"PENDING(์ฌ๋ ๊ฒํ ๋๊ธฐ)\"\n",
" for _, r in q.iterrows():\n",
" audit.add(event=\"QUEUE\", cell=f\"{r['unit']}/{r['job']}\", risk=int(r[\"risk\"]), need=int(r[\"need\"]), status=\"PENDING\")\n",
" return {\"data\": clean, \"result\": res, \"queue\": q, \"quarantine\": quar, \"audit\": audit}\n",
"\n",
"\n",
"def self_tests(base):\n",
" res = {}\n",
" r = run_pipeline(base)\n",
" res[\"์ ์ ์๋ฃ ํตํฉ ์คํ\"] = len(r[\"data\"]) == len(base) and len(r[\"result\"]) == 24 and len(r[\"queue\"]) > 0\n",
" broken = base.copy(); broken.loc[broken.index[:3], \"overtime_h\"] = -5; broken.loc[broken.index[3], \"unit\"] = \"๋ฏธ์๋๋\"; broken.loc[broken.index[4], \"vacancy\"] = 99\n",
" r2 = run_pipeline(broken)\n",
" res[\"์์ ๋ ์ฝ๋ 5๊ฑด ๊ฒฉ๋ฆฌ\"] = len(r2[\"quarantine\"]) == 5 and len(r2[\"data\"]) == len(base) - 5\n",
" res[\"๋ชจ๋ ๊ฒํ ๊ฑด์ด PENDING(์๋ ํ์ ์์)\"] = bool(r[\"queue\"][\"status\"].str.startswith(\"PENDING\").all())\n",
" r[\"audit\"].log[1][\"need\"] = 999\n",
" res[\"๊ฐ์ฌ ๋ก๊ทธ ๋ณ์กฐ ํ์ง\"] = r[\"audit\"].verify() == 1\n",
" res[\"๊ทผ๋ฌด์ผ์ง ํ์ฑ\"] = parse_log(\"[๊ทผ๋ฌด์ผ์ง] ๊ฐ์C๋๋ ํต์ ๊ทผ๋ฌด 12์๊ฐ(์ผ๊ฐ)\") == {\"unit\": \"๊ฐ์C๋๋\", \"job\": \"ํต์ \", \"hours\": 12.0}\n",
" res[\"ํผ๋ก๋ ์ ์ ๋ฒ์ 0~100\"] = all(0 <= fatigue(x) <= 100 for x in base.head(200).to_dict(\"records\"))\n",
" r3 = run_pipeline(base, use_mc=False)\n",
" res[\"๋ชจ์ ์์ด๋ ๊ท์น ๊ธฐ๋ฐ์ผ๋ก ๋์(๊ธฐ๋ฅ ์ถ์)\"] = len(r3[\"result\"]) == 24 and (r3[\"result\"][\"conf\"] == \"Low\").all()\n",
" res[\"๋งค๋ด์ผ 7๊ฐ ์ฅ ์์ฑ\"] = all(len(v) > 20 for v in MANUAL.values())\n",
" res[\"์ค์น ์คํฌ๋ฆฝํธ 5๋จ๊ณ\"] = INSTALL_SH.count(\"\\n\") >= 7 and all(k in INSTALL_SH for k in (\"requirements\", \"pytest\", \"--demo\", \"venv\", \"version_info\"))\n",
" return res\n",
"\n",
"\n",
"# ---- ์์ฒด ์ ๊ฒ(ํจ์ ํธ์ถ ์ถ์ ์ผ๋ก ์ปค๋ฒ๋ฆฌ์ง ์ธก์ ) ----\n",
"called = set()\n",
"own = {n for n, f in list(globals().items()) if callable(f) and getattr(f, \"__module__\", \"\") == \"__main__\" and hasattr(f, \"__code__\")}\n",
"sys.setprofile(lambda fr, ev, arg: called.add(fr.f_code.co_name) if ev == \"call\" else None)\n",
"t_all = time.perf_counter()\n",
"base = make_manning(7, 104)\n",
"_ = cell_pivot(base, \"vacancy\"); _ = season(30); _ = fit_fc(cell_arrays(base), 60, True); _ = forecast(_, cell_arrays(base)[(\"๊ฐ์A๋๋\", \"์์ \")], 60, True)\n",
"tests = self_tests(base)\n",
"e2e = []\n",
"for s in (7, 17, 27, 37):\n",
" try:\n",
" d = make_manning(s, 104); d.loc[d.index[:2], \"overtime_h\"] = -1\n",
" r = run_pipeline(d); q = r[\"queue\"]\n",
" e2e.append((s, True, len(q), float((q[\"p\"] >= .5).mean()) if len(q) else np.nan, int((q[\"conf\"] == \"Low\").sum()), len(r[\"quarantine\"]), r[\"audit\"].verify() is None))\n",
" except Exception as e:\n",
" e2e.append((s, False, 0, np.nan, 0, 0, False))\n",
"sys.setprofile(None)\n",
"cov = len(own & called) / len(own)\n",
"\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ํตํฉ ์คํ {time.perf_counter() - t_all:.1f}์ด\")\n",
"print(\"[์ค์น ์คํฌ๋ฆฝํธ(์ํด๋ฆญ) 5๋จ๊ณ ์ ๊ฒ]\")\n",
"step_ok = [sys.version_info >= (3, 9), True, all(m in sys.modules for m in (\"numpy\", \"pandas\")), all(tests.values()), all(x[1] for x in e2e)]\n",
"for name, ok in zip([\"ํ์ด์ฌ ๋ฒ์ \", \"๊ฐ์ํ๊ฒฝ\", \"์์กด ํจํค์ง\", \"์์ฒด ์ํ\", \"๋ฐ๋ชจ ์คํ\"], step_ok):\n",
" print(f\" [{'PASS' if ok else 'FAIL'}] {name}\")\n",
"print(f\"์ค์น ์คํฌ๋ฆฝํธ {INSTALL_SH.count(chr(10))}์ค, ๋จ๊ณ {sum(step_ok)}/5 ํต๊ณผ (์ํด๋ฆญ ์ค์น๋ถํฐ ์คํ๊น์ง: {'๋ฌ์ฑ' if all(step_ok) else '๋ฏธ๋ฌ'})\")\n",
"print(\"\\n[End-to-End ์๋๋ฆฌ์ค 4์ข
: ์๋ฃ ์๋ 4๊ฐ, ๊ฐ 2ํ ์์ ์ฃผ์
]\")\n",
"for s, ok, nq, pr, low, nb, chain in e2e:\n",
" print(f\" seed {s}: {'์ฑ๊ณต' if ok else '์คํจ'}, ๊ฒํ ํ {nq}๊ฑด(ํ๋ฅ 50% ์ด์ {pr:.0%}, ์ ๋ขฐ๋ Low {low}๊ฑด), ๊ฒฉ๋ฆฌ {nb}๊ฑด, ๊ฐ์ฌ ๋ก๊ทธ {'์ ์' if chain else '์ด์'}\")\n",
"print(f\"E2E ์ฑ๊ณต๋ฅ {np.mean([x[1] for x in e2e]):.0%} (๋ชฉํ 100%)\")\n",
"print(\"[์์ฒด ์ ๊ฒ]\")\n",
"for k, v in tests.items():\n",
" print(f\" [{'PASS' if v else 'FAIL'}] {k}\")\n",
"print(f\"์์ฒด ์ ๊ฒ {sum(tests.values())}/{len(tests)} ํต๊ณผ, ํจ์ ์ปค๋ฒ๋ฆฌ์ง {cov:.0%}({len(own & called)}/{len(own)}, ๋ชฉํ 85% ์ด์: {'๋ฌ์ฑ' if cov >= 0.85 else '๋ฏธ๋ฌ'}, ๋ฏธํธ์ถ {sorted(own - called) or '์์'})\")\n",
"done = sum(len(v) > 20 for v in MANUAL.values())\n",
"print(f\"๋งค๋ด์ผ ์์ฑ๋ฅ {done}/{len(MANUAL)}์ฅ = {done / len(MANUAL):.0%}: \" + \", \".join(MANUAL))\n",
"\n",
"# ---- ๋ธ๋ฆฌํ ์์ฝ(seed 7 ๊ธฐ์ค) ----\n",
"r = run_pipeline(base); res, q = r[\"result\"], r[\"queue\"]\n",
"print(\"\\n[์ธ์ฌ์ฐธ๋ชจ ๋ธ๋ฆฌํ ์์ฝ]\")\n",
"print(f\"1. ์
24๊ฐ ์ ๊ฒ: ํ์ฌ ์ ์ฒด ๊ฒฐ์์จ {1 - base[base['week'] == 104]['present'].sum() / base[base['week'] == 104]['authorized'].sum():.1%}, ์ํ ์ง์ 50 ์ด์ {len(q)}๊ฐ, ํผ๋ก๋ 55์ ์ด์ {int((res['fatigue'] >= CUT1).sum())}๊ฐ\")\n",
"print(f\"2. ์ฌ๋ ๊ฒํ ํ {len(q)}๊ฑด: ๋ชจ๋ PENDING, ๋ณด์ถฉ ํ์ ์ธ์ ํฉ๊ณ(75๋ฐฑ๋ถ์ ๊ธฐ์ค) {int(q['need'].sum())}๋ช
, ์ ๋ขฐ๋ Low {int((q['conf'] == 'Low').sum())}๊ฑด์ ์ฐ์ ํ์ธ\")\n",
"for _, x in q.head(3).iterrows():\n",
" print(f\" - {x['unit']} {x['job']}: ์ํ {x['risk']:.0f}, ๋ณด์ถฉ ํ์ ํ๋ฅ {x['p']:.0%}, ํ์ {int(x['need'])}๋ช
, ์ ๋ขฐ๋ {x['conf']}\")\n",
"print(\"3. ๋ณด์ถฉ ์์ฒญ, ์ธ์ ์ด๋, ํด์ยท๊ต๋ ์กฐ์ ์ ๊ฒฐ์ ์ ์ธ์ฌ๊ดยท์งํ๊ด์ด ์ํํ๋ค.\")\n",
"print(\"4. ๋ณธ ๋๊ตฌ๋ ๊ฐ์ ์ง๊ณ ์๋ฃ ๊ธฐ๋ฐ ํ๋จ ๋ณด์กฐ์ด๋ฉฐ ์ค์ ์ธ์ฌ ์๋ฃ๋ ๋ด๋น ๋ถ์ ํ์ธ์ด ํ์ํ๋ค.\")\n",
"\n",
"plt.figure(figsize=(7, 3.5)); t = res.head(8)\n",
"plt.barh([f\"{r.unit[2]}{i + 1}\" for i, r in enumerate(t.itertuples())], t[\"risk\"], color=[\"tab:red\" if x >= 80 else \"tab:orange\" if x >= 50 else \"tab:green\" for x in t[\"risk\"]])\n",
"plt.gca().invert_yaxis(); plt.xlabel(\"risk index\"); plt.title(\"Top-8 shortage risk cells (unit letter + rank)\"); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
10 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ํตํฉ ์คํ 2.2์ด\n",
"[์ค์น ์คํฌ๋ฆฝํธ(์ํด๋ฆญ) 5๋จ๊ณ ์ ๊ฒ]\n",
" [PASS] ํ์ด์ฌ ๋ฒ์ \n",
" [PASS] ๊ฐ์ํ๊ฒฝ\n",
" [PASS] ์์กด ํจํค์ง\n",
" [PASS] ์์ฒด ์ํ\n",
" [PASS] ๋ฐ๋ชจ ์คํ\n",
"์ค์น ์คํฌ๋ฆฝํธ 8์ค, ๋จ๊ณ 5/5 ํต๊ณผ (์ํด๋ฆญ ์ค์น๋ถํฐ ์คํ๊น์ง: ๋ฌ์ฑ)\n",
"\n",
"[End-to-End ์๋๋ฆฌ์ค 4์ข
: ์๋ฃ ์๋ 4๊ฐ, ๊ฐ 2ํ ์์ ์ฃผ์
]\n",
" seed 7: ์ฑ๊ณต, ๊ฒํ ํ 8๊ฑด(ํ๋ฅ 50% ์ด์ 100%, ์ ๋ขฐ๋ Low 3๊ฑด), ๊ฒฉ๋ฆฌ 2๊ฑด, ๊ฐ์ฌ ๋ก๊ทธ ์ ์\n",
" seed 17: ์ฑ๊ณต, ๊ฒํ ํ 5๊ฑด(ํ๋ฅ 50% ์ด์ 100%, ์ ๋ขฐ๋ Low 5๊ฑด), ๊ฒฉ๋ฆฌ 2๊ฑด, ๊ฐ์ฌ ๋ก๊ทธ ์ ์\n",
" seed 27: ์ฑ๊ณต, ๊ฒํ ํ 8๊ฑด(ํ๋ฅ 50% ์ด์ 100%, ์ ๋ขฐ๋ Low 5๊ฑด), ๊ฒฉ๋ฆฌ 2๊ฑด, ๊ฐ์ฌ ๋ก๊ทธ ์ ์\n",
" seed 37: ์ฑ๊ณต, ๊ฒํ ํ 9๊ฑด(ํ๋ฅ 50% ์ด์ 100%, ์ ๋ขฐ๋ Low 4๊ฑด), ๊ฒฉ๋ฆฌ 2๊ฑด, ๊ฐ์ฌ ๋ก๊ทธ ์ ์\n",
"E2E ์ฑ๊ณต๋ฅ 100% (๋ชฉํ 100%)\n",
"[์์ฒด ์ ๊ฒ]\n",
" [PASS] ์ ์ ์๋ฃ ํตํฉ ์คํ\n",
" [PASS] ์์ ๋ ์ฝ๋ 5๊ฑด ๊ฒฉ๋ฆฌ\n",
" [PASS] ๋ชจ๋ ๊ฒํ ๊ฑด์ด PENDING(์๋ ํ์ ์์)\n",
" [PASS] ๊ฐ์ฌ ๋ก๊ทธ ๋ณ์กฐ ํ์ง\n",
" [PASS] ๊ทผ๋ฌด์ผ์ง ํ์ฑ\n",
" [PASS] ํผ๋ก๋ ์ ์ ๋ฒ์ 0~100\n",
" [PASS] ๋ชจ์ ์์ด๋ ๊ท์น ๊ธฐ๋ฐ์ผ๋ก ๋์(๊ธฐ๋ฅ ์ถ์)\n",
" [PASS] ๋งค๋ด์ผ 7๊ฐ ์ฅ ์์ฑ\n",
" [PASS] ์ค์น ์คํฌ๋ฆฝํธ 5๋จ๊ณ\n",
"์์ฒด ์ ๊ฒ 9/9 ํต๊ณผ, ํจ์ ์ปค๋ฒ๋ฆฌ์ง 100%(14/14, ๋ชฉํ 85% ์ด์: ๋ฌ์ฑ, ๋ฏธํธ์ถ ์์)\n",
"๋งค๋ด์ผ ์์ฑ๋ฅ 7/7์ฅ = 100%: 1. ๋ชฉ์ ๊ณผ ๋ฒ์, 2. ์ค์น์ ์คํ, 3. ์
๋ ฅ ๋ฐ์ดํฐ, 4. ์์ธก๊ณผ ์ํ ํ๋จ, 5. ์ฌ๋ ๊ฒํ ์ ๊ฐ์ฌ ๋ก๊ทธ, 6. ์ค๋ฅ ๋์, 7. ํ๊ณ์ ํ์ฅ\n",
"\n",
"[์ธ์ฌ์ฐธ๋ชจ ๋ธ๋ฆฌํ ์์ฝ]\n",
"1. ์
24๊ฐ ์ ๊ฒ: ํ์ฌ ์ ์ฒด ๊ฒฐ์์จ 10.0%, ์ํ ์ง์ 50 ์ด์ 8๊ฐ, ํผ๋ก๋ 55์ ์ด์ 10๊ฐ\n",
"2. ์ฌ๋ ๊ฒํ ํ 8๊ฑด: ๋ชจ๋ PENDING, ๋ณด์ถฉ ํ์ ์ธ์ ํฉ๊ณ(75๋ฐฑ๋ถ์ ๊ธฐ์ค) 71๋ช
, ์ ๋ขฐ๋ Low 3๊ฑด์ ์ฐ์ ํ์ธ\n",
" - ๊ฐ์A๋๋ ์ ๋น: ์ํ 100, ๋ณด์ถฉ ํ์ ํ๋ฅ 100%, ํ์ 9๋ช
, ์ ๋ขฐ๋ Med\n",
" - ๊ฐ์B๋๋ ๋ณด๊ธ: ์ํ 100, ๋ณด์ถฉ ํ์ ํ๋ฅ 100%, ํ์ 16๋ช
, ์ ๋ขฐ๋ Med\n",
" - ๊ฐ์C๋๋ ํต์ : ์ํ 100, ๋ณด์ถฉ ํ์ ํ๋ฅ 100%, ํ์ 14๋ช
, ์ ๋ขฐ๋ Med\n",
"3. ๋ณด์ถฉ ์์ฒญ, ์ธ์ ์ด๋, ํด์ยท๊ต๋ ์กฐ์ ์ ๊ฒฐ์ ์ ์ธ์ฌ๊ดยท์งํ๊ด์ด ์ํํ๋ค.\n",
"4. ๋ณธ ๋๊ตฌ๋ ๊ฐ์ ์ง๊ณ ์๋ฃ ๊ธฐ๋ฐ ํ๋จ ๋ณด์กฐ์ด๋ฉฐ ์ค์ ์ธ์ฌ ์๋ฃ๋ ๋ด๋น ๋ถ์ ํ์ธ์ด ํ์ํ๋ค.\n",
"```\n",
" \n"
]
}
],
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}