{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# ๐ŸŽฏ [ADM01] ๊ฒฐ์›ยท๊ทผ๋ฌด๋ถ€ํ•˜ ์ถ”์„ธ ๋Œ€์‹œ๋ณด๋“œ์™€ ๋ณด์ถฉ ํ•„์š”๋Ÿ‰ ์˜ˆ์ธก\n", "### Google Colab ์‹ค์Šต ํ™˜๊ฒฝ (ํ–‰์ •ยท์ธ์‚ฌ / Level 3)\n", "\n", "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](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", " return df.pivot_table(index=\"week\", columns=[\"unit\", \"job\"], values=col)\n", "\n", "# 1) ์Šคํ‚ค๋งˆ: ํ•„๋“œ๋งˆ๋‹ค ์ž๋ฃŒํ˜•ยท๋‹จ์œ„ยทํ—ˆ์šฉ ๋ฒ”์œ„ยท์„ค๋ช…ยท์ถœ์ฒ˜๋ฅผ ๋ชจ๋‘ ์ •์˜ํ•œ๋‹ค(์™„์„ฑ๋„ ์ ๊ฒ€ ๋Œ€์ƒ)\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", " errs = []\n", " for k, r in SCHEMA.items():\n", " if k not in rec or rec[k] is None or (isinstance(rec[k], float) and np.isnan(rec[k])):\n", " errs.append(f\"{k}: ํ•„์ˆ˜ ๊ฐ’ ๋ˆ„๋ฝ\"); continue\n", " v = rec[k]\n", " ok_t = {\"string\": isinstance(v, str), \"integer\": isinstance(v, (int, np.integer)) and not isinstance(v, bool), \"number\": isinstance(v, (int, float, np.number)) and not isinstance(v, bool)}[r[\"type\"]]\n", " if not ok_t:\n", " errs.append(f\"{k}: ์ž๋ฃŒํ˜• ์˜ค๋ฅ˜({type(v).__name__})\"); continue\n", " if \"enum\" in r and v not in r[\"enum\"]:\n", " errs.append(f\"{k}: ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ ๊ฐ’ '{v}'\")\n", " if \"min\" in r and v < r[\"min\"]:\n", " errs.append(f\"{k}: ์ตœ์†Œ๊ฐ’ {r['min']} ๋ฏธ๋งŒ\")\n", " if \"max\" in r and v > r[\"max\"]:\n", " errs.append(f\"{k}: ์ตœ๋Œ€๊ฐ’ {r['max']} ์ดˆ๊ณผ\")\n", " if not errs: # ํ•„๋“œ ๊ฐ„ ๋…ผ๋ฆฌ ๊ทœ์น™\n", " if rec[\"vacancy\"] != rec[\"authorized\"] - rec[\"present\"]:\n", " errs.append(\"vacancy: ํŽธ์ œ - ํ˜„์žฌ์›๊ณผ ๋ถˆ์ผ์น˜\")\n", " if rec[\"leave\"] + rec[\"sick\"] + rec[\"training\"] > rec[\"present\"]:\n", " errs.append(\"leave+sick+training: ํ˜„์žฌ์› ์ดˆ๊ณผ\")\n", " if rec[\"present\"] > rec[\"authorized\"]:\n", " errs.append(\"present: ํŽธ์ œ ์ธ์› ์ดˆ๊ณผ\")\n", " return errs\n", "\n", "\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", " m = RE_CLK.search(t)\n", " if m:\n", " a, b = int(m[1]) * 60 + int(m[2]), int(m[3]) * 60 + int(m[4])\n", " return ((b - a) % 1440) / 60\n", " m = RE_KO.search(t)\n", " if m:\n", " a = int(m[2]) % 12 + (12 if m[1] == \"์˜คํ›„\" else 0); e = int(m[4]) % 12 + (12 if m[3] == \"์˜คํ›„\" else 0)\n", " return float((e - a) % 24)\n", " m = RE_HM.search(t)\n", " if m:\n", " return float(m[1]) + (int(m[2]) / 60 if m[2] else 0)\n", " m = RE_H.search(t)\n", " if m:\n", " return float(m[1])\n", " raise ValueError(\"์‹œ๊ฐ„ ํ‘œ๊ธฐ ํ•ด์„ ๋ถˆ๊ฐ€\")\n", "\n", "\n", "def classify(t):\n", " tail = re.split(r\"๊ฒฐ์›์‚ฌ์œ |๊ฒฐ์› ์›์ธ|๊ฒฐ์›:|์‚ฌ์œ \", t)[-1]\n", " for r, kws in REASONS.items():\n", " if any(k in tail for k in kws):\n", " return r\n", " return \"๊ธฐํƒ€\"\n", "\n", "\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", " return df.pivot_table(index=\"week\", columns=[\"unit\", \"job\"], values=col)\n", "\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", " return np.minimum(100 * X(d) @ np.asarray(w), 100)\n", "\n", "\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", " return df.pivot_table(index=\"week\", columns=[\"unit\", \"job\"], values=col)\n", "\n", "# ---- ๊ฒฐ์›์œจ ์˜ˆ์ธก ๋ชจ๋ธ(์ฃผ๊ฐ„ ๊ฒฐ์›์œจ ๋ณ€ํ™”๋Ÿ‰์„ ๋ฆฟ์ง€ ํšŒ๊ท€๋กœ ๋งžํžˆ๊ณ  13์ฃผ๊นŒ์ง€ ์žฌ๊ท€ ์ ์šฉ, ์™ธ๋ถ€ ์‹œ๊ณ„์—ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—†์ด) ----\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", " return float(abs(w % 52 - 22) < 4 or abs(w % 52 - 46) < 3)\n", "\n", "\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", " return df.pivot_table(index=\"week\", columns=[\"unit\", \"job\"], values=col)\n", "\n", "# ---- ๊ฒฐ์›์œจ ์˜ˆ์ธก ๋ชจ๋ธ(์ฃผ๊ฐ„ ๊ฒฐ์›์œจ ๋ณ€ํ™”๋Ÿ‰์„ ๋ฆฟ์ง€ ํšŒ๊ท€๋กœ ๋งžํžˆ๊ณ  13์ฃผ๊นŒ์ง€ ์žฌ๊ท€ ์ ์šฉ, ์™ธ๋ถ€ ์‹œ๊ณ„์—ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—†์ด) ----\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", " return float(abs(w % 52 - 22) < 4 or abs(w % 52 - 46) < 3)\n", "\n", "\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", " return df.pivot_table(index=\"week\", columns=[\"unit\", \"job\"], values=col)\n", "\n", "# ---- ๊ฒฐ์›์œจ ์˜ˆ์ธก ๋ชจ๋ธ(์ฃผ๊ฐ„ ๊ฒฐ์›์œจ ๋ณ€ํ™”๋Ÿ‰์„ ๋ฆฟ์ง€ ํšŒ๊ท€๋กœ ๋งžํžˆ๊ณ  13์ฃผ๊นŒ์ง€ ์žฌ๊ท€ ์ ์šฉ, ์™ธ๋ถ€ ์‹œ๊ณ„์—ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—†์ด) ----\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", " return float(abs(w % 52 - 22) < 4 or abs(w % 52 - 46) < 3)\n", "\n", "\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", " return df.pivot_table(index=\"week\", columns=[\"unit\", \"job\"], values=col)\n", "\n", "# ---- ๊ฒฐ์›์œจ ์˜ˆ์ธก ๋ชจ๋ธ(์ฃผ๊ฐ„ ๊ฒฐ์›์œจ ๋ณ€ํ™”๋Ÿ‰์„ ๋ฆฟ์ง€ ํšŒ๊ท€๋กœ ๋งžํžˆ๊ณ  13์ฃผ๊นŒ์ง€ ์žฌ๊ท€ ์ ์šฉ, ์™ธ๋ถ€ ์‹œ๊ณ„์—ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—†์ด) ----\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", " return float(abs(w % 52 - 22) < 4 or abs(w % 52 - 46) < 3)\n", "\n", "\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", " return df.pivot_table(index=\"week\", columns=[\"unit\", \"job\"], values=col)\n", "\n", "# ---- ๊ฒฐ์›์œจ ์˜ˆ์ธก ๋ชจ๋ธ(์ฃผ๊ฐ„ ๊ฒฐ์›์œจ ๋ณ€ํ™”๋Ÿ‰์„ ๋ฆฟ์ง€ ํšŒ๊ท€๋กœ ๋งžํžˆ๊ณ  13์ฃผ๊นŒ์ง€ ์žฌ๊ท€ ์ ์šฉ, ์™ธ๋ถ€ ์‹œ๊ณ„์—ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—†์ด) ----\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", " return float(abs(w % 52 - 22) < 4 or abs(w % 52 - 46) < 3)\n", "\n", "\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", " return df.pivot_table(index=\"week\", columns=[\"unit\", \"job\"], values=col)\n", "\n", "# ---- ๊ฒฐ์›์œจ ์˜ˆ์ธก ๋ชจ๋ธ(์ฃผ๊ฐ„ ๊ฒฐ์›์œจ ๋ณ€ํ™”๋Ÿ‰์„ ๋ฆฟ์ง€ ํšŒ๊ท€๋กœ ๋งžํžˆ๊ณ  13์ฃผ๊นŒ์ง€ ์žฌ๊ท€ ์ ์šฉ, ์™ธ๋ถ€ ์‹œ๊ณ„์—ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—†์ด) ----\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", " return float(abs(w % 52 - 22) < 4 or abs(w % 52 - 46) < 3)\n", "\n", "\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", " return df.pivot_table(index=\"week\", columns=[\"unit\", \"job\"], values=col)\n", "\n", "# ---- ๊ฒฐ์›์œจ ์˜ˆ์ธก ๋ชจ๋ธ(์ฃผ๊ฐ„ ๊ฒฐ์›์œจ ๋ณ€ํ™”๋Ÿ‰์„ ๋ฆฟ์ง€ ํšŒ๊ท€๋กœ ๋งžํžˆ๊ณ  13์ฃผ๊นŒ์ง€ ์žฌ๊ท€ ์ ์šฉ, ์™ธ๋ถ€ ์‹œ๊ณ„์—ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—†์ด) ----\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", " return float(abs(w % 52 - 22) < 4 or abs(w % 52 - 46) < 3)\n", "\n", "\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 }