{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# ๐ŸŽฏ [MNT10] AI ๊ธฐ๋ฐ˜ ์ •๋น„์ธ๋ ฅ ๋ถ€ํ•˜์œจ ์˜ˆ์ธกยท๊ณต์ •๊ท ํ˜•\n", "### Google Colab ์‹ค์Šต ํ™˜๊ฒฝ (์žฅ๋น„์ •๋น„ยท์ˆ˜๋ฆฌ๋ถ€์† / Level 3)\n", "\n", "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/edunlab/milicontent2/blob/main/uploads/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", " # [TODO : ํ•™์ƒ ์‹ค์Šต 1] ์•ผ์ „ ๊ธด๊ธ‰ ์ •๋น„ ์š”์ฒญ(๊ณ ์žฅ ์œ„์น˜, ์žฅ๋น„ ์ค‘์š”๋„, ๊ณ ์žฅ ์œ ํ˜•) ๋ฐ์ดํ„ฐ์…‹ ์Šคํ‚ค๋งˆ ์ •์˜ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def validate(rec):\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 1] ์•ผ์ „ ๊ธด๊ธ‰ ์ •๋น„ ์š”์ฒญ(๊ณ ์žฅ ์œ„์น˜, ์žฅ๋น„ ์ค‘์š”๋„, ๊ณ ์žฅ ์œ ํ˜•) ๋ฐ์ดํ„ฐ์…‹ ์Šคํ‚ค๋งˆ ์ •์˜ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\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", " # [TODO : ํ•™์ƒ ์‹ค์Šต 2] ์ด๋™์ •๋น„๋ฐ˜ ๋ณด์œ  ์žฅ๋น„/๊ณต๊ตฌ ๋ฐ ์ถœ๋™ ๊ฐ€๋Šฅ ์ƒํƒœ ์ •๋ณด ์ „์ฒ˜๋ฆฌ ๋ชจ๋“ˆ ๊ตฌํ˜„ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\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", " # [TODO : ํ•™์ƒ ์‹ค์Šต 3] ๊ฑฐ๋ฆฌ ๊ธฐ์ค€ 1์ฐจ ์ด๋™์ •๋น„๋ฐ˜ ๋ฐฐ์ • Baseline ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๊ตฌํ˜„ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def on_site_h(team, r):\n", " \"\"\"ํ˜„์žฅ ์ •๋น„์‹œ๊ฐ„: 4์ธ ๊ธฐ์ค€ ํ‘œ์ค€์‹œ๊ฐ„์„ ํˆฌ์ž… ์ธ์›์œผ๋กœ ํ™˜์‚ฐ. ํ˜„์žฅ ์ˆ˜๋ฆฌ ๋ถˆ๊ฐ€๋ฉด ์ง„๋‹จยทํ›„์†ก ์กฐ์น˜ 1์‹œ๊ฐ„.\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 3] ๊ฑฐ๋ฆฌ ๊ธฐ์ค€ 1์ฐจ ์ด๋™์ •๋น„๋ฐ˜ ๋ฐฐ์ • Baseline ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๊ตฌํ˜„ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\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", " # [TODO : ํ•™์ƒ ์‹ค์Šต 4] ์žฅ๋น„ ์ž‘์ „ ์ค‘์š”๋„, ์ •๋น„ ์†Œ์š” ์‹œ๊ฐ„, ์ด๋™ ๊ฒฝ๋กœ๋ฅผ ์ข…ํ•ฉํ•œ ์ตœ์  ์ถœ๋™ ์šฐ์„ ์ˆœ์œ„ ์ถ”์ฒœ ์—”์ง„ ๊ณ ๋„ํ™” ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def on_site_h(team, r):\n", " \"\"\"ํ˜„์žฅ ์ •๋น„์‹œ๊ฐ„: 4์ธ ๊ธฐ์ค€ ํ‘œ์ค€์‹œ๊ฐ„์„ ํˆฌ์ž… ์ธ์›์œผ๋กœ ํ™˜์‚ฐ. ํ˜„์žฅ ์ˆ˜๋ฆฌ ๋ถˆ๊ฐ€๋ฉด ์ง„๋‹จยทํ›„์†ก ์กฐ์น˜ 1์‹œ๊ฐ„.\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 4] ์žฅ๋น„ ์ž‘์ „ ์ค‘์š”๋„, ์ •๋น„ ์†Œ์š” ์‹œ๊ฐ„, ์ด๋™ ๊ฒฝ๋กœ๋ฅผ ์ข…ํ•ฉํ•œ ์ตœ์  ์ถœ๋™ ์šฐ์„ ์ˆœ์œ„ ์ถ”์ฒœ ์—”์ง„ ๊ณ ๋„ํ™” ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\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", " # [TODO : ํ•™์ƒ ์‹ค์Šต 5] ์ถœ๋™ ์ง€์—ฐ ์œ„ํ—˜๋„ ๋ฐ ํ˜„์žฅ ์ˆ˜๋ฆฌ ๊ฐ€๋Šฅ์„ฑ ์‹ ๋ขฐ๋„ ์ ์ˆ˜ ์‚ฐ์ถœ ๋ชจ๋“ˆ ๊ฐœ๋ฐœ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def on_site_h(team, r):\n", " \"\"\"ํ˜„์žฅ ์ •๋น„์‹œ๊ฐ„: 4์ธ ๊ธฐ์ค€ ํ‘œ์ค€์‹œ๊ฐ„์„ ํˆฌ์ž… ์ธ์›์œผ๋กœ ํ™˜์‚ฐ. ํ˜„์žฅ ์ˆ˜๋ฆฌ ๋ถˆ๊ฐ€๋ฉด ์ง„๋‹จยทํ›„์†ก ์กฐ์น˜ 1์‹œ๊ฐ„.\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 5] ์ถœ๋™ ์ง€์—ฐ ์œ„ํ—˜๋„ ๋ฐ ํ˜„์žฅ ์ˆ˜๋ฆฌ ๊ฐ€๋Šฅ์„ฑ ์‹ ๋ขฐ๋„ ์ ์ˆ˜ ์‚ฐ์ถœ ๋ชจ๋“ˆ ๊ฐœ๋ฐœ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\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", " # [TODO : ํ•™์ƒ ์‹ค์Šต 6] ์•ผ์ „ ์ •๋น„ ์š”์ฒญ ํ˜„ํ™ฉ ๋ฐ ์ด๋™์ •๋น„๋ฐ˜ ๊ด€์ œ ๋Œ€์‹œ๋ณด๋“œ UI ๊ตฌํ˜„ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def on_site_h(team, r):\n", " \"\"\"ํ˜„์žฅ ์ •๋น„์‹œ๊ฐ„: 4์ธ ๊ธฐ์ค€ ํ‘œ์ค€์‹œ๊ฐ„์„ ํˆฌ์ž… ์ธ์›์œผ๋กœ ํ™˜์‚ฐ. ํ˜„์žฅ ์ˆ˜๋ฆฌ ๋ถˆ๊ฐ€๋ฉด ์ง„๋‹จยทํ›„์†ก ์กฐ์น˜ 1์‹œ๊ฐ„.\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 6] ์•ผ์ „ ์ •๋น„ ์š”์ฒญ ํ˜„ํ™ฉ ๋ฐ ์ด๋™์ •๋น„๋ฐ˜ ๊ด€์ œ ๋Œ€์‹œ๋ณด๋“œ UI ๊ตฌํ˜„ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\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", " # [TODO : ํ•™์ƒ ์‹ค์Šต 7] ๊ธด๊ธ‰ ์ถœ๋™ ์ˆœ์„œ ๋ณ€๊ฒฝ ๋ฐ ์ •๋น„๋ฐ˜ ์žฌ๋ฐฐ์น˜๋ฅผ ์œ„ํ•œ ์ •๋น„๋Œ€๋Œ€์žฅ HITL ์Šน์ธ ์ฒด๊ณ„ ์„ค๊ณ„ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def on_site_h(team, r):\n", " \"\"\"ํ˜„์žฅ ์ •๋น„์‹œ๊ฐ„: 4์ธ ๊ธฐ์ค€ ํ‘œ์ค€์‹œ๊ฐ„์„ ํˆฌ์ž… ์ธ์›์œผ๋กœ ํ™˜์‚ฐ. ํ˜„์žฅ ์ˆ˜๋ฆฌ ๋ถˆ๊ฐ€๋ฉด ์ง„๋‹จยทํ›„์†ก ์กฐ์น˜ 1์‹œ๊ฐ„.\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 7] ๊ธด๊ธ‰ ์ถœ๋™ ์ˆœ์„œ ๋ณ€๊ฒฝ ๋ฐ ์ •๋น„๋ฐ˜ ์žฌ๋ฐฐ์น˜๋ฅผ ์œ„ํ•œ ์ •๋น„๋Œ€๋Œ€์žฅ HITL ์Šน์ธ ์ฒด๊ณ„ ์„ค๊ณ„ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\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", " # [TODO : ํ•™์ƒ ์‹ค์Šต 8] ๋™์‹œ 20๊ฐœ์†Œ ๊ธด๊ธ‰ ์ •๋น„ ์š”์ฒญ ๋ฐœ์ƒ ๋ชจ์˜ ์ถœ๋™ ์Šค์ผ€์ค„๋ง ์ŠคํŠธ๋ ˆ์Šค ํ…Œ์ŠคํŠธ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def on_site_h(team, r):\n", " \"\"\"ํ˜„์žฅ ์ •๋น„์‹œ๊ฐ„: 4์ธ ๊ธฐ์ค€ ํ‘œ์ค€์‹œ๊ฐ„์„ ํˆฌ์ž… ์ธ์›์œผ๋กœ ํ™˜์‚ฐ. ํ˜„์žฅ ์ˆ˜๋ฆฌ ๋ถˆ๊ฐ€๋ฉด ์ง„๋‹จยทํ›„์†ก ์กฐ์น˜ 1์‹œ๊ฐ„.\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 8] ๋™์‹œ 20๊ฐœ์†Œ ๊ธด๊ธ‰ ์ •๋น„ ์š”์ฒญ ๋ฐœ์ƒ ๋ชจ์˜ ์ถœ๋™ ์Šค์ผ€์ค„๋ง ์ŠคํŠธ๋ ˆ์Šค ํ…Œ์ŠคํŠธ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\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", " # [TODO : ํ•™์ƒ ์‹ค์Šต 9] AAR ๊ธฐ๋ฐ˜ ์ถœ๋™ ์šฐ์„ ์ˆœ์œ„ ๋ฐฐ์ • ์˜ค๋ฅ˜ ๋ถ„์„ ๋ฐ ๊ฐ€์ค‘์น˜ ๋งคํŠธ๋ฆญ์Šค ์žฌํŠœ๋‹ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def on_site_h(team, r):\n", " \"\"\"ํ˜„์žฅ ์ •๋น„์‹œ๊ฐ„: 4์ธ ๊ธฐ์ค€ ํ‘œ์ค€์‹œ๊ฐ„์„ ํˆฌ์ž… ์ธ์›์œผ๋กœ ํ™˜์‚ฐ. ํ˜„์žฅ ์ˆ˜๋ฆฌ ๋ถˆ๊ฐ€๋ฉด ์ง„๋‹จยทํ›„์†ก ์กฐ์น˜ 1์‹œ๊ฐ„.\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 9] AAR ๊ธฐ๋ฐ˜ ์ถœ๋™ ์šฐ์„ ์ˆœ์œ„ ๋ฐฐ์ • ์˜ค๋ฅ˜ ๋ถ„์„ ๋ฐ ๊ฐ€์ค‘์น˜ ๋งคํŠธ๋ฆญ์Šค ์žฌํŠœ๋‹ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\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", " # [TODO : ํ•™์ƒ ์‹ค์Šต 10] ์•ผ์ „ ์ด๋™์ •๋น„๋ฐ˜ ๊ธด๊ธ‰ ์ถœ๋™ ์šฐ์„ ์ˆœ์œ„ ๋ฐฐ์ • ๋ฐ ๊ฒฝ๋กœ ์ตœ์ ํ™” ๋„๊ตฌ ํ†ตํ•ฉ ์™„์„ฑ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def on_site_h(team, r):\n", " \"\"\"ํ˜„์žฅ ์ •๋น„์‹œ๊ฐ„: 4์ธ ๊ธฐ์ค€ ํ‘œ์ค€์‹œ๊ฐ„์„ ํˆฌ์ž… ์ธ์›์œผ๋กœ ํ™˜์‚ฐ. ํ˜„์žฅ ์ˆ˜๋ฆฌ ๋ถˆ๊ฐ€๋ฉด ์ง„๋‹จยทํ›„์†ก ์กฐ์น˜ 1์‹œ๊ฐ„.\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 10] ์•ผ์ „ ์ด๋™์ •๋น„๋ฐ˜ ๊ธด๊ธ‰ ์ถœ๋™ ์šฐ์„ ์ˆœ์œ„ ๋ฐฐ์ • ๋ฐ ๊ฒฝ๋กœ ์ตœ์ ํ™” ๋„๊ตฌ ํ†ตํ•ฉ ์™„์„ฑ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\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 }