{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# ๐ฏ [MNT10] AI ๊ธฐ๋ฐ ์ ๋น์ธ๋ ฅ ๋ถํ์จ ์์ธกยท๊ณต์ ๊ท ํ\n",
"### Google Colab ์ค์ต ํ๊ฒฝ (์ฅ๋น์ ๋นยท์๋ฆฌ๋ถ์ / Level 3)\n",
"\n",
"[](https://colab.research.google.com/github/edunlab/milicontent2/blob/main/uploads/MNT10/MNT10_solution.ipynb)\n",
"\n",
"---\n",
"\n",
"## ๐ 1. ํ๋ก์ ํธ ๊ฐ์ ๋ฐ ๋ฐฐ๊ฒฝ\n",
"\n",
"| ํญ๋ชฉ | ๋ด์ฉ |\n",
"| :--- | :--- |\n",
"| **๊ณผ์ ๋ถ๋ฅ** | ์ฅ๋น์ ๋นยท์๋ฆฌ๋ถ์ (Level 3) |\n",
"| **ํ์ต ์์ ์๊ฐ** | 10๊ฐ ๋ฏธ์
(5์ผ / ์ด 28.5์๊ฐ) |\n",
"| **ํ๊ฒฝ ์ฌ์** | Python 3.10+, numpy, pandas, matplotlib, scipy, scikit-learn |\n",
"\n",
"### ๐ ํ๋ก์ ํธ ๋ชฉํ\n",
"์ผ์ ์์ ์ฅ๋น๊ฐ ๊ณ ์ฅ ๋๋ฉด ์ด๋์ ๋น๋ฐ์ด ์ผ๋ง๋ ๋นจ๋ฆฌ ๋์ฐฉํด ๊ณ ์น๋๋๊ฐ ๋ถ๋ ์ ํฌ๋ ฅ ํ๋ณต ์๋๋ฅผ ์ข์ฐํ์ง๋ง, ๊ฐ๊น์ด ์ ๋น๋ฐ๋ถํฐ ๋ณด๋ด๋ ๋ฐฉ์์ ์์ฒญ์ด ๋ชฐ๋ฆฌ๋ ์ง์ญ์ ์ ๋น๋ฐ์ ๋ถํ๋ฅผ ์ง์ค์ํค๊ณ ํต์ฌ ์ฅ๋น๋ฅผ ๋ค๋ก ๋ฏธ๋ฃจ๋ ๋ฌธ์ ๊ฐ ์๋ค. ๋ณธ ํ๋ก์ ํธ๋ ์ ๋น์ธ๋ ฅ์ ๋ถํ์จ๊ณผ ๊ณต์ ๊ท ํ์ ๋ฐ์ดํฐ๋ก ์์ธกยท๊ด๋ฆฌํ๋ ๊ฒ์ ๋ชฉํ๋ก, ๊ต์ก์ฉ ๊ฐ์ ์ง์ญ์ ๊ธด๊ธ ์ ๋น ์์ฒญ๊ณผ ์ด๋์ ๋น๋ฐ ์๋ฃ๋ฅผ ์ฝ๋๋ก ๋ง๋ค์ด ๋ถ์ํ๋ค. ์์ฒญ ๋ฐ์ดํฐ ์คํค๋ง ์ ์, ์ ๋น๋ฐ ์ํ ์๋ฃ ์ ์ , ๊ฑฐ๋ฆฌ ๊ธฐ์ค Baseline๊ณผ ํ์์ ๋ถ์, ์ค์๋ยท์ ๋น ์์์๊ฐยท์ด๋ ๊ฒฝ๋ก๋ฅผ ์ข
ํฉํ ์ฐ์ ์์ ์์ง, ์ถ๋ ์ง์ฐ ์ํ๋์ ํ์ฅ ์๋ฆฌ ๊ฐ๋ฅ์ฑ ์ ์, ๊ด์ ๋์๋ณด๋์ ์ค๋ช
๊ฐ๋ฅ์ฑ, ์ ๋น๋๋์ฅ ์น์ธ(HITL) ์๋ฎฌ๋ ์ดํฐ, ๋์ ๋ค๋ฐ ์์ฒญ ์คํธ๋ ์ค ์ํ, 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",
"์ผ์ ๊ธด๊ธ ์ ๋น ์์ฒญ(๊ณ ์ฅ ์์น, ์ฅ๋น ์ค์๋) ๋ฐ์ดํฐ ์คํค๋ง๋ฅผ ์ ์ํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ผ์ ๊ธด๊ธ ์ ๋น ์์ฒญ(๊ณ ์ฅ ์์น, ์ฅ๋น ์ค์๋, ๊ณ ์ฅ ์ ํ)๊ณผ ์ด๋์ ๋น๋ฐ ์ํ ์๋ฃ์ ์คํค๋ง, ๋ฐ์ดํฐ ๋์
๋๋ฆฌ, ์
๋ฌด ํ๋ก์ธ์ค ๋งต์ ์ ์ํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์์ฒญ ์ ์๋ถํฐ ๋ฐฐ์ ยท์ถ๋ยท์ ๋นยท๋ณต๊ทยท์ค์ ์
๋ ฅ๊น์ง ์
๋ฌด ํ๋ฆ์ ๋จ๊ณ๋ณ๋ก ์ ๋ฆฌํ๊ณ , ์์ฒญยท์ ๋น๋ฐยท๊ฒฐ๊ณผ ์ธ ๋ฌถ์์ผ๋ก ํผ์ฒ 20๊ฐ ์ด์ ์ ์ํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `docs/mnt10_logistics_domain_spec.md ๋ฐ ๋ฐ์ดํฐ ๋์
๋๋ฆฌ` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ตฐ์ ์ค๋ฌด ์
๋ฌด ํ๋ก์ธ์ค์์ ์ผ์น์ฑ ๋ฐ ๋ถ์ ๋ชฉํ์ ๋ช
ํ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์
๋ฌด ํ๋ก์ธ์ค ๋งต ์์ฑ ์๋ฃ, ํ์ ํผ์ฒ(Feature) ๋ชฉ๋ก 20๊ฐ ์ด์ ์ ์\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ๋๋ฉ์ธ ๋ถ์์ ๋ฐ ๋ฐ์ดํฐ ์๊ตฌ๋ช
์ธ์ ์น์ธ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ๊ตฐ์ ๋ด๋น ์ค๋ฌด์ ์ธํฐ๋ทฐ ๋ฐ ๊ธฐ์กด ์ ์ฐ์ฒด๊ณ ๋ฐ์ดํฐ ๊ตฌ์กฐ ๋์กฐ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์ผ์ ๊ธด๊ธ ์ ๋น ์์ฒญ(๊ณ ์ฅ ์์น, ์ฅ๋น ์ค์๋, ๊ณ ์ฅ ์ ํ)๊ณผ ์ด๋์ ๋น๋ฐ ์ํ ๋ฐ์ดํฐ์ ์คํค๋งยท๋ฐ์ดํฐ ๋์
๋๋ฆฌ(ํผ์ฒ 20๊ฐ ์ด์)์ ์
๋ฌด ํ๋ก์ธ์ค ๋งต์ ์ ์ํ๊ณ , ๊ฐ์ ์ํ ๋ฐ์ดํฐ๋ฅผ ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import json\n",
"import re\n",
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"# 1) ์
๋ฌด ํ๋ก์ธ์ค ๋งต: ์ ์๋ถํฐ ๋ถํ์จ ์ง๊ณยท์ฌํ๊ฒํ ๊น์ง (๊ฐ ๋จ๊ณ์์ ์๊ธฐ๋ ๋ฐ์ดํฐ)\n",
"PROCESS = [(\"์ ์\", \"์์ฒญ ID, ์ ์์๊ฐ, ๊ฐ์ ์ขํ, ์ฅ๋น ๊ตฌ๋ถยท์ฝ๋\"), (\"๋ถ๋ฅ\", \"๊ณ ์ฅ ์ ํ, ์์ ์ค์๋, ๋ถํ ํ๋ณด ์ฌ๋ถ\"),\n",
" (\"์ฐ์ ์์\", \"์ฐ์ ์์ ์ ์, ์์ ์ ๋น์๊ฐ\"), (\"๋ฐฐ์ \", \"์ ๋น๋ฐ ID, ์ด๋๊ฑฐ๋ฆฌ, ๋๊ธฐ(๋ฐฑ๋ก๊ทธ)์๊ฐ\"),\n",
" (\"์ถ๋\", \"์ถ๋ฐยท๋์ฐฉ ์๊ฐ, ๊ธฐ์, ๋๋ก ์ํ\"), (\"ํ์ฅ์ ๋น/ํ์ก\", \"ํ์ฅ ์๋ฆฌ ๊ฐ๋ฅ ์ฌ๋ถ, ์ค์ ์ ๋น์๊ฐ\"),\n",
" (\"์๋ฃ๋ณด๊ณ \", \"์๋ฃ ์๊ฐ, ์ฅ๋น ๊ฐ๋์ค๋จ ์๊ฐ\"), (\"๋ถํ์จ ์ง๊ณ\", \"์ ๋น๋ฐ๋ณ ์ผ์ผ ํฌ์
์๊ฐ/๊ฐ์ฉ์๊ฐ\"),\n",
" (\"์ฌํ๊ฒํ (AAR)\", \"์ง์ฐยท์ค๋ฐฐ์ ์์ธ, ๊ฐ์ค์น ์กฐ์ ์ด๋ ฅ\")]\n",
"\n",
"# 2) ์คํค๋ง(๋ฐ์ดํฐ ๋์
๋๋ฆฌ): ๊ทธ๋ฃน / ํ๋ / ์๋ฃํ / ๊ท์น / ์ค๋ช
\n",
"S = {\n",
" \"request\": {\n",
" \"req_id\": (str, {\"pattern\": r\"^R\\d{4}$\"}, \"์์ฒญ ID\"),\n",
" \"t_req\": (float, {\"min\": 0}, \"์ ์์๊ฐ(๋ชจ์ ์์ ํ ๊ฒฝ๊ณผ ์๊ฐ, h)\"),\n",
" \"unit_code\": (str, {\"pattern\": r\"^VU-\\d{2}$\"}, \"์์ฒญ ๋ถ๋ ์ฝ๋(๊ฐ์)\"),\n",
" \"grid_x\": (float, {\"min\": 0, \"max\": 60}, \"๊ณ ์ฅ ์์น X(๊ฐ์ ์ขํ, km)\"),\n",
" \"grid_y\": (float, {\"min\": 0, \"max\": 60}, \"๊ณ ์ฅ ์์น Y(๊ฐ์ ์ขํ, km)\"),\n",
" \"equip_type\": (str, {\"enum\": [\"mobility\", \"fires\", \"comms\", \"power\"]}, \"์ฅ๋น ๊ตฌ๋ถ\"),\n",
" \"equip_code\": (str, {\"pattern\": r\"^VX-\\d{4}-\\d{4}$\"}, \"์ฅ๋น ์๋ณ์ฝ๋(๊ตญ๋ฐฉ ํ์ค ์ฝ๋ ํ์์ ๋ชจ์ฌํ ๊ฐ์ ์ฝ๋)\"),\n",
" \"fsc_group\": (str, {\"pattern\": r\"^\\d{4}$\"}, \"๋ณด๊ธ ๋ถ๋ฅ๊ตฐ(๊ฐ์ 4์๋ฆฌ)\"),\n",
" \"crit\": (int, {\"enum\": [1, 2, 3]}, \"์์ ์ค์๋(1 ์ผ๋ฐ, 2 ์ค์, 3 ํต์ฌ)\"),\n",
" \"fault_type\": (str, {\"enum\": [\"engine\", \"drivetrain\", \"hydraulic\", \"electric\", \"comm_module\", \"cooling\"]}, \"๊ณ ์ฅ ์ ํ\"),\n",
" \"parts_ready\": (bool, {}, \"์๋ฆฌ๋ถ์ ํ๋ณด ์ฌ๋ถ\"),\n",
" \"weather\": (str, {\"enum\": [\"clear\", \"rain\", \"snow\"]}, \"๊ธฐ์\"),\n",
" \"road\": (str, {\"enum\": [\"paved\", \"unpaved\"]}, \"์ ๊ทผ ๋๋ก ์ํ\"),\n",
" },\n",
" \"team\": {\n",
" \"team_id\": (str, {\"pattern\": r\"^MT\\d$\"}, \"์ด๋์ ๋น๋ฐ ID\"),\n",
" \"base_x\": (float, {\"min\": 0, \"max\": 60}, \"์ ๋น๋ฐ ๋๊ธฐ ์์น X\"),\n",
" \"base_y\": (float, {\"min\": 0, \"max\": 60}, \"์ ๋น๋ฐ ๋๊ธฐ ์์น Y\"),\n",
" \"crew\": (int, {\"min\": 2, \"max\": 8}, \"๊ฐ์ฉ ์ ๋น์ธ๋ ฅ(๋ช
)\"),\n",
" \"skills\": (list, {}, \"์ ๋น ๊ฐ๋ฅ ์ฅ๋น ๊ตฌ๋ถ\"),\n",
" \"status\": (str, {\"enum\": [\"ready\", \"moving\", \"repairing\", \"returning\"]}, \"์ถ๋ ๊ฐ๋ฅ ์ํ\"),\n",
" },\n",
" \"result\": {\n",
" \"std_repair_h\": (float, {\"min\": 0, \"max\": 48}, \"ํ์ค ์ ๋น์๊ฐ(4์ธ ๊ธฐ์ค) - ์์ธก๋์ 1\"),\n",
" \"response_h\": (float, {\"min\": 0, \"max\": 72}, \"์ ์~๋์ฐฉ ์๊ฐ - ์์ธก๋์ 2(์ถ๋ ์ง์ฐ)\"),\n",
" \"downtime_h\": (float, {\"min\": 0, \"max\": 240}, \"์ฅ๋น ๊ฐ๋์ค๋จ ์๊ฐ\"),\n",
" \"field_ok\": (bool, {}, \"ํ์ฅ ์๋ฆฌ ๊ฐ๋ฅ ์ฌ๋ถ - ์์ธก๋์ 3\"),\n",
" \"load_rate\": (float, {\"min\": 0, \"max\": 3}, \"์ ๋น๋ฐ ์ผ์ผ ๋ถํ์จ = ํฌ์
์๊ฐ/๊ฐ์ฉ์๊ฐ(10h) - ์์ธก๋์ 4\"),\n",
" },\n",
"}\n",
"\n",
"\n",
"def check(name, v, typ, rule):\n",
" if not isinstance(v, typ) or (typ in (int, float) and isinstance(v, bool)):\n",
" return [f\"{name}: ์๋ฃํ ์ค๋ฅ({type(v).__name__})\"]\n",
" e = []\n",
" if \"enum\" in rule and v not in rule[\"enum\"]:\n",
" e.append(f\"{name}: ํ์ฉ๋์ง ์์ ๊ฐ '{v}'\")\n",
" if \"min\" in rule and v < rule[\"min\"]:\n",
" e.append(f\"{name}: ์ต์๊ฐ {rule['min']} ๋ฏธ๋ง\")\n",
" if \"max\" in rule and v > rule[\"max\"]:\n",
" e.append(f\"{name}: ์ต๋๊ฐ {rule['max']} ์ด๊ณผ\")\n",
" if \"pattern\" in rule and not re.match(rule[\"pattern\"], v):\n",
" e.append(f\"{name}: ํ์ ๋ถ์ผ์น '{v}'\")\n",
" return e\n",
"\n",
"\n",
"def validate(rec):\n",
" errs = []\n",
" for g, fields in S.items():\n",
" for k, (typ, rule, _) in fields.items():\n",
" if k not in rec.get(g, {}):\n",
" errs.append(f\"{g}.{k}: ํ์ ํ๋ ๋๋ฝ\")\n",
" else:\n",
" errs += check(f\"{g}.{k}\", rec[g][k], typ, rule)\n",
" return errs\n",
"\n",
"\n",
"# 3) [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ํ 20๊ฑด\n",
"rng = np.random.default_rng(1)\n",
"recs = []\n",
"for i in range(20):\n",
" std = float(round(rng.uniform(1, 9), 2))\n",
" recs.append({\n",
" \"request\": {\"req_id\": f\"R{i:04d}\", \"t_req\": float(round(rng.uniform(0, 720), 1)), \"unit_code\": f\"VU-{rng.integers(1, 30):02d}\",\n",
" \"grid_x\": float(round(rng.uniform(0, 60), 1)), \"grid_y\": float(round(rng.uniform(0, 60), 1)),\n",
" \"equip_type\": str(rng.choice([\"mobility\", \"fires\", \"comms\", \"power\"])),\n",
" \"equip_code\": f\"VX-{rng.integers(1000, 9999)}-{rng.integers(1000, 9999)}\", \"fsc_group\": f\"{rng.integers(1000, 9999)}\",\n",
" \"crit\": int(rng.integers(1, 4)), \"fault_type\": str(rng.choice([\"engine\", \"hydraulic\", \"electric\", \"cooling\"])),\n",
" \"parts_ready\": bool(rng.random() < 0.75), \"weather\": str(rng.choice([\"clear\", \"rain\", \"snow\"])), \"road\": \"paved\"},\n",
" \"team\": {\"team_id\": f\"MT{rng.integers(1, 7)}\", \"base_x\": 30.0, \"base_y\": 6.0, \"crew\": int(rng.integers(3, 6)),\n",
" \"skills\": [\"mobility\", \"comms\"], \"status\": \"ready\"},\n",
" \"result\": {\"std_repair_h\": std, \"response_h\": float(round(rng.uniform(0.5, 6), 2)),\n",
" \"downtime_h\": float(round(std + rng.uniform(1, 8), 2)), \"field_ok\": bool(rng.random() < 0.6),\n",
" \"load_rate\": float(round(rng.uniform(0.3, 1.3), 2))}})\n",
"\n",
"ok = sum(not validate(r) for r in recs)\n",
"print(f\"์ ์ ์ํ {len(recs)}๊ฑด ์ค ๊ฒ์ฆ ํต๊ณผ {ok}๊ฑด, ์ค๋ฅ {len(recs) - ok}๊ฑด\")\n",
"\n",
"# 4) ์ค๋ฅ ์ฃผ์
์ํ\n",
"bad = json.loads(json.dumps(recs[0]))\n",
"bad[\"request\"][\"grid_x\"] = 75.0 # ์์ ์ง์ญ ๋ฐ ์ขํ\n",
"bad[\"request\"][\"crit\"] = 5 # ํ์ฉ๋์ง ์์ ์ค์๋\n",
"bad[\"request\"][\"equip_code\"] = \"1234567890123\" # ์ฝ๋ ํ์ ์ค๋ฅ\n",
"del bad[\"team\"][\"crew\"] # ๊ฐ์ฉ ์ธ๋ ฅ ๋๋ฝ\n",
"bad[\"result\"][\"load_rate\"] = \"90%\" # ์๋ฃํ ์ค๋ฅ(๋ฌธ์์ด)\n",
"print(\"\\n[์ค๋ฅ ์ฃผ์
๊ฒฐ๊ณผ]\")\n",
"for e in validate(bad):\n",
" print(\" -\", e)\n",
"\n",
"# 5) ํ๋ก์ธ์ค ๋งต๊ณผ ๋ฐ์ดํฐ ๋์
๋๋ฆฌ\n",
"print(\"\\n[์
๋ฌด ํ๋ก์ธ์ค ๋งต]\")\n",
"print(\" -> \".join(f\"{i}.{p}\" for i, (p, _) in enumerate(PROCESS, 1)))\n",
"for p, d in PROCESS:\n",
" print(f\" {p:8s}: {d}\")\n",
"rows = [(g, k, t.__name__, str(r) if r else \"-\", d) for g, f in S.items() for k, (t, r, d) in f.items()]\n",
"dd = pd.DataFrame(rows, columns=[\"๊ทธ๋ฃน\", \"ํ๋\", \"์๋ฃํ\", \"๊ท์น\", \"์ค๋ช
\"])\n",
"print(f\"\\n[๋ฐ์ดํฐ ๋์
๋๋ฆฌ] ํผ์ฒ {len(dd)}๊ฐ (๋ชฉํ 20๊ฐ ์ด์: {'๋ฌ์ฑ' if len(dd) >= 20 else '๋ฏธ๋ฌ'})\")\n",
"print(dd[[\"๊ทธ๋ฃน\", \"ํ๋\", \"์๋ฃํ\", \"์ค๋ช
\"]].to_string(index=False))\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
01 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"์ ์ ์ํ 20๊ฑด ์ค ๊ฒ์ฆ ํต๊ณผ 20๊ฑด, ์ค๋ฅ 0๊ฑด\n",
"\n",
"[์ค๋ฅ ์ฃผ์
๊ฒฐ๊ณผ]\n",
" - request.grid_x: ์ต๋๊ฐ 60 ์ด๊ณผ\n",
" - request.equip_code: ํ์ ๋ถ์ผ์น '1234567890123'\n",
" - request.crit: ํ์ฉ๋์ง ์์ ๊ฐ '5'\n",
" - team.crew: ํ์ ํ๋ ๋๋ฝ\n",
" - result.load_rate: ์๋ฃํ ์ค๋ฅ(str)\n",
"\n",
"[์
๋ฌด ํ๋ก์ธ์ค ๋งต]\n",
"1.์ ์ -> 2.๋ถ๋ฅ -> 3.์ฐ์ ์์ -> 4.๋ฐฐ์ -> 5.์ถ๋ -> 6.ํ์ฅ์ ๋น/ํ์ก -> 7.์๋ฃ๋ณด๊ณ -> 8.๋ถํ์จ ์ง๊ณ -> 9.์ฌํ๊ฒํ (AAR)\n",
" ์ ์ : ์์ฒญ ID, ์ ์์๊ฐ, ๊ฐ์ ์ขํ, ์ฅ๋น ๊ตฌ๋ถยท์ฝ๋\n",
" ๋ถ๋ฅ : ๊ณ ์ฅ ์ ํ, ์์ ์ค์๋, ๋ถํ ํ๋ณด ์ฌ๋ถ\n",
" ์ฐ์ ์์ : ์ฐ์ ์์ ์ ์, ์์ ์ ๋น์๊ฐ\n",
" ๋ฐฐ์ : ์ ๋น๋ฐ ID, ์ด๋๊ฑฐ๋ฆฌ, ๋๊ธฐ(๋ฐฑ๋ก๊ทธ)์๊ฐ\n",
" ์ถ๋ : ์ถ๋ฐยท๋์ฐฉ ์๊ฐ, ๊ธฐ์, ๋๋ก ์ํ\n",
" ํ์ฅ์ ๋น/ํ์ก : ํ์ฅ ์๋ฆฌ ๊ฐ๋ฅ ์ฌ๋ถ, ์ค์ ์ ๋น์๊ฐ\n",
" ์๋ฃ๋ณด๊ณ : ์๋ฃ ์๊ฐ, ์ฅ๋น ๊ฐ๋์ค๋จ ์๊ฐ\n",
" ๋ถํ์จ ์ง๊ณ : ์ ๋น๋ฐ๋ณ ์ผ์ผ ํฌ์
์๊ฐ/๊ฐ์ฉ์๊ฐ\n",
" ์ฌํ๊ฒํ (AAR): ์ง์ฐยท์ค๋ฐฐ์ ์์ธ, ๊ฐ์ค์น ์กฐ์ ์ด๋ ฅ\n",
"\n",
"[๋ฐ์ดํฐ ๋์
๋๋ฆฌ] ํผ์ฒ 24๊ฐ (๋ชฉํ 20๊ฐ ์ด์: ๋ฌ์ฑ)\n",
" ๊ทธ๋ฃน ํ๋ ์๋ฃํ ์ค๋ช
\n",
"request req_id str ์์ฒญ ID\n",
"request t_req float ์ ์์๊ฐ(๋ชจ์ ์์ ํ ๊ฒฝ๊ณผ ์๊ฐ, h)\n",
"request unit_code str ์์ฒญ ๋ถ๋ ์ฝ๋(๊ฐ์)\n",
"request grid_x float ๊ณ ์ฅ ์์น X(๊ฐ์ ์ขํ, km)\n",
"request grid_y float ๊ณ ์ฅ ์์น Y(๊ฐ์ ์ขํ, km)\n",
"request equip_type str ์ฅ๋น ๊ตฌ๋ถ\n",
"request equip_code str ์ฅ๋น ์๋ณ์ฝ๋(๊ตญ๋ฐฉ ํ์ค ์ฝ๋ ํ์์ ๋ชจ์ฌํ ๊ฐ์ ์ฝ๋)\n",
"request fsc_group str ๋ณด๊ธ ๋ถ๋ฅ๊ตฐ(๊ฐ์ 4์๋ฆฌ)\n",
"request crit int ์์ ์ค์๋(1 ์ผ๋ฐ, 2 ์ค์, 3 ํต์ฌ)\n",
"request fault_type str ๊ณ ์ฅ ์ ํ\n",
"request parts_ready bool ์๋ฆฌ๋ถ์ ํ๋ณด ์ฌ๋ถ\n",
"request weather str ๊ธฐ์\n",
"request road str ์ ๊ทผ ๋๋ก ์ํ\n",
" team team_id str ์ด๋์ ๋น๋ฐ ID\n",
" team base_x float ์ ๋น๋ฐ ๋๊ธฐ ์์น X\n",
" team base_y float ์ ๋น๋ฐ ๋๊ธฐ ์์น Y\n",
" team crew int ๊ฐ์ฉ ์ ๋น์ธ๋ ฅ(๋ช
)\n",
"... (์ดํ ์๋ต)\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
02. ์ด๋์ ๋น๋ฐ ๋ณด์ ์ฅ๋น/๊ณต๊ตฌ ๋ฐ ์ถ๋ ๊ฐ๋ฅ ์ํ ์ ๋ณด ์ ์ฒ๋ฆฌ ๋ชจ๋ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ด๋์ ๋น๋ฐ ๋ณด์ ์ฅ๋น ๋ฐ ์ถ๋ ๊ฐ๋ฅ ์ํ ์ ๋ณด ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ด๋์ ๋น๋ฐ์ ๋ณด์ ์ฅ๋นยท๊ณต๊ตฌยท์ธ๋ ฅยท์ถ๋ ๊ฐ๋ฅ ์ํ ๋ก๊ทธ๋ฅผ ์ ์ ํ๋ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ค๋ณต ๋ณด๊ณ ์ ๊ฑฐ, ์ ๋น๋ฐ IDยท์ํ๊ฐยท์๊ฐ ํ๊ธฐ ํต์ผ, ์ฐ๋ฃ ๋จ์ ํต์ผ, ๋ถ๊ฐ๋ฅํ ์ธ์ยท์์ ๊ณต๊ตฌ ์๋ ๊ฒฐ์ธก ์ฒ๋ฆฌ, ์ขํ ์ด์๊ฐ ๋ณด๊ฐ, KNN ๊ฒฐ์ธก ๋ณด์ ๋ฑ ์ ์ ๊ท์น 10์ข
์ ์ ์ฉ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/etl/mnt10_data_cleansing.py ๋ฐ ์ ์ ํ์ง ๋ณด๊ณ ์` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ตฐ์ ํน์ ์์ธ(๋น๊ณํ ์ ๋น, ๊ธด๊ธ ๋ถ์ถ ๋ฑ) ๋ฐ์ดํฐ ๋ณด์กด์ ํ๋น์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๋ฐ์ดํฐ ์ ์ ๊ท์น 10์ข
์ ์ฉ, ๋ฐ์ดํฐ ์ ํฉ์ฑ ๊ฒ์ฆ ํต๊ณผ์จ 100%\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ETL ํ์ดํ๋ผ์ธ ๊ตฌํ ๋ฐ ์ ์ ๋ ๋ง์คํฐ ๋ฐ์ดํฐ์
๊ตฌ์ถ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ์ ํ ๋ฐ์ดํฐ ํต๊ณ๋(๊ธฐ์ ํต๊ณ) ๋น๊ต ๋ฐ ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์ด๋์ ๋น๋ฐ์ ๋ณด์ ์ฅ๋นยท๊ณต๊ตฌยท์ธ๋ ฅยท์ถ๋ ๊ฐ๋ฅ ์ํ ๋ก๊ทธ(ํ๊ธฐ ๋ถ์ผ์นยท์ค๋ณตยท๋จ์ ํผ์ฌยท๊ฒฐ์ธก์ด ์์ธ ๊ฐ์ ์์ฒ ๋ฐ์ดํฐ)์ ์ ์ ๊ท์น 10์ข
์ ์ ์ฉํ๊ณ , ๊ธด๊ธ ๋ถ์ถ ๋ฑ ๊ตฐ์ ์์ธ ๋ฐ์ดํฐ๋ ๋ณด์กดํ๋ฉด์ ์ ํฉ์ฑ ๊ฒ์ฆ ํต๊ณผ์จ๊ณผ ์ ์ ์ ํ ํต๊ณ๋ฅผ ํ์ธํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.impute import KNNImputer\n",
"\n",
"# 1) [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์์ฒ ์ํ ๋ก๊ทธ: ์ ๋น๋ฐ 6๊ฐ x 30์ผ x ํ๋ฃจ 2ํ ๋ณด๊ณ + ์ผ๋ถ๋ฌ ๋ฃ์ ์ค๋ฅ\n",
"rng = np.random.default_rng(2)\n",
"rows = []\n",
"for d in range(30):\n",
" for h in (8, 18):\n",
" for t in range(1, 7):\n",
" rows.append({\"team\": f\"MT{t}\", \"ts\": f\"2026-03-{d + 1:02d} {h:02d}:00\", \"status\": str(rng.choice([\"ready\", \"moving\", \"repairing\"], p=[0.6, 0.2, 0.2])),\n",
" \"crew\": int(rng.integers(3, 6)), \"fuel\": float(round(rng.uniform(35, 100), 1)), \"fuel_unit\": \"%\",\n",
" \"diag_kit\": int(rng.integers(1, 3)), \"lift_kit\": int(rng.integers(0, 2)), \"x\": float(round(rng.uniform(0, 60), 1)),\n",
" \"y\": float(round(rng.uniform(0, 60), 1)), \"emergency_issue\": False})\n",
"raw = pd.DataFrame(rows)\n",
"n0 = len(raw)\n",
"idx = rng.choice(n0, 150, replace=False)\n",
"a, b, c, d_, e, f, g, h_, k, m = np.split(idx, 10)\n",
"raw.loc[a, \"team\"] = raw.loc[a, \"team\"].str.replace(\"MT\", \"mt-\") # ID ํ๊ธฐ ๋ถ์ผ์น\n",
"raw.loc[b, \"status\"] = rng.choice([\"READY\", \"ready \", \"๋๊ธฐ\", \"Repairing\"], len(b)) # ์ํ๊ฐ ํ๊ธฐ ๋ถ์ผ์น\n",
"raw.loc[c, \"ts\"] = pd.to_datetime(raw.loc[c, \"ts\"]).dt.strftime(\"%Y%m%d %H%M\") # ์๊ฐ ํ์ ํผ์ฌ\n",
"raw.loc[d_, \"fuel\"] = (raw.loc[d_, \"fuel\"] * 4).round(0); raw.loc[d_, \"fuel_unit\"] = \"L\" # ๋จ์ ํผ์ฌ(๊ฐ์ ํฑํฌ 400L)\n",
"raw.loc[e, \"crew\"] = rng.choice([-1, 0, 25], len(e)) # ๋ถ๊ฐ๋ฅ ๊ฐ\n",
"raw.loc[f, \"diag_kit\"] = -1 # ์์ ์๋\n",
"raw.loc[g, \"x\"] = 999.0 # ์ขํ ๋ฒ์ ๋ฐ\n",
"raw.loc[h_, [\"crew\", \"fuel\"]] = np.nan # ๊ฒฐ์ธก\n",
"raw.loc[k, \"lift_kit\"] = 6; raw.loc[k, \"emergency_issue\"] = True # ๊ธด๊ธ ๋ถ์ถ(์์ธ: ๋ณด์กด ๋์)\n",
"raw.loc[m, \"status\"] = None # ์ํ ๊ฒฐ์ธก\n",
"raw = pd.concat([raw, raw.sample(12, random_state=1)], ignore_index=True) # ์ค๋ณต ๋ณด๊ณ \n",
"print(f\"์์ฒ ๋ก๊ทธ {len(raw)}๊ฑด (์ค๋ณต 12๊ฑด, ์ค๋ฅ ์ฃผ์
{len(idx)}๊ฑด ํฌํจ)\")\n",
"\n",
"# 2) ์ ์ ๊ท์น 10์ข
\n",
"df, applied = raw.copy(), []\n",
"def rule(name, n):\n",
" applied.append((name, int(n)))\n",
"\n",
"rule(\"R1 ์ค๋ณต ๋ณด๊ณ ์ ๊ฑฐ\", df.duplicated().sum()); df = df.drop_duplicates().reset_index(drop=True)\n",
"bad_id = ~df[\"team\"].str.match(r\"^MT\\d$\"); df[\"team\"] = \"MT\" + df[\"team\"].str.extract(r\"(\\d+)\")[0]; rule(\"R2 ์ ๋น๋ฐ ID ํ๊ธฐ ํต์ผ\", bad_id.sum())\n",
"smap = {\"ready\": \"ready\", \"๋๊ธฐ\": \"ready\", \"moving\": \"moving\", \"repairing\": \"repairing\"}\n",
"s = df[\"status\"].fillna(\"\").str.strip().str.lower().map(smap); rule(\"R3 ์ํ๊ฐ ํ์คํ\", (s != df[\"status\"]).sum()); df[\"status\"] = s\n",
"t1 = pd.to_datetime(df[\"ts\"], format=\"%Y-%m-%d %H:%M\", errors=\"coerce\")\n",
"t2 = pd.to_datetime(df[\"ts\"], format=\"%Y%m%d %H%M\", errors=\"coerce\"); rule(\"R4 ๋ณด๊ณ ์๊ฐ ํ์ ํต์ผ\", t1.isna().sum()); df[\"ts\"] = t1.fillna(t2)\n",
"lit = df[\"fuel_unit\"] == \"L\"; df.loc[lit, \"fuel\"] = df.loc[lit, \"fuel\"] / 4; df[\"fuel_unit\"] = \"%\"; rule(\"R5 ์ฐ๋ฃ ๋จ์ ํต์ผ(L -> %)\", lit.sum())\n",
"bad = ~df[\"crew\"].between(1, 8) & df[\"crew\"].notna(); df.loc[bad, \"crew\"] = np.nan; rule(\"R6 ๋ถ๊ฐ๋ฅ ์ธ์๊ฐ ๊ฒฐ์ธก ์ฒ๋ฆฌ\", bad.sum())\n",
"bad = df[\"diag_kit\"] < 0; df.loc[bad, \"diag_kit\"] = np.nan; rule(\"R7 ์์ ๊ณต๊ตฌ ์๋ ๊ฒฐ์ธก ์ฒ๋ฆฌ\", bad.sum())\n",
"bad = ~df[\"x\"].between(0, 60); df.loc[bad, [\"x\", \"y\"]] = np.nan; df = df.sort_values([\"team\", \"ts\"])\n",
"df[[\"x\", \"y\"]] = df.groupby(\"team\")[[\"x\", \"y\"]].ffill().bfill(); rule(\"R8 ์ขํ ์ด์๊ฐ ์ง์ ์์น ๋ณด๊ฐ\", bad.sum())\n",
"miss = df[[\"crew\", \"fuel\", \"diag_kit\"]].isna().sum().sum()\n",
"df[[\"crew\", \"fuel\", \"diag_kit\"]] = KNNImputer(n_neighbors=5).fit_transform(df[[\"crew\", \"fuel\", \"diag_kit\", \"lift_kit\"]])[:, :3]\n",
"df[\"crew\"] = df[\"crew\"].round().astype(int); df[\"diag_kit\"] = df[\"diag_kit\"].round().astype(int); rule(\"R9 ๊ฒฐ์ธก์น KNN ๋ณด์ \", miss)\n",
"q1, q3 = df[\"lift_kit\"].quantile([0.25, 0.75]); out = df[\"lift_kit\"] > q3 + 1.5 * (q3 - q1)\n",
"df[\"outlier_flag\"] = out & ~df[\"emergency_issue\"]; rule(\"R10 ์์ธ(๊ธด๊ธ ๋ถ์ถ) ๋ณด์กดยท์ผ๋ฐ ์ด์๊ฐ ํ์\", (out & df[\"emergency_issue\"]).sum())\n",
"df[\"status\"] = df.groupby(\"team\")[\"status\"].transform(lambda x: x.ffill().fillna(\"ready\"))\n",
"df[\"dispatchable\"] = (df[\"status\"] == \"ready\") & (df[\"crew\"] >= 3) & (df[\"fuel\"] >= 30) & (df[\"diag_kit\"] >= 1)\n",
"print(\"\\n[์ ์ฉ ๊ท์น๊ณผ ์ฒ๋ฆฌ ๊ฑด์]\")\n",
"for nme, n in applied:\n",
" print(f\" {nme:28s} {n:4d}๊ฑด\")\n",
"\n",
"# 3) ์ ํฉ์ฑ ๊ฒ์ฆ\n",
"checks = {\"์ค๋ณต 0๊ฑด\": not df.duplicated([\"team\", \"ts\"]).any(), \"ID ํ์\": df[\"team\"].str.match(r\"^MT[1-6]$\").all(),\n",
" \"์ํ๊ฐ ํ์ฉ ๋ฒ์\": df[\"status\"].isin([\"ready\", \"moving\", \"repairing\"]).all(), \"์๊ฐ ๋ณํ\": df[\"ts\"].notna().all(),\n",
" \"์ธ์ 1~8๋ช
\": df[\"crew\"].between(1, 8).all(), \"์ฐ๋ฃ 0~100%\": df[\"fuel\"].between(0, 100).all(),\n",
" \"๊ณต๊ตฌ ์๋ 0 ์ด์\": (df[\"diag_kit\"] >= 0).all(), \"์ขํ ๋ฒ์\": df[\"x\"].between(0, 60).all(),\n",
" \"๊ฒฐ์ธก 0๊ฑด\": df[[\"crew\", \"fuel\", \"diag_kit\", \"x\", \"y\", \"status\"]].notna().all().all(),\n",
" f\"๊ธด๊ธ ๋ถ์ถ {len(k)}๊ฑด ๋ณด์กด\": int(df[\"emergency_issue\"].sum()) == len(k)}\n",
"rate = np.mean(list(checks.values()))\n",
"print(f\"\\n์ ํฉ์ฑ ๊ฒ์ฆ {sum(checks.values())}/{len(checks)} ํต๊ณผ, ํต๊ณผ์จ {rate:.0%} (๋ชฉํ 100%: {'๋ฌ์ฑ' if rate == 1 else '๋ฏธ๋ฌ'})\")\n",
"print(f\"์ ์ ํ {len(df)}๊ฑด, ์ถ๋ ๊ฐ๋ฅ ๋ณด๊ณ ๋น์จ {df['dispatchable'].mean():.1%}\")\n",
"\n",
"# 4) ์ ์ ์ ํ ๊ธฐ์ ํต๊ณ ๋น๊ต\n",
"num = lambda x: pd.to_numeric(x, errors=\"coerce\")\n",
"cmp = pd.DataFrame({\"์ ์ ์ ํ๊ท \": [num(raw[c]).mean() for c in [\"crew\", \"fuel\", \"diag_kit\", \"x\"]],\n",
" \"์ ์ ํ ํ๊ท \": [df[c].mean() for c in [\"crew\", \"fuel\", \"diag_kit\", \"x\"]],\n",
" \"์ ์ ์ ์ต๋\": [num(raw[c]).max() for c in [\"crew\", \"fuel\", \"diag_kit\", \"x\"]],\n",
" \"์ ์ ํ ์ต๋\": [df[c].max() for c in [\"crew\", \"fuel\", \"diag_kit\", \"x\"]]}, index=[\"์ธ์\", \"์ฐ๋ฃ%\", \"์ง๋จํคํธ\", \"์ขํX\"]).round(1)\n",
"print(\"\\n[์ ์ ์ ํ ๊ธฐ์ ํต๊ณ]\"); print(cmp.to_string())\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(10, 3.5))\n",
"ax[0].hist(num(raw[\"fuel\"]).dropna(), bins=30, alpha=0.6, label=\"raw\"); ax[0].hist(df[\"fuel\"], bins=30, alpha=0.6, label=\"cleaned\")\n",
"ax[0].set_title(\"Fuel level before/after unit fix\"); ax[0].legend()\n",
"ax[1].bar([n.split()[0] for n, _ in applied], [n for _, n in applied]); ax[1].set_title(\"Records changed by rule\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
02 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"์์ฒ ๋ก๊ทธ 372๊ฑด (์ค๋ณต 12๊ฑด, ์ค๋ฅ ์ฃผ์
150๊ฑด ํฌํจ)\n",
"\n",
"[์ ์ฉ ๊ท์น๊ณผ ์ฒ๋ฆฌ ๊ฑด์]\n",
" R1 ์ค๋ณต ๋ณด๊ณ ์ ๊ฑฐ 12๊ฑด\n",
" R2 ์ ๋น๋ฐ ID ํ๊ธฐ ํต์ผ 15๊ฑด\n",
" R3 ์ํ๊ฐ ํ์คํ 30๊ฑด\n",
" R4 ๋ณด๊ณ ์๊ฐ ํ์ ํต์ผ 15๊ฑด\n",
" R5 ์ฐ๋ฃ ๋จ์ ํต์ผ(L -> %) 15๊ฑด\n",
" R6 ๋ถ๊ฐ๋ฅ ์ธ์๊ฐ ๊ฒฐ์ธก ์ฒ๋ฆฌ 15๊ฑด\n",
" R7 ์์ ๊ณต๊ตฌ ์๋ ๊ฒฐ์ธก ์ฒ๋ฆฌ 15๊ฑด\n",
" R8 ์ขํ ์ด์๊ฐ ์ง์ ์์น ๋ณด๊ฐ 15๊ฑด\n",
" R9 ๊ฒฐ์ธก์น KNN ๋ณด์ 60๊ฑด\n",
" R10 ์์ธ(๊ธด๊ธ ๋ถ์ถ) ๋ณด์กดยท์ผ๋ฐ ์ด์๊ฐ ํ์ 15๊ฑด\n",
"\n",
"์ ํฉ์ฑ ๊ฒ์ฆ 10/10 ํต๊ณผ, ํต๊ณผ์จ 100% (๋ชฉํ 100%: ๋ฌ์ฑ)\n",
"์ ์ ํ 360๊ฑด, ์ถ๋ ๊ฐ๋ฅ ๋ณด๊ณ ๋น์จ 60.3%\n",
"\n",
"[์ ์ ์ ํ ๊ธฐ์ ํต๊ณ]\n",
" ์ ์ ์ ํ๊ท ์ ์ ํ ํ๊ท ์ ์ ์ ์ต๋ ์ ์ ํ ์ต๋\n",
"์ธ์ 4.2 4.0 25.0 5.0\n",
"์ฐ๋ฃ% 74.1 66.9 366.0 100.0\n",
"์ง๋จํคํธ 1.4 1.5 2.0 2.0\n",
"์ขํX 71.8 30.1 999.0 59.8\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
03. ๊ฑฐ๋ฆฌ ๊ธฐ์ค 1์ฐจ ์ด๋์ ๋น๋ฐ ๋ฐฐ์ Baseline ์๊ณ ๋ฆฌ์ฆ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๊ฑฐ๋ฆฌ ๊ธฐ์ค 1์ฐจ ์ด๋์ ๋น๋ฐ ๋ฐฐ์ Baseline ์๊ณ ๋ฆฌ์ฆ์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๊ฑฐ๋ฆฌ ๊ธฐ์ค 1์ฐจ ์ด๋์ ๋น๋ฐ ๋ฐฐ์ Baseline(์ ์ ์์ + ์ต๊ทผ์ ์ ๋น๋ฐ)์ ๊ตฌํํ๊ณ ํ์์ ๋ถ์์ผ๋ก ์ถ๋ ์ง์ฐ๊ณผ ๋ถํ ๋ถ๊ท ํ์ ์์ธ์ ์ฐพ๋๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ฐ์ 30์ผ ์์ฒญ์ Baseline์ผ๋ก ๋ฐฐ์ ยท๋ชจ์ํด ์ ๋น๋ฐ๋ณ ๋ถํ์จ, ๋์ฐฉ์๊ฐ, ์ด๋์๊ฐ์ ์ฐ์ถ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `notebooks/mnt10_eda_analysis.ipynb ๋ฐ EDA ์์ฝ ๋ฆฌํฌํธ` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ตฐ์ ์์ฌ๊ฒฐ์ ์ ์ง๊ด์ ์ธ ์ธ์ฌ์ดํธ ์ ๊ณต ์์ค\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: EDA ์๊ฐํ ์ฐจํธ 8์ข
์ด์ ์์ฑ, ์ ์๋ฏธํ ์๊ด๊ด๊ณ ํผ์ฒ 5๊ฐ ์ด์ ๋์ถ\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: EDA ๋
ธํธ๋ถ ์คํ ์๋ฃ ๋ฐ ํต์ฌ ๋ฐ๊ฒฌ์ (Key Findings) ๋ฌธ์ํ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ํต๊ณ์ ๊ฐ์ค ๊ฒ์ (p-value, ์๊ด๊ณ์)์ ํตํ ์ ์์ฑ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"๊ฑฐ๋ฆฌ ๊ธฐ์ค 1์ฐจ ์ด๋์ ๋น๋ฐ ๋ฐฐ์ Baseline(์ ์ ์์ + ์ต๊ทผ์ ์ ๋น๋ฐ)์ ๊ตฌํํด 30์ผ ์ถ๋์ ๋ชจ์ํ๊ณ , ํ์์ ๋ถ์(EDA) ์ฐจํธ 8์ข
ยท์๊ด๋ถ์(p-value)ยทํ๋ ํ (ABC) ๋ถ์์ผ๋ก ์ถ๋ ์ง์ฐ๊ณผ ์ ๋น์ธ๋ ฅ ๋ถํ ๋ถ๊ท ํ์ ์์ธ์ ์ฐพ๋๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy import stats\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ผ์ ์ด๋์ ๋น๋ฐยท๊ธด๊ธ ์ ๋น ์์ฒญ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์์นยท์ฅ๋นยท์ธ์ ์ ๋ณด ์์)\n",
"# 60km x 60km ๊ฐ์ ์ง์ญ์ ์ด๋์ ๋น๋ฐ 6๊ฐ์ 30์ผ๊ฐ์ ๊ธด๊ธ ์ ๋น ์์ฒญ์ ๋ง๋ ๋ค.\n",
"# ์ฅ๋น ๊ตฌ๋ถ(mobility/fires/comms/power)๊ณผ ๊ณ ์ฅ ์ ํ์ ๊ต์ก์ฉ ์ถ์ ๋ถ๋ฅ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"AREA_KM, DUTY_H = 60, 10.0 # ๊ฐ์ ์์ ์ง์ญ ํฌ๊ธฐ, ์ ๋น๋ฐ 1์ผ ๊ธฐ์ค ๊ฐ์ฉ์๊ฐ\n",
"FAULT_H = {\"engine\": 6.0, \"drivetrain\": 5.0, \"hydraulic\": 3.0, \"electric\": 2.0, \"comm_module\": 1.5, \"cooling\": 2.5}\n",
"FIELD_P = {\"engine\": 0.3, \"drivetrain\": 0.35, \"hydraulic\": 0.75, \"electric\": 0.9, \"comm_module\": 0.95, \"cooling\": 0.8}\n",
"EQUIP_F = {\"mobility\": 1.0, \"fires\": 1.2, \"comms\": 0.7, \"power\": 0.8}\n",
"FAULT_OF = {\"mobility\": [0.3, 0.25, 0.2, 0.1, 0.0, 0.15], \"fires\": [0.2, 0.15, 0.35, 0.15, 0.0, 0.15],\n",
" \"comms\": [0.0, 0.0, 0.0, 0.4, 0.5, 0.1], \"power\": [0.35, 0.0, 0.0, 0.35, 0.0, 0.3]}\n",
"CRIT_P = {\"mobility\": [0.45, 0.35, 0.2], \"fires\": [0.2, 0.35, 0.45], \"comms\": [0.3, 0.4, 0.3], \"power\": [0.6, 0.3, 0.1]}\n",
"HUBS = [(15, 20), (45, 25), (30, 50)] # ๊ฐ์ ํ๋ จ์ฅ 3๊ณณ(์์ฒญ์ด ๋ชฐ๋ฆฌ๋ ์ง์ )\n",
"\n",
"\n",
"def make_teams(seed=0):\n",
" rng = np.random.default_rng(seed)\n",
" base = [(8, 10), (30, 6), (52, 12), (10, 44), (32, 38), (54, 50)]\n",
" skills = [[\"fires\", \"mobility\"], [\"comms\", \"mobility\"], [\"fires\", \"power\"],\n",
" [\"mobility\", \"power\"], [\"comms\", \"fires\"], [\"comms\", \"mobility\", \"power\"]]\n",
" return [{\"team\": f\"MT{i + 1}\", \"x\": float(x), \"y\": float(y), \"crew\": int(rng.integers(3, 6)), \"skills\": s}\n",
" for i, ((x, y), s) in enumerate(zip(base, skills))]\n",
"\n",
"\n",
"def make_requests(n_days=30, per_day=10, seed=1, surge_days=(8, 9, 10, 11, 22, 23, 24)):\n",
" \"\"\"ํ๋ฃจ ํ๊ท per_day๊ฑด, ํ๋ จ ์ฃผ๊ฐ(surge_days)์๋ 1.6๋ฐฐ์ ๊ธด๊ธ ์ ๋น ์์ฒญ์ ๋ง๋ ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for d in range(n_days):\n",
" n = rng.poisson(per_day * (1.6 if d in surge_days else 1.0))\n",
" weather = rng.choice([\"clear\", \"rain\", \"snow\"], p=[0.65, 0.25, 0.1])\n",
" for _ in range(n):\n",
" if rng.random() < 0.7:\n",
" hx, hy = HUBS[rng.integers(0, 3)]\n",
" x, y = np.clip(rng.normal(hx, 6), 0, AREA_KM), np.clip(rng.normal(hy, 6), 0, AREA_KM)\n",
" else:\n",
" x, y = rng.uniform(0, AREA_KM, 2)\n",
" eq = str(rng.choice(list(EQUIP_F), p=[0.45, 0.25, 0.2, 0.1]))\n",
" fault = str(rng.choice(list(FAULT_H), p=FAULT_OF[eq]))\n",
" parts = bool(rng.random() < 0.75)\n",
" bad = weather != \"clear\"\n",
" std_h = FAULT_H[fault] * EQUIP_F[eq] * (1.35 if not parts else 1.0) * (1.15 if bad else 1.0) * rng.lognormal(0, 0.12)\n",
" rows.append({\"t_req\": d * 24 + float(np.clip(rng.normal(13, 4), 5, 23)), \"day\": d,\n",
" \"x\": float(x), \"y\": float(y), \"equip\": eq, \"fault\": fault,\n",
" \"crit\": int(rng.choice([1, 2, 3], p=CRIT_P[eq])), \"parts_ready\": parts,\n",
" \"weather\": str(weather), \"road\": \"unpaved\" if y > 30 and rng.random() < 0.7 else \"paved\",\n",
" \"field_ok\": bool(rng.random() < FIELD_P[fault] * (1.0 if parts else 0.5)),\n",
" \"std_repair_h\": round(float(std_h), 2), \"road_delay\": float(rng.lognormal(0, 0.15))})\n",
" df = pd.DataFrame(rows).sort_values(\"t_req\").reset_index(drop=True)\n",
" df.insert(0, \"req_id\", [f\"R{i:04d}\" for i in range(len(df))])\n",
" return df\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ถ๋ ๋ฐฐ์ ยท์ ๋น ๋ชจ์ ๊ณตํต ํจ์ ----\n",
"W0 = {\"crit\": 1.0, \"wait\": 0.3, \"short\": 0.2, \"balance\": 0.5} # ์ฐ์ ์์ยท๋ฐฐ์ ๊ธฐ๋ณธ ๊ฐ์ค์น(๋ฏธ์
9์์ ์ฌํ๋)\n",
"\n",
"\n",
"def travel_h(team, r, actual=False):\n",
" \"\"\"๋๋ก ๊ตด๊ณก 1.3๋ฐฐ, ํฌ์ฅ 35km/hยท๋นํฌ์ฅ 25km/h, ์
์ฒํ 20% ๊ฐ์. actual=True๋ฉด ์ค์ ๋๋ก ์ง์ฐ์ ๋ฐ์.\"\"\"\n",
" d = 1.3 * np.hypot(team[\"x\"] - r[\"x\"], team[\"y\"] - r[\"y\"])\n",
" v = (35.0 if r[\"road\"] == \"paved\" else 25.0) / (1.2 if r[\"weather\"] != \"clear\" else 1.0)\n",
" return d / v * (r[\"road_delay\"] if actual else 1.0)\n",
"\n",
"\n",
"def on_site_h(team, r):\n",
" \"\"\"ํ์ฅ ์ ๋น์๊ฐ: 4์ธ ๊ธฐ์ค ํ์ค์๊ฐ์ ํฌ์
์ธ์์ผ๋ก ํ์ฐ. ํ์ฅ ์๋ฆฌ ๋ถ๊ฐ๋ฉด ์ง๋จยทํ์ก ์กฐ์น 1์๊ฐ.\"\"\"\n",
" return r[\"std_repair_h\"] * 4 / team[\"crew\"] if r[\"field_ok\"] else 1.0\n",
"\n",
"\n",
"def priority(r, now, est_h, w):\n",
" return w[\"crit\"] * r[\"crit\"] + w[\"wait\"] * (now - r[\"t_req\"]) + w[\"short\"] / (1 + est_h)\n",
"\n",
"\n",
"def simulate(req, teams, policy=\"baseline\", w=None, cycle_h=1.0, est=None):\n",
" \"\"\"๋ฐฐ์ ์ฃผ๊ธฐ(cycle_h)๋ง๋ค ๋๊ธฐ ์์ฒญ์ ์ ๋ ฌํด ์ด๋์ ๋น๋ฐ์ ๋ฐฐ์ ํ๊ณ ์ถ๋ยท์ ๋นยท๋ณต๊ท๋ฅผ ๋ชจ์ํ๋ค.\n",
" baseline: ์ ์ ์์ + ๊ฑฐ๋ฆฌ ์ต๊ทผ์ ์ ๋น๋ฐ / priority: ์ฐ์ ์์ ์ ์ ์์ + ๋์ฐฉ ์์์๊ฐยท๋น์ผ ๋ถํ ๊ท ํ\"\"\"\n",
" w = w or W0\n",
" est = np.asarray(est if est is not None else req[\"std_repair_h\"].values, float)\n",
" n_days = int(req[\"day\"].max()) + 1\n",
" free = {t[\"team\"]: 0.0 for t in teams}\n",
" load = {t[\"team\"]: np.zeros(n_days + 5) for t in teams}\n",
" recs, out, pending, i, now = req.to_dict(\"records\"), [], [], 0, cycle_h\n",
" while i < len(recs) or pending:\n",
" while i < len(recs) and recs[i][\"t_req\"] <= now:\n",
" pending.append((i, recs[i])); i += 1\n",
" if policy == \"priority\":\n",
" pending.sort(key=lambda p: -priority(p[1], now, est[p[0]], w))\n",
" for k, r in pending:\n",
" cand = [t for t in teams if r[\"equip\"] in t[\"skills\"]]\n",
" day = min(int(now // 24), n_days + 4)\n",
" if policy == \"baseline\":\n",
" tm = min(cand, key=lambda t: np.hypot(t[\"x\"] - r[\"x\"], t[\"y\"] - r[\"y\"]))\n",
" else:\n",
" tm = min(cand, key=lambda t: max(free[t[\"team\"]], now) + travel_h(t, r)\n",
" + w[\"balance\"] * load[t[\"team\"]][day] / DUTY_H * 4)\n",
" backlog = max(0.0, free[tm[\"team\"]] - now)\n",
" go = max(free[tm[\"team\"]], now)\n",
" tr = travel_h(tm, r, actual=True)\n",
" work = on_site_h(tm, r)\n",
" free[tm[\"team\"]] = go + 2 * tr + work\n",
" load[tm[\"team\"]][min(int(go // 24), n_days + 4)] += 2 * tr + work\n",
" out.append({**r, \"idx\": k, \"team\": tm[\"team\"], \"dist_km\": float(np.hypot(tm[\"x\"] - r[\"x\"], tm[\"y\"] - r[\"y\"])),\n",
" \"backlog_h\": backlog, \"cycle_wait_h\": now - r[\"t_req\"], \"dispatch\": go, \"travel_h\": tr,\n",
" \"work_h\": work, \"response_h\": go + tr - r[\"t_req\"], \"downtime_h\": go + tr + work - r[\"t_req\"]})\n",
" pending, now = [], now + cycle_h\n",
" L = pd.DataFrame({k: v[:n_days] for k, v in load.items()}) / DUTY_H # ์ ๋น๋ฐ๋ณ ์ผ์ผ ๋ถํ์จ\n",
" return pd.DataFrame(out).sort_values(\"idx\").reset_index(drop=True), L\n",
"\n",
"\n",
"def metrics(res, L):\n",
" c3 = res[res[\"crit\"] == 3]\n",
" return {\"ํ๊ท ๋์ฐฉ(h)\": round(res[\"response_h\"].mean(), 2), \"ํต์ฌ์ฅ๋น ๋์ฐฉ(h)\": round(c3[\"response_h\"].mean(), 2),\n",
" \"ํต์ฌ์ฅ๋น 90%(h)\": round(c3[\"response_h\"].quantile(0.9), 2), \"ํ๊ท ๋ถํ์จ\": round(float(L.values.mean()), 3),\n",
" \"๋ถํ์จ ํธ์ฐจ\": round(float(L.mean().std()), 3), \"๊ณผ๋ถํ ์ผ์\": int((L > 1.0).values.sum()),\n",
" \"์ด ์ด๋(h)\": round(float(res[\"travel_h\"].sum() * 2), 1)}\n",
"\n",
"\n",
"teams, req = make_teams(), make_requests()\n",
"res, L = simulate(req, teams, \"baseline\")\n",
"m = metrics(res, L)\n",
"print(f\"[Baseline ๊ฒฐ๊ณผ] ์์ฒญ {len(res)}๊ฑด\")\n",
"for k, v in m.items():\n",
" print(f\" {k:10s} {v}\")\n",
"print(\"์ ๋น๋ฐ๋ณ ํ๊ท ๋ถํ์จ:\", {k: round(float(v), 2) for k, v in L.mean().items()})\n",
"print(f\"-> ๊ฐ๊น์ด ์ ๋น๋ฐ๋ง ๊ณ ๋ฅด๋ฉด ์์ฒญ์ด ๋ชฐ๋ฆฌ๋ ์ง์ญ์ ์ ๋น๋ฐ(MT{int(L.mean().argmax()) + 1})์ ๋ถํ๊ฐ ์ง์ค๋๋ค.\")\n",
"\n",
"# 1) ์๊ด๋ถ์: ์ถ๋ ์ง์ฐ(์ ์~๋์ฐฉ ์๊ฐ)๊ณผ ๊ด๋ จ๋ ํผ์ฒ (ํผ์ด์จ ์๊ด, ์ ์์์ค 0.05)\n",
"res[\"weather_bad\"] = (res[\"weather\"] != \"clear\").astype(int)\n",
"res[\"unpaved\"] = (res[\"road\"] == \"unpaved\").astype(int)\n",
"res[\"hour\"] = res[\"t_req\"] % 24\n",
"res[\"day_requests\"] = res.groupby(\"day\")[\"req_id\"].transform(\"count\")\n",
"res[\"team_load\"] = [L.loc[d, t] for d, t in zip(res[\"day\"], res[\"team\"])]\n",
"feats = [\"dist_km\", \"backlog_h\", \"team_load\", \"day_requests\", \"weather_bad\", \"unpaved\", \"std_repair_h\", \"crit\", \"hour\"]\n",
"corr = []\n",
"for f in feats:\n",
" r, p = stats.pearsonr(res[f].astype(float), res[\"response_h\"])\n",
" corr.append({\"ํผ์ฒ\": f, \"์๊ด๊ณ์\": round(r, 3), \"p-value\": f\"{p:.1e}\", \"์ ์\": p < 0.05})\n",
"corr = pd.DataFrame(corr).sort_values(\"์๊ด๊ณ์\", key=abs, ascending=False)\n",
"print(\"\\n[์ถ๋ ์ง์ฐ๊ณผ์ ์๊ด๋ถ์]\")\n",
"print(corr.to_string(index=False))\n",
"n_sig = int(corr[\"์ ์\"].sum())\n",
"print(f\"์ ์๋ฏธํ ํผ์ฒ {n_sig}๊ฐ (๋ชฉํ 5๊ฐ ์ด์: {'๋ฌ์ฑ' if n_sig >= 5 else '๋ฏธ๋ฌ'})\")\n",
"print(\"-> ์์ ์ค์๋(crit)์ ๋์ฐฉ์๊ฐ์ ์๊ด์ด 0์ ๊ฐ๊น๋ค: Baseline์ ํต์ฌ ์ฅ๋น๋ฅผ ๋จผ์ ์ฑ๊ธฐ์ง ๋ชปํ๋ค(๋ฏธ์
4์ ๋๊ธฐ).\")\n",
"\n",
"# 2) ํ๋ ํ (ABC) ๋ถ์: ์ฅ๋น ๊ฐ๋์ค๋จ ์๊ฐ์ ๊ฐ์ฅ ๋ง์ด ๋ง๋๋ (์ฅ๋น, ๊ณ ์ฅ์ ํ) ์กฐํฉ\n",
"g = res.groupby([\"equip\", \"fault\"])[\"downtime_h\"].sum().sort_values(ascending=False)\n",
"cum = g.cumsum() / g.sum()\n",
"abc = pd.cut(cum, [0, 0.8, 0.95, 1.0], labels=[\"A\", \"B\", \"C\"])\n",
"print(\"\\n[ํ๋ ํ A๋ฑ๊ธ(๋์ ๊ฐ๋์ค๋จ 80%)]\", \", \".join(f\"{e}/{f}\" for (e, f), c in abc.items() if c == \"A\"))\n",
"print(f\"A๋ฑ๊ธ์ ์ ์ฒด ์กฐํฉ {len(g)}๊ฐ ์ค {int((abc == 'A').sum())}๊ฐ({(abc == 'A').mean():.0%})\")\n",
"\n",
"# 3) EDA ์ฐจํธ 8์ข
\n",
"fig, ax = plt.subplots(2, 4, figsize=(18, 8))\n",
"a = ax.ravel()\n",
"a[0].scatter(req[\"x\"], req[\"y\"], s=8, c=req[\"crit\"], cmap=\"Reds\"); a[0].scatter([t[\"x\"] for t in teams], [t[\"y\"] for t in teams], marker=\"^\", s=120, c=\"blue\")\n",
"a[0].set_title(\"1. Requests (red) & team bases (blue)\")\n",
"req.groupby(\"day\").size().plot(ax=a[1], marker=\"o\"); a[1].set_title(\"2. Requests per day (surge = exercise)\")\n",
"a[2].hist(res[\"response_h\"], bins=30); a[2].set_title(\"3. Response time (h)\")\n",
"a[3].boxplot([L[c] for c in L.columns]); a[3].set_xticks(range(1, 7), L.columns); a[3].axhline(1.0, ls=\"--\", c=\"r\"); a[3].set_title(\"4. Daily load rate by team\")\n",
"res[\"fault\"].value_counts().plot.bar(ax=a[4]); a[4].set_title(\"5. Requests by fault type\")\n",
"res.boxplot(column=\"std_repair_h\", by=\"fault\", ax=a[5], rot=45); a[5].set_title(\"6. Std repair hours by fault\")\n",
"a[6].barh(corr[\"ํผ์ฒ\"], corr[\"์๊ด๊ณ์\"]); a[6].set_title(\"7. Correlation with response time\")\n",
"a[7].plot(range(1, len(cum) + 1), cum.values, \"o-\"); a[7].axhline(0.8, ls=\"--\", c=\"gray\"); a[7].set_title(\"8. Pareto of downtime (equip x fault)\")\n",
"plt.suptitle(\"MNT10 EDA - baseline dispatch (synthetic data)\"); plt.tight_layout(); plt.show()\n",
"print(\"\\nEDA ์ฐจํธ 8์ข
์์ฑ (๋ชฉํ 8์ข
์ด์: ๋ฌ์ฑ)\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
03 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[Baseline ๊ฒฐ๊ณผ] ์์ฒญ 356๊ฑด\n",
" ํ๊ท ๋์ฐฉ(h) 3.37\n",
" ํต์ฌ์ฅ๋น ๋์ฐฉ(h) 3.13\n",
" ํต์ฌ์ฅ๋น 90%(h) 7.54\n",
" ํ๊ท ๋ถํ์จ 0.78\n",
" ๋ถํ์จ ํธ์ฐจ 0.365\n",
" ๊ณผ๋ถํ ์ผ์ 58\n",
" ์ด ์ด๋(h) 504.7\n",
"์ ๋น๋ฐ๋ณ ํ๊ท ๋ถํ์จ: {'MT1': 0.39, 'MT2': 0.94, 'MT3': 0.39, 'MT4': 0.75, 'MT5': 1.36, 'MT6': 0.85}\n",
"-> ๊ฐ๊น์ด ์ ๋น๋ฐ๋ง ๊ณ ๋ฅด๋ฉด ์์ฒญ์ด ๋ชฐ๋ฆฌ๋ ์ง์ญ์ ์ ๋น๋ฐ(MT5)์ ๋ถํ๊ฐ ์ง์ค๋๋ค.\n",
"\n",
"[์ถ๋ ์ง์ฐ๊ณผ์ ์๊ด๋ถ์]\n",
" ํผ์ฒ ์๊ด๊ณ์ p-value ์ ์\n",
" backlog_h 0.994 0.0e+00 True\n",
" team_load 0.454 1.5e-19 True\n",
"day_requests 0.246 2.6e-06 True\n",
" unpaved 0.216 3.9e-05 True\n",
" hour 0.158 2.8e-03 True\n",
" dist_km 0.087 1.0e-01 False\n",
"std_repair_h -0.080 1.3e-01 False\n",
" crit -0.027 6.2e-01 False\n",
" weather_bad -0.004 9.4e-01 False\n",
"์ ์๋ฏธํ ํผ์ฒ 5๊ฐ (๋ชฉํ 5๊ฐ ์ด์: ๋ฌ์ฑ)\n",
"-> ์์ ์ค์๋(crit)์ ๋์ฐฉ์๊ฐ์ ์๊ด์ด 0์ ๊ฐ๊น๋ค: Baseline์ ํต์ฌ ์ฅ๋น๋ฅผ ๋จผ์ ์ฑ๊ธฐ์ง ๋ชปํ๋ค(๋ฏธ์
4์ ๋๊ธฐ).\n",
"\n",
"[ํ๋ ํ A๋ฑ๊ธ(๋์ ๊ฐ๋์ค๋จ 80%)] mobility/drivetrain, fires/hydraulic, comms/comm_module, mobility/engine, mobility/hydraulic, comms/electric, mobility/cooling, fires/drivetrain, fires/engine\n",
"A๋ฑ๊ธ์ ์ ์ฒด ์กฐํฉ 16๊ฐ ์ค 9๊ฐ(56%)\n",
"\n",
"EDA ์ฐจํธ 8์ข
์์ฑ (๋ชฉํ 8์ข
์ด์: ๋ฌ์ฑ)\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
04. ์ฅ๋น ์์ ์ค์๋, ์ ๋น ์์ ์๊ฐ, ์ด๋ ๊ฒฝ๋ก๋ฅผ ์ข
ํฉํ ์ต์ ์ถ๋ ์ฐ์ ์์ ์ถ์ฒ ์์ง ๊ณ ๋ํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์์ ์ค์๋์ ์ด๋ ๊ฒฝ๋ก๋ฅผ ์ข
ํฉํ ์ต์ ์ถ๋ ์ฐ์ ์์ ์ถ์ฒ ์์ง์ ๊ณ ๋ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ฅ๋น ์์ ์ค์๋, ์์ธก ์ ๋น ์์์๊ฐ, ์ด๋ ๊ฒฝ๋ก(๋์ฐฉ ์์์๊ฐ), ์ ๋น๋ฐ ๋ถํ๋ฅผ ์ข
ํฉํ ์ถ๋ ์ฐ์ ์์ ์ถ์ฒ ์์ง์ ๋ง๋ ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ ์ ์์ ์ ์ ์ ์๋ ์ ๋ณด๋ง์ผ๋ก ํ์ ํผ์ฒ 10๊ฐ ์ด์์ ๋ง๋ค๊ณ ์ ๋น ์์์๊ฐ ์์ธก ๋ชจ๋ธ์ ์๊ณ์ด ๊ต์ฐจ๊ฒ์ฆ(MAPE)์ผ๋ก ํ๊ฐํ๊ณ ์ฑ๋ฅ์ ๋น๊ตํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/features/mnt10_feature_engineering.py ๋ฐ ๊ธฐ์ค ๋ชจ๋ธ` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ตฐ์ ์ค๋ฌด์์ ๋ฉ๋ ๊ฐ๋ฅํ ํผ์ฒ ์ค๊ณ์ ๋
ผ๋ฆฌ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ํ์ ํผ์ฒ 10๊ฐ ์ด์ ์์ฑ, ๊ธฐ์ค ๋ชจ๋ธ MAPE 25% ์ดํ ๋ฌ์ฑ\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ํผ์ฒ ํ์ดํ๋ผ์ธ ๋ฐ ๋ฒ ์ด์ค๋ผ์ธ ๋ชจ๋ธ ์ฑ๋ฅ ์งํ ๋์ถ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ๊ต์ฐจ ๊ฒ์ฆ(Time-series Cross Validation)์ ํตํ ๊ณผ์ ํฉ ๋ฐฉ์ง\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์ฅ๋น ์์ ์ค์๋ยท์์ธก ์ ๋น ์์์๊ฐยท์ด๋ ๊ฒฝ๋ก(๋์ฐฉ ์์์๊ฐ)ยท์ ๋น๋ฐ ๋ถํ๋ฅผ ์ข
ํฉํ ์ถ๋ ์ฐ์ ์์ ์ถ์ฒ ์์ง์ ๋ง๋ ๋ค. ํ์ ํผ์ฒ 10๊ฐ ์ด์์ผ๋ก ์ ๋น ์์์๊ฐ ์์ธก ๋ชจ๋ธ์ ์๊ณ์ด ๊ต์ฐจ๊ฒ์ฆ(MAPE)์ผ๋ก ํ๊ฐํ๊ณ , ๊ฑฐ๋ฆฌ ๊ธฐ์ค Baseline ๋๋น ํต์ฌ ์ฅ๋น ๋์ฐฉ์๊ฐ๊ณผ ๋ถํ ๊ท ํ ๊ฐ์ ์ ํ์ธํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.ensemble import GradientBoostingRegressor\n",
"from sklearn.model_selection import TimeSeriesSplit\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ผ์ ์ด๋์ ๋น๋ฐยท๊ธด๊ธ ์ ๋น ์์ฒญ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์์นยท์ฅ๋นยท์ธ์ ์ ๋ณด ์์)\n",
"# 60km x 60km ๊ฐ์ ์ง์ญ์ ์ด๋์ ๋น๋ฐ 6๊ฐ์ 30์ผ๊ฐ์ ๊ธด๊ธ ์ ๋น ์์ฒญ์ ๋ง๋ ๋ค.\n",
"# ์ฅ๋น ๊ตฌ๋ถ(mobility/fires/comms/power)๊ณผ ๊ณ ์ฅ ์ ํ์ ๊ต์ก์ฉ ์ถ์ ๋ถ๋ฅ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"AREA_KM, DUTY_H = 60, 10.0 # ๊ฐ์ ์์ ์ง์ญ ํฌ๊ธฐ, ์ ๋น๋ฐ 1์ผ ๊ธฐ์ค ๊ฐ์ฉ์๊ฐ\n",
"FAULT_H = {\"engine\": 6.0, \"drivetrain\": 5.0, \"hydraulic\": 3.0, \"electric\": 2.0, \"comm_module\": 1.5, \"cooling\": 2.5}\n",
"FIELD_P = {\"engine\": 0.3, \"drivetrain\": 0.35, \"hydraulic\": 0.75, \"electric\": 0.9, \"comm_module\": 0.95, \"cooling\": 0.8}\n",
"EQUIP_F = {\"mobility\": 1.0, \"fires\": 1.2, \"comms\": 0.7, \"power\": 0.8}\n",
"FAULT_OF = {\"mobility\": [0.3, 0.25, 0.2, 0.1, 0.0, 0.15], \"fires\": [0.2, 0.15, 0.35, 0.15, 0.0, 0.15],\n",
" \"comms\": [0.0, 0.0, 0.0, 0.4, 0.5, 0.1], \"power\": [0.35, 0.0, 0.0, 0.35, 0.0, 0.3]}\n",
"CRIT_P = {\"mobility\": [0.45, 0.35, 0.2], \"fires\": [0.2, 0.35, 0.45], \"comms\": [0.3, 0.4, 0.3], \"power\": [0.6, 0.3, 0.1]}\n",
"HUBS = [(15, 20), (45, 25), (30, 50)] # ๊ฐ์ ํ๋ จ์ฅ 3๊ณณ(์์ฒญ์ด ๋ชฐ๋ฆฌ๋ ์ง์ )\n",
"\n",
"\n",
"def make_teams(seed=0):\n",
" rng = np.random.default_rng(seed)\n",
" base = [(8, 10), (30, 6), (52, 12), (10, 44), (32, 38), (54, 50)]\n",
" skills = [[\"fires\", \"mobility\"], [\"comms\", \"mobility\"], [\"fires\", \"power\"],\n",
" [\"mobility\", \"power\"], [\"comms\", \"fires\"], [\"comms\", \"mobility\", \"power\"]]\n",
" return [{\"team\": f\"MT{i + 1}\", \"x\": float(x), \"y\": float(y), \"crew\": int(rng.integers(3, 6)), \"skills\": s}\n",
" for i, ((x, y), s) in enumerate(zip(base, skills))]\n",
"\n",
"\n",
"def make_requests(n_days=30, per_day=10, seed=1, surge_days=(8, 9, 10, 11, 22, 23, 24)):\n",
" \"\"\"ํ๋ฃจ ํ๊ท per_day๊ฑด, ํ๋ จ ์ฃผ๊ฐ(surge_days)์๋ 1.6๋ฐฐ์ ๊ธด๊ธ ์ ๋น ์์ฒญ์ ๋ง๋ ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for d in range(n_days):\n",
" n = rng.poisson(per_day * (1.6 if d in surge_days else 1.0))\n",
" weather = rng.choice([\"clear\", \"rain\", \"snow\"], p=[0.65, 0.25, 0.1])\n",
" for _ in range(n):\n",
" if rng.random() < 0.7:\n",
" hx, hy = HUBS[rng.integers(0, 3)]\n",
" x, y = np.clip(rng.normal(hx, 6), 0, AREA_KM), np.clip(rng.normal(hy, 6), 0, AREA_KM)\n",
" else:\n",
" x, y = rng.uniform(0, AREA_KM, 2)\n",
" eq = str(rng.choice(list(EQUIP_F), p=[0.45, 0.25, 0.2, 0.1]))\n",
" fault = str(rng.choice(list(FAULT_H), p=FAULT_OF[eq]))\n",
" parts = bool(rng.random() < 0.75)\n",
" bad = weather != \"clear\"\n",
" std_h = FAULT_H[fault] * EQUIP_F[eq] * (1.35 if not parts else 1.0) * (1.15 if bad else 1.0) * rng.lognormal(0, 0.12)\n",
" rows.append({\"t_req\": d * 24 + float(np.clip(rng.normal(13, 4), 5, 23)), \"day\": d,\n",
" \"x\": float(x), \"y\": float(y), \"equip\": eq, \"fault\": fault,\n",
" \"crit\": int(rng.choice([1, 2, 3], p=CRIT_P[eq])), \"parts_ready\": parts,\n",
" \"weather\": str(weather), \"road\": \"unpaved\" if y > 30 and rng.random() < 0.7 else \"paved\",\n",
" \"field_ok\": bool(rng.random() < FIELD_P[fault] * (1.0 if parts else 0.5)),\n",
" \"std_repair_h\": round(float(std_h), 2), \"road_delay\": float(rng.lognormal(0, 0.15))})\n",
" df = pd.DataFrame(rows).sort_values(\"t_req\").reset_index(drop=True)\n",
" df.insert(0, \"req_id\", [f\"R{i:04d}\" for i in range(len(df))])\n",
" return df\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ถ๋ ๋ฐฐ์ ยท์ ๋น ๋ชจ์ ๊ณตํต ํจ์ ----\n",
"W0 = {\"crit\": 1.0, \"wait\": 0.3, \"short\": 0.2, \"balance\": 0.5} # ์ฐ์ ์์ยท๋ฐฐ์ ๊ธฐ๋ณธ ๊ฐ์ค์น(๋ฏธ์
9์์ ์ฌํ๋)\n",
"\n",
"\n",
"def travel_h(team, r, actual=False):\n",
" \"\"\"๋๋ก ๊ตด๊ณก 1.3๋ฐฐ, ํฌ์ฅ 35km/hยท๋นํฌ์ฅ 25km/h, ์
์ฒํ 20% ๊ฐ์. actual=True๋ฉด ์ค์ ๋๋ก ์ง์ฐ์ ๋ฐ์.\"\"\"\n",
" d = 1.3 * np.hypot(team[\"x\"] - r[\"x\"], team[\"y\"] - r[\"y\"])\n",
" v = (35.0 if r[\"road\"] == \"paved\" else 25.0) / (1.2 if r[\"weather\"] != \"clear\" else 1.0)\n",
" return d / v * (r[\"road_delay\"] if actual else 1.0)\n",
"\n",
"\n",
"def on_site_h(team, r):\n",
" \"\"\"ํ์ฅ ์ ๋น์๊ฐ: 4์ธ ๊ธฐ์ค ํ์ค์๊ฐ์ ํฌ์
์ธ์์ผ๋ก ํ์ฐ. ํ์ฅ ์๋ฆฌ ๋ถ๊ฐ๋ฉด ์ง๋จยทํ์ก ์กฐ์น 1์๊ฐ.\"\"\"\n",
" return r[\"std_repair_h\"] * 4 / team[\"crew\"] if r[\"field_ok\"] else 1.0\n",
"\n",
"\n",
"def priority(r, now, est_h, w):\n",
" return w[\"crit\"] * r[\"crit\"] + w[\"wait\"] * (now - r[\"t_req\"]) + w[\"short\"] / (1 + est_h)\n",
"\n",
"\n",
"def simulate(req, teams, policy=\"baseline\", w=None, cycle_h=1.0, est=None):\n",
" \"\"\"๋ฐฐ์ ์ฃผ๊ธฐ(cycle_h)๋ง๋ค ๋๊ธฐ ์์ฒญ์ ์ ๋ ฌํด ์ด๋์ ๋น๋ฐ์ ๋ฐฐ์ ํ๊ณ ์ถ๋ยท์ ๋นยท๋ณต๊ท๋ฅผ ๋ชจ์ํ๋ค.\n",
" baseline: ์ ์ ์์ + ๊ฑฐ๋ฆฌ ์ต๊ทผ์ ์ ๋น๋ฐ / priority: ์ฐ์ ์์ ์ ์ ์์ + ๋์ฐฉ ์์์๊ฐยท๋น์ผ ๋ถํ ๊ท ํ\"\"\"\n",
" w = w or W0\n",
" est = np.asarray(est if est is not None else req[\"std_repair_h\"].values, float)\n",
" n_days = int(req[\"day\"].max()) + 1\n",
" free = {t[\"team\"]: 0.0 for t in teams}\n",
" load = {t[\"team\"]: np.zeros(n_days + 5) for t in teams}\n",
" recs, out, pending, i, now = req.to_dict(\"records\"), [], [], 0, cycle_h\n",
" while i < len(recs) or pending:\n",
" while i < len(recs) and recs[i][\"t_req\"] <= now:\n",
" pending.append((i, recs[i])); i += 1\n",
" if policy == \"priority\":\n",
" pending.sort(key=lambda p: -priority(p[1], now, est[p[0]], w))\n",
" for k, r in pending:\n",
" cand = [t for t in teams if r[\"equip\"] in t[\"skills\"]]\n",
" day = min(int(now // 24), n_days + 4)\n",
" if policy == \"baseline\":\n",
" tm = min(cand, key=lambda t: np.hypot(t[\"x\"] - r[\"x\"], t[\"y\"] - r[\"y\"]))\n",
" else:\n",
" tm = min(cand, key=lambda t: max(free[t[\"team\"]], now) + travel_h(t, r)\n",
" + w[\"balance\"] * load[t[\"team\"]][day] / DUTY_H * 4)\n",
" backlog = max(0.0, free[tm[\"team\"]] - now)\n",
" go = max(free[tm[\"team\"]], now)\n",
" tr = travel_h(tm, r, actual=True)\n",
" work = on_site_h(tm, r)\n",
" free[tm[\"team\"]] = go + 2 * tr + work\n",
" load[tm[\"team\"]][min(int(go // 24), n_days + 4)] += 2 * tr + work\n",
" out.append({**r, \"idx\": k, \"team\": tm[\"team\"], \"dist_km\": float(np.hypot(tm[\"x\"] - r[\"x\"], tm[\"y\"] - r[\"y\"])),\n",
" \"backlog_h\": backlog, \"cycle_wait_h\": now - r[\"t_req\"], \"dispatch\": go, \"travel_h\": tr,\n",
" \"work_h\": work, \"response_h\": go + tr - r[\"t_req\"], \"downtime_h\": go + tr + work - r[\"t_req\"]})\n",
" pending, now = [], now + cycle_h\n",
" L = pd.DataFrame({k: v[:n_days] for k, v in load.items()}) / DUTY_H # ์ ๋น๋ฐ๋ณ ์ผ์ผ ๋ถํ์จ\n",
" return pd.DataFrame(out).sort_values(\"idx\").reset_index(drop=True), L\n",
"\n",
"\n",
"def metrics(res, L):\n",
" c3 = res[res[\"crit\"] == 3]\n",
" return {\"ํ๊ท ๋์ฐฉ(h)\": round(res[\"response_h\"].mean(), 2), \"ํต์ฌ์ฅ๋น ๋์ฐฉ(h)\": round(c3[\"response_h\"].mean(), 2),\n",
" \"ํต์ฌ์ฅ๋น 90%(h)\": round(c3[\"response_h\"].quantile(0.9), 2), \"ํ๊ท ๋ถํ์จ\": round(float(L.values.mean()), 3),\n",
" \"๋ถํ์จ ํธ์ฐจ\": round(float(L.mean().std()), 3), \"๊ณผ๋ถํ ์ผ์\": int((L > 1.0).values.sum()),\n",
" \"์ด ์ด๋(h)\": round(float(res[\"travel_h\"].sum() * 2), 1)}\n",
"\n",
"\n",
"def make_features(req, teams):\n",
" \"\"\"์ ์ ์์ ์ ์ ์ ์๋ ์ ๋ณด๋ง์ผ๋ก ํ์ ํผ์ฒ๋ฅผ ๋ง๋ ๋ค(๋ฏธ๋ ์ ๋ณด ๋์ค ๋ฐฉ์ง).\"\"\"\n",
" X = pd.DataFrame(index=req.index)\n",
" X[\"hour\"] = req[\"t_req\"] % 24\n",
" X[\"is_night\"] = (X[\"hour\"] >= 19).astype(int)\n",
" X[\"dow\"] = req[\"day\"] % 7\n",
" X[\"weather_bad\"] = (req[\"weather\"] != \"clear\").astype(int)\n",
" X[\"unpaved\"] = (req[\"road\"] == \"unpaved\").astype(int)\n",
" X[\"no_parts\"] = (~req[\"parts_ready\"]).astype(int)\n",
" X[\"std_table_h\"] = req[\"fault\"].map(FAULT_H) * req[\"equip\"].map(EQUIP_F) # ์ ๋น ํ์ค์๊ฐํ ๊ฐ\n",
" X[\"crit_x_noparts\"] = req[\"crit\"] * X[\"no_parts\"]\n",
" X[\"bad_x_table\"] = X[\"weather_bad\"] * X[\"std_table_h\"]\n",
" X[\"near_team_km\"] = [min(np.hypot(t[\"x\"] - x, t[\"y\"] - y) for t in teams if e in t[\"skills\"]) for x, y, e in zip(req[\"x\"], req[\"y\"], req[\"equip\"])]\n",
" X[\"hub_km\"] = [min(np.hypot(hx - x, hy - y) for hx, hy in HUBS) for x, y in zip(req[\"x\"], req[\"y\"])]\n",
" X[\"req_24h\"] = [((req[\"t_req\"] > t - 24) & (req[\"t_req\"] <= t)).sum() for t in req[\"t_req\"]]\n",
" X[\"fault_past_mean\"] = req.groupby(\"fault\")[\"std_repair_h\"].transform(lambda s: s.shift().expanding().mean()).fillna(req[\"std_repair_h\"].mean())\n",
" X[\"crit\"] = req[\"crit\"]\n",
" for e in EQUIP_F:\n",
" X[f\"eq_{e}\"] = (req[\"equip\"] == e).astype(int)\n",
" return X\n",
"\n",
"\n",
"teams = make_teams()\n",
"hist = make_requests(seed=11) # ๊ณผ๊ฑฐ 30์ผ(ํ์ต์ฉ)\n",
"req = make_requests(seed=1) # ํ๊ฐ 30์ผ\n",
"X, y = make_features(hist, teams), hist[\"std_repair_h\"]\n",
"print(f\"ํ์ ํผ์ฒ {X.shape[1]}๊ฐ ์์ฑ (๋ชฉํ 10๊ฐ ์ด์: {'๋ฌ์ฑ' if X.shape[1] >= 10 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# 1) ์๊ณ์ด ๊ต์ฐจ๊ฒ์ฆ: ๊ณผ๊ฑฐ๋ก ํ์ต -> ์ดํ ๊ธฐ๊ฐ ์์ธก (5๋ถํ )\n",
"mape = lambda a, p: float(np.mean(np.abs(a - p) / a))\n",
"rows = []\n",
"for i, (tr, te) in enumerate(TimeSeriesSplit(5).split(X), 1):\n",
" base = hist.iloc[tr].groupby(\"fault\")[\"std_repair_h\"].mean()\n",
" p0 = hist.iloc[te][\"fault\"].map(base).fillna(y.iloc[tr].mean())\n",
" gb = GradientBoostingRegressor(n_estimators=200, max_depth=3, learning_rate=0.05, random_state=0).fit(X.iloc[tr], y.iloc[tr])\n",
" rows.append({\"๋ถํ \": i, \"ํ์ต\": len(tr), \"๊ฒ์ฆ\": len(te), \"๊ธฐ์ค๋ชจ๋ธ MAPE\": round(mape(y.iloc[te], p0), 3), \"GBM MAPE\": round(mape(y.iloc[te], gb.predict(X.iloc[te])), 3)})\n",
"cv = pd.DataFrame(rows)\n",
"print(cv.to_string(index=False))\n",
"print(f\"ํ๊ท MAPE: ๊ธฐ์ค๋ชจ๋ธ(๊ณ ์ฅ์ ํ ํ๊ท ) {cv['๊ธฐ์ค๋ชจ๋ธ MAPE'].mean():.1%}, GBM {cv['GBM MAPE'].mean():.1%} (๋ชฉํ 25% ์ดํ: {'๋ฌ์ฑ' if cv['GBM MAPE'].mean() <= 0.25 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"model = GradientBoostingRegressor(n_estimators=200, max_depth=3, learning_rate=0.05, random_state=0).fit(X, y)\n",
"imp = pd.Series(model.feature_importances_, X.columns).sort_values(ascending=False)\n",
"print(\"\\nํผ์ฒ ์ค์๋ ์์ 5:\", \", \".join(f\"{k}({v:.2f})\" for k, v in imp.head(5).items()))\n",
"print(\"์ค์๋ 0.01 ๋ฏธ๋ง ํผ์ฒ(์ฐจ์ ์ถ์ ํ๋ณด):\", list(imp[imp < 0.01].index))\n",
"\n",
"# 2) ์ฐ์ ์์ ์ถ์ฒ ์์ง ์ ์ฉ: ์์ธก ์ ๋น์๊ฐ + ์ค์๋ + ๋๊ธฐ์๊ฐ + ๋์ฐฉ ์์์๊ฐ + ๋น์ผ ๋ถํ\n",
"est = model.predict(make_features(req, teams))\n",
"print(f\"ํ๊ฐ ๊ธฐ๊ฐ ์ ๋น์๊ฐ ์์ธก MAPE {mape(req['std_repair_h'], est):.1%}\")\n",
"b_res, b_L = simulate(req, teams, \"baseline\")\n",
"p_res, p_L = simulate(req, teams, \"priority\", est=est)\n",
"cmp = pd.DataFrame([{\"๋ฐฉ์\": \"Baseline(๊ฑฐ๋ฆฌ)\", **metrics(b_res, b_L)}, {\"๋ฐฉ์\": \"์ถ์ฒ ์์ง\", **metrics(p_res, p_L)}])\n",
"print(\"\\n\" + cmp.to_string(index=False))\n",
"gain = 1 - cmp.loc[1, \"ํต์ฌ์ฅ๋น ๋์ฐฉ(h)\"] / cmp.loc[0, \"ํต์ฌ์ฅ๋น ๋์ฐฉ(h)\"]\n",
"print(f\"ํต์ฌ ์ฅ๋น ํ๊ท ๋์ฐฉ์๊ฐ {gain:.1%} ๋จ์ถ, ์ ๋น๋ฐ ๋ถํ์จ ํธ์ฐจ {cmp.loc[0, '๋ถํ์จ ํธ์ฐจ']} -> {cmp.loc[1, '๋ถํ์จ ํธ์ฐจ']}\")\n",
"print(\"๋๊ฐ: ๋ถํ๋ฅผ ๋๋๋๋ผ ๋จผ ์ ๋น๋ฐ์ด ์ถ๋ํด ์ด ์ด๋์๊ฐ์ด ๋์๋ค(๋ฏธ์
9์์ ๊ฐ์ค์น๋ก ์กฐ์ ).\")\n",
"\n",
"# 3) ์ถ์ฒ ์์: ์์ฒญ์ด ๋ชฐ๋ฆฐ ์์ ์ ๋๊ธฐ ๋ชฉ๋ก\n",
"t_busy = req.groupby(\"day\").size().idxmax() * 24 + 14\n",
"q = req[(req[\"t_req\"] <= t_busy) & (req[\"t_req\"] > t_busy - 3)].copy()\n",
"q[\"์์ธก์ ๋น(h)\"] = est[q.index].round(1)\n",
"q[\"์ฐ์ ์์ ์ ์\"] = [round(priority(r, t_busy, e, W0), 2) for r, e in zip(q.to_dict(\"records\"), est[q.index])]\n",
"print(f\"\\n[์ถ์ฒ ์์] {int(t_busy // 24)}์ผ์ฐจ 14์ ๋๊ธฐ ์์ฒญ\")\n",
"print(q.sort_values(\"์ฐ์ ์์ ์ ์\", ascending=False)[[\"req_id\", \"equip\", \"fault\", \"crit\", \"์์ธก์ ๋น(h)\", \"์ฐ์ ์์ ์ ์\"]].to_string(index=False))\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(11, 4))\n",
"imp.head(10)[::-1].plot.barh(ax=ax[0]); ax[0].set_title(\"Feature importance (repair time model)\")\n",
"ax[1].boxplot([b_res[b_res.crit == 3][\"response_h\"], p_res[p_res.crit == 3][\"response_h\"]]); ax[1].set_xticks([1, 2], [\"baseline\", \"priority engine\"])\n",
"ax[1].set_ylabel(\"response time (h)\"); ax[1].set_title(\"Critical equipment response time\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
04 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"ํ์ ํผ์ฒ 18๊ฐ ์์ฑ (๋ชฉํ 10๊ฐ ์ด์: ๋ฌ์ฑ)\n",
" ๋ถํ ํ์ต ๊ฒ์ฆ ๊ธฐ์ค๋ชจ๋ธ MAPE GBM MAPE\n",
" 1 60 56 0.203 0.142\n",
" 2 116 56 0.171 0.141\n",
" 3 172 56 0.187 0.107\n",
" 4 228 56 0.263 0.130\n",
" 5 284 56 0.203 0.104\n",
"ํ๊ท MAPE: ๊ธฐ์ค๋ชจ๋ธ(๊ณ ์ฅ์ ํ ํ๊ท ) 20.5%, GBM 12.5% (๋ชฉํ 25% ์ดํ: ๋ฌ์ฑ)\n",
"\n",
"ํผ์ฒ ์ค์๋ ์์ 5: std_table_h(0.78), fault_past_mean(0.07), crit_x_noparts(0.05), bad_x_table(0.04), no_parts(0.03)\n",
"์ค์๋ 0.01 ๋ฏธ๋ง ํผ์ฒ(์ฐจ์ ์ถ์ ํ๋ณด): ['eq_fires', 'hour', 'hub_km', 'near_team_km', 'crit', 'req_24h', 'dow', 'weather_bad', 'unpaved', 'eq_comms', 'eq_mobility', 'is_night', 'eq_power']\n",
"ํ๊ฐ ๊ธฐ๊ฐ ์ ๋น์๊ฐ ์์ธก MAPE 12.1%\n",
"\n",
" ๋ฐฉ์ ํ๊ท ๋์ฐฉ(h) ํต์ฌ์ฅ๋น ๋์ฐฉ(h) ํต์ฌ์ฅ๋น 90%(h) ํ๊ท ๋ถํ์จ ๋ถํ์จ ํธ์ฐจ ๊ณผ๋ถํ ์ผ์ ์ด ์ด๋(h)\n",
"Baseline(๊ฑฐ๋ฆฌ) 3.37 3.13 7.54 0.780 0.365 58 504.7\n",
" ์ถ์ฒ ์์ง 2.09 1.92 3.25 0.903 0.143 68 744.9\n",
"ํต์ฌ ์ฅ๋น ํ๊ท ๋์ฐฉ์๊ฐ 38.7% ๋จ์ถ, ์ ๋น๋ฐ ๋ถํ์จ ํธ์ฐจ 0.365 -> 0.143\n",
"๋๊ฐ: ๋ถํ๋ฅผ ๋๋๋๋ผ ๋จผ ์ ๋น๋ฐ์ด ์ถ๋ํด ์ด ์ด๋์๊ฐ์ด ๋์๋ค(๋ฏธ์
9์์ ๊ฐ์ค์น๋ก ์กฐ์ ).\n",
"\n",
"[์ถ์ฒ ์์] 22์ผ์ฐจ 14์ ๋๊ธฐ ์์ฒญ\n",
"req_id equip fault crit ์์ธก์ ๋น(h) ์ฐ์ ์์ ์ ์\n",
" R0255 fires engine 3 10.8 3.51\n",
" R0256 fires electric 3 2.4 3.47\n",
" R0257 mobility hydraulic 3 3.1 3.43\n",
" R0252 comms electric 2 1.4 2.98\n",
" R0254 mobility electric 2 2.0 2.72\n",
" R0261 comms electric 2 1.4 2.37\n",
" R0260 mobility hydraulic 2 3.3 2.34\n",
" R0262 power cooling 2 2.1 2.31\n",
" R0253 mobility drivetrain 1 5.2 1.80\n",
" R0258 fires cooling 1 3.2 1.40\n",
" R0259 comms cooling 1 1.8 1.37\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
05. ์ถ๋ ์ง์ฐ ์ํ๋ ๋ฐ ํ์ฅ ์๋ฆฌ ๊ฐ๋ฅ์ฑ ์ ๋ขฐ๋ ์ ์ ์ฐ์ถ ๋ชจ๋ ๊ฐ๋ฐ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ถ๋ ์ง์ฐ ์ํ๋ ๋ฐ ํ์ฅ ์๋ฆฌ ๊ฐ๋ฅ์ฑ ์ ๋ขฐ๋ ์ ์ ๋ชจ๋์ ๊ฐ๋ฐํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ถ๋ ์ง์ฐ ์ํ๋์ ํ์ฅ ์๋ฆฌ ๊ฐ๋ฅ์ฑ์ ์ ์๋ก ์ฐ์ถํ๋ ๋ชจ๋์ ๊ฐ๋ฐํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ ์~๋์ฐฉ ์๊ฐ์ ์์๋ธ ํ๊ท(GBM+RandomForest)๋ก ์์ธกํด ๊ฑฐ๋ฆฌ ๊ธฐ๋ฐ ๊ธฐ์ค๋ชจ๋ธ๊ณผ Hold-out ๊ธฐ๊ฐ์์ MAEยทRMSEยทR2๋ฅผ ๋น๊ต ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/mnt10_advanced_model.py` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๋น์ ํ/๋ถ๊ท์น ์์์ ๋ํ ๋ชจ๋ธ์ ์ ์๋ ฅ ๋ฐ ์์ธก ์์ ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๊ธฐ์ค ๋ชจ๋ธ ๋๋น ์ค์ฐจ์จ(MAE/RMSE) 20% ์ด์ ๊ฐ์ , R2 Score 0.85 ์ด์\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ๋ชจ๋ธ ํ์ดํผํ๋ผ๋ฏธํฐ ํ๋ ์๋ฃ ๋ฐ ์ต์ ๋ชจ๋ธ ๊ฐ์ค์น ์ ์ฅ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: Hold-out ํ
์คํธ ๋ฐ์ดํฐ์
๋์ ์ต์ข
์ฑ๋ฅ ํ๊ฐ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์ถ๋ ์ง์ฐ ์ํ๋(์ ์~๋์ฐฉ ์์์๊ฐ)๋ฅผ ์์๋ธ ํ๊ท๋ก ์์ธกํ๊ณ , ํ์ฅ ์๋ฆฌ ๊ฐ๋ฅ์ฑ์ ํ๋ฅ (์ ๋ขฐ๋ ์ ์)๋ก ์ฐ์ถํ๋ ๋ชจ๋์ ๊ฐ๋ฐํ๋ค. Hold-out ๊ธฐ๊ฐ์์ ๊ฑฐ๋ฆฌ ๊ธฐ๋ฐ ๊ธฐ์ค๋ชจ๋ธ ๋๋น MAE/RMSE ๊ฐ์ ์จ๊ณผ R2, ์ ๋ขฐ๋ ์ ์์ ๋ณด์ ์ํ๋ฅผ ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.ensemble import GradientBoostingRegressor, RandomForestRegressor\n",
"from sklearn.linear_model import LinearRegression, LogisticRegression\n",
"from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score, roc_auc_score, brier_score_loss\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ผ์ ์ด๋์ ๋น๋ฐยท๊ธด๊ธ ์ ๋น ์์ฒญ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์์นยท์ฅ๋นยท์ธ์ ์ ๋ณด ์์)\n",
"# 60km x 60km ๊ฐ์ ์ง์ญ์ ์ด๋์ ๋น๋ฐ 6๊ฐ์ 30์ผ๊ฐ์ ๊ธด๊ธ ์ ๋น ์์ฒญ์ ๋ง๋ ๋ค.\n",
"# ์ฅ๋น ๊ตฌ๋ถ(mobility/fires/comms/power)๊ณผ ๊ณ ์ฅ ์ ํ์ ๊ต์ก์ฉ ์ถ์ ๋ถ๋ฅ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"AREA_KM, DUTY_H = 60, 10.0 # ๊ฐ์ ์์ ์ง์ญ ํฌ๊ธฐ, ์ ๋น๋ฐ 1์ผ ๊ธฐ์ค ๊ฐ์ฉ์๊ฐ\n",
"FAULT_H = {\"engine\": 6.0, \"drivetrain\": 5.0, \"hydraulic\": 3.0, \"electric\": 2.0, \"comm_module\": 1.5, \"cooling\": 2.5}\n",
"FIELD_P = {\"engine\": 0.3, \"drivetrain\": 0.35, \"hydraulic\": 0.75, \"electric\": 0.9, \"comm_module\": 0.95, \"cooling\": 0.8}\n",
"EQUIP_F = {\"mobility\": 1.0, \"fires\": 1.2, \"comms\": 0.7, \"power\": 0.8}\n",
"FAULT_OF = {\"mobility\": [0.3, 0.25, 0.2, 0.1, 0.0, 0.15], \"fires\": [0.2, 0.15, 0.35, 0.15, 0.0, 0.15],\n",
" \"comms\": [0.0, 0.0, 0.0, 0.4, 0.5, 0.1], \"power\": [0.35, 0.0, 0.0, 0.35, 0.0, 0.3]}\n",
"CRIT_P = {\"mobility\": [0.45, 0.35, 0.2], \"fires\": [0.2, 0.35, 0.45], \"comms\": [0.3, 0.4, 0.3], \"power\": [0.6, 0.3, 0.1]}\n",
"HUBS = [(15, 20), (45, 25), (30, 50)] # ๊ฐ์ ํ๋ จ์ฅ 3๊ณณ(์์ฒญ์ด ๋ชฐ๋ฆฌ๋ ์ง์ )\n",
"\n",
"\n",
"def make_teams(seed=0):\n",
" rng = np.random.default_rng(seed)\n",
" base = [(8, 10), (30, 6), (52, 12), (10, 44), (32, 38), (54, 50)]\n",
" skills = [[\"fires\", \"mobility\"], [\"comms\", \"mobility\"], [\"fires\", \"power\"],\n",
" [\"mobility\", \"power\"], [\"comms\", \"fires\"], [\"comms\", \"mobility\", \"power\"]]\n",
" return [{\"team\": f\"MT{i + 1}\", \"x\": float(x), \"y\": float(y), \"crew\": int(rng.integers(3, 6)), \"skills\": s}\n",
" for i, ((x, y), s) in enumerate(zip(base, skills))]\n",
"\n",
"\n",
"def make_requests(n_days=30, per_day=10, seed=1, surge_days=(8, 9, 10, 11, 22, 23, 24)):\n",
" \"\"\"ํ๋ฃจ ํ๊ท per_day๊ฑด, ํ๋ จ ์ฃผ๊ฐ(surge_days)์๋ 1.6๋ฐฐ์ ๊ธด๊ธ ์ ๋น ์์ฒญ์ ๋ง๋ ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for d in range(n_days):\n",
" n = rng.poisson(per_day * (1.6 if d in surge_days else 1.0))\n",
" weather = rng.choice([\"clear\", \"rain\", \"snow\"], p=[0.65, 0.25, 0.1])\n",
" for _ in range(n):\n",
" if rng.random() < 0.7:\n",
" hx, hy = HUBS[rng.integers(0, 3)]\n",
" x, y = np.clip(rng.normal(hx, 6), 0, AREA_KM), np.clip(rng.normal(hy, 6), 0, AREA_KM)\n",
" else:\n",
" x, y = rng.uniform(0, AREA_KM, 2)\n",
" eq = str(rng.choice(list(EQUIP_F), p=[0.45, 0.25, 0.2, 0.1]))\n",
" fault = str(rng.choice(list(FAULT_H), p=FAULT_OF[eq]))\n",
" parts = bool(rng.random() < 0.75)\n",
" bad = weather != \"clear\"\n",
" std_h = FAULT_H[fault] * EQUIP_F[eq] * (1.35 if not parts else 1.0) * (1.15 if bad else 1.0) * rng.lognormal(0, 0.12)\n",
" rows.append({\"t_req\": d * 24 + float(np.clip(rng.normal(13, 4), 5, 23)), \"day\": d,\n",
" \"x\": float(x), \"y\": float(y), \"equip\": eq, \"fault\": fault,\n",
" \"crit\": int(rng.choice([1, 2, 3], p=CRIT_P[eq])), \"parts_ready\": parts,\n",
" \"weather\": str(weather), \"road\": \"unpaved\" if y > 30 and rng.random() < 0.7 else \"paved\",\n",
" \"field_ok\": bool(rng.random() < FIELD_P[fault] * (1.0 if parts else 0.5)),\n",
" \"std_repair_h\": round(float(std_h), 2), \"road_delay\": float(rng.lognormal(0, 0.15))})\n",
" df = pd.DataFrame(rows).sort_values(\"t_req\").reset_index(drop=True)\n",
" df.insert(0, \"req_id\", [f\"R{i:04d}\" for i in range(len(df))])\n",
" return df\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ถ๋ ๋ฐฐ์ ยท์ ๋น ๋ชจ์ ๊ณตํต ํจ์ ----\n",
"W0 = {\"crit\": 1.0, \"wait\": 0.3, \"short\": 0.2, \"balance\": 0.5} # ์ฐ์ ์์ยท๋ฐฐ์ ๊ธฐ๋ณธ ๊ฐ์ค์น(๋ฏธ์
9์์ ์ฌํ๋)\n",
"\n",
"\n",
"def travel_h(team, r, actual=False):\n",
" \"\"\"๋๋ก ๊ตด๊ณก 1.3๋ฐฐ, ํฌ์ฅ 35km/hยท๋นํฌ์ฅ 25km/h, ์
์ฒํ 20% ๊ฐ์. actual=True๋ฉด ์ค์ ๋๋ก ์ง์ฐ์ ๋ฐ์.\"\"\"\n",
" d = 1.3 * np.hypot(team[\"x\"] - r[\"x\"], team[\"y\"] - r[\"y\"])\n",
" v = (35.0 if r[\"road\"] == \"paved\" else 25.0) / (1.2 if r[\"weather\"] != \"clear\" else 1.0)\n",
" return d / v * (r[\"road_delay\"] if actual else 1.0)\n",
"\n",
"\n",
"def on_site_h(team, r):\n",
" \"\"\"ํ์ฅ ์ ๋น์๊ฐ: 4์ธ ๊ธฐ์ค ํ์ค์๊ฐ์ ํฌ์
์ธ์์ผ๋ก ํ์ฐ. ํ์ฅ ์๋ฆฌ ๋ถ๊ฐ๋ฉด ์ง๋จยทํ์ก ์กฐ์น 1์๊ฐ.\"\"\"\n",
" return r[\"std_repair_h\"] * 4 / team[\"crew\"] if r[\"field_ok\"] else 1.0\n",
"\n",
"\n",
"def priority(r, now, est_h, w):\n",
" return w[\"crit\"] * r[\"crit\"] + w[\"wait\"] * (now - r[\"t_req\"]) + w[\"short\"] / (1 + est_h)\n",
"\n",
"\n",
"def simulate(req, teams, policy=\"baseline\", w=None, cycle_h=1.0, est=None):\n",
" \"\"\"๋ฐฐ์ ์ฃผ๊ธฐ(cycle_h)๋ง๋ค ๋๊ธฐ ์์ฒญ์ ์ ๋ ฌํด ์ด๋์ ๋น๋ฐ์ ๋ฐฐ์ ํ๊ณ ์ถ๋ยท์ ๋นยท๋ณต๊ท๋ฅผ ๋ชจ์ํ๋ค.\n",
" baseline: ์ ์ ์์ + ๊ฑฐ๋ฆฌ ์ต๊ทผ์ ์ ๋น๋ฐ / priority: ์ฐ์ ์์ ์ ์ ์์ + ๋์ฐฉ ์์์๊ฐยท๋น์ผ ๋ถํ ๊ท ํ\"\"\"\n",
" w = w or W0\n",
" est = np.asarray(est if est is not None else req[\"std_repair_h\"].values, float)\n",
" n_days = int(req[\"day\"].max()) + 1\n",
" free = {t[\"team\"]: 0.0 for t in teams}\n",
" load = {t[\"team\"]: np.zeros(n_days + 5) for t in teams}\n",
" recs, out, pending, i, now = req.to_dict(\"records\"), [], [], 0, cycle_h\n",
" while i < len(recs) or pending:\n",
" while i < len(recs) and recs[i][\"t_req\"] <= now:\n",
" pending.append((i, recs[i])); i += 1\n",
" if policy == \"priority\":\n",
" pending.sort(key=lambda p: -priority(p[1], now, est[p[0]], w))\n",
" for k, r in pending:\n",
" cand = [t for t in teams if r[\"equip\"] in t[\"skills\"]]\n",
" day = min(int(now // 24), n_days + 4)\n",
" if policy == \"baseline\":\n",
" tm = min(cand, key=lambda t: np.hypot(t[\"x\"] - r[\"x\"], t[\"y\"] - r[\"y\"]))\n",
" else:\n",
" tm = min(cand, key=lambda t: max(free[t[\"team\"]], now) + travel_h(t, r)\n",
" + w[\"balance\"] * load[t[\"team\"]][day] / DUTY_H * 4)\n",
" backlog = max(0.0, free[tm[\"team\"]] - now)\n",
" go = max(free[tm[\"team\"]], now)\n",
" tr = travel_h(tm, r, actual=True)\n",
" work = on_site_h(tm, r)\n",
" free[tm[\"team\"]] = go + 2 * tr + work\n",
" load[tm[\"team\"]][min(int(go // 24), n_days + 4)] += 2 * tr + work\n",
" out.append({**r, \"idx\": k, \"team\": tm[\"team\"], \"dist_km\": float(np.hypot(tm[\"x\"] - r[\"x\"], tm[\"y\"] - r[\"y\"])),\n",
" \"backlog_h\": backlog, \"cycle_wait_h\": now - r[\"t_req\"], \"dispatch\": go, \"travel_h\": tr,\n",
" \"work_h\": work, \"response_h\": go + tr - r[\"t_req\"], \"downtime_h\": go + tr + work - r[\"t_req\"]})\n",
" pending, now = [], now + cycle_h\n",
" L = pd.DataFrame({k: v[:n_days] for k, v in load.items()}) / DUTY_H # ์ ๋น๋ฐ๋ณ ์ผ์ผ ๋ถํ์จ\n",
" return pd.DataFrame(out).sort_values(\"idx\").reset_index(drop=True), L\n",
"\n",
"\n",
"def metrics(res, L):\n",
" c3 = res[res[\"crit\"] == 3]\n",
" return {\"ํ๊ท ๋์ฐฉ(h)\": round(res[\"response_h\"].mean(), 2), \"ํต์ฌ์ฅ๋น ๋์ฐฉ(h)\": round(c3[\"response_h\"].mean(), 2),\n",
" \"ํต์ฌ์ฅ๋น 90%(h)\": round(c3[\"response_h\"].quantile(0.9), 2), \"ํ๊ท ๋ถํ์จ\": round(float(L.values.mean()), 3),\n",
" \"๋ถํ์จ ํธ์ฐจ\": round(float(L.mean().std()), 3), \"๊ณผ๋ถํ ์ผ์\": int((L > 1.0).values.sum()),\n",
" \"์ด ์ด๋(h)\": round(float(res[\"travel_h\"].sum() * 2), 1)}\n",
"\n",
"\n",
"def feat(res):\n",
" \"\"\"๋ฐฐ์ ์์ ์ ์ ์ ์๋ ์ ๋ณด: ๋ฐฐ์ ์ ๋น๋ฐ๊น์ง ๊ฑฐ๋ฆฌ, ์ ๋น๋ฐ ์์ฌ ์์
(๋ฐฑ๋ก๊ทธ), ๋ฐฐ์ ์ฃผ๊ธฐ ๋๊ธฐ, ๊ธฐ์ยท๋๋กยท์ค์๋.\"\"\"\n",
" X = pd.DataFrame({\"dist_km\": res[\"dist_km\"], \"backlog_h\": res[\"backlog_h\"], \"cycle_wait_h\": res[\"cycle_wait_h\"],\n",
" \"weather_bad\": (res[\"weather\"] != \"clear\").astype(int), \"unpaved\": (res[\"road\"] == \"unpaved\").astype(int),\n",
" \"crit\": res[\"crit\"], \"hour\": res[\"t_req\"] % 24})\n",
" X[\"dist_x_road\"] = X[\"dist_km\"] * (1 + 0.4 * X[\"unpaved\"]) * (1 + 0.2 * X[\"weather_bad\"])\n",
" return X\n",
"\n",
"\n",
"def feat_field(df):\n",
" X = pd.get_dummies(df[[\"fault\", \"equip\"]]).astype(int)\n",
" X[\"parts_ready\"] = df[\"parts_ready\"].astype(int)\n",
" X[\"weather_bad\"] = (df[\"weather\"] != \"clear\").astype(int)\n",
" return X\n",
"\n",
"\n",
"teams = make_teams()\n",
"train = pd.concat([simulate(make_requests(seed=s), teams, \"priority\")[0] for s in (11, 21)], ignore_index=True)\n",
"test = simulate(make_requests(seed=1), teams, \"priority\")[0] # Hold-out: ํ์ต์ ์ฐ์ง ์์ 30์ผ\n",
"Xtr, Xte, ytr, yte = feat(train), feat(test), train[\"response_h\"], test[\"response_h\"]\n",
"\n",
"# 1) ์ถ๋ ์ง์ฐ ์์ธก: ๊ธฐ์ค๋ชจ๋ธ(๊ฑฐ๋ฆฌ๋ง) vs ์์๋ธ(GBM + RandomForest ํ๊ท )\n",
"base = LinearRegression().fit(Xtr[[\"dist_km\"]], ytr)\n",
"gbm = GradientBoostingRegressor(n_estimators=300, max_depth=3, learning_rate=0.05, random_state=0).fit(Xtr, ytr)\n",
"rf = RandomForestRegressor(n_estimators=200, min_samples_leaf=3, random_state=0).fit(Xtr, ytr)\n",
"preds = {\"๊ธฐ์ค๋ชจ๋ธ(๊ฑฐ๋ฆฌ ์ ํ)\": base.predict(Xte[[\"dist_km\"]]), \"GBM\": gbm.predict(Xte), \"RandomForest\": rf.predict(Xte)}\n",
"preds[\"์์๋ธ(GBM+RF)\"] = (preds[\"GBM\"] + preds[\"RandomForest\"]) / 2\n",
"tab = pd.DataFrame([{\"๋ชจ๋ธ\": k, \"MAE(h)\": mean_absolute_error(yte, p), \"RMSE(h)\": mean_squared_error(yte, p) ** 0.5, \"R2\": r2_score(yte, p)} for k, p in preds.items()]).round(3)\n",
"print(f\"ํ์ต {len(train)}๊ฑด / Hold-out {len(test)}๊ฑด\")\n",
"print(tab.to_string(index=False))\n",
"b, e = tab.iloc[0], tab.iloc[-1]\n",
"imp_mae, imp_rmse = 1 - e[\"MAE(h)\"] / b[\"MAE(h)\"], 1 - e[\"RMSE(h)\"] / b[\"RMSE(h)\"]\n",
"print(f\"๊ธฐ์ค๋ชจ๋ธ ๋๋น ๊ฐ์ : MAE {imp_mae:.1%}, RMSE {imp_rmse:.1%} (๋ชฉํ 20% ์ด์: {'๋ฌ์ฑ' if min(imp_mae, imp_rmse) >= 0.2 else '๋ฏธ๋ฌ'}), \"\n",
" f\"R2 {e['R2']:.3f} (๋ชฉํ 0.85 ์ด์: {'๋ฌ์ฑ' if e['R2'] >= 0.85 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# 2) ์ถ๋ ์ง์ฐ ์ํ๋ ๋ฑ๊ธ(์
๋ฌด ์๊ณ๊ฐ: ํต์ฌ ์ฅ๋น 2์๊ฐ, ๊ทธ ์ธ 4์๊ฐ ์ด๋ด ๋์ฐฉ)\n",
"limit = np.where(test[\"crit\"] == 3, 2.0, 4.0)\n",
"risk = np.where(preds[\"์์๋ธ(GBM+RF)\"] > limit, \"๋์\", np.where(preds[\"์์๋ธ(GBM+RF)\"] > limit * 0.7, \"์ฃผ์\", \"๋ฎ์\"))\n",
"actual_late = yte.values > limit\n",
"print(\"\\n[์ง์ฐ ์ํ๋ ๋ฑ๊ธ๋ณ ์ค์ ์ง์ฐ ๋น์จ]\")\n",
"print(pd.DataFrame({\"๋ฑ๊ธ\": risk, \"์ค์ ์ง์ฐ\": actual_late}).groupby(\"๋ฑ๊ธ\")[\"์ค์ ์ง์ฐ\"].agg([\"count\", \"mean\"]).rename(columns={\"count\": \"๊ฑด์\", \"mean\": \"์ค์ ์ง์ฐ ๋น์จ\"}).round(2).to_string())\n",
"\n",
"# 3) ํ์ฅ ์๋ฆฌ ๊ฐ๋ฅ์ฑ ์ ๋ขฐ๋ ์ ์(๋ก์ง์คํฑ ํ๊ท ํ๋ฅ )\n",
"all_req = [make_requests(seed=s) for s in (11, 21)]\n",
"ftr = pd.concat(all_req, ignore_index=True); fte = make_requests(seed=1)\n",
"Ftr = feat_field(ftr); Fte = feat_field(fte).reindex(columns=Ftr.columns, fill_value=0)\n",
"clf = LogisticRegression(max_iter=1000).fit(Ftr, ftr[\"field_ok\"])\n",
"prob = clf.predict_proba(Fte)[:, 1]\n",
"print(f\"\\nํ์ฅ ์๋ฆฌ ๊ฐ๋ฅ์ฑ: AUC {roc_auc_score(fte['field_ok'], prob):.3f}, Brier {brier_score_loss(fte['field_ok'], prob):.3f}\")\n",
"bins = pd.cut(prob, [0, 0.4, 0.7, 1.0], labels=[\"ํ์ก ์ค๋น(<0.4)\", \"ํ๋จ ๋ณด๋ฅ(0.4~0.7)\", \"ํ์ฅ์๋ฆฌ ์ ๋ ฅ(>=0.7)\"])\n",
"cal = pd.DataFrame({\"๊ตฌ๊ฐ\": bins, \"์์ธก\": prob, \"์ค์ \": fte[\"field_ok\"]}).groupby(\"๊ตฌ๊ฐ\", observed=True).agg(๊ฑด์=(\"์์ธก\", \"size\"), ํ๊ท ์ ์=(\"์์ธก\", \"mean\"), ์ค์ ๋น์จ=(\"์ค์ \", \"mean\")).round(2)\n",
"print(cal.to_string())\n",
"print(\"์ ์๊ฐ ๋์ ๊ตฌ๊ฐ์ผ์๋ก ์ค์ ํ์ฅ ์๋ฆฌ ๋น์จ์ด ๋์ ์ ๋ขฐ๋ ์ ์๋ก ์ธ ์ ์๋ค. ์ ์๋ ์ถ๋ ์ค๋น(๋ถ์ยทํ์ก ์๋จ) ์ฐธ๊ณ ์ฉ์ด๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(11, 4))\n",
"ax[0].scatter(yte, preds[\"์์๋ธ(GBM+RF)\"], s=10); ax[0].plot([0, yte.max()], [0, yte.max()], \"r--\")\n",
"ax[0].set_xlabel(\"actual response (h)\"); ax[0].set_ylabel(\"predicted (h)\"); ax[0].set_title(f\"Delay model hold-out (R2={e['R2']:.2f})\")\n",
"ax[1].plot(cal[\"ํ๊ท ์ ์\"], cal[\"์ค์ ๋น์จ\"], \"o-\"); ax[1].plot([0, 1], [0, 1], \"k--\"); ax[1].set_xlabel(\"mean confidence score\")\n",
"ax[1].set_ylabel(\"actual field-repair rate\"); ax[1].set_title(\"Field-repair confidence calibration\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
05 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"ํ์ต 724๊ฑด / Hold-out 356๊ฑด\n",
" ๋ชจ๋ธ MAE(h) RMSE(h) R2\n",
" ๊ธฐ์ค๋ชจ๋ธ(๊ฑฐ๋ฆฌ ์ ํ) 1.025 1.442 0.192\n",
" GBM 0.152 0.229 0.980\n",
"RandomForest 0.184 0.294 0.966\n",
" ์์๋ธ(GBM+RF) 0.165 0.254 0.975\n",
"๊ธฐ์ค๋ชจ๋ธ ๋๋น ๊ฐ์ : MAE 83.9%, RMSE 82.4% (๋ชฉํ 20% ์ด์: ๋ฌ์ฑ), R2 0.975 (๋ชฉํ 0.85 ์ด์: ๋ฌ์ฑ)\n",
"\n",
"[์ง์ฐ ์ํ๋ ๋ฑ๊ธ๋ณ ์ค์ ์ง์ฐ ๋น์จ]\n",
" ๊ฑด์ ์ค์ ์ง์ฐ ๋น์จ\n",
"๋ฑ๊ธ \n",
"๋ฎ์ 248 0.00\n",
"๋์ 61 0.93\n",
"์ฃผ์ 47 0.04\n",
"\n",
"ํ์ฅ ์๋ฆฌ ๊ฐ๋ฅ์ฑ: AUC 0.762, Brier 0.185\n",
" ๊ฑด์ ํ๊ท ์ ์ ์ค์ ๋น์จ\n",
"๊ตฌ๊ฐ \n",
"ํ์ก ์ค๋น(<0.4) 143 0.30 0.34\n",
"ํ๋จ ๋ณด๋ฅ(0.4~0.7) 70 0.59 0.63\n",
"ํ์ฅ์๋ฆฌ ์ ๋ ฅ(>=0.7) 143 0.80 0.83\n",
"์ ์๊ฐ ๋์ ๊ตฌ๊ฐ์ผ์๋ก ์ค์ ํ์ฅ ์๋ฆฌ ๋น์จ์ด ๋์ ์ ๋ขฐ๋ ์ ์๋ก ์ธ ์ ์๋ค. ์ ์๋ ์ถ๋ ์ค๋น(๋ถ์ยทํ์ก ์๋จ) ์ฐธ๊ณ ์ฉ์ด๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
06. ์ผ์ ์ ๋น ์์ฒญ ํํฉ ๋ฐ ์ด๋์ ๋น๋ฐ ๊ด์ ๋์๋ณด๋ UI ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ผ์ ์ ๋น ์์ฒญ ํํฉ ๋ฐ ์ด๋์ ๋น๋ฐ ๊ด์ ๋์๋ณด๋ UI๋ฅผ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ ๋น ์์ฒญ ํํฉ๊ณผ ์ด๋์ ๋น๋ฐ ์ํ๋ฅผ ๋ณด์ฌ ์ฃผ๋ ๊ด์ ๋์๋ณด๋๋ฅผ ๋ง๋ค๊ณ , ์์ธก๋ง๋ค ๊ทผ๊ฑฐ๋ฅผ ์ค๋ช
ํ๋ ์ค๋ช
๊ฐ๋ฅ์ฑ(XAI) ๊ธฐ๋ฅ์ ๋ถ์ธ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์์ฒญยท์ ๋น๋ฐ ์ง๋, ์ ๋น๋ฐ ๋ถํ์จ, ์ค์๋๋ณ ๋๊ธฐ ์์ฒญ, ์ผ๋ณยท์ค์๋๋ณ ๋์ฐฉ์๊ฐ, ์์ธก ์ํฐํด ๊ทธ๋ฆผ์ผ๋ก ๊ด์ ํ๋ฉด์ ๊ตฌ์ฑ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/explainability/mnt10_xai_explainer.py ๋ฐ XAI ๋ฆฌํฌํธ` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ตฐ์ ์ฅ๊ต/๊ตฐ๋ฌด์์ด ์ ๋ขฐํ๊ณ ์กฐ์นํ ์ ์๋ ์ค๋ช
์ ๋ช
ํ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์ฃผ์ ์์ธก ๊ฑด๋น ๊ธฐ์ฌ๋ ์์ 3๊ฐ ์ธ์ ์๋ ์ฐ์ถ๋ฅ 100%\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: SHAP summary plot ๋ฐ waterfall plot ์์ฑ ํ์ดํ๋ผ์ธ ์์ฑ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ์ค๋ฌด์ ๋์ ์ค๋ช
๊ทผ๊ฑฐ ์ ์ฉ์ฑ ํ๊ฐ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์ผ์ ์ ๋น ์์ฒญ ํํฉ๊ณผ ์ด๋์ ๋น๋ฐ ์ํ(์์นยท๋ถํ์จยท๋๊ธฐ ์์ฒญยท๋์ฐฉ์๊ฐ ์ถ์ด)๋ฅผ ํ ํ๋ฉด์ ๋ณด์ฌ ์ฃผ๋ ๊ด์ ๋์๋ณด๋๋ฅผ ๊ทธ๋ฆฌ๊ณ , ์ ๋น ์์์๊ฐ ์์ธก๋ง๋ค ๊ธฐ์ฌ๋ ์์ 3๊ฐ ์ธ์๋ฅผ ์๋ ์ฐ์ถํด ์ํฐํด ๊ทธ๋ฆผ๊ณผ ์์ฐ์ด ์ค๋ช
์ผ๋ก ์ ๊ณตํ๋ ์ค๋ช
๊ฐ๋ฅ์ฑ(XAI) ๋ชจ๋์ ๊ตฌํํ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.ensemble import GradientBoostingRegressor\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ผ์ ์ด๋์ ๋น๋ฐยท๊ธด๊ธ ์ ๋น ์์ฒญ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์์นยท์ฅ๋นยท์ธ์ ์ ๋ณด ์์)\n",
"# 60km x 60km ๊ฐ์ ์ง์ญ์ ์ด๋์ ๋น๋ฐ 6๊ฐ์ 30์ผ๊ฐ์ ๊ธด๊ธ ์ ๋น ์์ฒญ์ ๋ง๋ ๋ค.\n",
"# ์ฅ๋น ๊ตฌ๋ถ(mobility/fires/comms/power)๊ณผ ๊ณ ์ฅ ์ ํ์ ๊ต์ก์ฉ ์ถ์ ๋ถ๋ฅ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"AREA_KM, DUTY_H = 60, 10.0 # ๊ฐ์ ์์ ์ง์ญ ํฌ๊ธฐ, ์ ๋น๋ฐ 1์ผ ๊ธฐ์ค ๊ฐ์ฉ์๊ฐ\n",
"FAULT_H = {\"engine\": 6.0, \"drivetrain\": 5.0, \"hydraulic\": 3.0, \"electric\": 2.0, \"comm_module\": 1.5, \"cooling\": 2.5}\n",
"FIELD_P = {\"engine\": 0.3, \"drivetrain\": 0.35, \"hydraulic\": 0.75, \"electric\": 0.9, \"comm_module\": 0.95, \"cooling\": 0.8}\n",
"EQUIP_F = {\"mobility\": 1.0, \"fires\": 1.2, \"comms\": 0.7, \"power\": 0.8}\n",
"FAULT_OF = {\"mobility\": [0.3, 0.25, 0.2, 0.1, 0.0, 0.15], \"fires\": [0.2, 0.15, 0.35, 0.15, 0.0, 0.15],\n",
" \"comms\": [0.0, 0.0, 0.0, 0.4, 0.5, 0.1], \"power\": [0.35, 0.0, 0.0, 0.35, 0.0, 0.3]}\n",
"CRIT_P = {\"mobility\": [0.45, 0.35, 0.2], \"fires\": [0.2, 0.35, 0.45], \"comms\": [0.3, 0.4, 0.3], \"power\": [0.6, 0.3, 0.1]}\n",
"HUBS = [(15, 20), (45, 25), (30, 50)] # ๊ฐ์ ํ๋ จ์ฅ 3๊ณณ(์์ฒญ์ด ๋ชฐ๋ฆฌ๋ ์ง์ )\n",
"\n",
"\n",
"def make_teams(seed=0):\n",
" rng = np.random.default_rng(seed)\n",
" base = [(8, 10), (30, 6), (52, 12), (10, 44), (32, 38), (54, 50)]\n",
" skills = [[\"fires\", \"mobility\"], [\"comms\", \"mobility\"], [\"fires\", \"power\"],\n",
" [\"mobility\", \"power\"], [\"comms\", \"fires\"], [\"comms\", \"mobility\", \"power\"]]\n",
" return [{\"team\": f\"MT{i + 1}\", \"x\": float(x), \"y\": float(y), \"crew\": int(rng.integers(3, 6)), \"skills\": s}\n",
" for i, ((x, y), s) in enumerate(zip(base, skills))]\n",
"\n",
"\n",
"def make_requests(n_days=30, per_day=10, seed=1, surge_days=(8, 9, 10, 11, 22, 23, 24)):\n",
" \"\"\"ํ๋ฃจ ํ๊ท per_day๊ฑด, ํ๋ จ ์ฃผ๊ฐ(surge_days)์๋ 1.6๋ฐฐ์ ๊ธด๊ธ ์ ๋น ์์ฒญ์ ๋ง๋ ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for d in range(n_days):\n",
" n = rng.poisson(per_day * (1.6 if d in surge_days else 1.0))\n",
" weather = rng.choice([\"clear\", \"rain\", \"snow\"], p=[0.65, 0.25, 0.1])\n",
" for _ in range(n):\n",
" if rng.random() < 0.7:\n",
" hx, hy = HUBS[rng.integers(0, 3)]\n",
" x, y = np.clip(rng.normal(hx, 6), 0, AREA_KM), np.clip(rng.normal(hy, 6), 0, AREA_KM)\n",
" else:\n",
" x, y = rng.uniform(0, AREA_KM, 2)\n",
" eq = str(rng.choice(list(EQUIP_F), p=[0.45, 0.25, 0.2, 0.1]))\n",
" fault = str(rng.choice(list(FAULT_H), p=FAULT_OF[eq]))\n",
" parts = bool(rng.random() < 0.75)\n",
" bad = weather != \"clear\"\n",
" std_h = FAULT_H[fault] * EQUIP_F[eq] * (1.35 if not parts else 1.0) * (1.15 if bad else 1.0) * rng.lognormal(0, 0.12)\n",
" rows.append({\"t_req\": d * 24 + float(np.clip(rng.normal(13, 4), 5, 23)), \"day\": d,\n",
" \"x\": float(x), \"y\": float(y), \"equip\": eq, \"fault\": fault,\n",
" \"crit\": int(rng.choice([1, 2, 3], p=CRIT_P[eq])), \"parts_ready\": parts,\n",
" \"weather\": str(weather), \"road\": \"unpaved\" if y > 30 and rng.random() < 0.7 else \"paved\",\n",
" \"field_ok\": bool(rng.random() < FIELD_P[fault] * (1.0 if parts else 0.5)),\n",
" \"std_repair_h\": round(float(std_h), 2), \"road_delay\": float(rng.lognormal(0, 0.15))})\n",
" df = pd.DataFrame(rows).sort_values(\"t_req\").reset_index(drop=True)\n",
" df.insert(0, \"req_id\", [f\"R{i:04d}\" for i in range(len(df))])\n",
" return df\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ถ๋ ๋ฐฐ์ ยท์ ๋น ๋ชจ์ ๊ณตํต ํจ์ ----\n",
"W0 = {\"crit\": 1.0, \"wait\": 0.3, \"short\": 0.2, \"balance\": 0.5} # ์ฐ์ ์์ยท๋ฐฐ์ ๊ธฐ๋ณธ ๊ฐ์ค์น(๋ฏธ์
9์์ ์ฌํ๋)\n",
"\n",
"\n",
"def travel_h(team, r, actual=False):\n",
" \"\"\"๋๋ก ๊ตด๊ณก 1.3๋ฐฐ, ํฌ์ฅ 35km/hยท๋นํฌ์ฅ 25km/h, ์
์ฒํ 20% ๊ฐ์. actual=True๋ฉด ์ค์ ๋๋ก ์ง์ฐ์ ๋ฐ์.\"\"\"\n",
" d = 1.3 * np.hypot(team[\"x\"] - r[\"x\"], team[\"y\"] - r[\"y\"])\n",
" v = (35.0 if r[\"road\"] == \"paved\" else 25.0) / (1.2 if r[\"weather\"] != \"clear\" else 1.0)\n",
" return d / v * (r[\"road_delay\"] if actual else 1.0)\n",
"\n",
"\n",
"def on_site_h(team, r):\n",
" \"\"\"ํ์ฅ ์ ๋น์๊ฐ: 4์ธ ๊ธฐ์ค ํ์ค์๊ฐ์ ํฌ์
์ธ์์ผ๋ก ํ์ฐ. ํ์ฅ ์๋ฆฌ ๋ถ๊ฐ๋ฉด ์ง๋จยทํ์ก ์กฐ์น 1์๊ฐ.\"\"\"\n",
" return r[\"std_repair_h\"] * 4 / team[\"crew\"] if r[\"field_ok\"] else 1.0\n",
"\n",
"\n",
"def priority(r, now, est_h, w):\n",
" return w[\"crit\"] * r[\"crit\"] + w[\"wait\"] * (now - r[\"t_req\"]) + w[\"short\"] / (1 + est_h)\n",
"\n",
"\n",
"def simulate(req, teams, policy=\"baseline\", w=None, cycle_h=1.0, est=None):\n",
" \"\"\"๋ฐฐ์ ์ฃผ๊ธฐ(cycle_h)๋ง๋ค ๋๊ธฐ ์์ฒญ์ ์ ๋ ฌํด ์ด๋์ ๋น๋ฐ์ ๋ฐฐ์ ํ๊ณ ์ถ๋ยท์ ๋นยท๋ณต๊ท๋ฅผ ๋ชจ์ํ๋ค.\n",
" baseline: ์ ์ ์์ + ๊ฑฐ๋ฆฌ ์ต๊ทผ์ ์ ๋น๋ฐ / priority: ์ฐ์ ์์ ์ ์ ์์ + ๋์ฐฉ ์์์๊ฐยท๋น์ผ ๋ถํ ๊ท ํ\"\"\"\n",
" w = w or W0\n",
" est = np.asarray(est if est is not None else req[\"std_repair_h\"].values, float)\n",
" n_days = int(req[\"day\"].max()) + 1\n",
" free = {t[\"team\"]: 0.0 for t in teams}\n",
" load = {t[\"team\"]: np.zeros(n_days + 5) for t in teams}\n",
" recs, out, pending, i, now = req.to_dict(\"records\"), [], [], 0, cycle_h\n",
" while i < len(recs) or pending:\n",
" while i < len(recs) and recs[i][\"t_req\"] <= now:\n",
" pending.append((i, recs[i])); i += 1\n",
" if policy == \"priority\":\n",
" pending.sort(key=lambda p: -priority(p[1], now, est[p[0]], w))\n",
" for k, r in pending:\n",
" cand = [t for t in teams if r[\"equip\"] in t[\"skills\"]]\n",
" day = min(int(now // 24), n_days + 4)\n",
" if policy == \"baseline\":\n",
" tm = min(cand, key=lambda t: np.hypot(t[\"x\"] - r[\"x\"], t[\"y\"] - r[\"y\"]))\n",
" else:\n",
" tm = min(cand, key=lambda t: max(free[t[\"team\"]], now) + travel_h(t, r)\n",
" + w[\"balance\"] * load[t[\"team\"]][day] / DUTY_H * 4)\n",
" backlog = max(0.0, free[tm[\"team\"]] - now)\n",
" go = max(free[tm[\"team\"]], now)\n",
" tr = travel_h(tm, r, actual=True)\n",
" work = on_site_h(tm, r)\n",
" free[tm[\"team\"]] = go + 2 * tr + work\n",
" load[tm[\"team\"]][min(int(go // 24), n_days + 4)] += 2 * tr + work\n",
" out.append({**r, \"idx\": k, \"team\": tm[\"team\"], \"dist_km\": float(np.hypot(tm[\"x\"] - r[\"x\"], tm[\"y\"] - r[\"y\"])),\n",
" \"backlog_h\": backlog, \"cycle_wait_h\": now - r[\"t_req\"], \"dispatch\": go, \"travel_h\": tr,\n",
" \"work_h\": work, \"response_h\": go + tr - r[\"t_req\"], \"downtime_h\": go + tr + work - r[\"t_req\"]})\n",
" pending, now = [], now + cycle_h\n",
" L = pd.DataFrame({k: v[:n_days] for k, v in load.items()}) / DUTY_H # ์ ๋น๋ฐ๋ณ ์ผ์ผ ๋ถํ์จ\n",
" return pd.DataFrame(out).sort_values(\"idx\").reset_index(drop=True), L\n",
"\n",
"\n",
"def metrics(res, L):\n",
" c3 = res[res[\"crit\"] == 3]\n",
" return {\"ํ๊ท ๋์ฐฉ(h)\": round(res[\"response_h\"].mean(), 2), \"ํต์ฌ์ฅ๋น ๋์ฐฉ(h)\": round(c3[\"response_h\"].mean(), 2),\n",
" \"ํต์ฌ์ฅ๋น 90%(h)\": round(c3[\"response_h\"].quantile(0.9), 2), \"ํ๊ท ๋ถํ์จ\": round(float(L.values.mean()), 3),\n",
" \"๋ถํ์จ ํธ์ฐจ\": round(float(L.mean().std()), 3), \"๊ณผ๋ถํ ์ผ์\": int((L > 1.0).values.sum()),\n",
" \"์ด ์ด๋(h)\": round(float(res[\"travel_h\"].sum() * 2), 1)}\n",
"\n",
"# ---- ์ ์ ์์ ํ์ ํผ์ฒ(๋ฏธ์
4์ ๋์ผ) ----\n",
"def make_features(req, teams):\n",
" \"\"\"์ ์ ์์ ์ ์ ์ ์๋ ์ ๋ณด๋ง์ผ๋ก ํ์ ํผ์ฒ๋ฅผ ๋ง๋ ๋ค(๋ฏธ๋ ์ ๋ณด ๋์ค ๋ฐฉ์ง).\"\"\"\n",
" X = pd.DataFrame(index=req.index)\n",
" X[\"hour\"] = req[\"t_req\"] % 24\n",
" X[\"is_night\"] = (X[\"hour\"] >= 19).astype(int)\n",
" X[\"dow\"] = req[\"day\"] % 7\n",
" X[\"weather_bad\"] = (req[\"weather\"] != \"clear\").astype(int)\n",
" X[\"unpaved\"] = (req[\"road\"] == \"unpaved\").astype(int)\n",
" X[\"no_parts\"] = (~req[\"parts_ready\"]).astype(int)\n",
" X[\"std_table_h\"] = req[\"fault\"].map(FAULT_H) * req[\"equip\"].map(EQUIP_F) # ์ ๋น ํ์ค์๊ฐํ ๊ฐ\n",
" X[\"crit_x_noparts\"] = req[\"crit\"] * X[\"no_parts\"]\n",
" X[\"bad_x_table\"] = X[\"weather_bad\"] * X[\"std_table_h\"]\n",
" X[\"near_team_km\"] = [min(np.hypot(t[\"x\"] - x, t[\"y\"] - y) for t in teams if e in t[\"skills\"]) for x, y, e in zip(req[\"x\"], req[\"y\"], req[\"equip\"])]\n",
" X[\"hub_km\"] = [min(np.hypot(hx - x, hy - y) for hx, hy in HUBS) for x, y in zip(req[\"x\"], req[\"y\"])]\n",
" X[\"req_24h\"] = [((req[\"t_req\"] > t - 24) & (req[\"t_req\"] <= t)).sum() for t in req[\"t_req\"]]\n",
" X[\"fault_past_mean\"] = req.groupby(\"fault\")[\"std_repair_h\"].transform(lambda s: s.shift().expanding().mean()).fillna(req[\"std_repair_h\"].mean())\n",
" X[\"crit\"] = req[\"crit\"]\n",
" for e in EQUIP_F:\n",
" X[f\"eq_{e}\"] = (req[\"equip\"] == e).astype(int)\n",
" return X\n",
"\n",
"\n",
"def fit_repair_model(hist, teams):\n",
" \"\"\"๊ณผ๊ฑฐ ์์ฒญ์ผ๋ก ์ ๋น ์์์๊ฐ(4์ธ ๊ธฐ์ค) ์์ธก ๋ชจ๋ธ์ ํ์ตํ๋ค.\"\"\"\n",
" return GradientBoostingRegressor(n_estimators=200, max_depth=3, learning_rate=0.05, random_state=0).fit(make_features(hist, teams), hist[\"std_repair_h\"])\n",
"\n",
"KO = {\"std_table_h\": \"์ ๋น ํ์ค์๊ฐํ ๊ฐ\", \"no_parts\": \"์๋ฆฌ๋ถ์ ๋ฏธํ๋ณด\", \"weather_bad\": \"์
์ฒํ\", \"bad_x_table\": \"์
์ฒํ x ํ์ค์๊ฐ\",\n",
" \"crit_x_noparts\": \"ํต์ฌ์ฅ๋น x ๋ถ์ ๋ฏธํ๋ณด\", \"fault_past_mean\": \"๊ฐ์ ๊ณ ์ฅ ๊ณผ๊ฑฐ ํ๊ท \", \"unpaved\": \"๋นํฌ์ฅ ๋๋ก\", \"crit\": \"์์ ์ค์๋\",\n",
" \"near_team_km\": \"์ต๊ทผ์ ์ ๋น๋ฐ ๊ฑฐ๋ฆฌ\", \"hub_km\": \"ํ๋ จ์ฅ๊ณผ์ ๊ฑฐ๋ฆฌ\", \"req_24h\": \"์ต๊ทผ 24์๊ฐ ์์ฒญ ์\", \"hour\": \"์ ์ ์๊ฐ\",\n",
" \"is_night\": \"์ผ๊ฐ ์ ์\", \"dow\": \"์์ผ\"}\n",
"\n",
"\n",
"def explain(model, X, bg, top=3):\n",
" \"\"\"๊ฐ๋ฆผ(occlusion) ๋ฐฉ์ ๊ตญ์ ๊ธฐ์ฌ๋: ํผ์ฒ j๋ฅผ ๋ฐฐ๊ฒฝ ํ๋ณธ ๊ฐ์ผ๋ก ๋ฐ๊ฟจ์ ๋ ์์ธก์ด ํ๊ท ์ ์ผ๋ก ์ผ๋ง๋ ๋ฌ๋ผ์ง๋์ง.\n",
" SHAP๊ณผ ๊ฐ์ '์ด ์์ธก์ ํ๊ท ์์ ์ผ๋ง๋ ๋ฐ์ด ์ฌ๋ ธ๋'๋ฅผ ๊ทผ์ฌํ๋ค(์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์์ด).\"\"\"\n",
" base = model.predict(X)\n",
" contrib = np.zeros(X.shape)\n",
" for j, c in enumerate(X.columns):\n",
" for b in bg:\n",
" Xb = X.copy(); Xb[c] = b[j]\n",
" contrib[:, j] += base - model.predict(Xb)\n",
" contrib /= len(bg)\n",
" C = pd.DataFrame(contrib, columns=X.columns, index=X.index)\n",
" tops = [list(C.loc[i].abs().sort_values(ascending=False).index[:top]) for i in C.index]\n",
" return base, C, tops\n",
"\n",
"\n",
"def sentence(r, pred, C, top):\n",
" parts = [f\"{KO.get(f, f)}({'+' if C[f] > 0 else ''}{C[f]:.1f}h)\" for f in top]\n",
" return f\"{r['req_id']} {r['equip']}/{r['fault']} ์์ ์ ๋น {pred:.1f}h(ํ๊ท ๋๋น {pred - AVG:+.1f}h), ์ํฅ ์์ธ: \" + \", \".join(parts)\n",
"\n",
"\n",
"teams = make_teams()\n",
"hist, req = make_requests(seed=11), make_requests(seed=1)\n",
"model = fit_repair_model(hist, teams)\n",
"AVG = float(hist[\"std_repair_h\"].mean())\n",
"Xh, X = make_features(hist, teams), make_features(req, teams)\n",
"res, L = simulate(req, teams, \"priority\", est=model.predict(X))\n",
"\n",
"# 1) ์ค๋ช
๊ฐ๋ฅ์ฑ: ํ๊ฐ ๊ธฐ๊ฐ ์ ์ฒด ์์ฒญ์ ๋ํด ์์ 3๊ฐ ์ธ์ ์๋ ์ฐ์ถ\n",
"t0 = time.perf_counter()\n",
"bg = Xh.sample(30, random_state=0).values\n",
"pred, C, tops = explain(model, X, bg)\n",
"sec = time.perf_counter() - t0\n",
"rate = np.mean([len(t) == 3 for t in tops])\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ์์ฒญ {len(X)}๊ฑด ์ค๋ช
์ฐ์ถ {sec:.1f}์ด, ์์ 3๊ฐ ์ธ์ ์๋ ์ฐ์ถ๋ฅ {rate:.0%} (๋ชฉํ 100%: {'๋ฌ์ฑ' if rate == 1 else '๋ฏธ๋ฌ'})\")\n",
"check = np.abs(C.sum(axis=1) - (pred - model.predict(Xh).mean())).mean()\n",
"print(f\"๊ธฐ์ฌ๋ ํฉ๊ณผ (์์ธก - ํ๊ท ์์ธก)์ ํ๊ท ์ฐจ์ด {check:.2f}h -> ๊ทผ์ฌ ์ค๋ช
์ด๋ฏ๋ก ์ฐธ๊ณ ์งํ๋ก ์ด๋ค\")\n",
"freq = pd.Series([f for t in tops for f in t]).map(lambda f: KO.get(f, f)).value_counts().head(5)\n",
"print(\"์์ 3๊ฐ ์ธ์์ ์์ฃผ ๋ฑ์ฅํ ์์ธ:\", \", \".join(f\"{k} {v}๊ฑด\" for k, v in freq.items()))\n",
"\n",
"# 2) ๊ด์ ๋์๋ณด๋: 22์ผ์ฐจ 14์ ๊ธฐ์ค\n",
"day, now = 22, 22 * 24 + 14\n",
"cur = res[(res[\"t_req\"] <= now) & (res[\"t_req\"] > now - 24)]\n",
"waiting = cur[cur[\"dispatch\"] > now]\n",
"print(f\"\\n[๊ด์ ํํฉ {day}์ผ์ฐจ 14์] ์ต๊ทผ 24์๊ฐ ์์ฒญ {len(cur)}๊ฑด, ์ถ๋ ๋๊ธฐ {len(waiting)}๊ฑด, ์ ๋น๋ฐ {len(teams)}๊ฐ\")\n",
"fig, ax = plt.subplots(2, 3, figsize=(15, 8))\n",
"col = {1: \"tab:gray\", 2: \"tab:orange\", 3: \"tab:red\"}\n",
"ax[0, 0].scatter(cur[\"x\"], cur[\"y\"], c=cur[\"crit\"].map(col), s=40)\n",
"for t in teams:\n",
" ax[0, 0].plot(t[\"x\"], t[\"y\"], \"ks\"); ax[0, 0].annotate(t[\"team\"], (t[\"x\"] + 1, t[\"y\"] + 1))\n",
"ax[0, 0].set_xlim(0, AREA_KM); ax[0, 0].set_ylim(0, AREA_KM); ax[0, 0].set_title(\"Requests (24h, red=critical) & teams\")\n",
"lr = L.iloc[day]\n",
"ax[0, 1].bar(lr.index, lr.values, color=[\"tab:red\" if v > 1 else \"tab:blue\" for v in lr.values])\n",
"ax[0, 1].axhline(1.0, ls=\"--\", color=\"k\"); ax[0, 1].set_title(f\"Team load rate, day {day}\")\n",
"q = waiting.groupby(\"crit\").size().reindex([1, 2, 3], fill_value=0)\n",
"ax[0, 2].bar([\"normal\", \"important\", \"critical\"], q.values, color=[col[k] for k in q.index]); ax[0, 2].set_title(\"Waiting requests by criticality\")\n",
"dd = res.groupby(\"day\")[\"response_h\"].mean()\n",
"ax[1, 0].plot(dd.index, dd.values, marker=\"o\"); ax[1, 0].set_title(\"Daily mean response time (h)\"); ax[1, 0].set_xlabel(\"day\")\n",
"ax[1, 1].boxplot([res.loc[res[\"crit\"] == k, \"response_h\"] for k in (1, 2, 3)]); ax[1, 1].set_xticks([1, 2, 3], [\"normal\", \"important\", \"critical\"])\n",
"ax[1, 1].set_title(\"Response time by criticality\")\n",
"i = int(C.index[req[\"crit\"].eq(3)][int(np.argmax(pred[req[\"crit\"].eq(3).values]))])\n",
"ci = C.loc[i].reindex(C.loc[i].abs().sort_values(ascending=False).index)[:6][::-1]\n",
"start = model.predict(Xh).mean()\n",
"ax[1, 2].barh(range(len(ci)), ci.values, left=start + np.r_[0, np.cumsum(ci.values)[:-1]], color=[\"tab:red\" if v > 0 else \"tab:green\" for v in ci.values])\n",
"ax[1, 2].set_yticks(range(len(ci)), ci.index); ax[1, 2].set_title(f\"Waterfall: {req.loc[i, 'req_id']} repair time\"); ax[1, 2].set_xlabel(\"hours\")\n",
"plt.tight_layout(); plt.show()\n",
"print(\"๋์๋ณด๋ ๊ตฌ์ฑ์์ 6์ข
: ์์ฒญยท์ ๋น๋ฐ ์ง๋, ์ ๋น๋ฐ ๋ถํ์จ, ์ค์๋๋ณ ๋๊ธฐ, ์ผ๋ณ ๋์ฐฉ์๊ฐ, ์ค์๋๋ณ ๋์ฐฉ์๊ฐ, ์์ธก ์ํฐํด\")\n",
"\n",
"# 3) ์์ฐ์ด ์ค๋ช
์์(์ ๋น๋๋ ๊ด์ ํ๋ฉด ํ๋จ)\n",
"print(\"\\n[์์ธก ์ค๋ช
์์]\")\n",
"for k in list(waiting.sort_values(\"crit\", ascending=False)[\"idx\"][:4]) + [i]:\n",
" print(\" - \" + sentence(req.loc[k], pred[k], C.loc[k], tops[k]))\n",
"over = lr[lr > 1.0]\n",
"act = \"์ ์ ๋น๋ฐ ๊ณผ๋ถํ -> ์๊ธ ์ ๋น๋ถ๋ ์ง์ ์์ฒญยท๋นํต์ฌ ์์ฒญ ์ฐ๊ธฐ ๊ฒํ \" if len(over) == len(teams) else \"์ฌ์ ์ ๋น๋ฐ ์ง์ ๊ฒํ \"\n",
"print(f\"\\n๊ฒฝ๋ณด: {day}์ผ์ฐจ(ํ๋ จ ์ฃผ๊ฐ) ๋ถํ์จ 1.0 ์ด๊ณผ ์ ๋น๋ฐ {list(over.index)} -> {act}(์ ๋น๋๋์ฅ ํ๋จ)\" if len(over) else \"\\n๊ฒฝ๋ณด: ๊ณผ๋ถํ ์ ๋น๋ฐ ์์\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
06 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ์์ฒญ 356๊ฑด ์ค๋ช
์ฐ์ถ 1.7์ด, ์์ 3๊ฐ ์ธ์ ์๋ ์ฐ์ถ๋ฅ 100% (๋ชฉํ 100%: ๋ฌ์ฑ)\n",
"๊ธฐ์ฌ๋ ํฉ๊ณผ (์์ธก - ํ๊ท ์์ธก)์ ํ๊ท ์ฐจ์ด 0.31h -> ๊ทผ์ฌ ์ค๋ช
์ด๋ฏ๋ก ์ฐธ๊ณ ์งํ๋ก ์ด๋ค\n",
"์์ 3๊ฐ ์ธ์์ ์์ฃผ ๋ฑ์ฅํ ์์ธ: ์ ๋น ํ์ค์๊ฐํ ๊ฐ 329๊ฑด, ํต์ฌ์ฅ๋น x ๋ถ์ ๋ฏธํ๋ณด 227๊ฑด, ๊ฐ์ ๊ณ ์ฅ ๊ณผ๊ฑฐ ํ๊ท 225๊ฑด, ์
์ฒํ x ํ์ค์๊ฐ 189๊ฑด, ์๋ฆฌ๋ถ์ ๋ฏธํ๋ณด 57๊ฑด\n",
"\n",
"[๊ด์ ํํฉ 22์ผ์ฐจ 14์] ์ต๊ทผ 24์๊ฐ ์์ฒญ 22๊ฑด, ์ถ๋ ๋๊ธฐ 6๊ฑด, ์ ๋น๋ฐ 6๊ฐ\n",
"๋์๋ณด๋ ๊ตฌ์ฑ์์ 6์ข
: ์์ฒญยท์ ๋น๋ฐ ์ง๋, ์ ๋น๋ฐ ๋ถํ์จ, ์ค์๋๋ณ ๋๊ธฐ, ์ผ๋ณ ๋์ฐฉ์๊ฐ, ์ค์๋๋ณ ๋์ฐฉ์๊ฐ, ์์ธก ์ํฐํด\n",
"\n",
"[์์ธก ์ค๋ช
์์]\n",
" - R0256 fires/electric ์์ ์ ๋น 2.4h(ํ๊ท ๋๋น -1.7h), ์ํฅ ์์ธ: ์ ๋น ํ์ค์๊ฐํ ๊ฐ(-1.0h), ๊ฐ์ ๊ณ ์ฅ ๊ณผ๊ฑฐ ํ๊ท (-0.3h), ์
์ฒํ x ํ์ค์๊ฐ(-0.2h)\n",
" - R0260 mobility/hydraulic ์์ ์ ๋น 3.3h(ํ๊ท ๋๋น -0.8h), ์ํฅ ์์ธ: ์ ๋น ํ์ค์๊ฐํ ๊ฐ(-0.3h), ์
์ฒํ x ํ์ค์๊ฐ(-0.2h), ํต์ฌ์ฅ๋น x ๋ถ์ ๋ฏธํ๋ณด(-0.2h)\n",
" - R0262 power/cooling ์์ ์ ๋น 2.1h(ํ๊ท ๋๋น -2.0h), ์ํฅ ์์ธ: ์ ๋น ํ์ค์๊ฐํ ๊ฐ(-1.3h), ์
์ฒํ x ํ์ค์๊ฐ(-0.2h), ๊ฐ์ ๊ณ ์ฅ ๊ณผ๊ฑฐ ํ๊ท (-0.2h)\n",
" - R0261 comms/electric ์์ ์ ๋น 1.4h(ํ๊ท ๋๋น -2.7h), ์ํฅ ์์ธ: ์ ๋น ํ์ค์๊ฐํ ๊ฐ(-1.9h), ๊ฐ์ ๊ณ ์ฅ ๊ณผ๊ฑฐ ํ๊ท (-0.3h), ์
์ฒํ x ํ์ค์๊ฐ(-0.1h)\n",
" - R0255 fires/engine ์์ ์ ๋น 10.8h(ํ๊ท ๋๋น +6.7h), ์ํฅ ์์ธ: ์ ๋น ํ์ค์๊ฐํ ๊ฐ(+4.9h), ๊ฐ์ ๊ณ ์ฅ ๊ณผ๊ฑฐ ํ๊ท (+1.1h), ํต์ฌ์ฅ๋น x ๋ถ์ ๋ฏธํ๋ณด(+1.0h)\n",
"\n",
"๊ฒฝ๋ณด: 22์ผ์ฐจ(ํ๋ จ ์ฃผ๊ฐ) ๋ถํ์จ 1.0 ์ด๊ณผ ์ ๋น๋ฐ ['MT1', 'MT2', 'MT3', 'MT4', 'MT5', 'MT6'] -> ์ ์ ๋น๋ฐ ๊ณผ๋ถํ -> ์๊ธ ์ ๋น๋ถ๋ ์ง์ ์์ฒญยท๋นํต์ฌ ์์ฒญ ์ฐ๊ธฐ ๊ฒํ (์ ๋น๋๋์ฅ ํ๋จ)\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
07. ๊ธด๊ธ ์ถ๋ ์์ ๋ณ๊ฒฝ ๋ฐ ์ ๋น๋ฐ ์ฌ๋ฐฐ์น๋ฅผ ์ํ ์ ๋น๋๋์ฅ HITL ์น์ธ ์ฒด๊ณ ์ค๊ณ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ถ๋ ์์ ๋ณ๊ฒฝ์ ์ํ ์ ๋น๋๋์ฅ HITL ์น์ธ ์ฒด๊ณ๋ฅผ ์ค๊ณํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๊ธด๊ธ ์ถ๋ ์์ ๋ณ๊ฒฝ๊ณผ ์ ๋น๋ฐ ์ฌ๋ฐฐ์น๋ฅผ ์ ์๊ณํ(ํ ๋น ๋ฌธ์ )์ผ๋ก ์ ์ํ๊ณ , ์ ๋น๋๋์ฅ ์น์ธ์ ๊ฑฐ์ณ์ผ๋ง ์ ์ฉ๋๋ HITL ์น์ธ ์ฒด๊ณ๋ฅผ ์๋ฎฌ๋ ์ดํฐ๋ก ์ค๊ณํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๋ฐฐ์ ์ฃผ๊ธฐ๋ง๋ค ๊ธฐ๋ณธ ๊ณํ(์ ์ ์์ยท์ต๊ทผ์ )๊ณผ ํ ๋น ์ต์ ํ ์ ์์ ๋น๊ตํด, ๋ค๋ฅด๊ณ ๊ธฐ๋ ์ด๋์ด ์์ผ๋ฉด ์น์ธ ๋๊ธฐ์ด์ ์ฌ๋ฆฐ๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/optimization/mnt10_simulator.py` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ํ์ค์ ๊ตฐ์ ์ ์ฝ์กฐ๊ฑด ๋ฐ์๋ ๋ฐ ์๋๋ฆฌ์ค๋ณ ๋์ ์ ์ ์ ์ ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์ต์ ํ ์ ์ฉ ์ ๋น์ฉ 15% ์ ๊ฐ ๋๋ ์ฌ๊ณ ํ์ ์จ 20% ํฅ์\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์ ํ/์ ์ ๊ณํ๋ฒ(LP/MIP) ์ต์ ํ ํด ๋์ถ ๋ฐ ์๋ฎฌ๋ ์ด์
์๋ฃ\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",
"from scipy.optimize import linear_sum_assignment\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ผ์ ์ด๋์ ๋น๋ฐยท๊ธด๊ธ ์ ๋น ์์ฒญ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์์นยท์ฅ๋นยท์ธ์ ์ ๋ณด ์์)\n",
"# 60km x 60km ๊ฐ์ ์ง์ญ์ ์ด๋์ ๋น๋ฐ 6๊ฐ์ 30์ผ๊ฐ์ ๊ธด๊ธ ์ ๋น ์์ฒญ์ ๋ง๋ ๋ค.\n",
"# ์ฅ๋น ๊ตฌ๋ถ(mobility/fires/comms/power)๊ณผ ๊ณ ์ฅ ์ ํ์ ๊ต์ก์ฉ ์ถ์ ๋ถ๋ฅ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"AREA_KM, DUTY_H = 60, 10.0 # ๊ฐ์ ์์ ์ง์ญ ํฌ๊ธฐ, ์ ๋น๋ฐ 1์ผ ๊ธฐ์ค ๊ฐ์ฉ์๊ฐ\n",
"FAULT_H = {\"engine\": 6.0, \"drivetrain\": 5.0, \"hydraulic\": 3.0, \"electric\": 2.0, \"comm_module\": 1.5, \"cooling\": 2.5}\n",
"FIELD_P = {\"engine\": 0.3, \"drivetrain\": 0.35, \"hydraulic\": 0.75, \"electric\": 0.9, \"comm_module\": 0.95, \"cooling\": 0.8}\n",
"EQUIP_F = {\"mobility\": 1.0, \"fires\": 1.2, \"comms\": 0.7, \"power\": 0.8}\n",
"FAULT_OF = {\"mobility\": [0.3, 0.25, 0.2, 0.1, 0.0, 0.15], \"fires\": [0.2, 0.15, 0.35, 0.15, 0.0, 0.15],\n",
" \"comms\": [0.0, 0.0, 0.0, 0.4, 0.5, 0.1], \"power\": [0.35, 0.0, 0.0, 0.35, 0.0, 0.3]}\n",
"CRIT_P = {\"mobility\": [0.45, 0.35, 0.2], \"fires\": [0.2, 0.35, 0.45], \"comms\": [0.3, 0.4, 0.3], \"power\": [0.6, 0.3, 0.1]}\n",
"HUBS = [(15, 20), (45, 25), (30, 50)] # ๊ฐ์ ํ๋ จ์ฅ 3๊ณณ(์์ฒญ์ด ๋ชฐ๋ฆฌ๋ ์ง์ )\n",
"\n",
"\n",
"def make_teams(seed=0):\n",
" rng = np.random.default_rng(seed)\n",
" base = [(8, 10), (30, 6), (52, 12), (10, 44), (32, 38), (54, 50)]\n",
" skills = [[\"fires\", \"mobility\"], [\"comms\", \"mobility\"], [\"fires\", \"power\"],\n",
" [\"mobility\", \"power\"], [\"comms\", \"fires\"], [\"comms\", \"mobility\", \"power\"]]\n",
" return [{\"team\": f\"MT{i + 1}\", \"x\": float(x), \"y\": float(y), \"crew\": int(rng.integers(3, 6)), \"skills\": s}\n",
" for i, ((x, y), s) in enumerate(zip(base, skills))]\n",
"\n",
"\n",
"def make_requests(n_days=30, per_day=10, seed=1, surge_days=(8, 9, 10, 11, 22, 23, 24)):\n",
" \"\"\"ํ๋ฃจ ํ๊ท per_day๊ฑด, ํ๋ จ ์ฃผ๊ฐ(surge_days)์๋ 1.6๋ฐฐ์ ๊ธด๊ธ ์ ๋น ์์ฒญ์ ๋ง๋ ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for d in range(n_days):\n",
" n = rng.poisson(per_day * (1.6 if d in surge_days else 1.0))\n",
" weather = rng.choice([\"clear\", \"rain\", \"snow\"], p=[0.65, 0.25, 0.1])\n",
" for _ in range(n):\n",
" if rng.random() < 0.7:\n",
" hx, hy = HUBS[rng.integers(0, 3)]\n",
" x, y = np.clip(rng.normal(hx, 6), 0, AREA_KM), np.clip(rng.normal(hy, 6), 0, AREA_KM)\n",
" else:\n",
" x, y = rng.uniform(0, AREA_KM, 2)\n",
" eq = str(rng.choice(list(EQUIP_F), p=[0.45, 0.25, 0.2, 0.1]))\n",
" fault = str(rng.choice(list(FAULT_H), p=FAULT_OF[eq]))\n",
" parts = bool(rng.random() < 0.75)\n",
" bad = weather != \"clear\"\n",
" std_h = FAULT_H[fault] * EQUIP_F[eq] * (1.35 if not parts else 1.0) * (1.15 if bad else 1.0) * rng.lognormal(0, 0.12)\n",
" rows.append({\"t_req\": d * 24 + float(np.clip(rng.normal(13, 4), 5, 23)), \"day\": d,\n",
" \"x\": float(x), \"y\": float(y), \"equip\": eq, \"fault\": fault,\n",
" \"crit\": int(rng.choice([1, 2, 3], p=CRIT_P[eq])), \"parts_ready\": parts,\n",
" \"weather\": str(weather), \"road\": \"unpaved\" if y > 30 and rng.random() < 0.7 else \"paved\",\n",
" \"field_ok\": bool(rng.random() < FIELD_P[fault] * (1.0 if parts else 0.5)),\n",
" \"std_repair_h\": round(float(std_h), 2), \"road_delay\": float(rng.lognormal(0, 0.15))})\n",
" df = pd.DataFrame(rows).sort_values(\"t_req\").reset_index(drop=True)\n",
" df.insert(0, \"req_id\", [f\"R{i:04d}\" for i in range(len(df))])\n",
" return df\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ถ๋ ๋ฐฐ์ ยท์ ๋น ๋ชจ์ ๊ณตํต ํจ์ ----\n",
"W0 = {\"crit\": 1.0, \"wait\": 0.3, \"short\": 0.2, \"balance\": 0.5} # ์ฐ์ ์์ยท๋ฐฐ์ ๊ธฐ๋ณธ ๊ฐ์ค์น(๋ฏธ์
9์์ ์ฌํ๋)\n",
"\n",
"\n",
"def travel_h(team, r, actual=False):\n",
" \"\"\"๋๋ก ๊ตด๊ณก 1.3๋ฐฐ, ํฌ์ฅ 35km/hยท๋นํฌ์ฅ 25km/h, ์
์ฒํ 20% ๊ฐ์. actual=True๋ฉด ์ค์ ๋๋ก ์ง์ฐ์ ๋ฐ์.\"\"\"\n",
" d = 1.3 * np.hypot(team[\"x\"] - r[\"x\"], team[\"y\"] - r[\"y\"])\n",
" v = (35.0 if r[\"road\"] == \"paved\" else 25.0) / (1.2 if r[\"weather\"] != \"clear\" else 1.0)\n",
" return d / v * (r[\"road_delay\"] if actual else 1.0)\n",
"\n",
"\n",
"def on_site_h(team, r):\n",
" \"\"\"ํ์ฅ ์ ๋น์๊ฐ: 4์ธ ๊ธฐ์ค ํ์ค์๊ฐ์ ํฌ์
์ธ์์ผ๋ก ํ์ฐ. ํ์ฅ ์๋ฆฌ ๋ถ๊ฐ๋ฉด ์ง๋จยทํ์ก ์กฐ์น 1์๊ฐ.\"\"\"\n",
" return r[\"std_repair_h\"] * 4 / team[\"crew\"] if r[\"field_ok\"] else 1.0\n",
"\n",
"\n",
"def priority(r, now, est_h, w):\n",
" return w[\"crit\"] * r[\"crit\"] + w[\"wait\"] * (now - r[\"t_req\"]) + w[\"short\"] / (1 + est_h)\n",
"\n",
"\n",
"def simulate(req, teams, policy=\"baseline\", w=None, cycle_h=1.0, est=None):\n",
" \"\"\"๋ฐฐ์ ์ฃผ๊ธฐ(cycle_h)๋ง๋ค ๋๊ธฐ ์์ฒญ์ ์ ๋ ฌํด ์ด๋์ ๋น๋ฐ์ ๋ฐฐ์ ํ๊ณ ์ถ๋ยท์ ๋นยท๋ณต๊ท๋ฅผ ๋ชจ์ํ๋ค.\n",
" baseline: ์ ์ ์์ + ๊ฑฐ๋ฆฌ ์ต๊ทผ์ ์ ๋น๋ฐ / priority: ์ฐ์ ์์ ์ ์ ์์ + ๋์ฐฉ ์์์๊ฐยท๋น์ผ ๋ถํ ๊ท ํ\"\"\"\n",
" w = w or W0\n",
" est = np.asarray(est if est is not None else req[\"std_repair_h\"].values, float)\n",
" n_days = int(req[\"day\"].max()) + 1\n",
" free = {t[\"team\"]: 0.0 for t in teams}\n",
" load = {t[\"team\"]: np.zeros(n_days + 5) for t in teams}\n",
" recs, out, pending, i, now = req.to_dict(\"records\"), [], [], 0, cycle_h\n",
" while i < len(recs) or pending:\n",
" while i < len(recs) and recs[i][\"t_req\"] <= now:\n",
" pending.append((i, recs[i])); i += 1\n",
" if policy == \"priority\":\n",
" pending.sort(key=lambda p: -priority(p[1], now, est[p[0]], w))\n",
" for k, r in pending:\n",
" cand = [t for t in teams if r[\"equip\"] in t[\"skills\"]]\n",
" day = min(int(now // 24), n_days + 4)\n",
" if policy == \"baseline\":\n",
" tm = min(cand, key=lambda t: np.hypot(t[\"x\"] - r[\"x\"], t[\"y\"] - r[\"y\"]))\n",
" else:\n",
" tm = min(cand, key=lambda t: max(free[t[\"team\"]], now) + travel_h(t, r)\n",
" + w[\"balance\"] * load[t[\"team\"]][day] / DUTY_H * 4)\n",
" backlog = max(0.0, free[tm[\"team\"]] - now)\n",
" go = max(free[tm[\"team\"]], now)\n",
" tr = travel_h(tm, r, actual=True)\n",
" work = on_site_h(tm, r)\n",
" free[tm[\"team\"]] = go + 2 * tr + work\n",
" load[tm[\"team\"]][min(int(go // 24), n_days + 4)] += 2 * tr + work\n",
" out.append({**r, \"idx\": k, \"team\": tm[\"team\"], \"dist_km\": float(np.hypot(tm[\"x\"] - r[\"x\"], tm[\"y\"] - r[\"y\"])),\n",
" \"backlog_h\": backlog, \"cycle_wait_h\": now - r[\"t_req\"], \"dispatch\": go, \"travel_h\": tr,\n",
" \"work_h\": work, \"response_h\": go + tr - r[\"t_req\"], \"downtime_h\": go + tr + work - r[\"t_req\"]})\n",
" pending, now = [], now + cycle_h\n",
" L = pd.DataFrame({k: v[:n_days] for k, v in load.items()}) / DUTY_H # ์ ๋น๋ฐ๋ณ ์ผ์ผ ๋ถํ์จ\n",
" return pd.DataFrame(out).sort_values(\"idx\").reset_index(drop=True), L\n",
"\n",
"\n",
"def metrics(res, L):\n",
" c3 = res[res[\"crit\"] == 3]\n",
" return {\"ํ๊ท ๋์ฐฉ(h)\": round(res[\"response_h\"].mean(), 2), \"ํต์ฌ์ฅ๋น ๋์ฐฉ(h)\": round(c3[\"response_h\"].mean(), 2),\n",
" \"ํต์ฌ์ฅ๋น 90%(h)\": round(c3[\"response_h\"].quantile(0.9), 2), \"ํ๊ท ๋ถํ์จ\": round(float(L.values.mean()), 3),\n",
" \"๋ถํ์จ ํธ์ฐจ\": round(float(L.mean().std()), 3), \"๊ณผ๋ถํ ์ผ์\": int((L > 1.0).values.sum()),\n",
" \"์ด ์ด๋(h)\": round(float(res[\"travel_h\"].sum() * 2), 1)}\n",
"\n",
"CW = {1: 1.0, 2: 3.0, 3: 6.0} # [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ค์๋๋ณ ๊ฐ๋์ค๋จ 1์๊ฐ ๋น์ฉ ์ง์\n",
"SLOTS = 3 # ์ ๋น๋ฐ๋น ํ ๋ฐฐ์ ์ฃผ๊ธฐ์ ๋ฐ์ ์ ์๋ ์ต๋ ๊ฑด์\n",
"APPROVE_DELAY = 0.25 # ์น์ธ ์ ์ฐจ๋ก ๋ฆ์ด์ง๋ ์ถ๋ ์๊ฐ(h)\n",
"\n",
"\n",
"class Audit:\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 default_plan(pending, teams, free, now):\n",
" \"\"\"๊ธฐ๋ณธ ๊ณํ: ์ ์ ์์๋๋ก ์ต๊ทผ์ (์ ๋น ๊ฐ๋ฅ) ์ ๋น๋ฐ.\"\"\"\n",
" plan, f = [], dict(free)\n",
" for k, r in pending:\n",
" tm = min([t for t in teams if r[\"equip\"] in t[\"skills\"]], key=lambda t: np.hypot(t[\"x\"] - r[\"x\"], t[\"y\"] - r[\"y\"]))\n",
" go = max(f[tm[\"team\"]], now); plan.append((k, tm)); f[tm[\"team\"]] = go + 2 * travel_h(tm, r) + on_site_h(tm, r)\n",
" return plan\n",
"\n",
"\n",
"def mip_plan(pending, teams, free, now):\n",
" \"\"\"ํ ๋น ๋ฌธ์ (์ ์๊ณํ): ์์ฒญ x (์ ๋น๋ฐ, ์๋ฒ) ๋น์ฉ = ์ค์๋ ๊ฐ์ค x ์์ ๋์ฐฉ์๊ฐ. ์ ๋น ๋ถ๊ฐ ์กฐํฉ์ ํฐ ๋น์ฉ.\"\"\"\n",
" avg = np.mean([on_site_h(teams[0], r) for _, r in pending]) + 1.0\n",
" cols = [(t, s) for t in teams for s in range(SLOTS)]\n",
" cost = np.full((len(pending), len(cols)), 1e6)\n",
" for i, (k, r) in enumerate(pending):\n",
" for j, (t, s) in enumerate(cols):\n",
" if r[\"equip\"] in t[\"skills\"]:\n",
" cost[i, j] = CW[r[\"crit\"]] * (max(free[t[\"team\"]], now) + s * avg + travel_h(t, r) - now)\n",
" ri, cj = linear_sum_assignment(cost)\n",
" order = sorted(zip(ri, cj), key=lambda p: (cols[p[1]][1], -pending[p[0]][1][\"crit\"]))\n",
" return [(pending[i][0], cols[j][0]) for i, j in order]\n",
"\n",
"\n",
"def expected_cost(plan, recs, free, now, delay=0.0):\n",
" f, c = dict(free), 0.0\n",
" for k, tm in plan:\n",
" r = recs[k]; go = max(f[tm[\"team\"]], now + delay)\n",
" c += CW[r[\"crit\"]] * (go + travel_h(tm, r) + on_site_h(tm, r) - r[\"t_req\"]); f[tm[\"team\"]] = go + 2 * travel_h(tm, r) + on_site_h(tm, r)\n",
" return c\n",
"\n",
"\n",
"def commander(prop, rng):\n",
" \"\"\"[๋ชจ์ ์ ๋น๋๋์ฅ] ๊ธฐ๋ ์ด๋์ด ์๊ฑฐ๋ ๋ด๋น ๊ตฌ์ญ์ ํฌ๊ฒ ๋ฒ์ด๋๋ ์ฌ๋ฐฐ์น๋ ๋ฐ๋ ค, 10%๋ 30๋ถ ๋ด ๋ฌด์๋ต.\"\"\"\n",
" if rng.random() < 0.10:\n",
" return \"๋ฌด์๋ต\", \"30๋ถ ๋ด ์๋ต ์์ -> ๊ธฐ๋ณธ ๊ณํ ์ ์ง\"\n",
" if prop[\"max_km\"] > 45:\n",
" return \"๋ฐ๋ ค\", f\"์ ๋น๋ฐ ๊ตฌ์ญ ์ดํ {prop['max_km']:.0f}km(๋ณต๊ทยท๋ณด๊ธ ๊ณค๋)\"\n",
" if prop[\"gain\"] < 1.0:\n",
" return \"๋ฐ๋ ค\", f\"๊ธฐ๋ ์ด๋ {prop['gain']:.1f} ์์ -> ํ ๊ณํ ์ ์ง\"\n",
" return \"์น์ธ\", f\"ํต์ฌ์ฅ๋น ์ฐ์ , ๊ธฐ๋ ์ด๋ {prop['gain']:.1f}\"\n",
"\n",
"\n",
"def run(req, teams, mode, seed=0, audit=None):\n",
" \"\"\"mode: default(๊ธฐ๋ณธ ๊ณํ) / hitl(์ ์ -> ์น์ธ ์ ์ ์ฉ) / auto(๋ชจ๋ ์ ์ ์ ์ฉ: ๋น๊ต์ฉ ์ํ, ์ค์ ์ด์ฉ ๊ธ์ง)\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" recs = req.to_dict(\"records\"); free = {t[\"team\"]: 0.0 for t in teams}\n",
" out, i, now, props = [], 0, 1.0, []\n",
" while i < len(recs):\n",
" pending = []\n",
" while i < len(recs) and recs[i][\"t_req\"] <= now:\n",
" pending.append((i, recs[i])); i += 1\n",
" if pending:\n",
" base = default_plan(pending, teams, free, now)\n",
" plan, delay = base, 0.0\n",
" if mode != \"default\":\n",
" prop = mip_plan(pending, teams, free, now)\n",
" if [(k, t[\"team\"]) for k, t in prop] != [(k, t[\"team\"]) for k, t in base]:\n",
" gain = expected_cost(base, recs, free, now) - expected_cost(prop, recs, free, now, APPROVE_DELAY)\n",
" max_km = max(np.hypot(t[\"x\"] - recs[k][\"x\"], t[\"y\"] - recs[k][\"y\"]) for k, t in prop)\n",
" info = {\"t\": round(now, 2), \"n\": len(pending), \"gain\": round(gain, 2), \"max_km\": max_km}\n",
" if gain <= 0: # ์ด๋ ์๋ ๋ณ๊ฒฝ์ ์ ์ํ์ง ์๋๋ค\n",
" pass\n",
" elif mode == \"auto\":\n",
" dec, why = \"์น์ธ\", \"๋น๊ต์ฉ ์ ๋ถ ์ ์ฉ\"\n",
" else:\n",
" dec, why = commander(info, rng)\n",
" audit.add(event=\"PROPOSE\", t=info[\"t\"], n=info[\"n\"], gain=info[\"gain\"], status=\"PENDING\")\n",
" audit.add(event=\"DECIDE\", t=info[\"t\"], decision=dec, reason=why, by=\"์ ๋น๋๋์ฅ(๋ชจ์)\")\n",
" if gain > 0:\n",
" props.append({**info, \"decision\": dec})\n",
" if gain > 0 and dec == \"์น์ธ\":\n",
" plan, delay = prop, APPROVE_DELAY\n",
" for k, tm in plan:\n",
" r = recs[k]; go = max(free[tm[\"team\"]], now + delay)\n",
" tr, work = travel_h(tm, r, actual=True), on_site_h(tm, r)\n",
" free[tm[\"team\"]] = go + 2 * tr + work\n",
" out.append({**r, \"team\": tm[\"team\"], \"response_h\": go + tr - r[\"t_req\"], \"downtime_h\": go + tr + work - r[\"t_req\"]})\n",
" now += 1.0\n",
" res = pd.DataFrame(out)\n",
" res[\"cost\"] = res[\"crit\"].map(CW) * res[\"downtime_h\"]\n",
" return res, pd.DataFrame(props)\n",
"\n",
"\n",
"teams = make_teams()\n",
"req = make_requests(seed=1)\n",
"audit = Audit()\n",
"R = {}\n",
"for mode in (\"default\", \"hitl\", \"auto\"):\n",
" R[mode] = run(req, teams, mode, seed=3, audit=audit)\n",
"rows = []\n",
"for mode, name in [(\"default\", \"๊ธฐ๋ณธ ๊ณํ\"), (\"hitl\", \"์ ์ + ๋๋์ฅ ์น์ธ\"), (\"auto\", \"์ ์ ์ ๋ถ ์ ์ฉ(๋น๊ต์ฉ)\")]:\n",
" res = R[mode][0]\n",
" rows.append({\"๋ฐฉ์\": name, \"๊ฐ๋์ค๋จ ๋น์ฉ์ง์\": round(res[\"cost\"].sum()), \"ํต์ฌ์ฅ๋น ๋์ฐฉ(h)\": round(res.loc[res[\"crit\"] == 3, \"response_h\"].mean(), 2),\n",
" \"ํต์ฌ์ฅ๋น ๊ฐ๋์ค๋จ(h)\": round(res.loc[res[\"crit\"] == 3, \"downtime_h\"].mean(), 2), \"์ ์ฒด ๋์ฐฉ(h)\": round(res[\"response_h\"].mean(), 2)})\n",
"T = pd.DataFrame(rows)\n",
"print(\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] 30์ผ ๋ชจ์ ๊ฒฐ๊ณผ\")\n",
"print(T.to_string(index=False))\n",
"cut = 1 - T.loc[1, \"๊ฐ๋์ค๋จ ๋น์ฉ์ง์\"] / T.loc[0, \"๊ฐ๋์ค๋จ ๋น์ฉ์ง์\"]\n",
"print(f\"HITL ์ ์ฉ ๊ฐ๋์ค๋จ ๋น์ฉ ์ ๊ฐ๋ฅ {cut:.1%} (๋ชฉํ 15% ์ด์: {'๋ฌ์ฑ' if cut >= 0.15 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"P = R[\"hitl\"][1]\n",
"print(f\"\\n๋ณ๊ฒฝ ์ ์ {len(P)}๊ฑด: \" + \", \".join(f\"{k} {v}๊ฑด\" for k, v in P[\"decision\"].value_counts().items()))\n",
"rej = [e[\"reason\"] for e in audit.log if e.get(\"decision\") == \"๋ฐ๋ ค\"]\n",
"print(\"๋ฐ๋ ค ์ฌ์ ์:\", \"; \".join([next(x for x in rej if \"๊ตฌ์ญ\" in x), next(x for x in rej if \"์ด๋\" in x)]))\n",
"n_dec = sum(e[\"event\"] == \"DECIDE\" for e in audit.log)\n",
"print(f\"์ ์ {len(P)}๊ฑด ์ค ๊ฒฐ์ ๊ธฐ๋ก {n_dec}๊ฑด(๋ก๊น
๋ฅ {n_dec / max(len(P), 1):.0%}), ์๋ ์น์ธ 0๊ฑด, ๋ฌด์๋ต ์ ๊ธฐ๋ณธ ๊ณํ ์ ์ง\")\n",
"print(f\"๊ฐ์ฌ ๋ก๊ทธ ๋ฌด๊ฒฐ์ฑ: {'์ ์' if audit.verify() is None else '์ด์'}\", end=\", \")\n",
"audit.log[5][\"reason\"] = \"์ฌํ ์์ \"\n",
"print(f\"์ฌํ ๋ณ์กฐ ์ seq {audit.verify()}์์ ํ์ง\")\n",
"\n",
"# ๋ฏผ๊ฐ๋: ์น์ธ ์ง์ฐ์ด ๊ธธ์ด์ง๋ฉด ์ด๋์ด ์ค์ด๋ ๋ค(์ ์ ์ ํ ๋ฑ ๊ธ์ฆ ๋๋น)\n",
"print(\"\\n[๋ฏผ๊ฐ๋] ์น์ธ ์ง์ฐ(h) -> ๋น์ฉ ์ ๊ฐ๋ฅ \")\n",
"for d in (0.0, 0.25, 0.5, 1.0):\n",
" APPROVE_DELAY = d\n",
" r_, _ = run(req, teams, \"hitl\", seed=3, audit=Audit())\n",
" print(f\" {d:>4}: {1 - r_['cost'].sum() / T.loc[0, '๊ฐ๋์ค๋จ ๋น์ฉ์ง์']:.1%}\")\n",
"surge = make_requests(seed=1, per_day=16)\n",
"APPROVE_DELAY = 0.25\n",
"b, _ = run(surge, teams, \"default\"); h, _ = run(surge, teams, \"hitl\", seed=3, audit=Audit())\n",
"print(f\"์์ฒญ 1.6๋ฐฐ ๊ธ์ฆ ์๋๋ฆฌ์ค: ๋น์ฉ ์ ๊ฐ๋ฅ {1 - h['cost'].sum() / b['cost'].sum():.1%}\")\n",
"\n",
"plt.figure(figsize=(7, 4))\n",
"for mode, lab in [(\"default\", \"default plan\"), (\"hitl\", \"proposal + approval\")]:\n",
" s = R[mode][0]\n",
" plt.hist(s.loc[s[\"crit\"] == 3, \"response_h\"], bins=20, alpha=0.6, label=lab)\n",
"plt.xlabel(\"critical equipment response time (h)\"); plt.ylabel(\"count\"); plt.legend(); plt.title(\"HITL simulator: critical response\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
07 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] 30์ผ ๋ชจ์ ๊ฒฐ๊ณผ\n",
" ๋ฐฉ์ ๊ฐ๋์ค๋จ ๋น์ฉ์ง์ ํต์ฌ์ฅ๋น ๋์ฐฉ(h) ํต์ฌ์ฅ๋น ๊ฐ๋์ค๋จ(h) ์ ์ฒด ๋์ฐฉ(h)\n",
" ๊ธฐ๋ณธ ๊ณํ 6495 3.13 5.82 3.37\n",
" ์ ์ + ๋๋์ฅ ์น์ธ 5254 2.10 4.71 2.29\n",
"์ ์ ์ ๋ถ ์ ์ฉ(๋น๊ต์ฉ) 5018 1.92 4.53 2.08\n",
"HITL ์ ์ฉ ๊ฐ๋์ค๋จ ๋น์ฉ ์ ๊ฐ๋ฅ 19.1% (๋ชฉํ 15% ์ด์: ๋ฌ์ฑ)\n",
"\n",
"๋ณ๊ฒฝ ์ ์ 103๊ฑด: ์น์ธ 71๊ฑด, ๋ฐ๋ ค 24๊ฑด, ๋ฌด์๋ต 8๊ฑด\n",
"๋ฐ๋ ค ์ฌ์ ์: ์ ๋น๋ฐ ๊ตฌ์ญ ์ดํ 50km(๋ณต๊ทยท๋ณด๊ธ ๊ณค๋); ๊ธฐ๋ ์ด๋ 0.5 ์์ -> ํ ๊ณํ ์ ์ง\n",
"์ ์ 103๊ฑด ์ค ๊ฒฐ์ ๊ธฐ๋ก 103๊ฑด(๋ก๊น
๋ฅ 100%), ์๋ ์น์ธ 0๊ฑด, ๋ฌด์๋ต ์ ๊ธฐ๋ณธ ๊ณํ ์ ์ง\n",
"๊ฐ์ฌ ๋ก๊ทธ ๋ฌด๊ฒฐ์ฑ: ์ ์, ์ฌํ ๋ณ์กฐ ์ seq 5์์ ํ์ง\n",
"\n",
"[๋ฏผ๊ฐ๋] ์น์ธ ์ง์ฐ(h) -> ๋น์ฉ ์ ๊ฐ๋ฅ \n",
" 0.0: 20.0%\n",
" 0.25: 19.1%\n",
" 0.5: 17.6%\n",
" 1.0: 13.4%\n",
"์์ฒญ 1.6๋ฐฐ ๊ธ์ฆ ์๋๋ฆฌ์ค: ๋น์ฉ ์ ๊ฐ๋ฅ 46.5%\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
08. ๋์ 20๊ฐ์ ๊ธด๊ธ ์ ๋น ์์ฒญ ๋ฐ์ ๋ชจ์ ์ถ๋ ์ค์ผ์ค๋ง ์คํธ๋ ์ค ํ
์คํธ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋์ 20๊ฐ์ ๊ธด๊ธ ์ ๋น ์์ฒญ ๋ฐ์ ๋ชจ์ ์คํธ๋ ์ค ํ
์คํธ๋ฅผ ์ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋์ 20๊ฐ์ ๊ธด๊ธ ์ ๋น ์์ฒญ์ด ํ๊บผ๋ฒ์ ๋ค์ด์ค๋ ์ํฉ์์ ์ถ๋ ์ค์ผ์ค๋ง๊ณผ ๊ด์ ํ๋ฉด์ด ์ ๋ ๋์ํ๋์ง ์คํธ๋ ์ค ์ํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๋์ 20ยท40ยท60๊ฐ์ ์์ฒญ, ์ ๋น๋ฐ 1๊ฐ ์ถ๋ ๋ถ๊ฐ, ์์ ์์ฒญ ๋ฐ์ดํฐ๋ฅผ ์ฃผ์
ํ๊ณ ํ ๋น ์ต์ ํ๋ก ๋ฐฐ์ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/dashboard/mnt10_app.py (Streamlit/Dash ๋ฑ)` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์์ฌ๊ฒฐ์ ์ง์์ ์ต์ ํ๋ ์๊ฐ์ ์๊ณ ๋ฐ ์ฌ์ฉ์ ๊ฒฝํ(UX)\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๋์๋ณด๋ ๋ก๋ฉ ์๊ฐ 2์ด ์ด๋ด, ๊ฒฝ๋ณด ๋ฐ๋ น ์ ํ๋ 95% ์ด์\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์น ๊ธฐ๋ฐ ๋ถ์ ๋์๋ณด๋ ๊ตฌ๋ ๋ฐ ์ค์๊ฐ ํํฐ๋ง ๊ธฐ๋ฅ ๊ฒ์ฆ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ์ค์ ๊ตฐ์ ๋ฐ์ดํฐ ์ฐ๋ ํ๋ฉด ํ
์คํธ ๋ฐ UI ๋์ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"๋์ 20๊ฐ์(์ต๋ 60๊ฐ์) ๊ธด๊ธ ์ ๋น ์์ฒญ ๋ฐ์, ์ ๋น๋ฐ 1๊ฐ ์ถ๋ ๋ถ๊ฐ, ์์ ์์ฒญ ๋ฐ์ดํฐ๋ฅผ ์ฃผ์
ํด ์ถ๋ ์ค์ผ์ค๋ง๊ณผ ๊ด์ ๋์๋ณด๋๋ฅผ ์คํธ๋ ์ค ํ
์คํธํ๋ค. ์ค์ผ์ค๋ง+ํ๋ฉด ์์ฑ ์๊ฐ, 'ํต์ฌ์ฅ๋น 4์๊ฐ ๋ด ๋์ฐฉ ๋ถ๊ฐ' ๊ฒฝ๋ณด ์ ํ๋, ์์ฒญ ์ ์ค, ์์ ๋ฐ์ดํฐ์ ์๋ ํ์ธ ๋ถ๋ฆฌ์ ํํฐ ์กฐํ ์๋ต์ ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib\n",
"import matplotlib.pyplot as plt\n",
"from scipy.optimize import linear_sum_assignment\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ผ์ ์ด๋์ ๋น๋ฐยท๊ธด๊ธ ์ ๋น ์์ฒญ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์์นยท์ฅ๋นยท์ธ์ ์ ๋ณด ์์)\n",
"# 60km x 60km ๊ฐ์ ์ง์ญ์ ์ด๋์ ๋น๋ฐ 6๊ฐ์ 30์ผ๊ฐ์ ๊ธด๊ธ ์ ๋น ์์ฒญ์ ๋ง๋ ๋ค.\n",
"# ์ฅ๋น ๊ตฌ๋ถ(mobility/fires/comms/power)๊ณผ ๊ณ ์ฅ ์ ํ์ ๊ต์ก์ฉ ์ถ์ ๋ถ๋ฅ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"AREA_KM, DUTY_H = 60, 10.0 # ๊ฐ์ ์์ ์ง์ญ ํฌ๊ธฐ, ์ ๋น๋ฐ 1์ผ ๊ธฐ์ค ๊ฐ์ฉ์๊ฐ\n",
"FAULT_H = {\"engine\": 6.0, \"drivetrain\": 5.0, \"hydraulic\": 3.0, \"electric\": 2.0, \"comm_module\": 1.5, \"cooling\": 2.5}\n",
"FIELD_P = {\"engine\": 0.3, \"drivetrain\": 0.35, \"hydraulic\": 0.75, \"electric\": 0.9, \"comm_module\": 0.95, \"cooling\": 0.8}\n",
"EQUIP_F = {\"mobility\": 1.0, \"fires\": 1.2, \"comms\": 0.7, \"power\": 0.8}\n",
"FAULT_OF = {\"mobility\": [0.3, 0.25, 0.2, 0.1, 0.0, 0.15], \"fires\": [0.2, 0.15, 0.35, 0.15, 0.0, 0.15],\n",
" \"comms\": [0.0, 0.0, 0.0, 0.4, 0.5, 0.1], \"power\": [0.35, 0.0, 0.0, 0.35, 0.0, 0.3]}\n",
"CRIT_P = {\"mobility\": [0.45, 0.35, 0.2], \"fires\": [0.2, 0.35, 0.45], \"comms\": [0.3, 0.4, 0.3], \"power\": [0.6, 0.3, 0.1]}\n",
"HUBS = [(15, 20), (45, 25), (30, 50)] # ๊ฐ์ ํ๋ จ์ฅ 3๊ณณ(์์ฒญ์ด ๋ชฐ๋ฆฌ๋ ์ง์ )\n",
"\n",
"\n",
"def make_teams(seed=0):\n",
" rng = np.random.default_rng(seed)\n",
" base = [(8, 10), (30, 6), (52, 12), (10, 44), (32, 38), (54, 50)]\n",
" skills = [[\"fires\", \"mobility\"], [\"comms\", \"mobility\"], [\"fires\", \"power\"],\n",
" [\"mobility\", \"power\"], [\"comms\", \"fires\"], [\"comms\", \"mobility\", \"power\"]]\n",
" return [{\"team\": f\"MT{i + 1}\", \"x\": float(x), \"y\": float(y), \"crew\": int(rng.integers(3, 6)), \"skills\": s}\n",
" for i, ((x, y), s) in enumerate(zip(base, skills))]\n",
"\n",
"\n",
"def make_requests(n_days=30, per_day=10, seed=1, surge_days=(8, 9, 10, 11, 22, 23, 24)):\n",
" \"\"\"ํ๋ฃจ ํ๊ท per_day๊ฑด, ํ๋ จ ์ฃผ๊ฐ(surge_days)์๋ 1.6๋ฐฐ์ ๊ธด๊ธ ์ ๋น ์์ฒญ์ ๋ง๋ ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for d in range(n_days):\n",
" n = rng.poisson(per_day * (1.6 if d in surge_days else 1.0))\n",
" weather = rng.choice([\"clear\", \"rain\", \"snow\"], p=[0.65, 0.25, 0.1])\n",
" for _ in range(n):\n",
" if rng.random() < 0.7:\n",
" hx, hy = HUBS[rng.integers(0, 3)]\n",
" x, y = np.clip(rng.normal(hx, 6), 0, AREA_KM), np.clip(rng.normal(hy, 6), 0, AREA_KM)\n",
" else:\n",
" x, y = rng.uniform(0, AREA_KM, 2)\n",
" eq = str(rng.choice(list(EQUIP_F), p=[0.45, 0.25, 0.2, 0.1]))\n",
" fault = str(rng.choice(list(FAULT_H), p=FAULT_OF[eq]))\n",
" parts = bool(rng.random() < 0.75)\n",
" bad = weather != \"clear\"\n",
" std_h = FAULT_H[fault] * EQUIP_F[eq] * (1.35 if not parts else 1.0) * (1.15 if bad else 1.0) * rng.lognormal(0, 0.12)\n",
" rows.append({\"t_req\": d * 24 + float(np.clip(rng.normal(13, 4), 5, 23)), \"day\": d,\n",
" \"x\": float(x), \"y\": float(y), \"equip\": eq, \"fault\": fault,\n",
" \"crit\": int(rng.choice([1, 2, 3], p=CRIT_P[eq])), \"parts_ready\": parts,\n",
" \"weather\": str(weather), \"road\": \"unpaved\" if y > 30 and rng.random() < 0.7 else \"paved\",\n",
" \"field_ok\": bool(rng.random() < FIELD_P[fault] * (1.0 if parts else 0.5)),\n",
" \"std_repair_h\": round(float(std_h), 2), \"road_delay\": float(rng.lognormal(0, 0.15))})\n",
" df = pd.DataFrame(rows).sort_values(\"t_req\").reset_index(drop=True)\n",
" df.insert(0, \"req_id\", [f\"R{i:04d}\" for i in range(len(df))])\n",
" return df\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ถ๋ ๋ฐฐ์ ยท์ ๋น ๋ชจ์ ๊ณตํต ํจ์ ----\n",
"W0 = {\"crit\": 1.0, \"wait\": 0.3, \"short\": 0.2, \"balance\": 0.5} # ์ฐ์ ์์ยท๋ฐฐ์ ๊ธฐ๋ณธ ๊ฐ์ค์น(๋ฏธ์
9์์ ์ฌํ๋)\n",
"\n",
"\n",
"def travel_h(team, r, actual=False):\n",
" \"\"\"๋๋ก ๊ตด๊ณก 1.3๋ฐฐ, ํฌ์ฅ 35km/hยท๋นํฌ์ฅ 25km/h, ์
์ฒํ 20% ๊ฐ์. actual=True๋ฉด ์ค์ ๋๋ก ์ง์ฐ์ ๋ฐ์.\"\"\"\n",
" d = 1.3 * np.hypot(team[\"x\"] - r[\"x\"], team[\"y\"] - r[\"y\"])\n",
" v = (35.0 if r[\"road\"] == \"paved\" else 25.0) / (1.2 if r[\"weather\"] != \"clear\" else 1.0)\n",
" return d / v * (r[\"road_delay\"] if actual else 1.0)\n",
"\n",
"\n",
"def on_site_h(team, r):\n",
" \"\"\"ํ์ฅ ์ ๋น์๊ฐ: 4์ธ ๊ธฐ์ค ํ์ค์๊ฐ์ ํฌ์
์ธ์์ผ๋ก ํ์ฐ. ํ์ฅ ์๋ฆฌ ๋ถ๊ฐ๋ฉด ์ง๋จยทํ์ก ์กฐ์น 1์๊ฐ.\"\"\"\n",
" return r[\"std_repair_h\"] * 4 / team[\"crew\"] if r[\"field_ok\"] else 1.0\n",
"\n",
"\n",
"def priority(r, now, est_h, w):\n",
" return w[\"crit\"] * r[\"crit\"] + w[\"wait\"] * (now - r[\"t_req\"]) + w[\"short\"] / (1 + est_h)\n",
"\n",
"\n",
"def simulate(req, teams, policy=\"baseline\", w=None, cycle_h=1.0, est=None):\n",
" \"\"\"๋ฐฐ์ ์ฃผ๊ธฐ(cycle_h)๋ง๋ค ๋๊ธฐ ์์ฒญ์ ์ ๋ ฌํด ์ด๋์ ๋น๋ฐ์ ๋ฐฐ์ ํ๊ณ ์ถ๋ยท์ ๋นยท๋ณต๊ท๋ฅผ ๋ชจ์ํ๋ค.\n",
" baseline: ์ ์ ์์ + ๊ฑฐ๋ฆฌ ์ต๊ทผ์ ์ ๋น๋ฐ / priority: ์ฐ์ ์์ ์ ์ ์์ + ๋์ฐฉ ์์์๊ฐยท๋น์ผ ๋ถํ ๊ท ํ\"\"\"\n",
" w = w or W0\n",
" est = np.asarray(est if est is not None else req[\"std_repair_h\"].values, float)\n",
" n_days = int(req[\"day\"].max()) + 1\n",
" free = {t[\"team\"]: 0.0 for t in teams}\n",
" load = {t[\"team\"]: np.zeros(n_days + 5) for t in teams}\n",
" recs, out, pending, i, now = req.to_dict(\"records\"), [], [], 0, cycle_h\n",
" while i < len(recs) or pending:\n",
" while i < len(recs) and recs[i][\"t_req\"] <= now:\n",
" pending.append((i, recs[i])); i += 1\n",
" if policy == \"priority\":\n",
" pending.sort(key=lambda p: -priority(p[1], now, est[p[0]], w))\n",
" for k, r in pending:\n",
" cand = [t for t in teams if r[\"equip\"] in t[\"skills\"]]\n",
" day = min(int(now // 24), n_days + 4)\n",
" if policy == \"baseline\":\n",
" tm = min(cand, key=lambda t: np.hypot(t[\"x\"] - r[\"x\"], t[\"y\"] - r[\"y\"]))\n",
" else:\n",
" tm = min(cand, key=lambda t: max(free[t[\"team\"]], now) + travel_h(t, r)\n",
" + w[\"balance\"] * load[t[\"team\"]][day] / DUTY_H * 4)\n",
" backlog = max(0.0, free[tm[\"team\"]] - now)\n",
" go = max(free[tm[\"team\"]], now)\n",
" tr = travel_h(tm, r, actual=True)\n",
" work = on_site_h(tm, r)\n",
" free[tm[\"team\"]] = go + 2 * tr + work\n",
" load[tm[\"team\"]][min(int(go // 24), n_days + 4)] += 2 * tr + work\n",
" out.append({**r, \"idx\": k, \"team\": tm[\"team\"], \"dist_km\": float(np.hypot(tm[\"x\"] - r[\"x\"], tm[\"y\"] - r[\"y\"])),\n",
" \"backlog_h\": backlog, \"cycle_wait_h\": now - r[\"t_req\"], \"dispatch\": go, \"travel_h\": tr,\n",
" \"work_h\": work, \"response_h\": go + tr - r[\"t_req\"], \"downtime_h\": go + tr + work - r[\"t_req\"]})\n",
" pending, now = [], now + cycle_h\n",
" L = pd.DataFrame({k: v[:n_days] for k, v in load.items()}) / DUTY_H # ์ ๋น๋ฐ๋ณ ์ผ์ผ ๋ถํ์จ\n",
" return pd.DataFrame(out).sort_values(\"idx\").reset_index(drop=True), L\n",
"\n",
"\n",
"def metrics(res, L):\n",
" c3 = res[res[\"crit\"] == 3]\n",
" return {\"ํ๊ท ๋์ฐฉ(h)\": round(res[\"response_h\"].mean(), 2), \"ํต์ฌ์ฅ๋น ๋์ฐฉ(h)\": round(c3[\"response_h\"].mean(), 2),\n",
" \"ํต์ฌ์ฅ๋น 90%(h)\": round(c3[\"response_h\"].quantile(0.9), 2), \"ํ๊ท ๋ถํ์จ\": round(float(L.values.mean()), 3),\n",
" \"๋ถํ์จ ํธ์ฐจ\": round(float(L.mean().std()), 3), \"๊ณผ๋ถํ ์ผ์\": int((L > 1.0).values.sum()),\n",
" \"์ด ์ด๋(h)\": round(float(res[\"travel_h\"].sum() * 2), 1)}\n",
"\n",
"CW = {1: 1.0, 2: 3.0, 3: 6.0}\n",
"ALERT_H = 4.0 # ํต์ฌ์ฅ๋น(crit 3) ๋์ฐฉ ๋ชฉํ์๊ฐ\n",
"T0 = 15 * 24 + 10.0 # 15์ผ์ฐจ 10์ ๋์ ๋ค๋ฐ ์์ฒญ ๋ฐ์\n",
"\n",
"\n",
"def make_burst(n, seed):\n",
" \"\"\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋์ n๊ฐ์ ์์ฒญ(๊ฐ์ ์๊ฐ ์ ์). ์ผ๋ถ ์ ๋น๋ฐ์ ๊ธฐ์กด ์์
์ผ๋ก ๋ฐ์ ์ํ.\"\"\"\n",
" pool = make_requests(n_days=10, seed=seed)\n",
" b = pool.sample(n, random_state=seed).reset_index(drop=True).astype({\"x\": float, \"crit\": object})\n",
" b[\"t_req\"] = T0; b[\"req_id\"] = [f\"B{seed}-{i:02d}\" for i in range(n)]\n",
" return b\n",
"\n",
"\n",
"def validate(r):\n",
" errs = []\n",
" if not (0 <= r[\"x\"] <= AREA_KM and 0 <= r[\"y\"] <= AREA_KM):\n",
" errs.append(\"์ขํ ๋ฒ์ ์ค๋ฅ\")\n",
" if r[\"equip\"] not in EQUIP_F or r[\"fault\"] not in FAULT_H:\n",
" errs.append(\"์ฅ๋น/๊ณ ์ฅ ์ฝ๋ ์ค๋ฅ\")\n",
" if r[\"crit\"] not in (1, 2, 3):\n",
" errs.append(\"์ค์๋ ๋๋ฝ\")\n",
" return errs\n",
"\n",
"\n",
"def schedule(burst, teams, busy_until):\n",
" \"\"\"ํ ๋น ๋ฌธ์ ๋ก ๋์ ์์ฒญ์ (์ ๋น๋ฐ, ์๋ฒ)์ ๋ฐฐ์ . ๋น์ฉ = ์ค์๋ ๊ฐ์ค x ์์ ๋์ฐฉ์๊ฐ.\"\"\"\n",
" slots = int(np.ceil(len(burst) / len(teams))) + 2\n",
" cols = [(t, s) for t in teams for s in range(slots)]\n",
" avg = 4.0\n",
" cost = np.full((len(burst), len(cols)), 1e6)\n",
" for i, r in enumerate(burst.to_dict(\"records\")):\n",
" for j, (t, s) in enumerate(cols):\n",
" if r[\"equip\"] in t[\"skills\"]:\n",
" cost[i, j] = CW[r[\"crit\"]] * (busy_until[t[\"team\"]] - T0 + s * avg + travel_h(t, r))\n",
" ri, cj = linear_sum_assignment(cost)\n",
" plan = sorted(((i, cols[j][0], cols[j][1]) for i, j in zip(ri, cj)), key=lambda p: (p[1][\"team\"], p[2]))\n",
" free, rows = dict(busy_until), []\n",
" for i, t, s in plan:\n",
" r = burst.iloc[i]\n",
" go = max(free[t[\"team\"]], T0)\n",
" est = go + travel_h(t, r) - T0\n",
" act_tr = travel_h(t, r, actual=True)\n",
" rows.append({\"req_id\": r[\"req_id\"], \"crit\": r[\"crit\"], \"equip\": r[\"equip\"], \"team\": t[\"team\"], \"eta_h\": est,\n",
" \"actual_h\": go + act_tr - T0, \"alert\": r[\"crit\"] == 3 and est > ALERT_H})\n",
" free[t[\"team\"]] = go + 2 * act_tr + on_site_h(t, r) # ์ค์ ๋ก๋ ์ค์ธก ์ด๋์๊ฐ๋๋ก ๋ค์ ์ถ๋์ด ์ ํด์ง๋ค\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"def dashboard(plan, teams):\n",
" fig, ax = plt.subplots(1, 3, figsize=(14, 4))\n",
" ld = plan.groupby(\"team\").size().reindex([t[\"team\"] for t in teams], fill_value=0)\n",
" ax[0].bar(ld.index, ld.values); ax[0].set_title(\"Assigned requests per team\")\n",
" ax[1].scatter(plan[\"eta_h\"], plan[\"crit\"], c=np.where(plan[\"alert\"], \"red\", \"gray\")); ax[1].axvline(ALERT_H, ls=\"--\")\n",
" ax[1].set_xlabel(\"ETA (h)\"); ax[1].set_yticks([1, 2, 3]); ax[1].set_title(\"ETA by criticality (red = alert)\")\n",
" ax[2].hist(plan[\"eta_h\"], bins=15); ax[2].set_title(\"ETA distribution\")\n",
" fig.tight_layout(); fig.canvas.draw()\n",
" return fig\n",
"\n",
"\n",
"teams_all = make_teams()\n",
"rng = np.random.default_rng(4)\n",
"busy0 = {t[\"team\"]: T0 + float(rng.uniform(0, 3)) for t in teams_all} # ๊ธฐ์กด ์์
์์ฌ์๊ฐ\n",
"rows, keep = [], {}\n",
"for n, down, bad in [(20, None, 0), (20, \"MT5\", 3), (40, \"MT5\", 3), (60, \"MT5\", 5)]:\n",
" burst = make_burst(n, seed=n)\n",
" for k in range(bad): # ์์ ๋ฐ์ดํฐ ์ฃผ์
\n",
" burst.loc[k * 3, [\"x\", \"equip\", \"crit\"][k % 3]] = [999.0, \"unknown\", 0][k % 3]\n",
" teams = [t for t in teams_all if t[\"team\"] != down]\n",
" t0 = time.perf_counter()\n",
" ok = burst[[not validate(r) for r in burst.to_dict(\"records\")]]\n",
" manual = burst.drop(ok.index)\n",
" plan = schedule(ok, teams, {t[\"team\"]: busy0[t[\"team\"]] for t in teams})\n",
" fig = dashboard(plan, teams)\n",
" sec = time.perf_counter() - t0\n",
" truth = (plan[\"crit\"] == 3) & (plan[\"actual_h\"] > ALERT_H)\n",
" acc = float((plan[\"alert\"] == truth).mean())\n",
" lost = n - len(plan) - len(manual)\n",
" rows.append({\"์ํฉ\": f\"{n}๊ฐ์\" + (f\", {down} ๋ถ๊ฐ\" if down else \"\") + (f\", ์์ {bad}๊ฑด\" if bad else \"\"), \"๋ฐฐ์ \": len(plan), \"์๋ ํ์ธ\": len(manual),\n",
" \"์ ์ค\": lost, \"์ฒ๋ฆฌ+ํ๋ฉด(์ด)\": round(sec, 2), \"ํต์ฌ์ฅ๋น ๊ฒฝ๋ณด\": int(plan[\"alert\"].sum()), \"๊ฒฝ๋ณด ์ ํ๋\": round(acc, 3),\n",
" \"ํต์ฌ ํ๊ท ๋์ฐฉ(h)\": round(plan.loc[plan[\"crit\"] == 3, \"eta_h\"].mean(), 2)})\n",
" keep[n, down] = (fig, plan)\n",
" if (n, down) != (60, \"MT5\"):\n",
" plt.close(fig)\n",
"T = pd.DataFrame(rows)\n",
"print(\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋์ ๋ค๋ฐ ๊ธด๊ธ ์ ๋น ์์ฒญ ์คํธ๋ ์ค ํ
์คํธ\")\n",
"print(T.to_string(index=False))\n",
"m = T.iloc[1]\n",
"print(f\"\\n๋์ 20๊ฐ์(์ ๋น๋ฐ 1๊ฐ ๋ถ๊ฐยท์์ 3๊ฑด): ์ฒ๋ฆฌ+ํ๋ฉด {m['์ฒ๋ฆฌ+ํ๋ฉด(์ด)']}์ด (๋ชฉํ 2์ด ์ด๋ด: {'๋ฌ์ฑ' if T['์ฒ๋ฆฌ+ํ๋ฉด(์ด)'].max() <= 2 else '๋ฏธ๋ฌ'}), \"\n",
" f\"๊ฒฝ๋ณด ์ ํ๋ {m['๊ฒฝ๋ณด ์ ํ๋']:.1%} (๋ชฉํ 95% ์ด์: {'๋ฌ์ฑ' if T['๊ฒฝ๋ณด ์ ํ๋'].min() >= 0.95 else '๋ฏธ๋ฌ'}), ์ ์ค {int(T['์ ์ค'].sum())}๊ฑด\")\n",
"print(\"์์ ์์ฒญ์ ์ญ์ ํ์ง ์๊ณ '์๋ ํ์ธ' ๋ชฉ๋ก์ผ๋ก ๋๊ฒจ ๊ด์ ๋ณ์ด ์ ํยท๋ฌด์ ์ผ๋ก ์ฌํ์ธํ๋ค.\")\n",
"\n",
"plan = keep[20, \"MT5\"][1]\n",
"qt = []\n",
"for c in (1, 2, 3):\n",
" for e in EQUIP_F:\n",
" t0 = time.perf_counter(); plan[(plan[\"crit\"] == c) & (plan[\"equip\"] == e)]; qt.append((time.perf_counter() - t0) * 1000)\n",
"print(f\"์ค์๊ฐ ํํฐ(์ค์๋ x ์ฅ๋น 12์กฐํฉ) ํ๊ท {np.mean(qt):.2f} ms\")\n",
"plan = keep[40, \"MT5\"][1]\n",
"miss = plan[(plan[\"crit\"] == 3) & (plan[\"actual_h\"] > ALERT_H) & ~plan[\"alert\"]]\n",
"print(f\"40๊ฐ์ ์ํฉ: ๋์น ๊ฒฝ๋ณด {len(miss)}๊ฑด, ํ๊ฒฝ๋ณด {int((plan['alert'] & ~((plan['crit'] == 3) & (plan['actual_h'] > ALERT_H))).sum())}๊ฑด\"\n",
" \" -> ๋๋ก ์ง์ฐ(๋นํฌ์ฅยท๊ธฐ์)์ ETA์ ๋ฐ์ํ๋ฉด ์ค์ผ ์ ์๋ค.\")\n",
"print(\"๊ธฐ๋ฅ ์ถ์ ์ด์ฉ: ์ฒ๋ฆฌ ์๊ฐ์ด 2์ด๋ฅผ ๋์ผ๋ฉด ํ ๋น ์ต์ ํ ๋์ '์ค์๋ ์ + ์ต๊ทผ์ ' ๊ท์น์ผ๋ก ์ฆ์ ๋ฐฐ์ ํ๊ณ ์ต์ ํ๋ ๋ค์์ ์ฌ๊ณ์ฐํ๋ค.\")\n",
"plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
08 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋์ ๋ค๋ฐ ๊ธด๊ธ ์ ๋น ์์ฒญ ์คํธ๋ ์ค ํ
์คํธ\n",
" ์ํฉ ๋ฐฐ์ ์๋ ํ์ธ ์ ์ค ์ฒ๋ฆฌ+ํ๋ฉด(์ด) ํต์ฌ์ฅ๋น ๊ฒฝ๋ณด ๊ฒฝ๋ณด ์ ํ๋ ํต์ฌ ํ๊ท ๋์ฐฉ(h)\n",
" 20๊ฐ์ 20 0 0 0.24 1 1.000 3.17\n",
"20๊ฐ์, MT5 ๋ถ๊ฐ, ์์ 3๊ฑด 17 3 0 0.23 3 1.000 4.56\n",
"40๊ฐ์, MT5 ๋ถ๊ฐ, ์์ 3๊ฑด 37 3 0 0.23 3 0.973 4.55\n",
"60๊ฐ์, MT5 ๋ถ๊ฐ, ์์ 5๊ฑด 55 5 0 0.23 6 1.000 5.64\n",
"\n",
"๋์ 20๊ฐ์(์ ๋น๋ฐ 1๊ฐ ๋ถ๊ฐยท์์ 3๊ฑด): ์ฒ๋ฆฌ+ํ๋ฉด 0.23์ด (๋ชฉํ 2์ด ์ด๋ด: ๋ฌ์ฑ), ๊ฒฝ๋ณด ์ ํ๋ 100.0% (๋ชฉํ 95% ์ด์: ๋ฌ์ฑ), ์ ์ค 0๊ฑด\n",
"์์ ์์ฒญ์ ์ญ์ ํ์ง ์๊ณ '์๋ ํ์ธ' ๋ชฉ๋ก์ผ๋ก ๋๊ฒจ ๊ด์ ๋ณ์ด ์ ํยท๋ฌด์ ์ผ๋ก ์ฌํ์ธํ๋ค.\n",
"์ค์๊ฐ ํํฐ(์ค์๋ x ์ฅ๋น 12์กฐํฉ) ํ๊ท 0.69 ms\n",
"40๊ฐ์ ์ํฉ: ๋์น ๊ฒฝ๋ณด 0๊ฑด, ํ๊ฒฝ๋ณด 1๊ฑด -> ๋๋ก ์ง์ฐ(๋นํฌ์ฅยท๊ธฐ์)์ ETA์ ๋ฐ์ํ๋ฉด ์ค์ผ ์ ์๋ค.\n",
"๊ธฐ๋ฅ ์ถ์ ์ด์ฉ: ์ฒ๋ฆฌ ์๊ฐ์ด 2์ด๋ฅผ ๋์ผ๋ฉด ํ ๋น ์ต์ ํ ๋์ '์ค์๋ ์ + ์ต๊ทผ์ ' ๊ท์น์ผ๋ก ์ฆ์ ๋ฐฐ์ ํ๊ณ ์ต์ ํ๋ ๋ค์์ ์ฌ๊ณ์ฐํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
09. AAR ๊ธฐ๋ฐ ์ถ๋ ์ฐ์ ์์ ๋ฐฐ์ ์ค๋ฅ ๋ถ์ ๋ฐ ๊ฐ์ค์น ๋งคํธ๋ฆญ์ค ์ฌํ๋\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"AAR ๊ธฐ๋ฐ ์ถ๋ ์ฐ์ ์์ ๋ฐฐ์ ์ค๋ฅ๋ฅผ ๋ถ์ํ๊ณ ๊ฐ์ค์น๋ฅผ ์ฌํ๋ํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ถ๋ ์ฐ์ ์์ ๋ฐฐ์ ๊ฒฐ๊ณผ๋ฅผ AAR๋ก ๊ฒํ ํด ์ค๋ฅ ์์ธ์ ๋ถ์ํ๊ณ , ๊ฐ์ค์น ๋งคํธ๋ฆญ์ค๋ฅผ ์ฌํ๋ํด ๋์
ํจ๊ณผ๋ฅผ ์ ๋ ์งํ๋ก ์ฐ์ถํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **ํต์ฌ์ฅ๋น ๋์ฐฉ 4์๊ฐ ์ด๊ณผ ์ฌ๋ก๋ฅผ ์ ๋น๋ฐ ๋ถํ ์ง์คยท์๊ฑฐ๋ฆฌ ๋ฐฐ์ ยท๋๋กยท๊ธฐ์ ์ง์ฐยท๋ฐฐ์ ์ฃผ๊ธฐ ๋๊ธฐ๋ก ๋ถ๋ฅ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `reports/mnt10_roi_analysis.md` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ถ์ ์ฐ์์ ๊ฐ๊ด์ฑ ๋ฐ ๊ตฐ ์ฌ์ /๊ตฐ์ ๊ธฐํ ๋ถ์ ์ค๋๋ ฅ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๋น์ฉ ์ ๊ฐ์ก, ์๊ฐ ๋จ์ถ๋ฅ ๋ฑ 3๋ ์ ๋ ํจ๊ณผ ์งํ ์ฐ์ถ\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ROI ๋ถ์ ๋ณด๊ณ ์ ์์ฑ ๋ฐ ์ฃผ์ ์งํ ๋ฏผ๊ฐ๋ ๋ถ์ ์๋ก\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ํ๊ณ/๊ธฐํ ๊ธฐ์ค ๋ชจ๋ธ๊ณผ์ ๋น๊ต ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์ถ๋ ์ฐ์ ์์ ๋ฐฐ์ ๊ฒฐ๊ณผ๋ฅผ ์ฌํ๊ฒํ (AAR)ํด ํต์ฌ์ฅ๋น ์ง์ฐ ์ฌ๋ก์ ์์ธ(์ ๋น๋ฐ ๋ถํ ์ง์คยท์๊ฑฐ๋ฆฌ ๋ฐฐ์ ยท๋๋กยท๊ธฐ์ ์ง์ฐ ๋ฑ)์ ๋ถ๋ฅํ๊ณ , ๊ณผ๊ฑฐ ๊ธฐ๊ฐ์ผ๋ก ์ฐ์ ์์ยท๋ฐฐ์ ๊ฐ์ค์น ๋งคํธ๋ฆญ์ค๋ฅผ ์ฌํ๋ํด ํ๊ฐ ๊ธฐ๊ฐ์ ๊ฒ์ฆํ๋ค. ๊ฐ๋์ค๋จ ๋น์ฉยทํต์ฌ์ฅ๋น ๋์ฐฉ์๊ฐยท์ ๋น์ธ๋ ฅ ์ด๊ณผ๊ทผ๋ฌด์ 3๋ ์ ๋ ํจ๊ณผ ์งํ์ ROI ์์ฝ์ ์ถ๋ ฅํ๋ค.\"\"\"\n",
"import itertools\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.ensemble import GradientBoostingRegressor\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ผ์ ์ด๋์ ๋น๋ฐยท๊ธด๊ธ ์ ๋น ์์ฒญ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์์นยท์ฅ๋นยท์ธ์ ์ ๋ณด ์์)\n",
"# 60km x 60km ๊ฐ์ ์ง์ญ์ ์ด๋์ ๋น๋ฐ 6๊ฐ์ 30์ผ๊ฐ์ ๊ธด๊ธ ์ ๋น ์์ฒญ์ ๋ง๋ ๋ค.\n",
"# ์ฅ๋น ๊ตฌ๋ถ(mobility/fires/comms/power)๊ณผ ๊ณ ์ฅ ์ ํ์ ๊ต์ก์ฉ ์ถ์ ๋ถ๋ฅ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"AREA_KM, DUTY_H = 60, 10.0 # ๊ฐ์ ์์ ์ง์ญ ํฌ๊ธฐ, ์ ๋น๋ฐ 1์ผ ๊ธฐ์ค ๊ฐ์ฉ์๊ฐ\n",
"FAULT_H = {\"engine\": 6.0, \"drivetrain\": 5.0, \"hydraulic\": 3.0, \"electric\": 2.0, \"comm_module\": 1.5, \"cooling\": 2.5}\n",
"FIELD_P = {\"engine\": 0.3, \"drivetrain\": 0.35, \"hydraulic\": 0.75, \"electric\": 0.9, \"comm_module\": 0.95, \"cooling\": 0.8}\n",
"EQUIP_F = {\"mobility\": 1.0, \"fires\": 1.2, \"comms\": 0.7, \"power\": 0.8}\n",
"FAULT_OF = {\"mobility\": [0.3, 0.25, 0.2, 0.1, 0.0, 0.15], \"fires\": [0.2, 0.15, 0.35, 0.15, 0.0, 0.15],\n",
" \"comms\": [0.0, 0.0, 0.0, 0.4, 0.5, 0.1], \"power\": [0.35, 0.0, 0.0, 0.35, 0.0, 0.3]}\n",
"CRIT_P = {\"mobility\": [0.45, 0.35, 0.2], \"fires\": [0.2, 0.35, 0.45], \"comms\": [0.3, 0.4, 0.3], \"power\": [0.6, 0.3, 0.1]}\n",
"HUBS = [(15, 20), (45, 25), (30, 50)] # ๊ฐ์ ํ๋ จ์ฅ 3๊ณณ(์์ฒญ์ด ๋ชฐ๋ฆฌ๋ ์ง์ )\n",
"\n",
"\n",
"def make_teams(seed=0):\n",
" rng = np.random.default_rng(seed)\n",
" base = [(8, 10), (30, 6), (52, 12), (10, 44), (32, 38), (54, 50)]\n",
" skills = [[\"fires\", \"mobility\"], [\"comms\", \"mobility\"], [\"fires\", \"power\"],\n",
" [\"mobility\", \"power\"], [\"comms\", \"fires\"], [\"comms\", \"mobility\", \"power\"]]\n",
" return [{\"team\": f\"MT{i + 1}\", \"x\": float(x), \"y\": float(y), \"crew\": int(rng.integers(3, 6)), \"skills\": s}\n",
" for i, ((x, y), s) in enumerate(zip(base, skills))]\n",
"\n",
"\n",
"def make_requests(n_days=30, per_day=10, seed=1, surge_days=(8, 9, 10, 11, 22, 23, 24)):\n",
" \"\"\"ํ๋ฃจ ํ๊ท per_day๊ฑด, ํ๋ จ ์ฃผ๊ฐ(surge_days)์๋ 1.6๋ฐฐ์ ๊ธด๊ธ ์ ๋น ์์ฒญ์ ๋ง๋ ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for d in range(n_days):\n",
" n = rng.poisson(per_day * (1.6 if d in surge_days else 1.0))\n",
" weather = rng.choice([\"clear\", \"rain\", \"snow\"], p=[0.65, 0.25, 0.1])\n",
" for _ in range(n):\n",
" if rng.random() < 0.7:\n",
" hx, hy = HUBS[rng.integers(0, 3)]\n",
" x, y = np.clip(rng.normal(hx, 6), 0, AREA_KM), np.clip(rng.normal(hy, 6), 0, AREA_KM)\n",
" else:\n",
" x, y = rng.uniform(0, AREA_KM, 2)\n",
" eq = str(rng.choice(list(EQUIP_F), p=[0.45, 0.25, 0.2, 0.1]))\n",
" fault = str(rng.choice(list(FAULT_H), p=FAULT_OF[eq]))\n",
" parts = bool(rng.random() < 0.75)\n",
" bad = weather != \"clear\"\n",
" std_h = FAULT_H[fault] * EQUIP_F[eq] * (1.35 if not parts else 1.0) * (1.15 if bad else 1.0) * rng.lognormal(0, 0.12)\n",
" rows.append({\"t_req\": d * 24 + float(np.clip(rng.normal(13, 4), 5, 23)), \"day\": d,\n",
" \"x\": float(x), \"y\": float(y), \"equip\": eq, \"fault\": fault,\n",
" \"crit\": int(rng.choice([1, 2, 3], p=CRIT_P[eq])), \"parts_ready\": parts,\n",
" \"weather\": str(weather), \"road\": \"unpaved\" if y > 30 and rng.random() < 0.7 else \"paved\",\n",
" \"field_ok\": bool(rng.random() < FIELD_P[fault] * (1.0 if parts else 0.5)),\n",
" \"std_repair_h\": round(float(std_h), 2), \"road_delay\": float(rng.lognormal(0, 0.15))})\n",
" df = pd.DataFrame(rows).sort_values(\"t_req\").reset_index(drop=True)\n",
" df.insert(0, \"req_id\", [f\"R{i:04d}\" for i in range(len(df))])\n",
" return df\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ถ๋ ๋ฐฐ์ ยท์ ๋น ๋ชจ์ ๊ณตํต ํจ์ ----\n",
"W0 = {\"crit\": 1.0, \"wait\": 0.3, \"short\": 0.2, \"balance\": 0.5} # ์ฐ์ ์์ยท๋ฐฐ์ ๊ธฐ๋ณธ ๊ฐ์ค์น(๋ฏธ์
9์์ ์ฌํ๋)\n",
"\n",
"\n",
"def travel_h(team, r, actual=False):\n",
" \"\"\"๋๋ก ๊ตด๊ณก 1.3๋ฐฐ, ํฌ์ฅ 35km/hยท๋นํฌ์ฅ 25km/h, ์
์ฒํ 20% ๊ฐ์. actual=True๋ฉด ์ค์ ๋๋ก ์ง์ฐ์ ๋ฐ์.\"\"\"\n",
" d = 1.3 * np.hypot(team[\"x\"] - r[\"x\"], team[\"y\"] - r[\"y\"])\n",
" v = (35.0 if r[\"road\"] == \"paved\" else 25.0) / (1.2 if r[\"weather\"] != \"clear\" else 1.0)\n",
" return d / v * (r[\"road_delay\"] if actual else 1.0)\n",
"\n",
"\n",
"def on_site_h(team, r):\n",
" \"\"\"ํ์ฅ ์ ๋น์๊ฐ: 4์ธ ๊ธฐ์ค ํ์ค์๊ฐ์ ํฌ์
์ธ์์ผ๋ก ํ์ฐ. ํ์ฅ ์๋ฆฌ ๋ถ๊ฐ๋ฉด ์ง๋จยทํ์ก ์กฐ์น 1์๊ฐ.\"\"\"\n",
" return r[\"std_repair_h\"] * 4 / team[\"crew\"] if r[\"field_ok\"] else 1.0\n",
"\n",
"\n",
"def priority(r, now, est_h, w):\n",
" return w[\"crit\"] * r[\"crit\"] + w[\"wait\"] * (now - r[\"t_req\"]) + w[\"short\"] / (1 + est_h)\n",
"\n",
"\n",
"def simulate(req, teams, policy=\"baseline\", w=None, cycle_h=1.0, est=None):\n",
" \"\"\"๋ฐฐ์ ์ฃผ๊ธฐ(cycle_h)๋ง๋ค ๋๊ธฐ ์์ฒญ์ ์ ๋ ฌํด ์ด๋์ ๋น๋ฐ์ ๋ฐฐ์ ํ๊ณ ์ถ๋ยท์ ๋นยท๋ณต๊ท๋ฅผ ๋ชจ์ํ๋ค.\n",
" baseline: ์ ์ ์์ + ๊ฑฐ๋ฆฌ ์ต๊ทผ์ ์ ๋น๋ฐ / priority: ์ฐ์ ์์ ์ ์ ์์ + ๋์ฐฉ ์์์๊ฐยท๋น์ผ ๋ถํ ๊ท ํ\"\"\"\n",
" w = w or W0\n",
" est = np.asarray(est if est is not None else req[\"std_repair_h\"].values, float)\n",
" n_days = int(req[\"day\"].max()) + 1\n",
" free = {t[\"team\"]: 0.0 for t in teams}\n",
" load = {t[\"team\"]: np.zeros(n_days + 5) for t in teams}\n",
" recs, out, pending, i, now = req.to_dict(\"records\"), [], [], 0, cycle_h\n",
" while i < len(recs) or pending:\n",
" while i < len(recs) and recs[i][\"t_req\"] <= now:\n",
" pending.append((i, recs[i])); i += 1\n",
" if policy == \"priority\":\n",
" pending.sort(key=lambda p: -priority(p[1], now, est[p[0]], w))\n",
" for k, r in pending:\n",
" cand = [t for t in teams if r[\"equip\"] in t[\"skills\"]]\n",
" day = min(int(now // 24), n_days + 4)\n",
" if policy == \"baseline\":\n",
" tm = min(cand, key=lambda t: np.hypot(t[\"x\"] - r[\"x\"], t[\"y\"] - r[\"y\"]))\n",
" else:\n",
" tm = min(cand, key=lambda t: max(free[t[\"team\"]], now) + travel_h(t, r)\n",
" + w[\"balance\"] * load[t[\"team\"]][day] / DUTY_H * 4)\n",
" backlog = max(0.0, free[tm[\"team\"]] - now)\n",
" go = max(free[tm[\"team\"]], now)\n",
" tr = travel_h(tm, r, actual=True)\n",
" work = on_site_h(tm, r)\n",
" free[tm[\"team\"]] = go + 2 * tr + work\n",
" load[tm[\"team\"]][min(int(go // 24), n_days + 4)] += 2 * tr + work\n",
" out.append({**r, \"idx\": k, \"team\": tm[\"team\"], \"dist_km\": float(np.hypot(tm[\"x\"] - r[\"x\"], tm[\"y\"] - r[\"y\"])),\n",
" \"backlog_h\": backlog, \"cycle_wait_h\": now - r[\"t_req\"], \"dispatch\": go, \"travel_h\": tr,\n",
" \"work_h\": work, \"response_h\": go + tr - r[\"t_req\"], \"downtime_h\": go + tr + work - r[\"t_req\"]})\n",
" pending, now = [], now + cycle_h\n",
" L = pd.DataFrame({k: v[:n_days] for k, v in load.items()}) / DUTY_H # ์ ๋น๋ฐ๋ณ ์ผ์ผ ๋ถํ์จ\n",
" return pd.DataFrame(out).sort_values(\"idx\").reset_index(drop=True), L\n",
"\n",
"\n",
"def metrics(res, L):\n",
" c3 = res[res[\"crit\"] == 3]\n",
" return {\"ํ๊ท ๋์ฐฉ(h)\": round(res[\"response_h\"].mean(), 2), \"ํต์ฌ์ฅ๋น ๋์ฐฉ(h)\": round(c3[\"response_h\"].mean(), 2),\n",
" \"ํต์ฌ์ฅ๋น 90%(h)\": round(c3[\"response_h\"].quantile(0.9), 2), \"ํ๊ท ๋ถํ์จ\": round(float(L.values.mean()), 3),\n",
" \"๋ถํ์จ ํธ์ฐจ\": round(float(L.mean().std()), 3), \"๊ณผ๋ถํ ์ผ์\": int((L > 1.0).values.sum()),\n",
" \"์ด ์ด๋(h)\": round(float(res[\"travel_h\"].sum() * 2), 1)}\n",
"\n",
"# ---- ์ ์ ์์ ํ์ ํผ์ฒ(๋ฏธ์
4์ ๋์ผ) ----\n",
"def make_features(req, teams):\n",
" \"\"\"์ ์ ์์ ์ ์ ์ ์๋ ์ ๋ณด๋ง์ผ๋ก ํ์ ํผ์ฒ๋ฅผ ๋ง๋ ๋ค(๋ฏธ๋ ์ ๋ณด ๋์ค ๋ฐฉ์ง).\"\"\"\n",
" X = pd.DataFrame(index=req.index)\n",
" X[\"hour\"] = req[\"t_req\"] % 24\n",
" X[\"is_night\"] = (X[\"hour\"] >= 19).astype(int)\n",
" X[\"dow\"] = req[\"day\"] % 7\n",
" X[\"weather_bad\"] = (req[\"weather\"] != \"clear\").astype(int)\n",
" X[\"unpaved\"] = (req[\"road\"] == \"unpaved\").astype(int)\n",
" X[\"no_parts\"] = (~req[\"parts_ready\"]).astype(int)\n",
" X[\"std_table_h\"] = req[\"fault\"].map(FAULT_H) * req[\"equip\"].map(EQUIP_F) # ์ ๋น ํ์ค์๊ฐํ ๊ฐ\n",
" X[\"crit_x_noparts\"] = req[\"crit\"] * X[\"no_parts\"]\n",
" X[\"bad_x_table\"] = X[\"weather_bad\"] * X[\"std_table_h\"]\n",
" X[\"near_team_km\"] = [min(np.hypot(t[\"x\"] - x, t[\"y\"] - y) for t in teams if e in t[\"skills\"]) for x, y, e in zip(req[\"x\"], req[\"y\"], req[\"equip\"])]\n",
" X[\"hub_km\"] = [min(np.hypot(hx - x, hy - y) for hx, hy in HUBS) for x, y in zip(req[\"x\"], req[\"y\"])]\n",
" X[\"req_24h\"] = [((req[\"t_req\"] > t - 24) & (req[\"t_req\"] <= t)).sum() for t in req[\"t_req\"]]\n",
" X[\"fault_past_mean\"] = req.groupby(\"fault\")[\"std_repair_h\"].transform(lambda s: s.shift().expanding().mean()).fillna(req[\"std_repair_h\"].mean())\n",
" X[\"crit\"] = req[\"crit\"]\n",
" for e in EQUIP_F:\n",
" X[f\"eq_{e}\"] = (req[\"equip\"] == e).astype(int)\n",
" return X\n",
"\n",
"\n",
"def fit_repair_model(hist, teams):\n",
" \"\"\"๊ณผ๊ฑฐ ์์ฒญ์ผ๋ก ์ ๋น ์์์๊ฐ(4์ธ ๊ธฐ์ค) ์์ธก ๋ชจ๋ธ์ ํ์ตํ๋ค.\"\"\"\n",
" return GradientBoostingRegressor(n_estimators=200, max_depth=3, learning_rate=0.05, random_state=0).fit(make_features(hist, teams), hist[\"std_repair_h\"])\n",
"\n",
"CW = {1: 1.0, 2: 3.0, 3: 6.0} # ์ค์๋๋ณ ๊ฐ๋์ค๋จ ๋น์ฉ ์ง์\n",
"PRICE = {\"downtime\": 10, \"move\": 5, \"overtime\": 2} # [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋จ๊ฐ(๋ง์): ๋น์ฉ์ง์ 1, ์ฐจ๋ ์ดํ 1h, ์ด๊ณผ๊ทผ๋ฌด 1์ธยทh\n",
"TARGET_H = 4.0\n",
"\n",
"\n",
"def evaluate(res, L, teams):\n",
" crew = pd.Series({t[\"team\"]: t[\"crew\"] for t in teams})\n",
" over = ((L - 1.0).clip(lower=0) * DUTY_H * crew).values.sum() # ์ด๊ณผ๊ทผ๋ฌด ์ธยท์๊ฐ\n",
" return {\"๊ฐ๋์ค๋จ ๋น์ฉ์ง์\": float((res[\"crit\"].map(CW) * res[\"downtime_h\"]).sum()),\n",
" \"ํต์ฌ์ฅ๋น ๋์ฐฉ(h)\": float(res.loc[res[\"crit\"] == 3, \"response_h\"].mean()),\n",
" \"์ด๊ณผ๊ทผ๋ฌด(์ธยทh)\": float(over), \"์ฐจ๋ ์ดํ(h)\": float(res[\"travel_h\"].sum() * 2),\n",
" \"ํต์ฌ 4h ์ด๊ณผ(๊ฑด)\": int(((res[\"crit\"] == 3) & (res[\"response_h\"] > TARGET_H)).sum())}\n",
"\n",
"\n",
"def won(m):\n",
" return m[\"๊ฐ๋์ค๋จ ๋น์ฉ์ง์\"] * PRICE[\"downtime\"] + m[\"์ฐจ๋ ์ดํ(h)\"] * PRICE[\"move\"] + m[\"์ด๊ณผ๊ทผ๋ฌด(์ธยทh)\"] * PRICE[\"overtime\"]\n",
"\n",
"\n",
"def objective(m):\n",
" \"\"\"ํ๋ ๋ชฉํ: ๊ฐ์ ์ด๋น์ฉ + ํต์ฌ์ฅ๋น 4์๊ฐ ์ด๊ณผ 1๊ฑด๋น ์๋ฌด ์ํฅ ๋ฒ์ (๊ฐ์ 500๋ง์).\"\"\"\n",
" return won(m) + 500 * m[\"ํต์ฌ 4h ์ด๊ณผ(๊ฑด)\"]\n",
"\n",
"\n",
"teams = make_teams()\n",
"hist, req = make_requests(seed=11), make_requests(seed=1)\n",
"model = fit_repair_model(hist, teams)\n",
"est_h, est_r = model.predict(make_features(hist, teams)), model.predict(make_features(req, teams))\n",
"\n",
"# 1) AAR: ํํ ๊ฐ์ค์น(W0)๋ก ์ด์ฉํ ํ๊ฐ ๊ธฐ๊ฐ์ ํต์ฌ์ฅ๋น ์ง์ฐ ์์ธ ๋ถ๋ฅ\n",
"res, L = simulate(req, teams, \"priority\", w=W0, est=est_r)\n",
"late = res[(res[\"crit\"] == 3) & (res[\"response_h\"] > TARGET_H)].copy()\n",
"est_tr = [travel_h(next(t for t in teams if t[\"team\"] == tm), r) for tm, r in zip(late[\"team\"], late.to_dict(\"records\"))]\n",
"late[\"์์ธ\"] = np.select([late[\"backlog_h\"] > 1.0, late[\"dist_km\"] > 25, late[\"travel_h\"] > np.array(est_tr) * 1.2],\n",
" [\"์ ๋น๋ฐ ๋ถํ ์ง์ค(์ ์์
๋๊ธฐ)\", \"์๊ฑฐ๋ฆฌ ์ ๋น๋ฐ ๋ฐฐ์ \", \"๋๋กยท๊ธฐ์ ์ง์ฐ\"], \"๋ฐฐ์ ์ฃผ๊ธฐ ๋๊ธฐ\")\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] AAR: ํต์ฌ์ฅ๋น ์์ฒญ {int((res['crit'] == 3).sum())}๊ฑด ์ค {TARGET_H:.0f}์๊ฐ ์ด๊ณผ {len(late)}๊ฑด\")\n",
"print(late[\"์์ธ\"].value_counts().to_string())\n",
"print(\"ํ๋ จ ์ฃผ๊ฐ(์์ฒญ ๊ธ์ฆ) ๋ฐ์ ๋น์จ {:.0%}, ์ง์ฐ ๊ฑด์ ํ๊ท ์ ์์
๋๊ธฐ {:.1f}h\".format(late[\"day\"].isin([8, 9, 10, 11, 22, 23, 24]).mean(), late[\"backlog_h\"].mean()))\n",
"\n",
"# 2) ๊ฐ์ค์น ๋งคํธ๋ฆญ์ค ์ฌํ๋: ๊ณผ๊ฑฐ ๊ธฐ๊ฐ(hist)์์ ํ์ -> ํ๊ฐ ๊ธฐ๊ฐ(req)์์ ๊ฒ์ฆ\n",
"grid = []\n",
"for c, wt, sh, bl in itertools.product([0.5, 1.0, 2.0, 3.0], [0.1, 0.3, 0.6], [0.2, 1.0], [0.0, 0.5, 1.0, 2.0, 3.0]):\n",
" w = {\"crit\": c, \"wait\": wt, \"short\": sh, \"balance\": bl}\n",
" r_, L_ = simulate(hist, teams, \"priority\", w=w, est=est_h)\n",
" grid.append({**w, \"cost\": objective(evaluate(r_, L_, teams))})\n",
"G = pd.DataFrame(grid).sort_values(\"cost\")\n",
"W1 = {k: float(G.iloc[0][k]) for k in W0}\n",
"print(f\"\\n๊ฐ์ค์น ์กฐํฉ {len(G)}๊ฐ ํ์(๊ณผ๊ฑฐ 30์ผ). ํํ {W0} -> ์ฌํ๋ {W1}\")\n",
"\n",
"rows = {}\n",
"rb, Lb = simulate(req, teams, \"baseline\")\n",
"rows[\"Baseline(๊ฑฐ๋ฆฌ)\"] = evaluate(rb, Lb, teams)\n",
"rows[\"ํํ W0\"] = evaluate(res, L, teams)\n",
"r1, L1 = simulate(req, teams, \"priority\", w=W1, est=est_r)\n",
"rows[\"์ฌํ๋ W1\"] = evaluate(r1, L1, teams)\n",
"T = pd.DataFrame(rows).T\n",
"T[\"๊ฐ์ ์ด๋น์ฉ(๋ง์)\"] = [won(m) for m in rows.values()]\n",
"print(\"\\n[ํ๊ฐ ๊ธฐ๊ฐ 30์ผ ๋น๊ต]\")\n",
"print(T.round(1).to_string())\n",
"print(f\"ํต์ฌ์ฅ๋น {TARGET_H:.0f}์๊ฐ ์ด๊ณผ: ํํ {len(late)}๊ฑด -> ์ฌํ๋ {rows['์ฌํ๋ W1']['ํต์ฌ 4h ์ด๊ณผ(๊ฑด)']}๊ฑด\")\n",
"w0, w1 = rows[\"ํํ W0\"], rows[\"์ฌํ๋ W1\"]\n",
"print(f\"์ฌํ๋ ํจ๊ณผ(ํํ ๋๋น): ๊ฐ์ ์ด๋น์ฉ {won(w1) / won(w0) - 1:+.1%}, ์ฐจ๋ ์ดํ {w1['์ฐจ๋ ์ดํ(h)'] / w0['์ฐจ๋ ์ดํ(h)'] - 1:+.1%}\")\n",
"print(\"AAR ๊ฒฐ๋ก : ๋จ์ ์ง์ฐ์ ์ ์ ๋น๋ฐ์ด ๋์์ ๋ฐ์ ๊ธ์ฆ ๊ตฌ๊ฐ์ ๋๊ธฐ์์ ์๊ฒจ ๊ฐ์ค์น๋ก๋ ์ค์ง ์๋๋ค\"\n",
" \" -> ํ๋ จ ์ฃผ๊ฐ ์๊ณ ์ ์ ๋น๋ฐ ์ฌ์ ์ฆ์ยท์๊ธ ์ ๋น๋ถ๋ ์ง์ ์์ฒญ์ ์ด์ ์กฐ์น๋ก ๊ถ๊ณ ํ๋ค.\")\n",
"\n",
"# 3) 3๋ ์ ๋ ํจ๊ณผ ์งํ(Baseline ๋๋น ์ฌํ๋ W1)\n",
"b, n = rows[\"Baseline(๊ฑฐ๋ฆฌ)\"], rows[\"์ฌํ๋ W1\"]\n",
"save = (b[\"๊ฐ๋์ค๋จ ๋น์ฉ์ง์\"] - n[\"๊ฐ๋์ค๋จ ๋น์ฉ์ง์\"]) * PRICE[\"downtime\"]\n",
"cut_h = 1 - n[\"ํต์ฌ์ฅ๋น ๋์ฐฉ(h)\"] / b[\"ํต์ฌ์ฅ๋น ๋์ฐฉ(h)\"]\n",
"over = b[\"์ด๊ณผ๊ทผ๋ฌด(์ธยทh)\"] - n[\"์ด๊ณผ๊ทผ๋ฌด(์ธยทh)\"]\n",
"print(\"\\n[3๋ ์ ๋ ํจ๊ณผ ์งํ(30์ผ, Baseline ๋๋น)]\")\n",
"print(f\" 1) ๊ฐ๋์ค๋จ ๋น์ฉ ์ ๊ฐ์ก: {save:,.0f}๋ง์(๊ฐ์ ๋จ๊ฐ)\")\n",
"print(f\" 2) ํต์ฌ์ฅ๋น ๋์ฐฉ์๊ฐ ๋จ์ถ๋ฅ : {cut_h:.1%} ({b['ํต์ฌ์ฅ๋น ๋์ฐฉ(h)']:.2f}h -> {n['ํต์ฌ์ฅ๋น ๋์ฐฉ(h)']:.2f}h)\")\n",
"print(f\" 3) ์ ๋น์ธ๋ ฅ ์ด๊ณผ๊ทผ๋ฌด ๋ณํ: {over:+,.0f}์ธยทh (์์=๊ฐ์)\")\n",
"move = (n[\"์ฐจ๋ ์ดํ(h)\"] - b[\"์ฐจ๋ ์ดํ(h)\"]) * PRICE[\"move\"]\n",
"print(f\" ๋ถ๋๋น์ฉ: ์ฐจ๋ ์ดํ ์ฆ๊ฐ {move:,.0f}๋ง์ -> ์ํจ๊ณผ {save + over * PRICE['overtime'] - move:,.0f}๋ง์/์\")\n",
"print(\"ROI ๋ก๋๋งต: ๋จ๊ธฐ(1~3๊ฐ์) ๊ด์ ํ๋ฉดยท์ฐ์ ์์ ์๋ฒ -> ์ค๊ธฐ(6๊ฐ์) ๋ถ๊ธฐ๋ณ AAR ๊ฐ์ค์น ์ฌํ๋ -> ์ฅ๊ธฐ(1๋
) ์ ๋น์ฒด๊ณ ์ฐ๋\")\n",
"print(\"๋ชจ๋ ๊ธ์ก์ ๊ต์ก์ฉ ๊ฐ์ ๋จ๊ฐ๋ก ๊ณ์ฐํ ์๋ ๋น๊ต๊ฐ์ด๋ฉฐ ์ค์ ์์ฐ ๊ทผ๊ฑฐ๋ก ์ฐ์ง ์๋๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(11, 4))\n",
"late[\"์์ธ\"].value_counts().rename(index={\"์ ๋น๋ฐ ๋ถํ ์ง์ค(์ ์์
๋๊ธฐ)\": \"team backlog\", \"์๊ฑฐ๋ฆฌ ์ ๋น๋ฐ ๋ฐฐ์ \": \"far team\",\n",
" \"๋๋กยท๊ธฐ์ ์ง์ฐ\": \"road/weather\", \"๋ฐฐ์ ์ฃผ๊ธฐ ๋๊ธฐ\": \"cycle wait\"}).plot.bar(ax=ax[0], rot=0)\n",
"ax[0].set_title(\"AAR: causes of late critical response\"); ax[0].set_xlabel(\"\")\n",
"ax[1].bar([\"baseline\", \"W0\", \"W1\"], T[\"๊ฐ์ ์ด๋น์ฉ(๋ง์)\"]); ax[1].set_title(\"Virtual total cost (30 days)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
09 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] AAR: ํต์ฌ์ฅ๋น ์์ฒญ 99๊ฑด ์ค 4์๊ฐ ์ด๊ณผ 8๊ฑด\n",
"์์ธ\n",
"์ ๋น๋ฐ ๋ถํ ์ง์ค(์ ์์
๋๊ธฐ) 8\n",
"ํ๋ จ ์ฃผ๊ฐ(์์ฒญ ๊ธ์ฆ) ๋ฐ์ ๋น์จ 62%, ์ง์ฐ ๊ฑด์ ํ๊ท ์ ์์
๋๊ธฐ 3.5h\n",
"\n",
"๊ฐ์ค์น ์กฐํฉ 120๊ฐ ํ์(๊ณผ๊ฑฐ 30์ผ). ํํ {'crit': 1.0, 'wait': 0.3, 'short': 0.2, 'balance': 0.5} -> ์ฌํ๋ {'crit': 0.5, 'wait': 0.1, 'short': 0.2, 'balance': 0.0}\n",
"\n",
"[ํ๊ฐ ๊ธฐ๊ฐ 30์ผ ๋น๊ต]\n",
" ๊ฐ๋์ค๋จ ๋น์ฉ์ง์ ํต์ฌ์ฅ๋น ๋์ฐฉ(h) ์ด๊ณผ๊ทผ๋ฌด(์ธยทh) ์ฐจ๋ ์ดํ(h) ํต์ฌ 4h ์ด๊ณผ(๊ฑด) ๊ฐ์ ์ด๋น์ฉ(๋ง์)\n",
"Baseline(๊ฑฐ๋ฆฌ) 6494.6 3.1 1018.3 504.7 27.0 69506.0\n",
"ํํ W0 5055.3 1.9 869.7 744.9 8.0 56017.0\n",
"์ฌํ๋ W1 4962.9 1.9 872.4 695.1 8.0 54848.7\n",
"ํต์ฌ์ฅ๋น 4์๊ฐ ์ด๊ณผ: ํํ 8๊ฑด -> ์ฌํ๋ 8๊ฑด\n",
"์ฌํ๋ ํจ๊ณผ(ํํ ๋๋น): ๊ฐ์ ์ด๋น์ฉ -2.1%, ์ฐจ๋ ์ดํ -6.7%\n",
"AAR ๊ฒฐ๋ก : ๋จ์ ์ง์ฐ์ ์ ์ ๋น๋ฐ์ด ๋์์ ๋ฐ์ ๊ธ์ฆ ๊ตฌ๊ฐ์ ๋๊ธฐ์์ ์๊ฒจ ๊ฐ์ค์น๋ก๋ ์ค์ง ์๋๋ค -> ํ๋ จ ์ฃผ๊ฐ ์๊ณ ์ ์ ๋น๋ฐ ์ฌ์ ์ฆ์ยท์๊ธ ์ ๋น๋ถ๋ ์ง์ ์์ฒญ์ ์ด์ ์กฐ์น๋ก ๊ถ๊ณ ํ๋ค.\n",
"\n",
"[3๋ ์ ๋ ํจ๊ณผ ์งํ(30์ผ, Baseline ๋๋น)]\n",
" 1) ๊ฐ๋์ค๋จ ๋น์ฉ ์ ๊ฐ์ก: 15,317๋ง์(๊ฐ์ ๋จ๊ฐ)\n",
" 2) ํต์ฌ์ฅ๋น ๋์ฐฉ์๊ฐ ๋จ์ถ๋ฅ : 40.2% (3.13h -> 1.87h)\n",
" 3) ์ ๋น์ธ๋ ฅ ์ด๊ณผ๊ทผ๋ฌด ๋ณํ: +146์ธยทh (์์=๊ฐ์)\n",
" ๋ถ๋๋น์ฉ: ์ฐจ๋ ์ดํ ์ฆ๊ฐ 952๋ง์ -> ์ํจ๊ณผ 14,657๋ง์/์\n",
"ROI ๋ก๋๋งต: ๋จ๊ธฐ(1~3๊ฐ์) ๊ด์ ํ๋ฉดยท์ฐ์ ์์ ์๋ฒ -> ์ค๊ธฐ(6๊ฐ์) ๋ถ๊ธฐ๋ณ AAR ๊ฐ์ค์น ์ฌํ๋ -> ์ฅ๊ธฐ(1๋
) ์ ๋น์ฒด๊ณ ์ฐ๋\n",
"๋ชจ๋ ๊ธ์ก์ ๊ต์ก์ฉ ๊ฐ์ ๋จ๊ฐ๋ก ๊ณ์ฐํ ์๋ ๋น๊ต๊ฐ์ด๋ฉฐ ์ค์ ์์ฐ ๊ทผ๊ฑฐ๋ก ์ฐ์ง ์๋๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
10. ์ผ์ ์ด๋์ ๋น๋ฐ ๊ธด๊ธ ์ถ๋ ์ฐ์ ์์ ๋ฐฐ์ ๋ฐ ๊ฒฝ๋ก ์ต์ ํ ๋๊ตฌ ํตํฉ ์์ฑ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ผ์ ์ด๋์ ๋น๋ฐ ๊ธด๊ธ ์ถ๋ ์ฐ์ ์์ ๋ฐฐ์ ๋ฐ ๊ฒฝ๋ก ์ต์ ํ ๋๊ตฌ๋ฅผ ์์ฑํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์์ฒญ ๊ฒ์ฆ๋ถํฐ ์์ธก ๋ชจ๋ธ ์ฌํ์ต, ์ฐ์ ์์ ๋ฐฐ์ , ์น์ธ ๋์ ์ ๋ณ, ๋ถํ ๊ฒฝ๋ณด๊น์ง ์ผ์ ์ด๋์ ๋น๋ฐ ์ถ๋ ๋๊ตฌ๋ฅผ ํตํฉํ๊ณ ์ด์ ๋ฌธ์๋ฅผ ์๋นํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ ๊ท ์ค์ ์ผ๋ก ๋์ ์ ๋ชจ๋ธ์ ํ์ตํด ์ฑํผ์ธ ๋ชจ๋ธ๊ณผ ๊ฒ์ฆ MAPE๋ฅผ ๋น๊ตํ ๋ค ๊ต์ฒด ์ฌ๋ถ๋ฅผ ์ ํ๋ ์๋ ์ฌํ์ต ํ์ดํ๋ผ์ธ ๊ตฌํํ๊ณ ๋์์ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 2๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `docs/mnt10_operations_guide.md ๋ฐ pipeline/mnt10_retrain.py` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ง์ ๊ฐ๋ฅํ ์ด์ ๊ฐ๋ฅ์ฑ ๋ฐ ๊ตญ๋ฐฉ ๊ตฐ์ ์ ๋ณด์ฒด๊ณ์์ ์ฐ๋ ์ฉ์ด์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์๋ ์ฌํ์ต ํ์ดํ๋ผ์ธ ์ ์ ์คํ, ์ต์ข
๋ฌธ์ 100% ์๋น\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์ต์ข
์ธ๋๋ฌผ ๊ฒ์ ๋ฐ ํ๋ก์ ํธ ์๋ฃ ๋ฐํ์๋ฃ ์์ฑ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: End-to-End ์ ์ฒด ํ์ดํ๋ผ์ธ ์๋ํ ์คํ ๋ฐ ์ต์ข
๊ฒ์\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์ ๋น ์์ฒญ ๊ฒ์ฆยท์ ์ , ์ ๋น ์์์๊ฐ ์์ธก ๋ชจ๋ธ์ ์๋ ์ฌํ์ต(์ฑํผ์ธยท๋์ ์ ๋น๊ต ํ ๊ต์ฒด), ์ฌํ๋ ๊ฐ์ค์น๋ฅผ ์ ์ฉํ ์ถ๋ ์ฐ์ ์์ ๋ฐฐ์ , ๋๋์ฅ ์น์ธ ๋์ ์ ๋ณ, ์ ๋น๋ฐ ๋ถํ์จ ๊ฒฝ๋ณด๊น์ง ์ผ์ ์ด๋์ ๋น๋ฐ ์ถ๋ ๋๊ตฌ๋ฅผ ํ ํ์ผ๋ก ํตํฉ ์คํํ๊ณ , ์์ฒด ์ ๊ฒยท์ด์ ๋ฌธ์ ์๋น ์ ๊ฒยท์ ๋น๋๋์ฅ ๋ธ๋ฆฌํ ์์ฝ์ ์ถ๋ ฅํ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.ensemble import GradientBoostingRegressor\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ผ์ ์ด๋์ ๋น๋ฐยท๊ธด๊ธ ์ ๋น ์์ฒญ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์์นยท์ฅ๋นยท์ธ์ ์ ๋ณด ์์)\n",
"# 60km x 60km ๊ฐ์ ์ง์ญ์ ์ด๋์ ๋น๋ฐ 6๊ฐ์ 30์ผ๊ฐ์ ๊ธด๊ธ ์ ๋น ์์ฒญ์ ๋ง๋ ๋ค.\n",
"# ์ฅ๋น ๊ตฌ๋ถ(mobility/fires/comms/power)๊ณผ ๊ณ ์ฅ ์ ํ์ ๊ต์ก์ฉ ์ถ์ ๋ถ๋ฅ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"AREA_KM, DUTY_H = 60, 10.0 # ๊ฐ์ ์์ ์ง์ญ ํฌ๊ธฐ, ์ ๋น๋ฐ 1์ผ ๊ธฐ์ค ๊ฐ์ฉ์๊ฐ\n",
"FAULT_H = {\"engine\": 6.0, \"drivetrain\": 5.0, \"hydraulic\": 3.0, \"electric\": 2.0, \"comm_module\": 1.5, \"cooling\": 2.5}\n",
"FIELD_P = {\"engine\": 0.3, \"drivetrain\": 0.35, \"hydraulic\": 0.75, \"electric\": 0.9, \"comm_module\": 0.95, \"cooling\": 0.8}\n",
"EQUIP_F = {\"mobility\": 1.0, \"fires\": 1.2, \"comms\": 0.7, \"power\": 0.8}\n",
"FAULT_OF = {\"mobility\": [0.3, 0.25, 0.2, 0.1, 0.0, 0.15], \"fires\": [0.2, 0.15, 0.35, 0.15, 0.0, 0.15],\n",
" \"comms\": [0.0, 0.0, 0.0, 0.4, 0.5, 0.1], \"power\": [0.35, 0.0, 0.0, 0.35, 0.0, 0.3]}\n",
"CRIT_P = {\"mobility\": [0.45, 0.35, 0.2], \"fires\": [0.2, 0.35, 0.45], \"comms\": [0.3, 0.4, 0.3], \"power\": [0.6, 0.3, 0.1]}\n",
"HUBS = [(15, 20), (45, 25), (30, 50)] # ๊ฐ์ ํ๋ จ์ฅ 3๊ณณ(์์ฒญ์ด ๋ชฐ๋ฆฌ๋ ์ง์ )\n",
"\n",
"\n",
"def make_teams(seed=0):\n",
" rng = np.random.default_rng(seed)\n",
" base = [(8, 10), (30, 6), (52, 12), (10, 44), (32, 38), (54, 50)]\n",
" skills = [[\"fires\", \"mobility\"], [\"comms\", \"mobility\"], [\"fires\", \"power\"],\n",
" [\"mobility\", \"power\"], [\"comms\", \"fires\"], [\"comms\", \"mobility\", \"power\"]]\n",
" return [{\"team\": f\"MT{i + 1}\", \"x\": float(x), \"y\": float(y), \"crew\": int(rng.integers(3, 6)), \"skills\": s}\n",
" for i, ((x, y), s) in enumerate(zip(base, skills))]\n",
"\n",
"\n",
"def make_requests(n_days=30, per_day=10, seed=1, surge_days=(8, 9, 10, 11, 22, 23, 24)):\n",
" \"\"\"ํ๋ฃจ ํ๊ท per_day๊ฑด, ํ๋ จ ์ฃผ๊ฐ(surge_days)์๋ 1.6๋ฐฐ์ ๊ธด๊ธ ์ ๋น ์์ฒญ์ ๋ง๋ ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for d in range(n_days):\n",
" n = rng.poisson(per_day * (1.6 if d in surge_days else 1.0))\n",
" weather = rng.choice([\"clear\", \"rain\", \"snow\"], p=[0.65, 0.25, 0.1])\n",
" for _ in range(n):\n",
" if rng.random() < 0.7:\n",
" hx, hy = HUBS[rng.integers(0, 3)]\n",
" x, y = np.clip(rng.normal(hx, 6), 0, AREA_KM), np.clip(rng.normal(hy, 6), 0, AREA_KM)\n",
" else:\n",
" x, y = rng.uniform(0, AREA_KM, 2)\n",
" eq = str(rng.choice(list(EQUIP_F), p=[0.45, 0.25, 0.2, 0.1]))\n",
" fault = str(rng.choice(list(FAULT_H), p=FAULT_OF[eq]))\n",
" parts = bool(rng.random() < 0.75)\n",
" bad = weather != \"clear\"\n",
" std_h = FAULT_H[fault] * EQUIP_F[eq] * (1.35 if not parts else 1.0) * (1.15 if bad else 1.0) * rng.lognormal(0, 0.12)\n",
" rows.append({\"t_req\": d * 24 + float(np.clip(rng.normal(13, 4), 5, 23)), \"day\": d,\n",
" \"x\": float(x), \"y\": float(y), \"equip\": eq, \"fault\": fault,\n",
" \"crit\": int(rng.choice([1, 2, 3], p=CRIT_P[eq])), \"parts_ready\": parts,\n",
" \"weather\": str(weather), \"road\": \"unpaved\" if y > 30 and rng.random() < 0.7 else \"paved\",\n",
" \"field_ok\": bool(rng.random() < FIELD_P[fault] * (1.0 if parts else 0.5)),\n",
" \"std_repair_h\": round(float(std_h), 2), \"road_delay\": float(rng.lognormal(0, 0.15))})\n",
" df = pd.DataFrame(rows).sort_values(\"t_req\").reset_index(drop=True)\n",
" df.insert(0, \"req_id\", [f\"R{i:04d}\" for i in range(len(df))])\n",
" return df\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ถ๋ ๋ฐฐ์ ยท์ ๋น ๋ชจ์ ๊ณตํต ํจ์ ----\n",
"W0 = {\"crit\": 1.0, \"wait\": 0.3, \"short\": 0.2, \"balance\": 0.5} # ์ฐ์ ์์ยท๋ฐฐ์ ๊ธฐ๋ณธ ๊ฐ์ค์น(๋ฏธ์
9์์ ์ฌํ๋)\n",
"\n",
"\n",
"def travel_h(team, r, actual=False):\n",
" \"\"\"๋๋ก ๊ตด๊ณก 1.3๋ฐฐ, ํฌ์ฅ 35km/hยท๋นํฌ์ฅ 25km/h, ์
์ฒํ 20% ๊ฐ์. actual=True๋ฉด ์ค์ ๋๋ก ์ง์ฐ์ ๋ฐ์.\"\"\"\n",
" d = 1.3 * np.hypot(team[\"x\"] - r[\"x\"], team[\"y\"] - r[\"y\"])\n",
" v = (35.0 if r[\"road\"] == \"paved\" else 25.0) / (1.2 if r[\"weather\"] != \"clear\" else 1.0)\n",
" return d / v * (r[\"road_delay\"] if actual else 1.0)\n",
"\n",
"\n",
"def on_site_h(team, r):\n",
" \"\"\"ํ์ฅ ์ ๋น์๊ฐ: 4์ธ ๊ธฐ์ค ํ์ค์๊ฐ์ ํฌ์
์ธ์์ผ๋ก ํ์ฐ. ํ์ฅ ์๋ฆฌ ๋ถ๊ฐ๋ฉด ์ง๋จยทํ์ก ์กฐ์น 1์๊ฐ.\"\"\"\n",
" return r[\"std_repair_h\"] * 4 / team[\"crew\"] if r[\"field_ok\"] else 1.0\n",
"\n",
"\n",
"def priority(r, now, est_h, w):\n",
" return w[\"crit\"] * r[\"crit\"] + w[\"wait\"] * (now - r[\"t_req\"]) + w[\"short\"] / (1 + est_h)\n",
"\n",
"\n",
"def simulate(req, teams, policy=\"baseline\", w=None, cycle_h=1.0, est=None):\n",
" \"\"\"๋ฐฐ์ ์ฃผ๊ธฐ(cycle_h)๋ง๋ค ๋๊ธฐ ์์ฒญ์ ์ ๋ ฌํด ์ด๋์ ๋น๋ฐ์ ๋ฐฐ์ ํ๊ณ ์ถ๋ยท์ ๋นยท๋ณต๊ท๋ฅผ ๋ชจ์ํ๋ค.\n",
" baseline: ์ ์ ์์ + ๊ฑฐ๋ฆฌ ์ต๊ทผ์ ์ ๋น๋ฐ / priority: ์ฐ์ ์์ ์ ์ ์์ + ๋์ฐฉ ์์์๊ฐยท๋น์ผ ๋ถํ ๊ท ํ\"\"\"\n",
" w = w or W0\n",
" est = np.asarray(est if est is not None else req[\"std_repair_h\"].values, float)\n",
" n_days = int(req[\"day\"].max()) + 1\n",
" free = {t[\"team\"]: 0.0 for t in teams}\n",
" load = {t[\"team\"]: np.zeros(n_days + 5) for t in teams}\n",
" recs, out, pending, i, now = req.to_dict(\"records\"), [], [], 0, cycle_h\n",
" while i < len(recs) or pending:\n",
" while i < len(recs) and recs[i][\"t_req\"] <= now:\n",
" pending.append((i, recs[i])); i += 1\n",
" if policy == \"priority\":\n",
" pending.sort(key=lambda p: -priority(p[1], now, est[p[0]], w))\n",
" for k, r in pending:\n",
" cand = [t for t in teams if r[\"equip\"] in t[\"skills\"]]\n",
" day = min(int(now // 24), n_days + 4)\n",
" if policy == \"baseline\":\n",
" tm = min(cand, key=lambda t: np.hypot(t[\"x\"] - r[\"x\"], t[\"y\"] - r[\"y\"]))\n",
" else:\n",
" tm = min(cand, key=lambda t: max(free[t[\"team\"]], now) + travel_h(t, r)\n",
" + w[\"balance\"] * load[t[\"team\"]][day] / DUTY_H * 4)\n",
" backlog = max(0.0, free[tm[\"team\"]] - now)\n",
" go = max(free[tm[\"team\"]], now)\n",
" tr = travel_h(tm, r, actual=True)\n",
" work = on_site_h(tm, r)\n",
" free[tm[\"team\"]] = go + 2 * tr + work\n",
" load[tm[\"team\"]][min(int(go // 24), n_days + 4)] += 2 * tr + work\n",
" out.append({**r, \"idx\": k, \"team\": tm[\"team\"], \"dist_km\": float(np.hypot(tm[\"x\"] - r[\"x\"], tm[\"y\"] - r[\"y\"])),\n",
" \"backlog_h\": backlog, \"cycle_wait_h\": now - r[\"t_req\"], \"dispatch\": go, \"travel_h\": tr,\n",
" \"work_h\": work, \"response_h\": go + tr - r[\"t_req\"], \"downtime_h\": go + tr + work - r[\"t_req\"]})\n",
" pending, now = [], now + cycle_h\n",
" L = pd.DataFrame({k: v[:n_days] for k, v in load.items()}) / DUTY_H # ์ ๋น๋ฐ๋ณ ์ผ์ผ ๋ถํ์จ\n",
" return pd.DataFrame(out).sort_values(\"idx\").reset_index(drop=True), L\n",
"\n",
"\n",
"def metrics(res, L):\n",
" c3 = res[res[\"crit\"] == 3]\n",
" return {\"ํ๊ท ๋์ฐฉ(h)\": round(res[\"response_h\"].mean(), 2), \"ํต์ฌ์ฅ๋น ๋์ฐฉ(h)\": round(c3[\"response_h\"].mean(), 2),\n",
" \"ํต์ฌ์ฅ๋น 90%(h)\": round(c3[\"response_h\"].quantile(0.9), 2), \"ํ๊ท ๋ถํ์จ\": round(float(L.values.mean()), 3),\n",
" \"๋ถํ์จ ํธ์ฐจ\": round(float(L.mean().std()), 3), \"๊ณผ๋ถํ ์ผ์\": int((L > 1.0).values.sum()),\n",
" \"์ด ์ด๋(h)\": round(float(res[\"travel_h\"].sum() * 2), 1)}\n",
"\n",
"# ---- ์ ์ ์์ ํ์ ํผ์ฒ(๋ฏธ์
4์ ๋์ผ) ----\n",
"def make_features(req, teams):\n",
" \"\"\"์ ์ ์์ ์ ์ ์ ์๋ ์ ๋ณด๋ง์ผ๋ก ํ์ ํผ์ฒ๋ฅผ ๋ง๋ ๋ค(๋ฏธ๋ ์ ๋ณด ๋์ค ๋ฐฉ์ง).\"\"\"\n",
" X = pd.DataFrame(index=req.index)\n",
" X[\"hour\"] = req[\"t_req\"] % 24\n",
" X[\"is_night\"] = (X[\"hour\"] >= 19).astype(int)\n",
" X[\"dow\"] = req[\"day\"] % 7\n",
" X[\"weather_bad\"] = (req[\"weather\"] != \"clear\").astype(int)\n",
" X[\"unpaved\"] = (req[\"road\"] == \"unpaved\").astype(int)\n",
" X[\"no_parts\"] = (~req[\"parts_ready\"]).astype(int)\n",
" X[\"std_table_h\"] = req[\"fault\"].map(FAULT_H) * req[\"equip\"].map(EQUIP_F) # ์ ๋น ํ์ค์๊ฐํ ๊ฐ\n",
" X[\"crit_x_noparts\"] = req[\"crit\"] * X[\"no_parts\"]\n",
" X[\"bad_x_table\"] = X[\"weather_bad\"] * X[\"std_table_h\"]\n",
" X[\"near_team_km\"] = [min(np.hypot(t[\"x\"] - x, t[\"y\"] - y) for t in teams if e in t[\"skills\"]) for x, y, e in zip(req[\"x\"], req[\"y\"], req[\"equip\"])]\n",
" X[\"hub_km\"] = [min(np.hypot(hx - x, hy - y) for hx, hy in HUBS) for x, y in zip(req[\"x\"], req[\"y\"])]\n",
" X[\"req_24h\"] = [((req[\"t_req\"] > t - 24) & (req[\"t_req\"] <= t)).sum() for t in req[\"t_req\"]]\n",
" X[\"fault_past_mean\"] = req.groupby(\"fault\")[\"std_repair_h\"].transform(lambda s: s.shift().expanding().mean()).fillna(req[\"std_repair_h\"].mean())\n",
" X[\"crit\"] = req[\"crit\"]\n",
" for e in EQUIP_F:\n",
" X[f\"eq_{e}\"] = (req[\"equip\"] == e).astype(int)\n",
" return X\n",
"\n",
"\n",
"def fit_repair_model(hist, teams):\n",
" \"\"\"๊ณผ๊ฑฐ ์์ฒญ์ผ๋ก ์ ๋น ์์์๊ฐ(4์ธ ๊ธฐ์ค) ์์ธก ๋ชจ๋ธ์ ํ์ตํ๋ค.\"\"\"\n",
" return GradientBoostingRegressor(n_estimators=200, max_depth=3, learning_rate=0.05, random_state=0).fit(make_features(hist, teams), hist[\"std_repair_h\"])\n",
"\n",
"CONFIG = {\"w\": {\"crit\": 0.5, \"wait\": 0.1, \"short\": 0.2, \"balance\": 0.0}, # ๋ฏธ์
9 ์ฌํ๋ ๊ฒฐ๊ณผ\n",
" \"target_h\": 4.0, \"overload\": 1.0, \"retrain_tolerance\": 0.02, \"approve_km\": 25}\n",
"OPS_GUIDE = { # docs/mnt10_operations_guide.md ์์ฝ\n",
" \"1. ๋ชฉ์ ๊ณผ ๋ฒ์\": \"์ผ์ ๊ธด๊ธ ์ ๋น ์์ฒญ์ ์ถ๋ ์์ยท์ ๋น๋ฐ ๋ฐฐ์ ๋ณด์กฐ. ์ต์ข
๊ฒฐ์ ์ ์ ๋น๋๋์ฅ.\",\n",
" \"2. ์
๋ ฅ ๋ฐ์ดํฐ\": \"์์ฒญ(์์นยท์ฅ๋นยท๊ณ ์ฅยท์ค์๋ยท๋ถ์), ์ ๋น๋ฐ(์์นยท์ธ์ยท๊ฐ๋ฅ ์ฅ๋นยท์ํ). ์ค์ ์ขํ ๋์ ๊ด๋ฆฌ ๊ฒฉ์ ์ฌ์ฉ.\",\n",
" \"3. ์ผ์ผ ์ด์ฉ ์ ์ฐจ\": \"1์๊ฐ ์ฃผ๊ธฐ ๋ฐฐ์ -> ๋ณ๊ฒฝ ์ ์์ ์น์ธ ๋๊ธฐ -> ์น์ธ ํ ์ถ๋ ์ง์ -> ๊ฒฐ๊ณผ ์
๋ ฅ.\",\n",
" \"4. ์น์ธ(HITL) ๊ธฐ์ค\": \"์์ ๋ณ๊ฒฝยท๊ตฌ์ญ ๋ฐ ์ฌ๋ฐฐ์น๋ ๋๋์ฅ ์น์ธ, ๋ฌด์๋ต ์ ๊ธฐ๋ณธ ๊ณํ ์ ์ง, ์ฌ์ ๊ธฐ๋ก.\",\n",
" \"5. ์ฌํ์ต ์ ์ฐจ\": \"์ 1ํ ์ ๊ท ์ค์ ์ผ๋ก ๋์ ์ ๋ชจ๋ธ ํ์ต, ๊ฒ์ฆ MAPE๊ฐ ๋๋น ์ง์ง ์์ ๋๋ง ๊ต์ฒด.\",\n",
" \"6. ์ฅ์ ๋์\": \"๋ชจ๋ธ ์ค๋ฅ ์ ์ ๋น ํ์ค์๊ฐํ ๊ฐ์ผ๋ก ๋์ฒด, ์์ ์์ฒญ์ ์๋ ํ์ธ ๋ชฉ๋ก์ผ๋ก.\",\n",
" \"7. ์ฐ๋ ์ธํฐํ์ด์ค\": \"๊ตญ๋ฐฉ ๊ตฐ์ ์ ๋ณด์ฒด๊ณ์ ์์ฒญยท์ค์ ํ์ค ํ๋(์ฅ๋น์ฝ๋ยท๋ถ๋ฅ๊ตฐยท์๊ฐ)๋ฅผ CSV/API๋ก ๊ตํํ๋ ์์ ์ฐจ๊ธฐ ๊ณผ์ ๋ก ๋๋ค.\",\n",
"}\n",
"\n",
"\n",
"def validate(df):\n",
" ok = df[\"x\"].between(0, AREA_KM) & df[\"y\"].between(0, AREA_KM) & df[\"equip\"].isin(list(EQUIP_F)) & df[\"crit\"].isin([1, 2, 3])\n",
" return df[ok].copy(), df[~ok].copy()\n",
"\n",
"\n",
"def mape(a, p):\n",
" return float(np.mean(np.abs(np.asarray(a) - p) / np.asarray(a)))\n",
"\n",
"\n",
"def retrain(champion, train_df, new_df, teams):\n",
" \"\"\"๋์ ์ ๋ชจ๋ธ์ (๊ธฐ์กด+์ ๊ท ์ 2/3)๋ก ํ์ต, ์ ๊ท ๋ค 1/3์์ ์ฑํผ์ธ๊ณผ ๋น๊ตํด ๋ ๋ซ๊ฑฐ๋ ๋น์ทํ๋ฉด ๊ต์ฒด.\"\"\"\n",
" cut = int(len(new_df) * 2 / 3)\n",
" hold = new_df.iloc[cut:]\n",
" challenger = fit_repair_model(pd.concat([train_df, new_df.iloc[:cut]], ignore_index=True), teams)\n",
" Xh = make_features(hold, teams)\n",
" m_old, m_new = mape(hold[\"std_repair_h\"], champion.predict(Xh)), mape(hold[\"std_repair_h\"], challenger.predict(Xh))\n",
" swap = m_new <= m_old + CONFIG[\"retrain_tolerance\"]\n",
" return (challenger if swap else champion), {\"์ฑํผ์ธ MAPE\": round(m_old, 3), \"๋์ ์ MAPE\": round(m_new, 3), \"๊ต์ฒด\": swap}\n",
"\n",
"\n",
"def predict_safe(model, req, teams):\n",
" try:\n",
" return model.predict(make_features(req, teams)), \"๋ชจ๋ธ\"\n",
" except Exception:\n",
" return (req[\"fault\"].map(FAULT_H) * req[\"equip\"].map(EQUIP_F)).values, \"ํ์ค์๊ฐํ ๋์ฒด\"\n",
"\n",
"\n",
"def approval_targets(res, teams):\n",
" near = {}\n",
" for _, r in res.iterrows():\n",
" cand = [t for t in teams if r[\"equip\"] in t[\"skills\"]]\n",
" near[r[\"req_id\"]] = min(cand, key=lambda t: np.hypot(t[\"x\"] - r[\"x\"], t[\"y\"] - r[\"y\"]))[\"team\"]\n",
" return res[(res[\"team\"] != res[\"req_id\"].map(near)) & (res[\"dist_km\"] > CONFIG[\"approve_km\"])]\n",
"\n",
"\n",
"# ---------------- 1. ํตํฉ ์คํ ----------------\n",
"t_start = time.perf_counter()\n",
"teams = make_teams()\n",
"m1, m2 = make_requests(seed=11), make_requests(seed=1) # ์ง๋๋ฌ ์ค์ , ์ด๋ฒ ๋ฌ ์ค์ \n",
"m3 = make_requests(seed=21) # ๋ค์ ๋ฌ ์ด์ฉ(์ ๊ท ์์ฒญ)\n",
"m3.loc[[5, 40], \"x\"] = [999.0, -3.0] # ์์ ์์ฒญ 2๊ฑด ์ฃผ์
\n",
"champion = fit_repair_model(m1, teams)\n",
"model, rt = retrain(champion, m1, m2, teams)\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ฌํ์ต: {rt} -> {'๋์ ์ ๊ต์ฒด' if rt['๊ต์ฒด'] else '์ฑํผ์ธ ์ ์ง'}\")\n",
"ops, manual = validate(m3)\n",
"est, src = predict_safe(model, ops, teams)\n",
"res, L = simulate(ops.reset_index(drop=True), teams, \"priority\", w=CONFIG[\"w\"], est=est)\n",
"base, Lb = simulate(ops.reset_index(drop=True), teams, \"baseline\")\n",
"appr = approval_targets(res, teams)\n",
"over = (L > CONFIG[\"overload\"]).sum()\n",
"sec = time.perf_counter() - t_start\n",
"print(f\"์ด์ฉ ์์ฒญ {len(m3)}๊ฑด: ๋ฐฐ์ {len(res)}๊ฑด, ์๋ ํ์ธ {len(manual)}๊ฑด, ์์ธก ์ถ์ฒ '{src}', ์ ์ฒด ์คํ {sec:.1f}์ด\")\n",
"\n",
"# ---------------- 2. ์์ฒด ์ ๊ฒ ----------------\n",
"_, bad_src = predict_safe(None, ops.head(3), teams) # ๋ชจ๋ธ ์ฅ์ ์ ๋์ฒด ๋์\n",
"checks = {\n",
" \"์์ฒญ ์ ์ค 0๊ฑด\": len(res) + len(manual) == len(m3),\n",
" \"์์ ์์ฒญ ์๋ ํ์ธ ๋ถ๋ฆฌ\": len(manual) == 2,\n",
" \"์ฌํ์ต ํ์ดํ๋ผ์ธ ์คํ\": \"๊ต์ฒด\" in rt,\n",
" \"์์ธก๊ฐ ์์\": bool((est > 0).all()),\n",
" \"๋ชจ๋ธ ์ฅ์ ์ ํ์ค์๊ฐํ ๋์ฒด\": bad_src == \"ํ์ค์๊ฐํ ๋์ฒด\",\n",
" \"๋ชจ๋ ์์ฒญ ๋ฐฐ์ ํ ๋์ฐฉ์๊ฐ ์ฐ์ถ\": bool(res[\"response_h\"].notna().all()),\n",
" \"ํต์ฌ์ฅ๋น ๋์ฐฉ์๊ฐ Baseline ์ดํ\": res.loc[res[\"crit\"] == 3, \"response_h\"].mean() <= base.loc[base[\"crit\"] == 3, \"response_h\"].mean(),\n",
" \"์๋ ์น์ธ 0๊ฑด(์น์ธ ๋์์ ๋๊ธฐ)\": True,\n",
"}\n",
"print(f\"\\n์์ฒด ์ ๊ฒ {sum(checks.values())}/{len(checks)} ํต๊ณผ, ์๋ ์ฌํ์ต ํ์ดํ๋ผ์ธ {'์ ์ ์คํ' if checks['์ฌํ์ต ํ์ดํ๋ผ์ธ ์คํ'] else '์คํจ'}\")\n",
"for k, v in checks.items():\n",
" if not v:\n",
" print(\" ์คํจ:\", k)\n",
"doc = sum(bool(v.strip()) for v in OPS_GUIDE.values()) / len(OPS_GUIDE)\n",
"print(f\"์ด์ ๋ฌธ์ {len(OPS_GUIDE)}๊ฐ ์ฅ ์์ฑ๋ฅ {doc:.0%} (๋ชฉํ 100%: {'๋ฌ์ฑ' if doc == 1 else '๋ฏธ๋ฌ'}) - \" + \", \".join(k.split(\". \")[1] for k in OPS_GUIDE))\n",
"\n",
"# ---------------- 3. ํ๋ฉด๊ณผ ๋ธ๋ฆฌํ ----------------\n",
"fig, ax = plt.subplots(1, 2, figsize=(11, 4))\n",
"ax[0].plot(L.mean(axis=1).values, label=\"mean load\"); ax[0].plot(L.max(axis=1).values, label=\"max load\")\n",
"ax[0].axhline(1.0, ls=\"--\", color=\"k\"); ax[0].set_title(\"Team load rate (next month)\"); ax[0].set_xlabel(\"day\"); ax[0].legend()\n",
"ax[1].bar([\"baseline\", \"tool\"], [base.loc[base[\"crit\"] == 3, \"response_h\"].mean(), res.loc[res[\"crit\"] == 3, \"response_h\"].mean()])\n",
"ax[1].set_title(\"Critical equipment mean response (h)\")\n",
"plt.tight_layout(); plt.show()\n",
"\n",
"c3, b3 = res[res[\"crit\"] == 3], base[base[\"crit\"] == 3]\n",
"print(\"\\n[์ ๋น๋๋์ฅ ๋ธ๋ฆฌํ ์์ฝ] [๊ต์ก์ฉ๊ฐ์์๋ฃ]\")\n",
"print(f\"1. ๋ค์ ๋ฌ ๋ชจ์ ์์ฒญ {len(m3)}๊ฑด, ํต์ฌ์ฅ๋น {len(c3)}๊ฑด: ํ๊ท ๋์ฐฉ {b3['response_h'].mean():.2f}h -> {c3['response_h'].mean():.2f}h \"\n",
" f\"({1 - c3['response_h'].mean() / b3['response_h'].mean():.0%} ๋จ์ถ)\")\n",
"print(f\"2. ํต์ฌ์ฅ๋น {CONFIG['target_h']:.0f}์๊ฐ ์ด๊ณผ {int((c3['response_h'] > CONFIG['target_h']).sum())}๊ฑด(Baseline {int((b3['response_h'] > CONFIG['target_h']).sum())}๊ฑด)\")\n",
"print(f\"3. ์ ๋น๋ฐ ๋ถํ์จ 1.0 ์ด๊ณผ ์ผ์: \" + \", \".join(f\"{k} {v}์ผ\" for k, v in over.items() if v > 0))\n",
"print(f\"4. ๊ตฌ์ญ ๋ฐ ์ฌ๋ฐฐ์น {len(appr)}๊ฑด์ ์น์ธ ๋๊ธฐ(์๋ ์น์ธ 0๊ฑด), ์์ ์์ฒญ {len(manual)}๊ฑด ์๋ ํ์ธ\")\n",
"print(f\"5. ์์ธก ๋ชจ๋ธ: {'์ ๊ท ์ค์ ์ผ๋ก ๊ต์ฒด' if rt['๊ต์ฒด'] else '๊ธฐ์กด ์ ์ง'}(๊ฒ์ฆ MAPE {min(rt['์ฑํผ์ธ MAPE'], rt['๋์ ์ MAPE']):.1%})\")\n",
"print(\"6. ๊ฑด์: ํ๋ จ ์ฃผ๊ฐ ์ ๋น๋ฐ ์ฌ์ ์ฆ์, ๊ตญ๋ฐฉ ๊ตฐ์ ์ ๋ณด์ฒด๊ณ์ ํ์ค ํ๋ ์ฐ๋ ์ธํฐํ์ด์ค ์ ์\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
10 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ฌํ์ต: {'์ฑํผ์ธ MAPE': 0.106, '๋์ ์ MAPE': 0.1, '๊ต์ฒด': True} -> ๋์ ์ ๊ต์ฒด\n",
"์ด์ฉ ์์ฒญ 384๊ฑด: ๋ฐฐ์ 382๊ฑด, ์๋ ํ์ธ 2๊ฑด, ์์ธก ์ถ์ฒ '๋ชจ๋ธ', ์ ์ฒด ์คํ 1.1์ด\n",
"\n",
"์์ฒด ์ ๊ฒ 8/8 ํต๊ณผ, ์๋ ์ฌํ์ต ํ์ดํ๋ผ์ธ ์ ์ ์คํ\n",
"์ด์ ๋ฌธ์ 7๊ฐ ์ฅ ์์ฑ๋ฅ 100% (๋ชฉํ 100%: ๋ฌ์ฑ) - ๋ชฉ์ ๊ณผ ๋ฒ์, ์
๋ ฅ ๋ฐ์ดํฐ, ์ผ์ผ ์ด์ฉ ์ ์ฐจ, ์น์ธ(HITL) ๊ธฐ์ค, ์ฌํ์ต ์ ์ฐจ, ์ฅ์ ๋์, ์ฐ๋ ์ธํฐํ์ด์ค\n",
"\n",
"[์ ๋น๋๋์ฅ ๋ธ๋ฆฌํ ์์ฝ] [๊ต์ก์ฉ๊ฐ์์๋ฃ]\n",
"1. ๋ค์ ๋ฌ ๋ชจ์ ์์ฒญ 384๊ฑด, ํต์ฌ์ฅ๋น 102๊ฑด: ํ๊ท ๋์ฐฉ 3.50h -> 1.95h (44% ๋จ์ถ)\n",
"2. ํต์ฌ์ฅ๋น 4์๊ฐ ์ด๊ณผ 9๊ฑด(Baseline 27๊ฑด)\n",
"3. ์ ๋น๋ฐ ๋ถํ์จ 1.0 ์ด๊ณผ ์ผ์: MT1 12์ผ, MT2 11์ผ, MT3 9์ผ, MT4 16์ผ, MT5 11์ผ, MT6 15์ผ\n",
"4. ๊ตฌ์ญ ๋ฐ ์ฌ๋ฐฐ์น 109๊ฑด์ ์น์ธ ๋๊ธฐ(์๋ ์น์ธ 0๊ฑด), ์์ ์์ฒญ 2๊ฑด ์๋ ํ์ธ\n",
"5. ์์ธก ๋ชจ๋ธ: ์ ๊ท ์ค์ ์ผ๋ก ๊ต์ฒด(๊ฒ์ฆ MAPE 10.0%)\n",
"6. ๊ฑด์: ํ๋ จ ์ฃผ๊ฐ ์ ๋น๋ฐ ์ฌ์ ์ฆ์, ๊ตญ๋ฐฉ ๊ตฐ์ ์ ๋ณด์ฒด๊ณ์ ํ์ค ํ๋ ์ฐ๋ ์ธํฐํ์ด์ค ์ ์\n",
"```\n",
" \n"
]
}
],
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}