{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# ๐ฏ [AMM02] AI ๊ธฐ๋ฐ ํ์ฝ ์ ์ฅ ์ต์ ํ ๋ชจ๋ธ\n",
"### Google Colab ์ค์ต ํ๊ฒฝ (ํ์ฝยทํน์๋ฌผ์ / Level 3)\n",
"\n",
"[](https://colab.research.google.com/github/edunlab/milicontent2/blob/main/uploads/AMM02/AMM02_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",
"ํ์ฝ ์ ์ฅ๊ณ ๋ ๊ณต๊ฐ์ด ํ์ ๋์ด ์๊ณ ํ๋ชฉ๋ง๋ค ํจ๊ป ๋ ์ ์๋ ์ ์ฝ์ด ์์ด, ์์ผ๋ก ์ผ๋ง๋ ์๋ชจ๋๊ณ ์ผ๋ง๋ ์์ฃผ ๋ฐ์ถ๋ ์ง๋ฅผ ์์์ผ ์
๊ณ ์์น์ ์ฌ๋ณด๊ธ ์์๋ฅผ ํฉ๋ฆฌ์ ์ผ๋ก ์ ํ ์ ์๋ค. ๊ทธ๋ฌ๋ ์์ ๋จ๊ณ๋ณ ๊ณ ์ ๊ธฐ์ค ์๋ชจ์จ(CSR)๋ง์ผ๋ก ๊ณํํ๋ฉด ๊ต์ ๊ฐ๋๊ฐ ๋ฎ์ ๋ ์ ๊ณผ์ ์ฌ๊ณ ๊ฐ ์์ด๊ณ ๋์ ๋ ์ ๊ธด๊ธ ์ฌ๋ณด๊ธ์ด ๋ฐ๋ณต๋์ด ์ ์ฅ ๊ณต๊ฐ๊ณผ ์์
๋์ ์ด ๋ญ๋น๋๋ค. ๋ณธ ํ๋ก์ ํธ๋ ๊ต์ก์ฉ ๊ฐ์ ๋ถ๋ยท๊ฐ์ ํ๋ชฉยท๊ฐ์ ์ ์ฅ๊ณ ์๋ฃ๋ฅผ ์ฝ๋๋ก ๋ง๋ค์ด, ํ์ฝยท์ ๋ฅ ์๋ชจ์จ ๋ฐ์ดํฐ ์ ์์ ์ ์ฒ๋ฆฌ, CSR ๊ธฐ๋ฐ Baseline๊ณผ ํ์์ ๋ถ์, ์ ์ ํญยทํ๋ ฅ ๊ณํ์ ๋ฐ์ํ ์๋ชจ๋ ์์ธก, ๋ณด๊ธ ์์ค(DOS) ์ฐ๊ณ ์ฌ๋ณด๊ธ ์ฐ์ถ, ์์ ์ง๋ ๋์๋ณด๋์ ์ค๋ช
๊ฐ๋ฅ์ฑ, ์ ์ฅ ๋ฐฐ์น ์ต์ ํ์ ๊ตฐ์์ฐธ๋ชจ ๊ฒฐ์ฌ์ (HITL), 7์ผ ๊ณ ๊ฐ๋ ์คํธ๋ ์ค ์ํ, 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. ์์ ๋จ๊ณ๋ณ ํ์ฝ(๊ตฌ๊ฒฝ๋ณ) ๋ฐ ์ ๋ฅ(ํญ๊ณต์ /๊ฒฝ์ ) ๊ธฐ์ค ์๋ชจ์จ(CSR) ๋ฐ์ดํฐ ์ ์\n",
"**์์์๊ฐ**: ์ฝ 1.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์์ ๋จ๊ณ๋ณ ํ์ฝ ๋ฐ ์ ๋ฅ ๊ธฐ์ค ์๋ชจ์จ(CSR) ๋ฐ์ดํฐ ๊ท๊ฒฉ์ ์ ์ํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์์ ๋จ๊ณ๋ณ ๊ฐ์ ํ์ฝ(ํ์ข
A~D)ยท์ ๋ฅ(ํญ๊ณต์ /๊ฒฝ์ ) ๊ธฐ์ค ์๋ชจ์จ(CSR) ๋ฐ์ดํฐ ๊ท๊ฒฉ๊ณผ ์
๋ฌด ํ๋ก์ธ์ค ๋งต, ๋ฐ์ดํฐ ๋์
๋๋ฆฌ๋ฅผ ์ ์ํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ผ์ผ ์๋ชจ ๋ณด๊ณ ์์ง๋ถํฐ ์๋ชจ๋ ์ฐ์ถ, ๋ณด๊ธ ์์ค(DOS) ๊ณ์ฐ, ์ฌ๋ณด๊ธ ๊ฒฐ์ฌ, ๊ฐ์ ์ ์ฅ๊ณ ์
๊ณ ยท๋ฐฐ์น๊น์ง ์
๋ฌด ํ๋ฆ์ 5๋จ๊ณ๋ก ์ ๋ฆฌํ๊ณ ๊ท๊ฒฉํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `docs/amm02_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": [
"\"\"\"์์ ๋จ๊ณ๋ณ ํ์ฝ(๊ฐ์ ํ์ข
A~D)ยท์ ๋ฅ(ํญ๊ณต์ /๊ฒฝ์ ) ๊ธฐ์ค ์๋ชจ์จ(CSR) ๋ฐ์ดํฐ ๊ท๊ฒฉ, ์
๋ฌด ํ๋ก์ธ์ค ๋งต, ๋ฐ์ดํฐ ๋์
๋๋ฆฌ(ํ๋ 20๊ฐ ์ด์)๋ฅผ ์ ์ํ๊ณ ๊ฐ์ ์ํ ๊ธฐ๋ก์ ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import json\n",
"import re\n",
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"# 1) ์
๋ฌด ํ๋ก์ธ์ค ๋งต: ์๋ชจ ๊ธฐ๋ก -> ์๋ชจ ์์ธก -> ๋ณด๊ธ ์์ค(DOS) -> ์ฌ๋ณด๊ธยท์ ์ฅ ๋ฐฐ์น\n",
"PROCESS = [\"(1) ์ผ์ผ ์๋ชจ ๋ณด๊ณ ์์ง\", \"(2) ์์ ๋จ๊ณยท๊ต์ ๊ฐ๋ ๋ฐ์ ์๋ชจ๋ ์ฐ์ถ\", \"(3) ๋ณด์ ๋ ๋๋น ๋ณด๊ธ ์์ค(DOS) ๊ณ์ฐ\",\n",
" \"(4) ์ฌ๋ณด๊ธ ์์ยท์ฐ์ ์์ ๊ฒฐ์ (๊ตฐ์์ฐธ๋ชจ ๊ฒฐ์ฌ)\", \"(5) ๊ฐ์ ์ ์ฅ๊ณ ์
๊ณ ยท๋ฐฐ์น(์์น์ ์ยท์์
๋์ )\"]\n",
"\n",
"# 2) ๊ฐ์ ํ๋ชฉยท์์ ๋จ๊ณยทCSR ๊ณ์ (์ค์ ํ์ฝ ์ ์ยท์๋ชจ์จ์ด ์๋ ๊ต์ก์ฉ ๊ฐ์ ๊ณ์)\n",
"PHASES = [\"prep\", \"attack\", \"defense\", \"stability\"]\n",
"ITEMS = {\"ammo_A\": \"ํ์ฝ\", \"ammo_B\": \"ํ์ฝ\", \"ammo_C\": \"ํ์ฝ\", \"ammo_D\": \"ํ์ฝ\", \"fuel_jet\": \"์ ๋ฅ\", \"fuel_diesel\": \"์ ๋ฅ\"}\n",
"CSR = pd.DataFrame([[1.0, 0.6, 0.3, 0.2, 1.5, 6.0], [6.0, 3.5, 2.0, 1.2, 5.0, 12.0],\n",
" [4.5, 3.0, 1.6, 0.8, 3.0, 7.0], [0.8, 0.5, 0.3, 0.2, 1.0, 5.0]], index=PHASES, columns=list(ITEMS))\n",
"\n",
"# 3) ๋ฐ์ดํฐ ๋์
๋๋ฆฌ: (๊ทธ๋ฃน, ํ๋, ์๋ฃํ, ๊ท์น, ์ค๋ช
)\n",
"FIELDS = [\n",
" (\"์๋ณ\", \"record_id\", str, {\"pattern\": r\"^R\\d{5}$\"}, \"๊ธฐ๋ก ๋ฒํธ\"),\n",
" (\"์๋ณ\", \"unit_id\", str, {\"pattern\": r\"^U\\d{2}$\"}, \"๊ฐ์ ๋ถ๋ ์ฝ๋\"),\n",
" (\"์๋ณ\", \"day_of_op\", int, {\"min\": 0, \"max\": 365}, \"์์ ๊ฒฝ๊ณผ์ผ\"),\n",
" (\"์์ \", \"phase\", str, {\"enum\": PHASES}, \"์์ ๋จ๊ณ\"),\n",
" (\"์์ \", \"unit_size\", int, {\"min\": 50, \"max\": 3000}, \"๊ฐ์ ๋ณ๋ ฅ ๊ท๋ชจ(๋ช
)\"),\n",
" (\"์์ \", \"intensity\", float, {\"min\": 0, \"max\": 1}, \"๊ต์ ๊ฐ๋(0~1)\"),\n",
" (\"์์ \", \"resistance\", float, {\"min\": 0, \"max\": 5}, \"์ ์ ํญ ์์ค(0~5)\"),\n",
" (\"์์ \", \"fire_missions\", int, {\"min\": 0, \"max\": 100}, \"๊ณํ ํ๋ ฅ ์๋ฌด ์\"),\n",
" (\"์์ \", \"move_km\", float, {\"min\": 0, \"max\": 300}, \"์ผ์ผ ์ด๋ ๊ฑฐ๋ฆฌ(km)\"),\n",
" (\"์์ \", \"weather\", str, {\"enum\": [\"clear\", \"rain\", \"snow\"]}, \"๊ธฐ์\"),\n",
" (\"ํ๋ชฉ\", \"item_code\", str, {\"enum\": list(ITEMS)}, \"๊ฐ์ ํ๋ชฉ ์ฝ๋\"),\n",
" (\"ํ๋ชฉ\", \"item_class\", str, {\"enum\": [\"ํ์ฝ\", \"์ ๋ฅ\"]}, \"ํ๋ชฉ ๋ถ๋ฅ\"),\n",
" (\"ํ๋ชฉ\", \"csr_coeff\", float, {\"min\": 0, \"max\": 50}, \"๊ธฐ์ค ์๋ชจ์จ(๊ฐ์ ๊ณ์, 100๋ช
ยท1์ผ)\"),\n",
" (\"ํ๋ชฉ\", \"unit_per_pallet\", int, {\"min\": 1, \"max\": 200}, \"ํ๋ ํธ๋น ๋จ์ ์(๊ฐ์)\"),\n",
" (\"์๋ชจ\", \"consumption_qty\", float, {\"min\": 0, \"max\": 5000}, \"์ค์ ์๋ชจ๋(๋จ์)\"),\n",
" (\"์๋ชจ\", \"emergency_issue\", bool, {}, \"๊ธด๊ธ ๋ถ์ถ ์ฌ๋ถ(์์ธ ๋ณด์กด)\"),\n",
" (\"์ฌ๊ณ \", \"on_hand_qty\", float, {\"min\": 0, \"max\": 50000}, \"๋ณด์ ๋(๋จ์)\"),\n",
" (\"์ฌ๊ณ \", \"dos\", float, {\"min\": 0, \"max\": 60}, \"๋ณด๊ธ ์์ค(์ผ๋ถ)\"),\n",
" (\"์ ์ฅ\", \"storage_id\", str, {\"enum\": [\"S1\", \"S2\", \"S3\"]}, \"๊ฐ์ ์ ์ฅ๊ณ \"),\n",
" (\"์ ์ฅ\", \"slot_id\", str, {\"pattern\": r\"^[A-C]\\d{2}$\"}, \"์ ์ฅ ์์น(์ดยท์นธ)\"),\n",
" (\"์ ์ฅ\", \"slot_distance_m\", float, {\"min\": 0, \"max\": 500}, \"์ถ์
๊ตฌ๊น์ง ๊ฑฐ๋ฆฌ(์์
๋์ )\"),\n",
" (\"์ ์ฅ\", \"occupancy_rate\", float, {\"min\": 0, \"max\": 1}, \"์ ์ฅ ์์น ์ ์ ์จ\"),\n",
" (\"๊ด๋ฆฌ\", \"source\", str, {\"enum\": [\"์ผ์ผ๋ณด๊ณ \", \"AAR\", \"์ถ์ \"]}, \"์๋ฃ ์ถ์ฒ\"),\n",
"]\n",
"\n",
"\n",
"def check(name, value, typ, rule):\n",
" if typ is float and isinstance(value, int) and not isinstance(value, bool):\n",
" value = float(value)\n",
" if not isinstance(value, typ) or (typ is not bool and isinstance(value, bool)):\n",
" return [f\"{name}: ์๋ฃํ ์ค๋ฅ({type(value).__name__})\"]\n",
" errs = []\n",
" if \"enum\" in rule and value not in rule[\"enum\"]:\n",
" errs.append(f\"{name}: ํ์ฉ๋์ง ์์ ๊ฐ '{value}'\")\n",
" if \"min\" in rule and value < rule[\"min\"]:\n",
" errs.append(f\"{name}: ์ต์๊ฐ {rule['min']} ๋ฏธ๋ง\")\n",
" if \"max\" in rule and value > rule[\"max\"]:\n",
" errs.append(f\"{name}: ์ต๋๊ฐ {rule['max']} ์ด๊ณผ\")\n",
" if \"pattern\" in rule and not re.match(rule[\"pattern\"], value):\n",
" errs.append(f\"{name}: ํ์ ๋ถ์ผ์น '{value}'\")\n",
" return errs\n",
"\n",
"\n",
"def validate(rec):\n",
" errs = []\n",
" for _, f, typ, rule, _ in FIELDS:\n",
" errs += [f\"{f}: ํ์ ํ๋ ๋๋ฝ\"] if f not in rec else check(f, rec[f], typ, rule)\n",
" if not errs and ITEMS[rec[\"item_code\"]] != rec[\"item_class\"]:\n",
" errs.append(\"item_class: ํ๋ชฉ ์ฝ๋์ ๋ถ๋ฅ ๋ถ์ผ์น\")\n",
" return errs\n",
"\n",
"\n",
"# 4) [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ํ ๊ธฐ๋ก 30๊ฑด ์์ฑ\n",
"rng = np.random.default_rng(1)\n",
"records = []\n",
"for i in range(30):\n",
" ph, it, size = str(rng.choice(PHASES)), str(rng.choice(list(ITEMS))), int(rng.integers(3, 9)) * 100\n",
" qty = float(round(CSR.loc[ph, it] * size / 100 * rng.uniform(0.7, 1.4), 1))\n",
" stock = float(round(qty * rng.uniform(1, 8), 1))\n",
" records.append({\"record_id\": f\"R{i:05d}\", \"unit_id\": f\"U{int(rng.integers(1, 9)):02d}\", \"day_of_op\": int(rng.integers(0, 90)),\n",
" \"phase\": ph, \"unit_size\": size, \"intensity\": float(round(rng.uniform(0, 1), 2)),\n",
" \"resistance\": float(round(rng.uniform(0, 5), 1)), \"fire_missions\": int(rng.integers(0, 12)),\n",
" \"move_km\": float(round(rng.uniform(0, 40), 1)), \"weather\": str(rng.choice([\"clear\", \"rain\", \"snow\"])),\n",
" \"item_code\": it, \"item_class\": ITEMS[it], \"csr_coeff\": float(CSR.loc[ph, it]),\n",
" \"unit_per_pallet\": 20 if ITEMS[it] == \"ํ์ฝ\" else 4, \"consumption_qty\": qty,\n",
" \"emergency_issue\": bool(rng.random() < 0.05), \"on_hand_qty\": stock, \"dos\": round(stock / qty, 1),\n",
" \"storage_id\": str(rng.choice([\"S1\", \"S2\", \"S3\"])), \"slot_id\": f\"{'ABC'[i % 3]}{i % 10:02d}\",\n",
" \"slot_distance_m\": float(5 + (i % 10) * 6), \"occupancy_rate\": float(round(rng.uniform(0.2, 1), 2)), \"source\": \"์ผ์ผ๋ณด๊ณ \"})\n",
"\n",
"print(\"[์
๋ฌด ํ๋ก์ธ์ค ๋งต] \" + \" -> \".join(PROCESS))\n",
"ok = sum(1 for r in records if not validate(r))\n",
"print(f\"\\n์ ์ ์ํ {len(records)}๊ฑด ์ค ๊ฒ์ฆ ํต๊ณผ {ok}๊ฑด, ์ค๋ฅ {len(records) - ok}๊ฑด\")\n",
"\n",
"# 5) ์ค๋ฅ ์ฃผ์
์ํ\n",
"bad = json.loads(json.dumps(records[0]))\n",
"bad[\"phase\"] = \"assault\"\n",
"bad[\"intensity\"] = 1.7\n",
"del bad[\"dos\"]\n",
"bad[\"slot_id\"] = \"Z-1\"\n",
"bad[\"unit_size\"] = \"600๋ช
\"\n",
"print(\"\\n[์ค๋ฅ ์ฃผ์
๊ฒฐ๊ณผ]\")\n",
"for e in validate(bad):\n",
" print(\" -\", e)\n",
"bad2 = dict(records[1], item_code=\"fuel_jet\", item_class=\"ํ์ฝ\")\n",
"print(\" -\", validate(bad2)[0])\n",
"\n",
"# 6) ๋ฐ์ดํฐ ๋์
๋๋ฆฌ์ CSR ๊ธฐ์คํ\n",
"dd = pd.DataFrame([(g, f, t.__name__, \", \".join(f\"{k}={v}\" for k, v in r.items()) or \"-\", d) for g, f, t, r, d in FIELDS],\n",
" columns=[\"๊ทธ๋ฃน\", \"ํ๋\", \"์๋ฃํ\", \"๊ท์น\", \"์ค๋ช
\"])\n",
"print(f\"\\n[๋ฐ์ดํฐ ๋์
๋๋ฆฌ] ํ๋ {len(dd)}๊ฐ (๋ชฉํ 20๊ฐ ์ด์: {'๋ฌ์ฑ' if len(dd) >= 20 else '๋ฏธ๋ฌ'})\")\n",
"for g, s in dd.groupby(\"๊ทธ๋ฃน\", sort=False)[\"ํ๋\"]:\n",
" print(f\" {g}: {', '.join(s)}\")\n",
"print(\"\\n[์์ ๋จ๊ณ๋ณ ๊ธฐ์ค ์๋ชจ์จ(CSR) - ๊ฐ์ ๊ณ์, 100๋ช
ยท1์ผ ๊ธฐ์ค]\")\n",
"print(CSR.to_string())\n",
"print(\"\\n์ฃผ์: ํ๋ชฉ ์ฝ๋ยท๊ณ์ยท์ ์ฅ๊ณ ๋ ๋ชจ๋ ๊ต์ก์ฉ ๊ฐ์ ๊ฐ์ด๋ค. ์ค์ ์ฒด๊ณ ์ฐ๋ ์ ๊ตญ๋ฐฉ ํ์ค ๊ตฐ์ ์ฝ๋(์ฌ๊ณ ๋ฒํธ ๋ฑ) ์ฒด๊ณ์ ๋ง์ถฐ ํ๋๋ฅผ ๋์์ํจ๋ค.\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
01 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[์
๋ฌด ํ๋ก์ธ์ค ๋งต] (1) ์ผ์ผ ์๋ชจ ๋ณด๊ณ ์์ง -> (2) ์์ ๋จ๊ณยท๊ต์ ๊ฐ๋ ๋ฐ์ ์๋ชจ๋ ์ฐ์ถ -> (3) ๋ณด์ ๋ ๋๋น ๋ณด๊ธ ์์ค(DOS) ๊ณ์ฐ -> (4) ์ฌ๋ณด๊ธ ์์ยท์ฐ์ ์์ ๊ฒฐ์ (๊ตฐ์์ฐธ๋ชจ ๊ฒฐ์ฌ) -> (5) ๊ฐ์ ์ ์ฅ๊ณ ์
๊ณ ยท๋ฐฐ์น(์์น์ ์ยท์์
๋์ )\n",
"\n",
"์ ์ ์ํ 30๊ฑด ์ค ๊ฒ์ฆ ํต๊ณผ 30๊ฑด, ์ค๋ฅ 0๊ฑด\n",
"\n",
"[์ค๋ฅ ์ฃผ์
๊ฒฐ๊ณผ]\n",
" - phase: ํ์ฉ๋์ง ์์ ๊ฐ 'assault'\n",
" - unit_size: ์๋ฃํ ์ค๋ฅ(str)\n",
" - intensity: ์ต๋๊ฐ 1 ์ด๊ณผ\n",
" - dos: ํ์ ํ๋ ๋๋ฝ\n",
" - slot_id: ํ์ ๋ถ์ผ์น 'Z-1'\n",
" - item_class: ํ๋ชฉ ์ฝ๋์ ๋ถ๋ฅ ๋ถ์ผ์น\n",
"\n",
"[๋ฐ์ดํฐ ๋์
๋๋ฆฌ] ํ๋ 23๊ฐ (๋ชฉํ 20๊ฐ ์ด์: ๋ฌ์ฑ)\n",
" ์๋ณ: record_id, unit_id, day_of_op\n",
" ์์ : phase, unit_size, intensity, resistance, fire_missions, move_km, weather\n",
" ํ๋ชฉ: item_code, item_class, csr_coeff, unit_per_pallet\n",
" ์๋ชจ: consumption_qty, emergency_issue\n",
" ์ฌ๊ณ : on_hand_qty, dos\n",
" ์ ์ฅ: storage_id, slot_id, slot_distance_m, occupancy_rate\n",
" ๊ด๋ฆฌ: source\n",
"\n",
"[์์ ๋จ๊ณ๋ณ ๊ธฐ์ค ์๋ชจ์จ(CSR) - ๊ฐ์ ๊ณ์, 100๋ช
ยท1์ผ ๊ธฐ์ค]\n",
" ammo_A ammo_B ammo_C ammo_D fuel_jet fuel_diesel\n",
"prep 1.0 0.6 0.3 0.2 1.5 6.0\n",
"attack 6.0 3.5 2.0 1.2 5.0 12.0\n",
"defense 4.5 3.0 1.6 0.8 3.0 7.0\n",
"stability 0.8 0.5 0.3 0.2 1.0 5.0\n",
"\n",
"์ฃผ์: ํ๋ชฉ ์ฝ๋ยท๊ณ์ยท์ ์ฅ๊ณ ๋ ๋ชจ๋ ๊ต์ก์ฉ ๊ฐ์ ๊ฐ์ด๋ค. ์ค์ ์ฒด๊ณ ์ฐ๋ ์ ๊ตญ๋ฐฉ ํ์ค ๊ตฐ์ ์ฝ๋(์ฌ๊ณ ๋ฒํธ ๋ฑ) ์ฒด๊ณ์ ๋ง์ถฐ ํ๋๋ฅผ ๋์์ํจ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
02. ๋ถ๋ ๊ท๋ชจ, ๊ต์ ๊ฐ๋, ์ด๋ ๊ฑฐ๋ฆฌ ๋ณ์ ์ ๊ทํ ๋ฐ ์๋ชจ๋ ์ฐ์ถ ์ ์ฒ๋ฆฌ ๋ชจ๋ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋ถ๋ ๊ท๋ชจ, ๊ต์ ๊ฐ๋, ์ด๋ ๊ฑฐ๋ฆฌ ๋ณ์ ์ ๊ทํ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋ถ๋ ๊ท๋ชจ, ๊ต์ ๊ฐ๋, ์ด๋ ๊ฑฐ๋ฆฌ ๋ณ์๋ฅผ ์ ์ ยท์ ๊ทํํ๊ณ ๋ณ๋ ฅ 100๋ช
๋น ํ์ฐ ์๋ชจ์จ์ ์ฐ์ถํ๋ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ค๋ณต ๊ธฐ๋ก ์ ๊ฑฐ, ํ๊ธฐ ํต์ผ, ๋ฐฑ๋ถ์จยท์๋ฆฟ์ ์ค๊ธฐ ๋ณด์ , ์์ยท์ด์์น ๊ฒฐ์ธก ์ฒ๋ฆฌ, KNN ๊ฒฐ์ธก ๋ณด์ ๋ฑ ์ ์ ๊ท์น 10์ข
์ ์ ์ฉ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/etl/amm02_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": [
"\"\"\"๋ถ๋ ๊ท๋ชจยท๊ต์ ๊ฐ๋ยท์ด๋ ๊ฑฐ๋ฆฌ ๋ณ์๋ฅผ ์ ์ ยท์ ๊ทํํ๊ณ , ๋ณ๋ ฅ 100๋ช
๋น ํ์ฐ ์๋ชจ์จ์ ์ฐ์ถํ๋ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค. ์ ์ ๊ท์น 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",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋ถ๋ยท๊ฐ์ ํ๋ชฉ ์ผ์ผ ์๋ชจ ๊ธฐ๋ก ์์ฑ๊ธฐ (์ค์ ํ์ฝ ์ ์ยท์๋ชจ์จ๊ณผ ๋ฌด๊ด)\n",
"# ๊ฐ์ ๋ถ๋ 8๊ฐ x 90์ผ. ์๋ชจ๋ = ๊ฐ์ CSR ๊ณ์ x ๋ณ๋ ฅ ๊ท๋ชจ x (๊ต์ ๊ฐ๋ยท์ ์ ํญยทํ๋ ฅ ๊ณํ ํจ๊ณผ) x ์ก์\n",
"# ---------------------------------------------------------------------------\n",
"UNITS = [f\"U{i:02d}\" for i in range(1, 9)]\n",
"ITEMS = [\"ammo_A\", \"ammo_B\", \"ammo_C\", \"ammo_D\", \"fuel_jet\", \"fuel_diesel\"]\n",
"AMMO, FUEL = ITEMS[:4], ITEMS[4:]\n",
"PHASES = [\"prep\", \"attack\", \"defense\", \"stability\"]\n",
"CSR = { # ๊ฐ์ ๊ณ์: ๋ณ๋ ฅ 100๋ช
ยท1์ผ ๊ธฐ์ค ์๋ชจ ๋จ์(ํ์ฝ=์์, ์ ๋ฅ=๋๋ผ)\n",
" \"prep\": [1.0, 0.6, 0.3, 0.2, 1.5, 6.0],\n",
" \"attack\": [6.0, 3.5, 2.0, 1.2, 5.0, 12.0],\n",
" \"defense\": [4.5, 3.0, 1.6, 0.8, 3.0, 7.0],\n",
" \"stability\": [0.8, 0.5, 0.3, 0.2, 1.0, 5.0],\n",
"}\n",
"PHASE_RESIST = {\"prep\": 1.0, \"attack\": 3.2, \"defense\": 3.6, \"stability\": 0.8}\n",
"\n",
"\n",
"def make_ops(n_days=90, seed=7, surge=0):\n",
" \"\"\"๊ฐ์ ๋ถ๋๋ณ ์ผ์ผ ์์ ๊ธฐ๋ก๊ณผ ํ๋ชฉ๋ณ ์ค์ ์๋ชจ๋(์ ๋ต)์ ๋ง๋ ๋ค. surge>0์ด๋ฉด ๋ง์ง๋ง surge์ผ์ ๊ณ ๊ฐ๋ ๊ณต๊ฒฉ ๋จ๊ณ๋ก ๋ฐ๊พผ๋ค(์คํธ๋ ์ค ์ํ์ฉ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for uid in UNITS:\n",
" size = int(rng.integers(3, 9)) * 100\n",
" lens = rng.integers(8, 20, 4)\n",
" sched = [\"prep\"] * lens[0] + [\"attack\"] * lens[1] + [\"defense\"] * lens[2] + [\"attack\"] * lens[3]\n",
" sched = (sched + [\"stability\"] * n_days)[:n_days]\n",
" if surge:\n",
" sched[-surge:] = [\"attack\"] * surge\n",
" r = 1.0\n",
" for d, ph in enumerate(sched):\n",
" r = float(np.clip(0.7 * r + 0.3 * (4.8 if surge and d >= n_days - surge else PHASE_RESIST[ph]) + rng.normal(0, 0.35), 0, 5))\n",
" inten = float(np.clip(r / 5 * 0.9 + rng.normal(0, 0.06), 0, 1))\n",
" fire = int(rng.poisson(1 + 8 * inten))\n",
" move = float(rng.gamma(2, {\"prep\": 4, \"attack\": 12, \"defense\": 2, \"stability\": 5}[ph]))\n",
" weather = str(rng.choice([\"clear\", \"rain\", \"snow\"], p=[0.7, 0.2, 0.1]))\n",
" emerg = bool(rng.random() < 0.02)\n",
" rec = {\"unit\": uid, \"day\": d, \"phase\": ph, \"unit_size\": size, \"intensity\": round(inten, 3),\n",
" \"resistance\": round(r, 2), \"fire_missions\": fire, \"move_km\": round(move, 1),\n",
" \"weather\": weather, \"emergency\": emerg}\n",
" base = size / 100\n",
" for i, it in enumerate(ITEMS):\n",
" c = CSR[ph][i] * base\n",
" if it in AMMO:\n",
" c *= (0.5 + 0.6 * inten) * (1 + 0.04 * fire * (1.0 if it in (\"ammo_C\", \"ammo_D\") else 0.4))\n",
" elif it == \"fuel_jet\":\n",
" c *= (0.6 + 0.5 * inten) + 0.03 * fire\n",
" else:\n",
" c = c * (0.8 + 0.2 * (weather != \"clear\")) + 0.05 * move * base\n",
" c *= rng.lognormal(0, 0.12) * (2.5 if emerg else 1.0)\n",
" rec[it] = round(float(c), 1)\n",
" rows.append(rec)\n",
" return pd.DataFrame(rows)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"\n",
"# 1) [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์์ฒ ๊ธฐ๋ก์ ํ์ฅ ์
๋ ฅ ์ค๋ฅ๋ฅผ ์ผ๋ถ๋ฌ ์๋๋ค\n",
"clean_truth = make_ops(90, seed=7)\n",
"raw = clean_truth.copy()\n",
"rng = np.random.default_rng(3)\n",
"n = len(raw)\n",
"idx = lambda k: rng.choice(n, k, replace=False)\n",
"raw = raw.astype({\"phase\": object, \"weather\": object, \"unit_size\": float, \"fire_missions\": float})\n",
"raw.loc[idx(15), \"phase\"] = raw[\"phase\"].str.upper() # ํ๊ธฐ ๋ถ์ผ์น\n",
"raw.loc[idx(20), \"weather\"] = \"๋น\" # ํ๊ธ ํ๊ธฐ ํผ์ฉ\n",
"raw.loc[idx(12), \"intensity\"] = raw[\"intensity\"] * 100 # ๋ฐฑ๋ถ์จ๋ก ์
๋ ฅ\n",
"raw.loc[idx(10), \"ammo_A\"] = -raw[\"ammo_A\"] # ๋ถํธ ์ค๋ฅ\n",
"raw.loc[idx(6), \"fire_missions\"] = -1\n",
"raw.loc[idx(8), \"move_km\"] = 999.0 # ๊ณ๊ธฐ ์ค๋ฅ\n",
"raw.loc[idx(18), \"intensity\"] = np.nan # ๊ฒฐ์ธก\n",
"raw.loc[idx(14), \"move_km\"] = np.nan\n",
"raw.loc[idx(5), \"unit_size\"] = raw[\"unit_size\"] * 10 # ๋จ์ ์ค๊ธฐ(์๋ฆฟ์)\n",
"raw = pd.concat([raw, raw.sample(12, random_state=1)]).sample(frac=1, random_state=2).reset_index(drop=True) # ์ค๋ณตยท์์ ์์\n",
"n_emerg = int(clean_truth[\"emergency\"].sum())\n",
"\n",
"# 2) ์ ์ ๊ท์น 10์ข
\n",
"log = []\n",
"df = raw.copy()\n",
"before = len(df); df = df.drop_duplicates([\"unit\", \"day\"]); log.append((\"R01\", \"๋ถ๋ยท์ผ์ ์ค๋ณต ๊ธฐ๋ก ์ ๊ฑฐ\", before - len(df)))\n",
"m = df[\"phase\"] != df[\"phase\"].str.strip().str.lower(); df[\"phase\"] = df[\"phase\"].str.strip().str.lower(); log.append((\"R02\", \"์์ ๋จ๊ณ ํ๊ธฐ ํต์ผ\", int(m.sum())))\n",
"wmap = {\"๋น\": \"rain\", \"๋\": \"snow\", \"๋ง์\": \"clear\"}; m = df[\"weather\"].isin(wmap); df[\"weather\"] = df[\"weather\"].replace(wmap); log.append((\"R03\", \"๊ธฐ์ ํ๊ธฐ ํต์ผ(ํ๊ธ->์ฝ๋)\", int(m.sum())))\n",
"m = df[\"intensity\"] > 1; df.loc[m, \"intensity\"] /= 100; log.append((\"R04\", \"๊ต์ ๊ฐ๋ ๋ฐฑ๋ถ์จ ์
๋ ฅ ํ์ฐ\", int(m.sum())))\n",
"m = df[\"unit_size\"] > 3000; df.loc[m, \"unit_size\"] /= 10; log.append((\"R05\", \"๋ณ๋ ฅ ๊ท๋ชจ ์๋ฆฟ์ ์ค๊ธฐ ๋ณด์ \", int(m.sum())))\n",
"m = df[ITEMS].lt(0).any(axis=1) | df[\"fire_missions\"].lt(0)\n",
"df[ITEMS] = df[ITEMS].mask(df[ITEMS] < 0); df[\"fire_missions\"] = df[\"fire_missions\"].mask(df[\"fire_missions\"] < 0); log.append((\"R06\", \"์์ ์๋ชจ๋ยทํ๋ ฅ ์๋ฌด ๊ฒฐ์ธก ์ฒ๋ฆฌ\", int(m.sum())))\n",
"m = (df[\"move_km\"] > 150) & ~df[\"emergency\"]; df.loc[m, \"move_km\"] = np.nan; log.append((\"R07\", \"์ด๋ ๊ฑฐ๋ฆฌ ์ด์์น(150km ์ด๊ณผ) ๊ฒฐ์ธก ์ฒ๋ฆฌ\", int(m.sum())))\n",
"num = [\"unit_size\", \"intensity\", \"resistance\", \"fire_missions\", \"move_km\"] + ITEMS\n",
"n_missing = int(df[num].isna().any(axis=1).sum())\n",
"mu, sd = df[num].mean(), df[num].std()\n",
"df[num] = KNNImputer(n_neighbors=5).fit_transform((df[num] - mu) / sd) * sd.values + mu.values\n",
"df[\"fire_missions\"] = df[\"fire_missions\"].round(); log.append((\"R08\", \"๊ฒฐ์ธก๊ฐ KNN ๋ณด์ (์ ์ฌ ๊ธฐ๋ก 5๊ฑด)\", n_missing))\n",
"df = df.sort_values([\"unit\", \"day\"]).reset_index(drop=True)\n",
"gaps = int(df.groupby(\"unit\")[\"day\"].apply(lambda s: (s.diff().dropna() != 1).sum()).sum()); log.append((\"R09\", \"๋ถ๋๋ณ ์ผ์ ์ ๋ ฌยท์ฐ์์ฑ ํ์ธ(๋๋ฝ์ผ)\", gaps))\n",
"kept = int(df[\"emergency\"].sum()); log.append((\"R10\", \"๊ธด๊ธ ๋ถ์ถ ์์ธ ๊ธฐ๋ก ๋ณด์กด(์ญ์ ๊ธ์ง)\", kept))\n",
"print(pd.DataFrame(log, columns=[\"๊ท์น\", \"๋ด์ฉ\", \"์ฒ๋ฆฌ ๊ฑด์\"]).to_string(index=False))\n",
"\n",
"# 3) ์ ๊ทํ์ ํ์ฐ ์๋ชจ์จ ์ฐ์ถ\n",
"for c in [\"unit_size\", \"intensity\", \"move_km\"]:\n",
" df[c + \"_n\"] = (df[c] - df[c].min()) / (df[c].max() - df[c].min())\n",
"for it in ITEMS:\n",
" df[it + \"_rate\"] = df[it] / (df[\"unit_size\"] / 100) # ๋ณ๋ ฅ 100๋ช
ยท1์ผ ํ์ฐ ์๋ชจ์จ\n",
"\n",
"# 4) ์ ํฉ์ฑ ๊ฒ์ฆ\n",
"checks = {\n",
" \"์ค๋ณต 0๊ฑด\": not df.duplicated([\"unit\", \"day\"]).any(),\n",
" \"๊ฒฐ์ธก 0๊ฑด\": not df[num].isna().any().any(),\n",
" \"์์ ๋จ๊ณ ํ์ฉ๊ฐ\": df[\"phase\"].isin(PHASES).all(),\n",
" \"๊ธฐ์ ํ์ฉ๊ฐ\": df[\"weather\"].isin([\"clear\", \"rain\", \"snow\"]).all(),\n",
" \"๊ต์ ๊ฐ๋ 0~1\": df[\"intensity\"].between(0, 1).all(),\n",
" \"์๋ชจ๋ 0 ์ด์\": (df[ITEMS] >= 0).all().all(),\n",
" \"๋ณ๋ ฅ ๊ท๋ชจ ๋ณต์ ์ผ์น\": (df[\"unit_size\"].values == clean_truth.sort_values([\"unit\", \"day\"])[\"unit_size\"].values).all(),\n",
" \"์ ๊ทํ ๋ฒ์ 0~1\": df[[c + \"_n\" for c in [\"unit_size\", \"intensity\", \"move_km\"]]].stack().between(0, 1).all(),\n",
" \"์ผ์ ์ฐ์์ฑ\": gaps == 0,\n",
" \"๊ธด๊ธ ๋ถ์ถ ๊ธฐ๋ก ๋ณด์กด\": kept == n_emerg,\n",
"}\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(log)}์ข
์ ์ฉ, ์์ฒ {len(raw)}๊ฑด -> ์ ์ {len(df)}๊ฑด, ๊ธด๊ธ ๋ถ์ถ {kept}๊ฑด ๋ณด์กด\")\n",
"\n",
"# 5) ์ ํ ๊ธฐ์ ํต๊ณ ๋น๊ต\n",
"cols = [\"unit_size\", \"intensity\", \"move_km\", \"ammo_A\"]\n",
"comp = pd.DataFrame({\"์ ์ ์ ํ๊ท \": raw[cols].mean(), \"์ ์ ํ ํ๊ท \": df[cols].mean(),\n",
" \"์ ์ ์ ์ต๋\": raw[cols].max(), \"์ ์ ํ ์ต๋\": df[cols].max()}).round(2)\n",
"print(\"\\n[์ ์ ์ ํ ๊ธฐ์ ํต๊ณ]\")\n",
"print(comp.to_string())\n",
"print(\"\\n[์์ ๋จ๊ณ๋ณ ํ์ฐ ์๋ชจ์จ ํ๊ท (100๋ช
ยท1์ผ, ๊ฐ์ ๋จ์)]\")\n",
"print(df.groupby(\"phase\")[[it + \"_rate\" for it in ITEMS]].mean().round(2).rename(columns=lambda c: c[:-5]).to_string())\n",
"\n",
"fig, ax = plt.subplots(1, 3, figsize=(12, 3.5))\n",
"for a, c in zip(ax, [\"intensity\", \"move_km\", \"unit_size\"]):\n",
" a.hist(raw[c].dropna(), bins=30, alpha=0.5, label=\"raw\"); a.hist(df[c], bins=30, alpha=0.7, label=\"cleaned\")\n",
" a.set_title(c); a.set_yscale(\"log\"); a.legend()\n",
"plt.suptitle(\"AMM02 preprocessing: raw vs cleaned (synthetic)\"); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
02 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
" ๊ท์น ๋ด์ฉ ์ฒ๋ฆฌ ๊ฑด์\n",
"R01 ๋ถ๋ยท์ผ์ ์ค๋ณต ๊ธฐ๋ก ์ ๊ฑฐ 12\n",
"R02 ์์ ๋จ๊ณ ํ๊ธฐ ํต์ผ 15\n",
"R03 ๊ธฐ์ ํ๊ธฐ ํต์ผ(ํ๊ธ->์ฝ๋) 20\n",
"R04 ๊ต์ ๊ฐ๋ ๋ฐฑ๋ถ์จ ์
๋ ฅ ํ์ฐ 12\n",
"R05 ๋ณ๋ ฅ ๊ท๋ชจ ์๋ฆฟ์ ์ค๊ธฐ ๋ณด์ 5\n",
"R06 ์์ ์๋ชจ๋ยทํ๋ ฅ ์๋ฌด ๊ฒฐ์ธก ์ฒ๋ฆฌ 16\n",
"R07 ์ด๋ ๊ฑฐ๋ฆฌ ์ด์์น(150km ์ด๊ณผ) ๊ฒฐ์ธก ์ฒ๋ฆฌ 8\n",
"R08 ๊ฒฐ์ธก๊ฐ KNN ๋ณด์ (์ ์ฌ ๊ธฐ๋ก 5๊ฑด) 54\n",
"R09 ๋ถ๋๋ณ ์ผ์ ์ ๋ ฌยท์ฐ์์ฑ ํ์ธ(๋๋ฝ์ผ) 0\n",
"R10 ๊ธด๊ธ ๋ถ์ถ ์์ธ ๊ธฐ๋ก ๋ณด์กด(์ญ์ ๊ธ์ง) 13\n",
"\n",
"์ ํฉ์ฑ ๊ฒ์ฆ 10/10 ํต๊ณผ, ํต๊ณผ์จ 100% (๋ชฉํ 100%: ๋ฌ์ฑ)\n",
"์ ์ ๊ท์น 10์ข
์ ์ฉ, ์์ฒ 732๊ฑด -> ์ ์ 720๊ฑด, ๊ธด๊ธ ๋ถ์ถ 13๊ฑด ๋ณด์กด\n",
"\n",
"[์ ์ ์ ํ ๊ธฐ์ ํต๊ณ]\n",
" ์ ์ ์ ํ๊ท ์ ์ ํ ํ๊ท ์ ์ ์ ์ต๋ ์ ์ ํ ์ต๋\n",
"unit_size 688.66 650.00 8000.0 800.00\n",
"intensity 0.89 0.35 74.4 0.89\n",
"move_km 23.52 12.51 999.0 93.10\n",
"ammo_A 16.89 17.27 142.1 142.10\n",
"\n",
"[์์ ๋จ๊ณ๋ณ ํ์ฐ ์๋ชจ์จ ํ๊ท (100๋ช
ยท1์ผ, ๊ฐ์ ๋จ์)]\n",
" ammo_A ammo_B ammo_C ammo_D fuel_jet fuel_diesel\n",
"phase \n",
"attack 5.63 3.24 2.07 1.25 5.36 11.93\n",
"defense 4.61 3.08 1.82 0.93 3.55 6.58\n",
"prep 0.64 0.39 0.20 0.14 1.18 5.66\n",
"stability 0.50 0.32 0.20 0.13 0.77 4.86\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
03. ๊ต์ ์ผ์ ๊ธฐ๋ฐ 1์ฐจ ํ์ฝ/์ ๋ฅ ๊ธฐ๋ณธ ์๋ชจ๋ ๊ณ์ฐ Baseline ์๊ณ ๋ฆฌ์ฆ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๊ต์ ์ผ์ ๊ธฐ๋ฐ 1์ฐจ ํ์ฝ/์ ๋ฅ ๊ธฐ๋ณธ ์๋ชจ๋ ๊ณ์ฐ Baseline ์๊ณ ๋ฆฌ์ฆ์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๊ต์ ์ผ์ x ์์ ๋จ๊ณ๋ณ CSR x ๋ณ๋ ฅ ๊ท๋ชจ๋ก 1์ฐจ ํ์ฝยท์ ๋ฅ ๊ธฐ๋ณธ ์๋ชจ๋์ ๊ณ์ฐํ๋ Baseline ์๊ณ ๋ฆฌ์ฆ์ ๊ตฌํํ๊ณ ํ์์ ๋ถ์์ผ๋ก ํ๊ณ๋ฅผ ์ฐพ๋๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๋ถ๋๋ณ 90์ผ ๊ณํ๋๊ณผ ์ค์ ์๋ชจ๋์ ๋น๊ตํ๊ณ ํ๋ชฉ๋ณ ์ผ์ผ MAPE ์ฐ์ถํ๊ณ ๋ชจ๋ธ์ ์ ์ฉํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `notebooks/amm02_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": [
"\"\"\"๊ต์ ์ผ์ x ์์ ๋จ๊ณ๋ณ ๊ธฐ์ค ์๋ชจ์จ(CSR) x ๋ณ๋ ฅ ๊ท๋ชจ๋ก 1์ฐจ ํ์ฝยท์ ๋ฅ ๊ธฐ๋ณธ ์๋ชจ๋์ ๊ณ์ฐํ๋ Baseline ์๊ณ ๋ฆฌ์ฆ์ ๊ตฌํํ๊ณ , ํ์์ ๋ถ์(EDA ์ฐจํธ 8์ข
, ์๊ดยท์ ์์ฑ ๊ฒ์ , ABC ํ๋ ํ ๋ถ์)์ผ๋ก Baseline์ ํ๊ณ์ ํต์ฌ ๋ณ์๋ฅผ ์ฐพ๋๋ค.\"\"\"\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",
"# ๊ฐ์ ๋ถ๋ 8๊ฐ x 90์ผ. ์๋ชจ๋ = ๊ฐ์ CSR ๊ณ์ x ๋ณ๋ ฅ ๊ท๋ชจ x (๊ต์ ๊ฐ๋ยท์ ์ ํญยทํ๋ ฅ ๊ณํ ํจ๊ณผ) x ์ก์\n",
"# ---------------------------------------------------------------------------\n",
"UNITS = [f\"U{i:02d}\" for i in range(1, 9)]\n",
"ITEMS = [\"ammo_A\", \"ammo_B\", \"ammo_C\", \"ammo_D\", \"fuel_jet\", \"fuel_diesel\"]\n",
"AMMO, FUEL = ITEMS[:4], ITEMS[4:]\n",
"PHASES = [\"prep\", \"attack\", \"defense\", \"stability\"]\n",
"CSR = { # ๊ฐ์ ๊ณ์: ๋ณ๋ ฅ 100๋ช
ยท1์ผ ๊ธฐ์ค ์๋ชจ ๋จ์(ํ์ฝ=์์, ์ ๋ฅ=๋๋ผ)\n",
" \"prep\": [1.0, 0.6, 0.3, 0.2, 1.5, 6.0],\n",
" \"attack\": [6.0, 3.5, 2.0, 1.2, 5.0, 12.0],\n",
" \"defense\": [4.5, 3.0, 1.6, 0.8, 3.0, 7.0],\n",
" \"stability\": [0.8, 0.5, 0.3, 0.2, 1.0, 5.0],\n",
"}\n",
"PHASE_RESIST = {\"prep\": 1.0, \"attack\": 3.2, \"defense\": 3.6, \"stability\": 0.8}\n",
"\n",
"\n",
"def make_ops(n_days=90, seed=7, surge=0):\n",
" \"\"\"๊ฐ์ ๋ถ๋๋ณ ์ผ์ผ ์์ ๊ธฐ๋ก๊ณผ ํ๋ชฉ๋ณ ์ค์ ์๋ชจ๋(์ ๋ต)์ ๋ง๋ ๋ค. surge>0์ด๋ฉด ๋ง์ง๋ง surge์ผ์ ๊ณ ๊ฐ๋ ๊ณต๊ฒฉ ๋จ๊ณ๋ก ๋ฐ๊พผ๋ค(์คํธ๋ ์ค ์ํ์ฉ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for uid in UNITS:\n",
" size = int(rng.integers(3, 9)) * 100\n",
" lens = rng.integers(8, 20, 4)\n",
" sched = [\"prep\"] * lens[0] + [\"attack\"] * lens[1] + [\"defense\"] * lens[2] + [\"attack\"] * lens[3]\n",
" sched = (sched + [\"stability\"] * n_days)[:n_days]\n",
" if surge:\n",
" sched[-surge:] = [\"attack\"] * surge\n",
" r = 1.0\n",
" for d, ph in enumerate(sched):\n",
" r = float(np.clip(0.7 * r + 0.3 * (4.8 if surge and d >= n_days - surge else PHASE_RESIST[ph]) + rng.normal(0, 0.35), 0, 5))\n",
" inten = float(np.clip(r / 5 * 0.9 + rng.normal(0, 0.06), 0, 1))\n",
" fire = int(rng.poisson(1 + 8 * inten))\n",
" move = float(rng.gamma(2, {\"prep\": 4, \"attack\": 12, \"defense\": 2, \"stability\": 5}[ph]))\n",
" weather = str(rng.choice([\"clear\", \"rain\", \"snow\"], p=[0.7, 0.2, 0.1]))\n",
" emerg = bool(rng.random() < 0.02)\n",
" rec = {\"unit\": uid, \"day\": d, \"phase\": ph, \"unit_size\": size, \"intensity\": round(inten, 3),\n",
" \"resistance\": round(r, 2), \"fire_missions\": fire, \"move_km\": round(move, 1),\n",
" \"weather\": weather, \"emergency\": emerg}\n",
" base = size / 100\n",
" for i, it in enumerate(ITEMS):\n",
" c = CSR[ph][i] * base\n",
" if it in AMMO:\n",
" c *= (0.5 + 0.6 * inten) * (1 + 0.04 * fire * (1.0 if it in (\"ammo_C\", \"ammo_D\") else 0.4))\n",
" elif it == \"fuel_jet\":\n",
" c *= (0.6 + 0.5 * inten) + 0.03 * fire\n",
" else:\n",
" c = c * (0.8 + 0.2 * (weather != \"clear\")) + 0.05 * move * base\n",
" c *= rng.lognormal(0, 0.12) * (2.5 if emerg else 1.0)\n",
" rec[it] = round(float(c), 1)\n",
" rows.append(rec)\n",
" return pd.DataFrame(rows)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"\n",
"def baseline(df):\n",
" \"\"\"Baseline: ํด๋น ์ผ์ ์์ ๋จ๊ณ CSR x (๋ณ๋ ฅ/100). ๊ต์ ๊ฐ๋ยท์ ์ ํญยทํ๋ ฅ ๊ณํ์ ๋ฐ์ํ์ง ์๋๋ค.\"\"\"\n",
" return pd.DataFrame({it: [CSR[p][i] * s / 100 for p, s in zip(df[\"phase\"], df[\"unit_size\"])] for i, it in enumerate(ITEMS)}, index=df.index)\n",
"\n",
"\n",
"df = make_ops(90, seed=7)\n",
"base = baseline(df)\n",
"\n",
"# 1) ๊ต์ ์ผ์ ๊ธฐ๋ฐ ๋ถ๋๋ณ ์์ ๊ธฐ๊ฐ ์ด์๋ชจ๋(๊ณํ) vs ์ค์ \n",
"df[\"engaged\"] = df[\"phase\"].isin([\"attack\", \"defense\"])\n",
"plan = pd.concat([df[[\"unit\"]], base], axis=1).groupby(\"unit\").sum()\n",
"real = df.groupby(\"unit\")[ITEMS].sum()\n",
"days = df.groupby(\"unit\")[\"engaged\"].sum()\n",
"tot = pd.DataFrame({\"๊ต์ ์ผ์\": days, \"ํ์ฝ ๊ณํ\": plan[AMMO].sum(axis=1).round(), \"ํ์ฝ ์ค์ \": real[AMMO].sum(axis=1).round(),\n",
" \"์ ๋ฅ ๊ณํ\": plan[FUEL].sum(axis=1).round(), \"์ ๋ฅ ์ค์ \": real[FUEL].sum(axis=1).round()})\n",
"tot[\"ํ์ฝ ์ค์ฐจ์จ\"] = ((tot[\"ํ์ฝ ๊ณํ\"] - tot[\"ํ์ฝ ์ค์ \"]) / tot[\"ํ์ฝ ์ค์ \"]).map(\"{:+.1%}\".format)\n",
"print(\"[๋ถ๋๋ณ 90์ผ ์ด์๋ชจ๋: Baseline ๊ณํ vs ์ค์ (๊ฐ์ ๋จ์)]\")\n",
"print(tot.to_string())\n",
"\n",
"# 2) ์ผ์ผ ์์ธก ์ค์ฐจ(MAPE)\n",
"mape = (np.abs(base - df[ITEMS]) / df[ITEMS]).mean()\n",
"print(\"\\nBaseline ์ผ์ผ MAPE:\", \", \".join(f\"{k} {v:.1%}\" for k, v in mape.items()))\n",
"\n",
"# 3) ์๊ดยท์ ์์ฑ ๊ฒ์ : ๋ณ๋ ฅ 100๋ช
๋น ํ์ฝ ์๋ชจ์จ๊ณผ ๊ฐ ๋ณ์\n",
"df[\"ammo_rate\"] = df[AMMO].sum(axis=1) / (df[\"unit_size\"] / 100)\n",
"df[\"bad_weather\"] = (df[\"weather\"] != \"clear\").astype(int)\n",
"cand = [\"intensity\", \"resistance\", \"fire_missions\", \"move_km\", \"engaged\", \"day\", \"bad_weather\", \"unit_size\"]\n",
"rows = []\n",
"for c in cand:\n",
" r, p = stats.pearsonr(df[c].astype(float), df[\"ammo_rate\"])\n",
" rows.append({\"๋ณ์\": c, \"์๊ด๊ณ์\": round(r, 3), \"p-value\": f\"{p:.1e}\", \"์ ์(p<0.05,|r|>=0.2)\": p < 0.05 and abs(r) >= 0.2})\n",
"corr = pd.DataFrame(rows).sort_values(\"์๊ด๊ณ์\", key=abs, ascending=False)\n",
"n_sig = int(corr.iloc[:, 3].sum())\n",
"print(\"\\n[ํ์ฝ ํ์ฐ ์๋ชจ์จ๊ณผ์ ์๊ดยท์ ์์ฑ]\")\n",
"print(corr.to_string(index=False))\n",
"print(f\"์ ์๋ฏธํ ์๊ด ๋ณ์ {n_sig}๊ฐ (๋ชฉํ 5๊ฐ ์ด์: {'๋ฌ์ฑ' if n_sig >= 5 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# 4) ABC ํ๋ ํ ๋ถ์: ๋ถ๋ x ํ์ข
์กฐํฉ๋ณ ์ด์๋ชจ๋(์์) ์์ 20%\n",
"pareto = real[AMMO].stack().sort_values(ascending=False)\n",
"top = pareto.iloc[:int(np.ceil(len(pareto) * 0.2))]\n",
"print(f\"\\nABC ๋ถ์: ์์ 20% ์กฐํฉ({len(top)}๊ฐ)์ด ์ ์ฒด ์๋ชจ๋์ {top.sum() / pareto.sum():.1%}๋ฅผ ์ฐจ์ง -> A๋ฑ๊ธ ์ง์ค ๊ด๋ฆฌ\")\n",
"print(\"A๋ฑ๊ธ ์์:\", \", \".join(f\"{u}-{i}\" for u, i in top.index[:5]))\n",
"\n",
"# 5) EDA ์ฐจํธ 8์ข
\n",
"fig, ax = plt.subplots(2, 4, figsize=(16, 7))\n",
"ax = ax.ravel()\n",
"df.groupby(\"phase\")[AMMO].mean().plot.bar(ax=ax[0], title=\"1. Mean ammo use by phase\")\n",
"ax[1].scatter(df[\"intensity\"], df[\"ammo_rate\"], s=6, alpha=0.5); ax[1].set_title(\"2. Intensity vs ammo rate\")\n",
"ax[2].scatter(df[\"fire_missions\"], df[\"ammo_rate\"], s=6, alpha=0.5, c=\"tab:red\"); ax[2].set_title(\"3. Fire missions vs ammo rate\")\n",
"ax[3].scatter(df[\"move_km\"], df[\"fuel_diesel\"] / (df[\"unit_size\"] / 100), s=6, alpha=0.5, c=\"tab:green\"); ax[3].set_title(\"4. Move km vs diesel rate\")\n",
"u = df[df[\"unit\"] == \"U01\"]\n",
"ax[4].plot(u[\"day\"], u[AMMO].sum(axis=1), label=\"actual\"); ax[4].plot(u[\"day\"], base.loc[u.index, AMMO].sum(axis=1), label=\"baseline\")\n",
"ax[4].legend(); ax[4].set_title(\"5. U01 daily ammo: actual vs baseline\")\n",
"im = ax[5].imshow(df[cand + [\"ammo_rate\"]].astype(float).corr(), cmap=\"coolwarm\", vmin=-1, vmax=1)\n",
"ax[5].set_xticks(range(9)); ax[5].set_xticklabels(cand + [\"ammo_rate\"], rotation=90, fontsize=7); ax[5].set_yticks([])\n",
"ax[5].set_title(\"6. Correlation heatmap\")\n",
"ax[6].bar(range(len(pareto)), pareto.cumsum() / pareto.sum()); ax[6].axhline(0.8, ls=\"--\", c=\"k\"); ax[6].set_title(\"7. Pareto (unit x ammo item)\")\n",
"ax[7].bar(mape.index, mape.values, color=\"gray\"); ax[7].set_title(\"8. Baseline MAPE by item\"); ax[7].tick_params(axis=\"x\", rotation=45)\n",
"plt.suptitle(\"AMM02 EDA - synthetic consumption data\"); plt.tight_layout(); plt.show()\n",
"print(\"EDA ์ฐจํธ 8์ข
์์ฑ (๋ชฉํ 8์ข
์ด์: ๋ฌ์ฑ)\")\n",
"print(\"ํต์ฌ ๋ฐ๊ฒฌ: Baseline์ ๊ต์ ๊ฐ๋ยทํ๋ ฅ ๊ณํ์ ๋ฐ์ํ์ง ์์ ๊ณ ๊ฐ๋ ์ผ์๋ ๊ณผ์, ์ ๊ฐ๋ ์ผ์๋ ๊ณผ๋ ์ฐ์ถํ๋ค -> ๋ฏธ์
4์์ ๋ณ์ ๋ฐ์ ๋ชจ๋ธ๋ก ๊ณ ๋ํํ๋ค.\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
03 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๋ถ๋๋ณ 90์ผ ์ด์๋ชจ๋: Baseline ๊ณํ vs ์ค์ (๊ฐ์ ๋จ์)]\n",
" ๊ต์ ์ผ์ ํ์ฝ ๊ณํ ํ์ฝ ์ค์ ์ ๋ฅ ๊ณํ ์ ๋ฅ ์ค์ ํ์ฝ ์ค์ฐจ์จ\n",
"unit \n",
"U01 48 5114.0 5120.0 7716.0 7959.0 -0.1%\n",
"U02 40 3952.0 3885.0 6328.0 6678.0 +1.7%\n",
"U03 46 3725.0 3493.0 5715.0 5616.0 +6.6%\n",
"U04 41 2893.0 2586.0 4618.0 4504.0 +11.9%\n",
"U05 36 2139.0 1971.0 3574.0 3355.0 +8.5%\n",
"U06 34 4028.0 3598.0 6820.0 6404.0 +12.0%\n",
"U07 26 2945.0 2578.0 5408.0 5282.0 +14.2%\n",
"U08 49 4541.0 4124.0 6776.0 6643.0 +10.1%\n",
"\n",
"Baseline ์ผ์ผ MAPE: ammo_A 43.8%, ammo_B 42.9%, ammo_C 38.1%, ammo_D 37.9%, fuel_jet 25.9%, fuel_diesel 16.8%\n",
"\n",
"[ํ์ฝ ํ์ฐ ์๋ชจ์จ๊ณผ์ ์๊ดยท์ ์์ฑ]\n",
" ๋ณ์ ์๊ด๊ณ์ p-value ์ ์(p<0.05,|r|>=0.2)\n",
" engaged 0.923 1.4e-300 True\n",
" resistance 0.854 9.2e-206 True\n",
" intensity 0.838 1.1e-190 True\n",
"fire_missions 0.639 7.9e-84 True\n",
" day -0.378 6.9e-26 True\n",
" move_km 0.294 7.5e-16 True\n",
" bad_weather 0.072 5.2e-02 False\n",
" unit_size 0.017 6.4e-01 False\n",
"์ ์๋ฏธํ ์๊ด ๋ณ์ 6๊ฐ (๋ชฉํ 5๊ฐ ์ด์: ๋ฌ์ฑ)\n",
"\n",
"ABC ๋ถ์: ์์ 20% ์กฐํฉ(7๊ฐ)์ด ์ ์ฒด ์๋ชจ๋์ 43.0%๋ฅผ ์ฐจ์ง -> A๋ฑ๊ธ ์ง์ค ๊ด๋ฆฌ\n",
"A๋ฑ๊ธ ์์: U01-ammo_A, U08-ammo_A, U02-ammo_A, U06-ammo_A, U03-ammo_A\n",
"EDA ์ฐจํธ 8์ข
์์ฑ (๋ชฉํ 8์ข
์ด์: ๋ฌ์ฑ)\n",
"ํต์ฌ ๋ฐ๊ฒฌ: Baseline์ ๊ต์ ๊ฐ๋ยทํ๋ ฅ ๊ณํ์ ๋ฐ์ํ์ง ์์ ๊ณ ๊ฐ๋ ์ผ์๋ ๊ณผ์, ์ ๊ฐ๋ ์ผ์๋ ๊ณผ๋ ์ฐ์ถํ๋ค -> ๋ฏธ์
4์์ ๋ณ์ ๋ฐ์ ๋ชจ๋ธ๋ก ๊ณ ๋ํํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
04. ์ ์ ํญ ์์ค ๋ฐ ํ๋ ฅ ์ด์ฉ ๊ณํ ์ฐ๊ณ ์ผ์๋ณ ์๋ชจ๋ ์๊ณ์ด ์์ธก ๋ชจ๋ธ ๊ณ ๋ํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์์ ๊ณํ ์ฐ๊ณ ์ผ์๋ณ ์๋ชจ๋ ์๊ณ์ด ์์ธก ๋ชจ๋ธ์ ๊ณ ๋ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ ์ ํญ ์์ค(์ ์ผ ์ค์ )๊ณผ ํ๋ ฅ ์ด์ฉ ๊ณํ์ ์ฐ๊ณํ ํ์ ํผ์ฒ๋ก ์ผ์๋ณ ์๋ชจ๋ ์๊ณ์ด ์์ธก ๋ชจ๋ธ์ ๊ณ ๋ํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ ์ผยท์ต๊ทผ ํ๊ท ์๋ชจ ๋น์จ, CSR ๊ณ์, ๊ต์ ์ค ํ๋ ฅ ๊ณํ, ์ ์ ํญ ์ถ์ธ ๋ฑ ์ฌ์ ์ ์ ์ ์๋ ์ ๋ณด๋ง์ผ๋ก ํ์ ํผ์ฒ 10๊ฐ ์ด์ ์์ฑํ์ฌ ์์ธ์ ๋ถ์ํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/features/amm02_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": [
"\"\"\"์ ์ ํญ ์์ค(์ ์ผ ์ค์ )๊ณผ ํ๋ ฅ ์ด์ฉ ๊ณํ(๋น์ผ ๊ณํ ํ๋ ฅ ์๋ฌด ์)์ ์ฐ๊ณํ ํ์ ํผ์ฒ๋ฅผ ๋ง๋ค๊ณ , ์ผ์๋ณ ํ์ฝยท์ ๋ฅ ์๋ชจ๋ ์๊ณ์ด ์์ธก ๋ชจ๋ธ์ ์๊ณ์ด ๊ต์ฐจ๊ฒ์ฆ์ผ๋ก ํ๊ฐํด Baseline(CSR ๊ณ ์ ๊ณ์)๊ณผ ๋น๊ตํ๋ค.\"\"\"\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",
"# ๊ฐ์ ๋ถ๋ 8๊ฐ x 90์ผ. ์๋ชจ๋ = ๊ฐ์ CSR ๊ณ์ x ๋ณ๋ ฅ ๊ท๋ชจ x (๊ต์ ๊ฐ๋ยท์ ์ ํญยทํ๋ ฅ ๊ณํ ํจ๊ณผ) x ์ก์\n",
"# ---------------------------------------------------------------------------\n",
"UNITS = [f\"U{i:02d}\" for i in range(1, 9)]\n",
"ITEMS = [\"ammo_A\", \"ammo_B\", \"ammo_C\", \"ammo_D\", \"fuel_jet\", \"fuel_diesel\"]\n",
"AMMO, FUEL = ITEMS[:4], ITEMS[4:]\n",
"PHASES = [\"prep\", \"attack\", \"defense\", \"stability\"]\n",
"CSR = { # ๊ฐ์ ๊ณ์: ๋ณ๋ ฅ 100๋ช
ยท1์ผ ๊ธฐ์ค ์๋ชจ ๋จ์(ํ์ฝ=์์, ์ ๋ฅ=๋๋ผ)\n",
" \"prep\": [1.0, 0.6, 0.3, 0.2, 1.5, 6.0],\n",
" \"attack\": [6.0, 3.5, 2.0, 1.2, 5.0, 12.0],\n",
" \"defense\": [4.5, 3.0, 1.6, 0.8, 3.0, 7.0],\n",
" \"stability\": [0.8, 0.5, 0.3, 0.2, 1.0, 5.0],\n",
"}\n",
"PHASE_RESIST = {\"prep\": 1.0, \"attack\": 3.2, \"defense\": 3.6, \"stability\": 0.8}\n",
"\n",
"\n",
"def make_ops(n_days=90, seed=7, surge=0):\n",
" \"\"\"๊ฐ์ ๋ถ๋๋ณ ์ผ์ผ ์์ ๊ธฐ๋ก๊ณผ ํ๋ชฉ๋ณ ์ค์ ์๋ชจ๋(์ ๋ต)์ ๋ง๋ ๋ค. surge>0์ด๋ฉด ๋ง์ง๋ง surge์ผ์ ๊ณ ๊ฐ๋ ๊ณต๊ฒฉ ๋จ๊ณ๋ก ๋ฐ๊พผ๋ค(์คํธ๋ ์ค ์ํ์ฉ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for uid in UNITS:\n",
" size = int(rng.integers(3, 9)) * 100\n",
" lens = rng.integers(8, 20, 4)\n",
" sched = [\"prep\"] * lens[0] + [\"attack\"] * lens[1] + [\"defense\"] * lens[2] + [\"attack\"] * lens[3]\n",
" sched = (sched + [\"stability\"] * n_days)[:n_days]\n",
" if surge:\n",
" sched[-surge:] = [\"attack\"] * surge\n",
" r = 1.0\n",
" for d, ph in enumerate(sched):\n",
" r = float(np.clip(0.7 * r + 0.3 * (4.8 if surge and d >= n_days - surge else PHASE_RESIST[ph]) + rng.normal(0, 0.35), 0, 5))\n",
" inten = float(np.clip(r / 5 * 0.9 + rng.normal(0, 0.06), 0, 1))\n",
" fire = int(rng.poisson(1 + 8 * inten))\n",
" move = float(rng.gamma(2, {\"prep\": 4, \"attack\": 12, \"defense\": 2, \"stability\": 5}[ph]))\n",
" weather = str(rng.choice([\"clear\", \"rain\", \"snow\"], p=[0.7, 0.2, 0.1]))\n",
" emerg = bool(rng.random() < 0.02)\n",
" rec = {\"unit\": uid, \"day\": d, \"phase\": ph, \"unit_size\": size, \"intensity\": round(inten, 3),\n",
" \"resistance\": round(r, 2), \"fire_missions\": fire, \"move_km\": round(move, 1),\n",
" \"weather\": weather, \"emergency\": emerg}\n",
" base = size / 100\n",
" for i, it in enumerate(ITEMS):\n",
" c = CSR[ph][i] * base\n",
" if it in AMMO:\n",
" c *= (0.5 + 0.6 * inten) * (1 + 0.04 * fire * (1.0 if it in (\"ammo_C\", \"ammo_D\") else 0.4))\n",
" elif it == \"fuel_jet\":\n",
" c *= (0.6 + 0.5 * inten) + 0.03 * fire\n",
" else:\n",
" c = c * (0.8 + 0.2 * (weather != \"clear\")) + 0.05 * move * base\n",
" c *= rng.lognormal(0, 0.12) * (2.5 if emerg else 1.0)\n",
" rec[it] = round(float(c), 1)\n",
" rows.append(rec)\n",
" return pd.DataFrame(rows)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์๋ชจ๋ ์์ธก์ฉ ํ์ ํผ์ฒ: ์ ์ผ๊น์ง์ ์ค์ + ๋น์ผ ์์ ยทํ๋ ฅ ๊ณํ(์ฌ์ ์ ์ ์ ์๋ ๊ฐ๋ง) ----\n",
"FEATS = [\"lag1\", \"lag2\", \"roll3\", \"roll7\", \"csr_base\", \"size100\", \"fire_plan\", \"fire_x_engaged\", \"resist_lag1\",\n",
" \"resist_trend\", \"inten_lag1\", \"move_plan\", \"days_in_phase\", \"engaged\", \"ph_attack\", \"ph_defense\", \"ph_prep\",\n",
" \"item_idx\", \"is_fuel\"]\n",
"DERIVED = [\"lag1\", \"lag2\", \"roll3\", \"roll7\", \"csr_base\", \"fire_x_engaged\", \"resist_lag1\", \"resist_trend\",\n",
" \"inten_lag1\", \"days_in_phase\", \"engaged\", \"ph_attack\", \"ph_defense\", \"ph_prep\"]\n",
"\n",
"\n",
"def build_long(ops):\n",
" \"\"\"๋ถ๋x์ผ์ ๊ธฐ๋ก์ (๋ถ๋, ์ผ์, ํ๋ชฉ) ํ์ผ๋ก ํผ์น๊ณ ํ์ ํผ์ฒ๋ฅผ ๋ง๋ ๋ค.\n",
" ๋ชฉํ๊ฐ y = log(์ค์ ์๋ชจ๋ / CSR ๊ธฐ์ค๋): CSR ๊ธฐ์ค๋์ ๊ณฑํ '๋ณด์ ๊ณ์'๋ฅผ ์์ธกํ๋ค. lagยทroll ํผ์ฒ๋ ๊ฐ์ ๋น์จ(์ค์ /๊ธฐ์ค)์ด๋ค.\"\"\"\n",
" df = ops.sort_values([\"unit\", \"day\"]).reset_index(drop=True)\n",
" g = df.groupby(\"unit\")\n",
" df[\"resist_lag1\"] = g[\"resistance\"].shift(1)\n",
" df[\"resist_trend\"] = df[\"resist_lag1\"] - g[\"resistance\"].shift(3)\n",
" df[\"inten_lag1\"] = g[\"intensity\"].shift(1)\n",
" df[\"days_in_phase\"] = df.groupby((df[\"phase\"] != g[\"phase\"].shift()).cumsum()).cumcount()\n",
" out = []\n",
" for i, it in enumerate(ITEMS):\n",
" csr = np.array([CSR[p][i] for p in df[\"phase\"]])\n",
" ratio = df[it] / (csr * df[\"unit_size\"] / 100)\n",
" prev = ratio.groupby(df[\"unit\"]).shift(1)\n",
" d = pd.DataFrame({\"unit\": df[\"unit\"], \"day\": df[\"day\"], \"phase\": df[\"phase\"], \"item\": it, \"item_idx\": i,\n",
" \"is_fuel\": int(it in FUEL), \"lag1\": prev, \"lag2\": ratio.groupby(df[\"unit\"]).shift(2),\n",
" \"roll3\": prev.groupby(df[\"unit\"]).transform(lambda s: s.rolling(3).mean()),\n",
" \"roll7\": prev.groupby(df[\"unit\"]).transform(lambda s: s.rolling(7, min_periods=3).mean()),\n",
" \"csr_base\": csr, \"size100\": df[\"unit_size\"] / 100,\n",
" \"fire_plan\": df[\"fire_missions\"], \"fire_x_engaged\": df[\"fire_missions\"] * df[\"phase\"].isin([\"attack\", \"defense\"]),\n",
" \"resist_lag1\": df[\"resist_lag1\"], \"resist_trend\": df[\"resist_trend\"], \"inten_lag1\": df[\"inten_lag1\"],\n",
" \"move_plan\": df[\"move_km\"], \"days_in_phase\": df[\"days_in_phase\"],\n",
" \"engaged\": df[\"phase\"].isin([\"attack\", \"defense\"]).astype(int),\n",
" \"ph_attack\": (df[\"phase\"] == \"attack\").astype(int), \"ph_defense\": (df[\"phase\"] == \"defense\").astype(int),\n",
" \"ph_prep\": (df[\"phase\"] == \"prep\").astype(int), \"emergency\": df[\"emergency\"],\n",
" \"y\": np.log(ratio), \"qty\": df[it]})\n",
" out.append(d)\n",
" return pd.concat(out, ignore_index=True).dropna(subset=[\"lag2\", \"roll3\", \"resist_trend\"]).reset_index(drop=True)\n",
"\n",
"\n",
"def to_qty(d, y_pred):\n",
" \"\"\"์์ธก ๋ณด์ ๊ณ์(log)๋ฅผ ์๋ชจ๋์ผ๋ก ๋๋๋ฆฐ๋ค: ๊ธฐ์ค๋ x exp(์์ธก).\"\"\"\n",
" return np.exp(y_pred) * d[\"csr_base\"].values * d[\"size100\"].values\n",
"\n",
"\n",
"def mape(y, p):\n",
" return float(np.mean(np.abs(np.asarray(p) - np.asarray(y)) / np.asarray(y)))\n",
"\n",
"\n",
"data = build_long(make_ops(90, seed=7)).sort_values([\"day\", \"unit\", \"item\"]).reset_index(drop=True)\n",
"print(f\"ํ์ต ํ {len(data)}๊ฐ(๋ถ๋ 8 x ํ๋ชฉ 6 x ์ผ์), ์
๋ ฅ ํผ์ฒ {len(FEATS)}๊ฐ ์ค ํ์ ํผ์ฒ {len(DERIVED)}๊ฐ (๋ชฉํ 10๊ฐ ์ด์: {'๋ฌ์ฑ' if len(DERIVED) >= 10 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# 1) ์๊ณ์ด ๊ต์ฐจ๊ฒ์ฆ: ๊ณผ๊ฑฐ ์ผ์๋ก ํ์ตํ๊ณ ์ดํ ์ผ์๋ฅผ ์์ธก(๋ฏธ๋ ์ ๋ณด ๋์ค ๋ฐฉ์ง)\n",
"days = np.sort(data[\"day\"].unique())\n",
"rows = []\n",
"for k, (tr_d, te_d) in enumerate(TimeSeriesSplit(n_splits=5).split(days), start=1):\n",
" tr, te = data[data[\"day\"].isin(days[tr_d])], data[data[\"day\"].isin(days[te_d])]\n",
" m = GradientBoostingRegressor(n_estimators=150, max_depth=3, learning_rate=0.08, subsample=0.8, random_state=0)\n",
" m.fit(tr[FEATS], tr[\"y\"])\n",
" pred = to_qty(te, m.predict(te[FEATS]))\n",
" base = te[\"csr_base\"] * te[\"size100\"]\n",
" rows.append({\"ํด๋\": k, \"๊ฒ์ฆ ์ผ์\": f\"{days[te_d][0]}~{days[te_d][-1]}\", \"Baseline MAPE\": mape(te[\"qty\"], base),\n",
" \"๋ชจ๋ธ MAPE\": mape(te[\"qty\"], pred), \"์ ์ผ ๋น์จ ์ ์ง MAPE\": mape(te[\"qty\"], te[\"lag1\"] * te[\"csr_base\"] * te[\"size100\"])})\n",
"cv = pd.DataFrame(rows)\n",
"print(\"\\n[์๊ณ์ด ๊ต์ฐจ๊ฒ์ฆ ๊ฒฐ๊ณผ]\")\n",
"print(cv.to_string(index=False, float_format=lambda x: f\"{x:.1%}\"))\n",
"avg = cv[\"๋ชจ๋ธ MAPE\"].mean()\n",
"print(f\"ํ๊ท MAPE: Baseline {cv['Baseline MAPE'].mean():.1%} -> ๋ชจ๋ธ {avg:.1%} (๋ชฉํ 25% ์ดํ: {'๋ฌ์ฑ' if avg <= 0.25 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# 2) ์ต์ข
๋ชจ๋ธ: ์ 70์ผ ํ์ต, ๋ง์ง๋ง 18์ผ ๊ฒ์ฆ\n",
"tr, te = data[data[\"day\"] < 72], data[data[\"day\"] >= 72]\n",
"model = GradientBoostingRegressor(n_estimators=150, max_depth=3, learning_rate=0.08, subsample=0.8, random_state=0).fit(tr[FEATS], tr[\"y\"])\n",
"te = te.assign(pred=to_qty(te, model.predict(te[FEATS])), base=te[\"csr_base\"] * te[\"size100\"])\n",
"by_item = te.groupby(\"item\")[[\"qty\", \"pred\", \"base\"]].apply(lambda g: pd.Series({\"Baseline\": mape(g[\"qty\"], g[\"base\"]), \"๋ชจ๋ธ\": mape(g[\"qty\"], g[\"pred\"])}))\n",
"print(\"\\n[ํ๋ชฉ๋ณ ๊ฒ์ฆ MAPE(๋ง์ง๋ง 18์ผ)]\")\n",
"print(by_item.T.to_string(float_format=lambda x: f\"{x:.1%}\"))\n",
"\n",
"# 3) ํผ์ฒ ์ค์๋(์ฐจ์ ์ถ์ ํ๋ณด ํ์ธ)\n",
"imp = pd.Series(model.feature_importances_, index=FEATS).sort_values(ascending=False)\n",
"print(\"\\nํผ์ฒ ์ค์๋ ์์ 6:\", \", \".join(f\"{k}({v:.2f})\" for k, v in imp.head(6).items()))\n",
"print(\"์ค์๋ 0.01 ๋ฏธ๋ง(์ ๊ฑฐ ํ๋ณด):\", \", \".join(imp[imp < 0.01].index) or \"์์\")\n",
"\n",
"# 4) ์๊ฐํ\n",
"full = data.assign(pred=to_qty(data, model.predict(data[FEATS])))\n",
"u = full[(full[\"unit\"] == \"U01\") & (full[\"item\"] == \"ammo_A\")]\n",
"fig, ax = plt.subplots(1, 2, figsize=(13, 4))\n",
"ax[0].plot(u[\"day\"], u[\"qty\"], label=\"actual\"); ax[0].plot(u[\"day\"], u[\"pred\"], label=\"model\")\n",
"ax[0].plot(u[\"day\"], u[\"csr_base\"] * u[\"size100\"], ls=\"--\", label=\"baseline (CSR)\"); ax[0].axvline(72, c=\"gray\", ls=\":\")\n",
"ax[0].set_title(\"U01 ammo_A daily forecast (right of dotted line = hold-out)\"); ax[0].set_xlabel(\"day\"); ax[0].legend()\n",
"imp.head(10)[::-1].plot.barh(ax=ax[1], title=\"Feature importance (top 10)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
04 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"ํ์ต ํ 4176๊ฐ(๋ถ๋ 8 x ํ๋ชฉ 6 x ์ผ์), ์
๋ ฅ ํผ์ฒ 19๊ฐ ์ค ํ์ ํผ์ฒ 14๊ฐ (๋ชฉํ 10๊ฐ ์ด์: ๋ฌ์ฑ)\n",
"\n",
"[์๊ณ์ด ๊ต์ฐจ๊ฒ์ฆ ๊ฒฐ๊ณผ]\n",
" ํด๋ ๊ฒ์ฆ ์ผ์ Baseline MAPE ๋ชจ๋ธ MAPE ์ ์ผ ๋น์จ ์ ์ง MAPE\n",
" 1 20~33 15.4% 15.4% 23.0%\n",
" 2 34~47 17.7% 19.0% 20.4%\n",
" 3 48~61 33.4% 13.0% 17.3%\n",
" 4 62~75 51.8% 13.6% 21.7%\n",
" 5 76~89 48.7% 13.3% 21.8%\n",
"ํ๊ท MAPE: Baseline 33.4% -> ๋ชจ๋ธ 14.9% (๋ชฉํ 25% ์ดํ: ๋ฌ์ฑ)\n",
"\n",
"[ํ๋ชฉ๋ณ ๊ฒ์ฆ MAPE(๋ง์ง๋ง 18์ผ)]\n",
"item ammo_A ammo_B ammo_C ammo_D fuel_diesel fuel_jet\n",
"Baseline 66.2% 64.7% 60.0% 55.2% 15.4% 36.6%\n",
"๋ชจ๋ธ 12.7% 12.3% 13.0% 13.6% 13.5% 12.6%\n",
"\n",
"ํผ์ฒ ์ค์๋ ์์ 6: roll3(0.25), resist_lag1(0.25), csr_base(0.09), fire_x_engaged(0.09), fire_plan(0.07), roll7(0.04)\n",
"์ค์๋ 0.01 ๋ฏธ๋ง(์ ๊ฑฐ ํ๋ณด): lag2, ph_defense, ph_attack, engaged, ph_prep\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
05. ๋ถ๋๋ณ ์ผ์ผ ๋ณด๊ธ ์์ค(DOS) ์ฐ๊ณ ๊ธด๊ธ ์ฌ๋ณด๊ธ ์์ ์ฐ์ถ ์์ง ๊ฐ๋ฐ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋ณด๊ธ ์์ค(DOS) ์ฐ๊ณ ๊ธด๊ธ ์ฌ๋ณด๊ธ ์์ ์ฐ์ถ ์์ง์ ๊ฐ๋ฐํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์์๋ธ ์์ธก ๋ชจ๋ธ๊ณผ ๋ณด์ ๋์ผ๋ก ๋ถ๋ยทํ๋ชฉ๋ณ ๋ณด๊ธ ์์ค(DOS)์ ๊ณ์ฐํด ๊ธด๊ธ ์ฌ๋ณด๊ธ ์์๋ฅผ ์ฐ์ถํ๋ ์์ง์ ๊ฐ๋ฐํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **GBR๊ณผ ๋๋คํฌ๋ ์คํธ๋ฅผ ์์๋ธํ๊ณ ๊ฐ์ค์น๋ ํ์ต ๊ธฐ๊ฐ ์์ ๊ฒ์ฆ ๊ตฌ๊ฐ์ผ๋ก๋ง ๊ณ ๋ฅธ๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/amm02_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": [
"\"\"\"GBRยท๋๋คํฌ๋ ์คํธ ์์๋ธ๋ก ์๋ชจ๋ ์์ธก์ ๊ณ ๋ํํด Hold-out ๊ธฐ๊ฐ์์ Baseline ๋๋น MAE/RMSE ๊ฐ์ ์จ๊ณผ R2๋ฅผ ๊ฒ์ฆํ๊ณ , ์์ธก ์๋ชจ๋๊ณผ ๋ณด์ ๋์ผ๋ก ๋ถ๋ยทํ๋ชฉ๋ณ ๋ณด๊ธ ์์ค(DOS)์ ๊ณ์ฐํด ๊ธด๊ธ ์ฌ๋ณด๊ธ ์์๋ฅผ ์ฐ์ถํ๋ ์์ง์ ๊ตฌํํ๋ค.\"\"\"\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.metrics import mean_absolute_error, mean_squared_error, r2_score\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋ถ๋ยท๊ฐ์ ํ๋ชฉ ์ผ์ผ ์๋ชจ ๊ธฐ๋ก ์์ฑ๊ธฐ (์ค์ ํ์ฝ ์ ์ยท์๋ชจ์จ๊ณผ ๋ฌด๊ด)\n",
"# ๊ฐ์ ๋ถ๋ 8๊ฐ x 90์ผ. ์๋ชจ๋ = ๊ฐ์ CSR ๊ณ์ x ๋ณ๋ ฅ ๊ท๋ชจ x (๊ต์ ๊ฐ๋ยท์ ์ ํญยทํ๋ ฅ ๊ณํ ํจ๊ณผ) x ์ก์\n",
"# ---------------------------------------------------------------------------\n",
"UNITS = [f\"U{i:02d}\" for i in range(1, 9)]\n",
"ITEMS = [\"ammo_A\", \"ammo_B\", \"ammo_C\", \"ammo_D\", \"fuel_jet\", \"fuel_diesel\"]\n",
"AMMO, FUEL = ITEMS[:4], ITEMS[4:]\n",
"PHASES = [\"prep\", \"attack\", \"defense\", \"stability\"]\n",
"CSR = { # ๊ฐ์ ๊ณ์: ๋ณ๋ ฅ 100๋ช
ยท1์ผ ๊ธฐ์ค ์๋ชจ ๋จ์(ํ์ฝ=์์, ์ ๋ฅ=๋๋ผ)\n",
" \"prep\": [1.0, 0.6, 0.3, 0.2, 1.5, 6.0],\n",
" \"attack\": [6.0, 3.5, 2.0, 1.2, 5.0, 12.0],\n",
" \"defense\": [4.5, 3.0, 1.6, 0.8, 3.0, 7.0],\n",
" \"stability\": [0.8, 0.5, 0.3, 0.2, 1.0, 5.0],\n",
"}\n",
"PHASE_RESIST = {\"prep\": 1.0, \"attack\": 3.2, \"defense\": 3.6, \"stability\": 0.8}\n",
"\n",
"\n",
"def make_ops(n_days=90, seed=7, surge=0):\n",
" \"\"\"๊ฐ์ ๋ถ๋๋ณ ์ผ์ผ ์์ ๊ธฐ๋ก๊ณผ ํ๋ชฉ๋ณ ์ค์ ์๋ชจ๋(์ ๋ต)์ ๋ง๋ ๋ค. surge>0์ด๋ฉด ๋ง์ง๋ง surge์ผ์ ๊ณ ๊ฐ๋ ๊ณต๊ฒฉ ๋จ๊ณ๋ก ๋ฐ๊พผ๋ค(์คํธ๋ ์ค ์ํ์ฉ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for uid in UNITS:\n",
" size = int(rng.integers(3, 9)) * 100\n",
" lens = rng.integers(8, 20, 4)\n",
" sched = [\"prep\"] * lens[0] + [\"attack\"] * lens[1] + [\"defense\"] * lens[2] + [\"attack\"] * lens[3]\n",
" sched = (sched + [\"stability\"] * n_days)[:n_days]\n",
" if surge:\n",
" sched[-surge:] = [\"attack\"] * surge\n",
" r = 1.0\n",
" for d, ph in enumerate(sched):\n",
" r = float(np.clip(0.7 * r + 0.3 * (4.8 if surge and d >= n_days - surge else PHASE_RESIST[ph]) + rng.normal(0, 0.35), 0, 5))\n",
" inten = float(np.clip(r / 5 * 0.9 + rng.normal(0, 0.06), 0, 1))\n",
" fire = int(rng.poisson(1 + 8 * inten))\n",
" move = float(rng.gamma(2, {\"prep\": 4, \"attack\": 12, \"defense\": 2, \"stability\": 5}[ph]))\n",
" weather = str(rng.choice([\"clear\", \"rain\", \"snow\"], p=[0.7, 0.2, 0.1]))\n",
" emerg = bool(rng.random() < 0.02)\n",
" rec = {\"unit\": uid, \"day\": d, \"phase\": ph, \"unit_size\": size, \"intensity\": round(inten, 3),\n",
" \"resistance\": round(r, 2), \"fire_missions\": fire, \"move_km\": round(move, 1),\n",
" \"weather\": weather, \"emergency\": emerg}\n",
" base = size / 100\n",
" for i, it in enumerate(ITEMS):\n",
" c = CSR[ph][i] * base\n",
" if it in AMMO:\n",
" c *= (0.5 + 0.6 * inten) * (1 + 0.04 * fire * (1.0 if it in (\"ammo_C\", \"ammo_D\") else 0.4))\n",
" elif it == \"fuel_jet\":\n",
" c *= (0.6 + 0.5 * inten) + 0.03 * fire\n",
" else:\n",
" c = c * (0.8 + 0.2 * (weather != \"clear\")) + 0.05 * move * base\n",
" c *= rng.lognormal(0, 0.12) * (2.5 if emerg else 1.0)\n",
" rec[it] = round(float(c), 1)\n",
" rows.append(rec)\n",
" return pd.DataFrame(rows)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์๋ชจ๋ ์์ธก์ฉ ํ์ ํผ์ฒ: ์ ์ผ๊น์ง์ ์ค์ + ๋น์ผ ์์ ยทํ๋ ฅ ๊ณํ(์ฌ์ ์ ์ ์ ์๋ ๊ฐ๋ง) ----\n",
"FEATS = [\"lag1\", \"lag2\", \"roll3\", \"roll7\", \"csr_base\", \"size100\", \"fire_plan\", \"fire_x_engaged\", \"resist_lag1\",\n",
" \"resist_trend\", \"inten_lag1\", \"move_plan\", \"days_in_phase\", \"engaged\", \"ph_attack\", \"ph_defense\", \"ph_prep\",\n",
" \"item_idx\", \"is_fuel\"]\n",
"DERIVED = [\"lag1\", \"lag2\", \"roll3\", \"roll7\", \"csr_base\", \"fire_x_engaged\", \"resist_lag1\", \"resist_trend\",\n",
" \"inten_lag1\", \"days_in_phase\", \"engaged\", \"ph_attack\", \"ph_defense\", \"ph_prep\"]\n",
"\n",
"\n",
"def build_long(ops):\n",
" \"\"\"๋ถ๋x์ผ์ ๊ธฐ๋ก์ (๋ถ๋, ์ผ์, ํ๋ชฉ) ํ์ผ๋ก ํผ์น๊ณ ํ์ ํผ์ฒ๋ฅผ ๋ง๋ ๋ค.\n",
" ๋ชฉํ๊ฐ y = log(์ค์ ์๋ชจ๋ / CSR ๊ธฐ์ค๋): CSR ๊ธฐ์ค๋์ ๊ณฑํ '๋ณด์ ๊ณ์'๋ฅผ ์์ธกํ๋ค. lagยทroll ํผ์ฒ๋ ๊ฐ์ ๋น์จ(์ค์ /๊ธฐ์ค)์ด๋ค.\"\"\"\n",
" df = ops.sort_values([\"unit\", \"day\"]).reset_index(drop=True)\n",
" g = df.groupby(\"unit\")\n",
" df[\"resist_lag1\"] = g[\"resistance\"].shift(1)\n",
" df[\"resist_trend\"] = df[\"resist_lag1\"] - g[\"resistance\"].shift(3)\n",
" df[\"inten_lag1\"] = g[\"intensity\"].shift(1)\n",
" df[\"days_in_phase\"] = df.groupby((df[\"phase\"] != g[\"phase\"].shift()).cumsum()).cumcount()\n",
" out = []\n",
" for i, it in enumerate(ITEMS):\n",
" csr = np.array([CSR[p][i] for p in df[\"phase\"]])\n",
" ratio = df[it] / (csr * df[\"unit_size\"] / 100)\n",
" prev = ratio.groupby(df[\"unit\"]).shift(1)\n",
" d = pd.DataFrame({\"unit\": df[\"unit\"], \"day\": df[\"day\"], \"phase\": df[\"phase\"], \"item\": it, \"item_idx\": i,\n",
" \"is_fuel\": int(it in FUEL), \"lag1\": prev, \"lag2\": ratio.groupby(df[\"unit\"]).shift(2),\n",
" \"roll3\": prev.groupby(df[\"unit\"]).transform(lambda s: s.rolling(3).mean()),\n",
" \"roll7\": prev.groupby(df[\"unit\"]).transform(lambda s: s.rolling(7, min_periods=3).mean()),\n",
" \"csr_base\": csr, \"size100\": df[\"unit_size\"] / 100,\n",
" \"fire_plan\": df[\"fire_missions\"], \"fire_x_engaged\": df[\"fire_missions\"] * df[\"phase\"].isin([\"attack\", \"defense\"]),\n",
" \"resist_lag1\": df[\"resist_lag1\"], \"resist_trend\": df[\"resist_trend\"], \"inten_lag1\": df[\"inten_lag1\"],\n",
" \"move_plan\": df[\"move_km\"], \"days_in_phase\": df[\"days_in_phase\"],\n",
" \"engaged\": df[\"phase\"].isin([\"attack\", \"defense\"]).astype(int),\n",
" \"ph_attack\": (df[\"phase\"] == \"attack\").astype(int), \"ph_defense\": (df[\"phase\"] == \"defense\").astype(int),\n",
" \"ph_prep\": (df[\"phase\"] == \"prep\").astype(int), \"emergency\": df[\"emergency\"],\n",
" \"y\": np.log(ratio), \"qty\": df[it]})\n",
" out.append(d)\n",
" return pd.concat(out, ignore_index=True).dropna(subset=[\"lag2\", \"roll3\", \"resist_trend\"]).reset_index(drop=True)\n",
"\n",
"\n",
"def to_qty(d, y_pred):\n",
" \"\"\"์์ธก ๋ณด์ ๊ณ์(log)๋ฅผ ์๋ชจ๋์ผ๋ก ๋๋๋ฆฐ๋ค: ๊ธฐ์ค๋ x exp(์์ธก).\"\"\"\n",
" return np.exp(y_pred) * d[\"csr_base\"].values * d[\"size100\"].values\n",
"\n",
"\n",
"def mape(y, p):\n",
" return float(np.mean(np.abs(np.asarray(p) - np.asarray(y)) / np.asarray(y)))\n",
"\n",
"# ---- ์๋ชจ๋ ์์ธก ๋ชจ๋ธ(์์๋ธ) ํ์ตยท์์ธก ----\n",
"def fit_models(tr, seed=0):\n",
" \"\"\"GBR + ๋๋คํฌ๋ ์คํธ ๋ ๋ชจ๋ธ์ ํ์ตํ๋ค(๋ชฉํ๊ฐ์ CSR ๊ธฐ์ค๋ ๋๋น log ๋ณด์ ๊ณ์).\"\"\"\n",
" g = GradientBoostingRegressor(n_estimators=150, max_depth=3, learning_rate=0.08, subsample=0.8, random_state=seed).fit(tr[FEATS], tr[\"y\"])\n",
" r = RandomForestRegressor(n_estimators=120, min_samples_leaf=3, random_state=seed, n_jobs=1).fit(tr[FEATS], tr[\"y\"])\n",
" return g, r\n",
"\n",
"\n",
"def predict_qty(models, d, w=0.5):\n",
" \"\"\"๋ ๋ชจ๋ธ์ log ์์ธก์ ๊ฐ์ค ํ๊ท ํด ์๋ชจ๋์ผ๋ก ๋๋๋ฆฐ๋ค.\"\"\"\n",
" g, r = models\n",
" return to_qty(d, w * g.predict(d[FEATS]) + (1 - w) * r.predict(d[FEATS]))\n",
"\n",
"DOS_ALERT = 2.0 # [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ธด๊ธ ์ฌ๋ณด๊ธ ๊ฒฝ๋ณด ๊ธฐ์ค(๋ณด๊ธ ์์ค, ์ผ)\n",
"DOS_TARGET = 3.0 # ์ฌ๋ณด๊ธ ํ ๋ชฉํ ๋ณด๊ธ ์์ค(์ผ)\n",
"\n",
"data = build_long(make_ops(90, seed=7))\n",
"tr, te = data[data[\"day\"] < 60], data[data[\"day\"] >= 60].copy()\n",
"val = tr[tr[\"day\"] >= 48]\n",
"\n",
"# 1) ์์๋ธ ๊ฐ์ค์น ํ๋: ํ์ต ๊ธฐ๊ฐ ๋ง์ง๋ง 12์ผ(๊ฒ์ฆ์ฉ)๋ก ๊ณ ๋ฅด๊ณ , Hold-out(60~89์ผ)์ ์ต์ข
ํ๊ฐ์๋ง ์ฌ์ฉ\n",
"m_inner = fit_models(tr[tr[\"day\"] < 48])\n",
"best_w = min(np.arange(0, 1.01, 0.25), key=lambda w: mape(val[\"qty\"], predict_qty(m_inner, val, w)))\n",
"models = fit_models(tr)\n",
"te[\"base\"] = te[\"csr_base\"] * te[\"size100\"]\n",
"te[\"gbr\"] = to_qty(te, models[0].predict(te[FEATS]))\n",
"te[\"ens\"] = predict_qty(models, te, best_w)\n",
"\n",
"\n",
"def scores(col, d):\n",
" \"\"\"์ค์ฐจ์จ์ CSR ๊ธฐ์ค๋ ๋๋น ๋น์จ(ํ์ฐ ์๋ชจ์จ) ์ฒ๋๋ก ๊ณ์ฐํด ํ๋ชฉ ๊ฐ ๋จ์ ์ฐจ์ด๋ฅผ ์์ค๋ค. R2๋ ์๋ชจ๋ ์ฒ๋.\"\"\"\n",
" k = d[\"csr_base\"] * d[\"size100\"]\n",
" y, p = d[\"qty\"] / k, d[col] / k\n",
" return {\"MAE(๋น์จ)\": float((y - p).abs().mean()), \"RMSE(๋น์จ)\": float(np.sqrt(((y - p) ** 2).mean())),\n",
" \"MAPE\": mape(d[\"qty\"], d[col]), \"R2(์ ์ฒด)\": r2_score(d[\"qty\"], d[col]),\n",
" \"R2(์ ์์๋ชจ)\": r2_score(d.loc[~d[\"emergency\"], \"qty\"], d.loc[~d[\"emergency\"], col])}\n",
"\n",
"\n",
"res = pd.DataFrame({\"Baseline(CSR)\": scores(\"base\", te), \"๋จ์ผ GBR\": scores(\"gbr\", te), f\"์์๋ธ(w={best_w:.2f})\": scores(\"ens\", te)}).T\n",
"print(\"[Hold-out 60~89์ผ ์ฑ๋ฅ, ๋ถ๋ 8 x ํ๋ชฉ 6]\")\n",
"print(res.to_string(float_format=lambda x: f\"{x:.3f}\"))\n",
"b, e = res.iloc[0], res.iloc[2]\n",
"imp_mae, imp_rmse = 1 - e[\"MAE(๋น์จ)\"] / b[\"MAE(๋น์จ)\"], 1 - e[\"RMSE(๋น์จ)\"] / b[\"RMSE(๋น์จ)\"]\n",
"print(f\"์ค์ฐจ์จ ๊ฐ์ : MAE {imp_mae:.1%}, RMSE {imp_rmse:.1%} (๋ชฉํ 20% ์ด์: {'๋ฌ์ฑ' if min(imp_mae, imp_rmse) >= 0.2 else '๋ฏธ๋ฌ'})\")\n",
"print(f\"R2 ์ ์ฒด {e['R2(์ ์ฒด)']:.3f} ({'๋ฌ์ฑ' if e['R2(์ ์ฒด)'] >= 0.85 else '๋ฏธ๋ฌ'}), ๊ธด๊ธ ๋ถ์ถ ์ ์ธ ์ ์ ์๋ชจ {e['R2(์ ์์๋ชจ)']:.3f} (๋ชฉํ 0.85 ์ด์: {'๋ฌ์ฑ' if e['R2(์ ์์๋ชจ)'] >= 0.85 else '๋ฏธ๋ฌ'})\")\n",
"print(\"-> ์ ์ฒด R2 ๋ฏธ๋ฌ ์์ธ์ ์ฌ์ ์ ๋ณด๊ฐ ์๋ ๊ธด๊ธ ๋ถ์ถ ๊ธ์ฆ(2~3๋ฐฐ)์ด๋ค. ์์ธก ๋ชจ๋ธ์ด ์๋๋ผ ๊ธด๊ธ ๊ฒฝ๋ณดยท์๋น ์ฌ๊ณ ๋ก ๋์ํ๋ค.\")\n",
"em = te[te[\"emergency\"]]\n",
"print(f\"๋น์ ํ ์์(๊ธด๊ธ ๋ถ์ถ {len(em)}๊ฑด) MAPE: Baseline {mape(em['qty'], em['base']):.1%}, ์์๋ธ {mape(em['qty'], em['ens']):.1%} -> ๊ธ์ฆ์ ์ฌ์ ์ ๋ณด๊ฐ ์์ด ๋ณ๋ ๊ฒฝ๋ณด๋ก ๋ณด์\")\n",
"\n",
"# 2) DOS ์ฐ๊ณ ๊ธด๊ธ ์ฌ๋ณด๊ธ ์์ง: 75์ผ ์์นจ ๊ธฐ์ค, ๋ณด์ ๋(๊ฐ์) / ์์ธก ์ผ์ผ ์๋ชจ๋\n",
"rng = np.random.default_rng(11)\n",
"day = te[te[\"day\"] == 75].copy()\n",
"day[\"on_hand\"] = np.round(day[\"base\"] * rng.uniform(1.0, 5.0, len(day)), 0)\n",
"day[\"dos\"] = day[\"on_hand\"] / day[\"ens\"]\n",
"day[\"need\"] = np.maximum(0, np.ceil(DOS_TARGET * day[\"ens\"] - day[\"on_hand\"]))\n",
"day[\"flag\"] = np.where(day[\"dos\"] < DOS_ALERT, \"๊ธด๊ธ\", np.where(day[\"dos\"] < DOS_TARGET, \"์ฃผ์\", \"์ ์\"))\n",
"pri = {\"๊ธด๊ธ\": 0, \"์ฃผ์\": 1, \"์ ์\": 2}\n",
"out = day.sort_values([\"flag\", \"dos\"], key=lambda s: s.map(pri) if s.name == \"flag\" else s)\n",
"print(f\"\\n[75์ผ ์ฌ๋ณด๊ธ ์์(DOS {DOS_ALERT:.0f}์ผ ๋ฏธ๋ง ๊ธด๊ธ, ๋ชฉํ {DOS_TARGET:.0f}์ผ)] ๊ธด๊ธ {sum(day['flag'] == '๊ธด๊ธ')}๊ฑด, ์ฃผ์ {sum(day['flag'] == '์ฃผ์')}๊ฑด, ์ ์ {sum(day['flag'] == '์ ์')}๊ฑด\")\n",
"show = out[out[\"flag\"] != \"์ ์\"].head(10)[[\"unit\", \"item\", \"phase\", \"on_hand\", \"ens\", \"dos\", \"need\", \"flag\"]]\n",
"show.columns = [\"๋ถ๋\", \"ํ๋ชฉ\", \"์์ ๋จ๊ณ\", \"๋ณด์ ๋\", \"์์ธก์๋ชจ/์ผ\", \"DOS(์ผ)\", \"์ฌ๋ณด๊ธ๋\", \"ํ์ \"]\n",
"print(show.to_string(index=False, float_format=lambda x: f\"{x:.1f}\"))\n",
"real_dos = day[\"on_hand\"] / day[\"qty\"]\n",
"hit = ((day[\"dos\"] < DOS_ALERT) == (real_dos < DOS_ALERT)).mean()\n",
"print(f\"๊ธด๊ธ ํ์ ์ผ์น์จ(์ค์ ์๋ชจ ๊ธฐ์ค DOS์ ๋น๊ต): {hit:.1%}\")\n",
"print(\"์ฌ๋ณด๊ธ ์์๋ ๊ตฐ์์ฐธ๋ชจ ๊ฒฐ์ฌ ์ ์ถ์ฒ๊ฐ์ด๋ฉฐ ์๋ ๋ถ์ถํ์ง ์๋๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(12, 4))\n",
"ax[0].scatter(te[\"qty\"], te[\"base\"], s=5, alpha=0.4, label=\"baseline\")\n",
"ax[0].scatter(te[\"qty\"], te[\"ens\"], s=5, alpha=0.4, label=\"ensemble\")\n",
"mx = te[\"qty\"].max(); ax[0].plot([0, mx], [0, mx], \"k--\", lw=1)\n",
"ax[0].set_xlabel(\"actual\"); ax[0].set_ylabel(\"predicted\"); ax[0].set_title(\"Hold-out actual vs predicted\"); ax[0].legend()\n",
"piv = day.pivot(index=\"unit\", columns=\"item\", values=\"dos\").clip(upper=6)\n",
"im = ax[1].imshow(piv.values, cmap=\"RdYlGn\", vmin=0, vmax=6, aspect=\"auto\")\n",
"ax[1].set_xticks(range(len(piv.columns))); ax[1].set_xticklabels(piv.columns, rotation=30)\n",
"ax[1].set_yticks(range(len(piv.index))); ax[1].set_yticklabels(piv.index)\n",
"ax[1].set_title(\"Days of supply by unit/item (day 75)\"); plt.colorbar(im, ax=ax[1])\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
05 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[Hold-out 60~89์ผ ์ฑ๋ฅ, ๋ถ๋ 8 x ํ๋ชฉ 6]\n",
" MAE(๋น์จ) RMSE(๋น์จ) MAPE R2(์ ์ฒด) R2(์ ์์๋ชจ)\n",
"Baseline(CSR) 0.313 0.371 0.493 0.828 0.956\n",
"๋จ์ผ GBR 0.110 0.211 0.140 0.827 0.963\n",
"์์๋ธ(w=0.25) 0.107 0.210 0.133 0.827 0.966\n",
"์ค์ฐจ์จ ๊ฐ์ : MAE 66.0%, RMSE 43.2% (๋ชฉํ 20% ์ด์: ๋ฌ์ฑ)\n",
"R2 ์ ์ฒด 0.827 (๋ฏธ๋ฌ), ๊ธด๊ธ ๋ถ์ถ ์ ์ธ ์ ์ ์๋ชจ 0.966 (๋ชฉํ 0.85 ์ด์: ๋ฌ์ฑ)\n",
"-> ์ ์ฒด R2 ๋ฏธ๋ฌ ์์ธ์ ์ฌ์ ์ ๋ณด๊ฐ ์๋ ๊ธด๊ธ ๋ถ์ถ ๊ธ์ฆ(2~3๋ฐฐ)์ด๋ค. ์์ธก ๋ชจ๋ธ์ด ์๋๋ผ ๊ธด๊ธ ๊ฒฝ๋ณดยท์๋น ์ฌ๊ณ ๋ก ๋์ํ๋ค.\n",
"๋น์ ํ ์์(๊ธด๊ธ ๋ถ์ถ 24๊ฑด) MAPE: Baseline 38.7%, ์์๋ธ 58.8% -> ๊ธ์ฆ์ ์ฌ์ ์ ๋ณด๊ฐ ์์ด ๋ณ๋ ๊ฒฝ๋ณด๋ก ๋ณด์\n",
"\n",
"[75์ผ ์ฌ๋ณด๊ธ ์์(DOS 2์ผ ๋ฏธ๋ง ๊ธด๊ธ, ๋ชฉํ 3์ผ)] ๊ธด๊ธ 4๊ฑด, ์ฃผ์ 14๊ฑด, ์ ์ 30๊ฑด\n",
" ๋ถ๋ ํ๋ชฉ ์์ ๋จ๊ณ ๋ณด์ ๋ ์์ธก์๋ชจ/์ผ DOS(์ผ) ์ฌ๋ณด๊ธ๋ ํ์ \n",
"U01 fuel_diesel stability 45.0 35.7 1.3 62.0 ๊ธด๊ธ\n",
"U08 fuel_diesel stability 37.0 29.2 1.3 51.0 ๊ธด๊ธ\n",
"U01 fuel_jet stability 11.0 6.2 1.8 8.0 ๊ธด๊ธ\n",
"U04 ammo_A stability 4.0 2.2 1.8 3.0 ๊ธด๊ธ\n",
"U02 fuel_diesel stability 63.0 31.4 2.0 32.0 ์ฃผ์\n",
"U07 ammo_A stability 7.0 3.5 2.0 4.0 ์ฃผ์\n",
"U06 ammo_D stability 2.0 1.0 2.1 1.0 ์ฃผ์\n",
"U01 ammo_A stability 10.0 4.5 2.2 4.0 ์ฃผ์\n",
"U05 ammo_A stability 5.0 2.3 2.2 2.0 ์ฃผ์\n",
"U07 ammo_B stability 5.0 2.2 2.3 2.0 ์ฃผ์\n",
"๊ธด๊ธ ํ์ ์ผ์น์จ(์ค์ ์๋ชจ ๊ธฐ์ค DOS์ ๋น๊ต): 95.8%\n",
"์ฌ๋ณด๊ธ ์์๋ ๊ตฐ์์ฐธ๋ชจ ๊ฒฐ์ฌ ์ ์ถ์ฒ๊ฐ์ด๋ฉฐ ์๋ ๋ถ์ถํ์ง ์๋๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
06. ์์ ์ง๋ ์ฐ๊ณ ํ์ฝ/์ ๋ฅ ์๋ชจ ์์ธก ๋ฐ ์ฌ๋ณด๊ธ ์์ ๊ฐ์ํ ๋์๋ณด๋ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์์ ์ง๋ ์ฐ๊ณ ํ์ฝ/์ ๋ฅ ์๋ชจ ์์ธก ๋ฐ ์ฌ๋ณด๊ธ ๋์๋ณด๋๋ฅผ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๊ฐ์ ์์ ์ง๋์ ๋ถ๋๋ณ ํ์ฝยท์ ๋ฅ ์์ธก ์๋ชจ์ ๋ณด๊ธ ์ํ๋ฅผ ํ์ํ๊ณ , ์์ธก ๊ฑด๋ง๋ค ๊ธฐ์ฌ๋ ์์ 3๊ฐ ์ธ์์ ์์ฐ์ด ์ค๋ช
์ ์๋ ์์ฑํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ฐ์ ๊ฒฉ์ ์ขํ ์ง๋์ ๋ถ๋๋ณ ์์ธก ์๋ชจ๋๊ณผ ๊ต์ ์ฌ๋ถ๋ฅผ ํ์ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/explainability/amm02_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": [
"\"\"\"์์ ์ง๋(๊ฐ์ ๊ฒฉ์ ์ขํ)์ ๋ถ๋๋ณ ํ์ฝยท์ ๋ฅ ์์ธก ์๋ชจ๋๊ณผ ๋ณด๊ธ ์์ค(DOS)์ ํ์ํ๋ ๋์๋ณด๋๋ฅผ ๋ง๋ค๊ณ , ์์ธก ๊ฑด๋ง๋ค ๊ธฐ์ฌ๋ ์์ 3๊ฐ ์ธ์๋ฅผ ๋์ฒด๊ฐ ๋น๊ต(occlusion) ๋ฐฉ์์ผ๋ก ์๋ ์ฐ์ถํด ์์ฐ์ด ์ค๋ช
๋ฌธ์ ์์ฑํ๋ค(SHAP summaryยทwaterfall ๋์ฒด ๊ทธ๋ฆผ ํฌํจ).\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.ensemble import GradientBoostingRegressor, RandomForestRegressor\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋ถ๋ยท๊ฐ์ ํ๋ชฉ ์ผ์ผ ์๋ชจ ๊ธฐ๋ก ์์ฑ๊ธฐ (์ค์ ํ์ฝ ์ ์ยท์๋ชจ์จ๊ณผ ๋ฌด๊ด)\n",
"# ๊ฐ์ ๋ถ๋ 8๊ฐ x 90์ผ. ์๋ชจ๋ = ๊ฐ์ CSR ๊ณ์ x ๋ณ๋ ฅ ๊ท๋ชจ x (๊ต์ ๊ฐ๋ยท์ ์ ํญยทํ๋ ฅ ๊ณํ ํจ๊ณผ) x ์ก์\n",
"# ---------------------------------------------------------------------------\n",
"UNITS = [f\"U{i:02d}\" for i in range(1, 9)]\n",
"ITEMS = [\"ammo_A\", \"ammo_B\", \"ammo_C\", \"ammo_D\", \"fuel_jet\", \"fuel_diesel\"]\n",
"AMMO, FUEL = ITEMS[:4], ITEMS[4:]\n",
"PHASES = [\"prep\", \"attack\", \"defense\", \"stability\"]\n",
"CSR = { # ๊ฐ์ ๊ณ์: ๋ณ๋ ฅ 100๋ช
ยท1์ผ ๊ธฐ์ค ์๋ชจ ๋จ์(ํ์ฝ=์์, ์ ๋ฅ=๋๋ผ)\n",
" \"prep\": [1.0, 0.6, 0.3, 0.2, 1.5, 6.0],\n",
" \"attack\": [6.0, 3.5, 2.0, 1.2, 5.0, 12.0],\n",
" \"defense\": [4.5, 3.0, 1.6, 0.8, 3.0, 7.0],\n",
" \"stability\": [0.8, 0.5, 0.3, 0.2, 1.0, 5.0],\n",
"}\n",
"PHASE_RESIST = {\"prep\": 1.0, \"attack\": 3.2, \"defense\": 3.6, \"stability\": 0.8}\n",
"\n",
"\n",
"def make_ops(n_days=90, seed=7, surge=0):\n",
" \"\"\"๊ฐ์ ๋ถ๋๋ณ ์ผ์ผ ์์ ๊ธฐ๋ก๊ณผ ํ๋ชฉ๋ณ ์ค์ ์๋ชจ๋(์ ๋ต)์ ๋ง๋ ๋ค. surge>0์ด๋ฉด ๋ง์ง๋ง surge์ผ์ ๊ณ ๊ฐ๋ ๊ณต๊ฒฉ ๋จ๊ณ๋ก ๋ฐ๊พผ๋ค(์คํธ๋ ์ค ์ํ์ฉ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for uid in UNITS:\n",
" size = int(rng.integers(3, 9)) * 100\n",
" lens = rng.integers(8, 20, 4)\n",
" sched = [\"prep\"] * lens[0] + [\"attack\"] * lens[1] + [\"defense\"] * lens[2] + [\"attack\"] * lens[3]\n",
" sched = (sched + [\"stability\"] * n_days)[:n_days]\n",
" if surge:\n",
" sched[-surge:] = [\"attack\"] * surge\n",
" r = 1.0\n",
" for d, ph in enumerate(sched):\n",
" r = float(np.clip(0.7 * r + 0.3 * (4.8 if surge and d >= n_days - surge else PHASE_RESIST[ph]) + rng.normal(0, 0.35), 0, 5))\n",
" inten = float(np.clip(r / 5 * 0.9 + rng.normal(0, 0.06), 0, 1))\n",
" fire = int(rng.poisson(1 + 8 * inten))\n",
" move = float(rng.gamma(2, {\"prep\": 4, \"attack\": 12, \"defense\": 2, \"stability\": 5}[ph]))\n",
" weather = str(rng.choice([\"clear\", \"rain\", \"snow\"], p=[0.7, 0.2, 0.1]))\n",
" emerg = bool(rng.random() < 0.02)\n",
" rec = {\"unit\": uid, \"day\": d, \"phase\": ph, \"unit_size\": size, \"intensity\": round(inten, 3),\n",
" \"resistance\": round(r, 2), \"fire_missions\": fire, \"move_km\": round(move, 1),\n",
" \"weather\": weather, \"emergency\": emerg}\n",
" base = size / 100\n",
" for i, it in enumerate(ITEMS):\n",
" c = CSR[ph][i] * base\n",
" if it in AMMO:\n",
" c *= (0.5 + 0.6 * inten) * (1 + 0.04 * fire * (1.0 if it in (\"ammo_C\", \"ammo_D\") else 0.4))\n",
" elif it == \"fuel_jet\":\n",
" c *= (0.6 + 0.5 * inten) + 0.03 * fire\n",
" else:\n",
" c = c * (0.8 + 0.2 * (weather != \"clear\")) + 0.05 * move * base\n",
" c *= rng.lognormal(0, 0.12) * (2.5 if emerg else 1.0)\n",
" rec[it] = round(float(c), 1)\n",
" rows.append(rec)\n",
" return pd.DataFrame(rows)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์๋ชจ๋ ์์ธก์ฉ ํ์ ํผ์ฒ: ์ ์ผ๊น์ง์ ์ค์ + ๋น์ผ ์์ ยทํ๋ ฅ ๊ณํ(์ฌ์ ์ ์ ์ ์๋ ๊ฐ๋ง) ----\n",
"FEATS = [\"lag1\", \"lag2\", \"roll3\", \"roll7\", \"csr_base\", \"size100\", \"fire_plan\", \"fire_x_engaged\", \"resist_lag1\",\n",
" \"resist_trend\", \"inten_lag1\", \"move_plan\", \"days_in_phase\", \"engaged\", \"ph_attack\", \"ph_defense\", \"ph_prep\",\n",
" \"item_idx\", \"is_fuel\"]\n",
"DERIVED = [\"lag1\", \"lag2\", \"roll3\", \"roll7\", \"csr_base\", \"fire_x_engaged\", \"resist_lag1\", \"resist_trend\",\n",
" \"inten_lag1\", \"days_in_phase\", \"engaged\", \"ph_attack\", \"ph_defense\", \"ph_prep\"]\n",
"\n",
"\n",
"def build_long(ops):\n",
" \"\"\"๋ถ๋x์ผ์ ๊ธฐ๋ก์ (๋ถ๋, ์ผ์, ํ๋ชฉ) ํ์ผ๋ก ํผ์น๊ณ ํ์ ํผ์ฒ๋ฅผ ๋ง๋ ๋ค.\n",
" ๋ชฉํ๊ฐ y = log(์ค์ ์๋ชจ๋ / CSR ๊ธฐ์ค๋): CSR ๊ธฐ์ค๋์ ๊ณฑํ '๋ณด์ ๊ณ์'๋ฅผ ์์ธกํ๋ค. lagยทroll ํผ์ฒ๋ ๊ฐ์ ๋น์จ(์ค์ /๊ธฐ์ค)์ด๋ค.\"\"\"\n",
" df = ops.sort_values([\"unit\", \"day\"]).reset_index(drop=True)\n",
" g = df.groupby(\"unit\")\n",
" df[\"resist_lag1\"] = g[\"resistance\"].shift(1)\n",
" df[\"resist_trend\"] = df[\"resist_lag1\"] - g[\"resistance\"].shift(3)\n",
" df[\"inten_lag1\"] = g[\"intensity\"].shift(1)\n",
" df[\"days_in_phase\"] = df.groupby((df[\"phase\"] != g[\"phase\"].shift()).cumsum()).cumcount()\n",
" out = []\n",
" for i, it in enumerate(ITEMS):\n",
" csr = np.array([CSR[p][i] for p in df[\"phase\"]])\n",
" ratio = df[it] / (csr * df[\"unit_size\"] / 100)\n",
" prev = ratio.groupby(df[\"unit\"]).shift(1)\n",
" d = pd.DataFrame({\"unit\": df[\"unit\"], \"day\": df[\"day\"], \"phase\": df[\"phase\"], \"item\": it, \"item_idx\": i,\n",
" \"is_fuel\": int(it in FUEL), \"lag1\": prev, \"lag2\": ratio.groupby(df[\"unit\"]).shift(2),\n",
" \"roll3\": prev.groupby(df[\"unit\"]).transform(lambda s: s.rolling(3).mean()),\n",
" \"roll7\": prev.groupby(df[\"unit\"]).transform(lambda s: s.rolling(7, min_periods=3).mean()),\n",
" \"csr_base\": csr, \"size100\": df[\"unit_size\"] / 100,\n",
" \"fire_plan\": df[\"fire_missions\"], \"fire_x_engaged\": df[\"fire_missions\"] * df[\"phase\"].isin([\"attack\", \"defense\"]),\n",
" \"resist_lag1\": df[\"resist_lag1\"], \"resist_trend\": df[\"resist_trend\"], \"inten_lag1\": df[\"inten_lag1\"],\n",
" \"move_plan\": df[\"move_km\"], \"days_in_phase\": df[\"days_in_phase\"],\n",
" \"engaged\": df[\"phase\"].isin([\"attack\", \"defense\"]).astype(int),\n",
" \"ph_attack\": (df[\"phase\"] == \"attack\").astype(int), \"ph_defense\": (df[\"phase\"] == \"defense\").astype(int),\n",
" \"ph_prep\": (df[\"phase\"] == \"prep\").astype(int), \"emergency\": df[\"emergency\"],\n",
" \"y\": np.log(ratio), \"qty\": df[it]})\n",
" out.append(d)\n",
" return pd.concat(out, ignore_index=True).dropna(subset=[\"lag2\", \"roll3\", \"resist_trend\"]).reset_index(drop=True)\n",
"\n",
"\n",
"def to_qty(d, y_pred):\n",
" \"\"\"์์ธก ๋ณด์ ๊ณ์(log)๋ฅผ ์๋ชจ๋์ผ๋ก ๋๋๋ฆฐ๋ค: ๊ธฐ์ค๋ x exp(์์ธก).\"\"\"\n",
" return np.exp(y_pred) * d[\"csr_base\"].values * d[\"size100\"].values\n",
"\n",
"\n",
"def mape(y, p):\n",
" return float(np.mean(np.abs(np.asarray(p) - np.asarray(y)) / np.asarray(y)))\n",
"\n",
"# ---- ์๋ชจ๋ ์์ธก ๋ชจ๋ธ(์์๋ธ) ํ์ตยท์์ธก ----\n",
"def fit_models(tr, seed=0):\n",
" \"\"\"GBR + ๋๋คํฌ๋ ์คํธ ๋ ๋ชจ๋ธ์ ํ์ตํ๋ค(๋ชฉํ๊ฐ์ CSR ๊ธฐ์ค๋ ๋๋น log ๋ณด์ ๊ณ์).\"\"\"\n",
" g = GradientBoostingRegressor(n_estimators=150, max_depth=3, learning_rate=0.08, subsample=0.8, random_state=seed).fit(tr[FEATS], tr[\"y\"])\n",
" r = RandomForestRegressor(n_estimators=120, min_samples_leaf=3, random_state=seed, n_jobs=1).fit(tr[FEATS], tr[\"y\"])\n",
" return g, r\n",
"\n",
"\n",
"def predict_qty(models, d, w=0.5):\n",
" \"\"\"๋ ๋ชจ๋ธ์ log ์์ธก์ ๊ฐ์ค ํ๊ท ํด ์๋ชจ๋์ผ๋ก ๋๋๋ฆฐ๋ค.\"\"\"\n",
" g, r = models\n",
" return to_qty(d, w * g.predict(d[FEATS]) + (1 - w) * r.predict(d[FEATS]))\n",
"\n",
"KOR = {\"lag1\": \"์ ์ผ ์๋ชจ ๋น์จ\", \"lag2\": \"2์ผ ์ ์๋ชจ ๋น์จ\", \"roll3\": \"์ต๊ทผ 3์ผ ํ๊ท ์๋ชจ ๋น์จ\", \"roll7\": \"์ต๊ทผ 7์ผ ํ๊ท ์๋ชจ ๋น์จ\",\n",
" \"csr_base\": \"์์ ๋จ๊ณ CSR ๊ณ์\", \"size100\": \"๋ณ๋ ฅ ๊ท๋ชจ\", \"fire_plan\": \"๊ณํ ํ๋ ฅ ์๋ฌด ์\", \"fire_x_engaged\": \"๊ต์ ์ค ํ๋ ฅ ๊ณํ\",\n",
" \"resist_lag1\": \"์ ์ผ ์ ์ ํญ ์์ค\", \"resist_trend\": \"์ ์ ํญ ์ถ์ธ\", \"inten_lag1\": \"์ ์ผ ๊ต์ ๊ฐ๋\", \"move_plan\": \"๊ณํ ์ด๋ ๊ฑฐ๋ฆฌ\",\n",
" \"days_in_phase\": \"๋จ๊ณ ๊ฒฝ๊ณผ์ผ\", \"engaged\": \"๊ต์ ๋จ๊ณ ์ฌ๋ถ\", \"ph_attack\": \"๊ณต๊ฒฉ ๋จ๊ณ\", \"ph_defense\": \"๋ฐฉ์ด ๋จ๊ณ\",\n",
" \"ph_prep\": \"์ค๋น ๋จ๊ณ\", \"item_idx\": \"ํ๋ชฉ\", \"is_fuel\": \"์ ๋ฅ ์ฌ๋ถ\"}\n",
"\n",
"data = build_long(make_ops(90, seed=7))\n",
"tr = data[data[\"day\"] < 40]\n",
"models = fit_models(tr)\n",
"D = 41 # ๋์๋ณด๋ ๊ธฐ์ค์ผ(๊ต์ ๋จ๊ณ๊ฐ ์์ธ ๋ )\n",
"day = data[data[\"day\"] == D].reset_index(drop=True)\n",
"day[\"pred\"] = predict_qty(models, day)\n",
"background = tr[FEATS].median()\n",
"\n",
"\n",
"def logpred(X):\n",
" return 0.5 * models[0].predict(X) + 0.5 * models[1].predict(X)\n",
"\n",
"\n",
"def explain(X):\n",
" \"\"\"๊ฐ ํผ์ฒ๋ฅผ ํ์ต ๊ธฐ๊ฐ ์ค์๊ฐ์ผ๋ก ๋ฐ๊ฟจ์ ๋ ์์ธก(log ๋ณด์ ๊ณ์)์ด ์ผ๋ง๋ ๋ณํ๋์ง = ๊ทธ ํผ์ฒ์ ๊ธฐ์ฌ๋.\"\"\"\n",
" base = logpred(X)\n",
" C = np.zeros((len(X), len(FEATS)))\n",
" for j, f in enumerate(FEATS):\n",
" Z = X.copy(); Z[f] = background[f]\n",
" C[:, j] = base - logpred(Z)\n",
" return pd.DataFrame(C, columns=FEATS)\n",
"\n",
"\n",
"C = explain(day[FEATS])\n",
"top3 = C.abs().apply(lambda r: list(r.sort_values(ascending=False).index[:3]), axis=1)\n",
"cover = np.mean([len(t) == 3 for t in top3])\n",
"print(f\"[๊ธฐ์ค์ผ {D}์ผ] ์์ธก {len(day)}๊ฑด(๋ถ๋ 8 x ํ๋ชฉ 6), ์์ 3๊ฐ ์ธ์ ์๋ ์ฐ์ถ๋ฅ {cover:.0%} (๋ชฉํ 100%: {'๋ฌ์ฑ' if cover == 1 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"\n",
"def sentence(i):\n",
" r = day.loc[i]; parts = []\n",
" for f in top3[i]:\n",
" eff = np.exp(C.loc[i, f]) - 1\n",
" parts.append(f\"{KOR[f]}({'+' if eff >= 0 else ''}{eff:.0%})\")\n",
" return f\"{r['unit']} {r['item']} ์์ธก {r['pred']:.1f}(๊ธฐ์ค๋ ๋๋น {r['pred'] / (r['csr_base'] * r['size100']):.2f}๋ฐฐ): ์ฃผ์ ์์ธ \" + \", \".join(parts)\n",
"\n",
"\n",
"print(\"\\n[๋ถ๋๋ณ ์ต๋ ์๋ชจ ํ๋ชฉ ์ค๋ช
(์์ฐ์ด ์์ฝ)]\")\n",
"lead = day.assign(rel=day[\"pred\"] / (day[\"csr_base\"] * day[\"size100\"])).sort_values(\"rel\", ascending=False).groupby(\"unit\").head(1)\n",
"for i in lead.sort_values(\"unit\").index:\n",
" print(\" -\", sentence(i))\n",
"\n",
"imp = C.abs().mean().sort_values(ascending=False)\n",
"print(\"\\n[์ ์ฒด ํ๊ท ๊ธฐ์ฌ๋ ์์ 5(์์ฝ ๊ทธ๋ฆผ)]\", \", \".join(f\"{KOR[k]} {v:.3f}\" for k, v in imp.head(5).items()))\n",
"recon = np.corrcoef(C.sum(axis=1), logpred(day[FEATS]) - logpred(pd.DataFrame([background] * len(day))))[0, 1]\n",
"print(f\"์ค๋ช
์ ํฉ์ฑ: ๊ธฐ์ฌ๋ ํฉ๊ณผ (์์ธก-๊ธฐ์ค ์์ธก)์ ์๊ด {recon:.2f} (1์ ๊ฐ๊น์ธ์๋ก ์ค๋ช
์ด ์์ธก์ ์ ๋ถํด)\")\n",
"print(\"์ค๋ช
์ ํ๋จ ๋ณด์กฐ ๊ทผ๊ฑฐ์ด๋ฉฐ ์ฌ๋ณด๊ธ ๊ฒฐ์ ์ ๊ตฐ์์ฐธ๋ชจ๊ฐ ๊ทผ๊ฑฐ๋ฅผ ํ์ธํด ๋ด๋ฆฐ๋ค.\")\n",
"\n",
"# ๋์๋ณด๋: ๊ฐ์ ์์ ์ง๋ + ๊ธฐ์ฌ๋ ๊ทธ๋ฆผ\n",
"rng = np.random.default_rng(2)\n",
"pos = {u: (rng.uniform(0, 50), rng.uniform(0, 30)) for u in UNITS}\n",
"ag = day.groupby(\"unit\").apply(lambda g: pd.Series({\"ammo\": g.loc[g[\"is_fuel\"] == 0, \"pred\"].sum(), \"fuel\": g.loc[g[\"is_fuel\"] == 1, \"pred\"].sum(),\n",
" \"phase\": g[\"phase\"].iloc[0]}), include_groups=False)\n",
"fig, ax = plt.subplots(1, 3, figsize=(16, 4.5))\n",
"for u, r in ag.iterrows():\n",
" x, y = pos[u]\n",
" ax[0].scatter(x, y, s=r[\"ammo\"] * 8 + 20, c=\"tab:red\" if r[\"phase\"] in (\"attack\", \"defense\") else \"tab:blue\", alpha=0.6)\n",
" ax[0].annotate(f\"{u}\\nA{r['ammo']:.0f}/F{r['fuel']:.0f}\", (x, y), fontsize=8, ha=\"center\")\n",
"ax[0].set_title(f\"Virtual op map day {D} (size=ammo forecast, red=engaged)\"); ax[0].set_xlabel(\"grid x\"); ax[0].set_ylabel(\"grid y\")\n",
"imp.head(10)[::-1].rename(index=lambda k: k).plot.barh(ax=ax[1], title=\"Mean |contribution| (summary)\")\n",
"i0 = lead.index[0]; w = C.loc[i0].reindex(C.loc[i0].abs().sort_values(ascending=False).index[:6])\n",
"ax[2].barh(w.index[::-1], w.values[::-1], color=[\"tab:red\" if v > 0 else \"tab:green\" for v in w.values[::-1]])\n",
"ax[2].set_title(f\"Waterfall-style: {day.loc[i0, 'unit']} {day.loc[i0, 'item']} (log ratio)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
06 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ธฐ์ค์ผ 41์ผ] ์์ธก 48๊ฑด(๋ถ๋ 8 x ํ๋ชฉ 6), ์์ 3๊ฐ ์ธ์ ์๋ ์ฐ์ถ๋ฅ 100% (๋ชฉํ 100%: ๋ฌ์ฑ)\n",
"\n",
"[๋ถ๋๋ณ ์ต๋ ์๋ชจ ํ๋ชฉ ์ค๋ช
(์์ฐ์ด ์์ฝ)]\n",
" - U01 fuel_jet ์์ธก 25.6(๊ธฐ์ค๋ ๋๋น 1.07๋ฐฐ): ์ฃผ์ ์์ธ ์ ์ผ ์ ์ ํญ ์์ค(-13%), ์ต๊ทผ 7์ผ ํ๊ท ์๋ชจ ๋น์จ(+5%), 2์ผ ์ ์๋ชจ ๋น์จ(-3%)\n",
" - U02 ammo_D ์์ธก 8.9(๊ธฐ์ค๋ ๋๋น 1.05๋ฐฐ): ์ฃผ์ ์์ธ ์ ์ผ ์ ์ ํญ ์์ค(-14%), ์ต๊ทผ 7์ผ ํ๊ท ์๋ชจ ๋น์จ(+3%), ์ต๊ทผ 3์ผ ํ๊ท ์๋ชจ ๋น์จ(-1%)\n",
" - U03 ammo_D ์์ธก 4.9(๊ธฐ์ค๋ ๋๋น 1.01๋ฐฐ): ์ฃผ์ ์์ธ ์ ์ผ ์ ์ ํญ ์์ค(-12%), ์ต๊ทผ 7์ผ ํ๊ท ์๋ชจ ๋น์จ(+4%), ๋จ๊ณ ๊ฒฝ๊ณผ์ผ(-2%)\n",
" - U04 fuel_diesel ์์ธก 61.6(๊ธฐ์ค๋ ๋๋น 1.03๋ฐฐ): ์ฃผ์ ์์ธ ๊ณํ ์ด๋ ๊ฑฐ๋ฆฌ(+11%), ์ ์ผ ์ ์ ํญ ์์ค(-8%), ํ๋ชฉ(+8%)\n",
" - U05 ammo_C ์์ธก 9.1(๊ธฐ์ค๋ ๋๋น 1.14๋ฐฐ): ์ฃผ์ ์์ธ ์ ์ผ ์ ์ ํญ ์์ค(-11%), ๊ณํ ํ๋ ฅ ์๋ฌด ์(+6%), ๊ต์ ์ค ํ๋ ฅ ๊ณํ(+4%)\n",
" - U06 ammo_D ์์ธก 10.1(๊ธฐ์ค๋ ๋๋น 1.05๋ฐฐ): ์ฃผ์ ์์ธ ์ ์ผ ์ ์ ํญ ์์ค(-12%), ์ต๊ทผ 7์ผ ํ๊ท ์๋ชจ ๋น์จ(+3%), ํ๋ชฉ(+1%)\n",
" - U07 fuel_diesel ์์ธก 33.8(๊ธฐ์ค๋ ๋๋น 0.97๋ฐฐ): ์ฃผ์ ์์ธ ํ๋ชฉ(+18%), ์ ์ผ ์ ์ ํญ ์์ค(-14%), ์์ ๋จ๊ณ CSR ๊ณ์(+13%)\n",
" - U08 ammo_D ์์ธก 6.0(๊ธฐ์ค๋ ๋๋น 1.08๋ฐฐ): ์ฃผ์ ์์ธ ์ ์ผ ์ ์ ํญ ์์ค(-16%), ์ ์ ํญ ์ถ์ธ(+3%), ์ต๊ทผ 7์ผ ํ๊ท ์๋ชจ ๋น์จ(+3%)\n",
"\n",
"[์ ์ฒด ํ๊ท ๊ธฐ์ฌ๋ ์์ 5(์์ฝ ๊ทธ๋ฆผ)] ์ ์ผ ์ ์ ํญ ์์ค 0.158, ์์ ๋จ๊ณ CSR ๊ณ์ 0.038, ํ๋ชฉ 0.026, ๊ณํ ํ๋ ฅ ์๋ฌด ์ 0.023, ์ต๊ทผ 7์ผ ํ๊ท ์๋ชจ ๋น์จ 0.021\n",
"์ค๋ช
์ ํฉ์ฑ: ๊ธฐ์ฌ๋ ํฉ๊ณผ (์์ธก-๊ธฐ์ค ์์ธก)์ ์๊ด 0.91 (1์ ๊ฐ๊น์ธ์๋ก ์ค๋ช
์ด ์์ธก์ ์ ๋ถํด)\n",
"์ค๋ช
์ ํ๋จ ๋ณด์กฐ ๊ทผ๊ฑฐ์ด๋ฉฐ ์ฌ๋ณด๊ธ ๊ฒฐ์ ์ ๊ตฐ์์ฐธ๋ชจ๊ฐ ๊ทผ๊ฑฐ๋ฅผ ํ์ธํด ๋ด๋ฆฐ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
07. ๋ณด๊ธ ์ฐ์ ์์ ์ง์ ๋ฐ ๋์ฒด ํ์ข
๋ถ์ถ์ ์ํ ๊ตฐ์์ฐธ๋ชจ HITL ๊ฒฐ์ฌ์ ์ค๊ณ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋ณด๊ธ ์ฐ์ ์์ ์ง์ ์ ์ํ ๊ตฐ์์ฐธ๋ชจ HITL ๊ฒฐ์ฌ์ ์ ์ค๊ณํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๊ฐ์ ์ ์ฅ๊ณ ๋ฐฐ์น๋ฅผ ์ต์ ํํ๊ณ , ์ฌ๋ฐฐ์นยท๊ธด๊ธ ์ฌ๋ณด๊ธ ์ฐ์ ์์ยท๋์ฒด ํ์ข
๋ถ์ถ ์ ์์ ๊ตฐ์์ฐธ๋ชจ ๊ฒฐ์ฌ์ (HITL)์ผ๋ก ์ฒ๋ฆฌํ๋ ์๋ฎฌ๋ ์ดํฐ๋ฅผ ์ค๊ณํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ ์ฅ์นธ ์ฉ๋ยทํ์ฌ ์ ์ ยท๊ฐ์ ํผ์ ์ ํ์ ์ ์ฝ์ผ๋ก ๋๊ณ ๋ฐ์ถ ๋น๋ x ๋์ ๊ฑฐ๋ฆฌ์ ๊ณต๊ฐ๋ญ๋น๋ฅผ ์ต์ํํ๋ ํ ๋น ๋ฌธ์ (์ ์๊ณํ)๋ฅผ ํ์ด ํํ(์
๊ณ ์) ๋ฐฐ์น์ ์์
๋น์ฉยท๊ณต๊ฐ๋ญ๋น์จยท์์น์ ์๋ฅผ ๋น๊ต ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/optimization/amm02_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",
"# ๊ฐ์ ๋ถ๋ 8๊ฐ x 90์ผ. ์๋ชจ๋ = ๊ฐ์ CSR ๊ณ์ x ๋ณ๋ ฅ ๊ท๋ชจ x (๊ต์ ๊ฐ๋ยท์ ์ ํญยทํ๋ ฅ ๊ณํ ํจ๊ณผ) x ์ก์\n",
"# ---------------------------------------------------------------------------\n",
"UNITS = [f\"U{i:02d}\" for i in range(1, 9)]\n",
"ITEMS = [\"ammo_A\", \"ammo_B\", \"ammo_C\", \"ammo_D\", \"fuel_jet\", \"fuel_diesel\"]\n",
"AMMO, FUEL = ITEMS[:4], ITEMS[4:]\n",
"PHASES = [\"prep\", \"attack\", \"defense\", \"stability\"]\n",
"CSR = { # ๊ฐ์ ๊ณ์: ๋ณ๋ ฅ 100๋ช
ยท1์ผ ๊ธฐ์ค ์๋ชจ ๋จ์(ํ์ฝ=์์, ์ ๋ฅ=๋๋ผ)\n",
" \"prep\": [1.0, 0.6, 0.3, 0.2, 1.5, 6.0],\n",
" \"attack\": [6.0, 3.5, 2.0, 1.2, 5.0, 12.0],\n",
" \"defense\": [4.5, 3.0, 1.6, 0.8, 3.0, 7.0],\n",
" \"stability\": [0.8, 0.5, 0.3, 0.2, 1.0, 5.0],\n",
"}\n",
"PHASE_RESIST = {\"prep\": 1.0, \"attack\": 3.2, \"defense\": 3.6, \"stability\": 0.8}\n",
"\n",
"\n",
"def make_ops(n_days=90, seed=7, surge=0):\n",
" \"\"\"๊ฐ์ ๋ถ๋๋ณ ์ผ์ผ ์์ ๊ธฐ๋ก๊ณผ ํ๋ชฉ๋ณ ์ค์ ์๋ชจ๋(์ ๋ต)์ ๋ง๋ ๋ค. surge>0์ด๋ฉด ๋ง์ง๋ง surge์ผ์ ๊ณ ๊ฐ๋ ๊ณต๊ฒฉ ๋จ๊ณ๋ก ๋ฐ๊พผ๋ค(์คํธ๋ ์ค ์ํ์ฉ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for uid in UNITS:\n",
" size = int(rng.integers(3, 9)) * 100\n",
" lens = rng.integers(8, 20, 4)\n",
" sched = [\"prep\"] * lens[0] + [\"attack\"] * lens[1] + [\"defense\"] * lens[2] + [\"attack\"] * lens[3]\n",
" sched = (sched + [\"stability\"] * n_days)[:n_days]\n",
" if surge:\n",
" sched[-surge:] = [\"attack\"] * surge\n",
" r = 1.0\n",
" for d, ph in enumerate(sched):\n",
" r = float(np.clip(0.7 * r + 0.3 * (4.8 if surge and d >= n_days - surge else PHASE_RESIST[ph]) + rng.normal(0, 0.35), 0, 5))\n",
" inten = float(np.clip(r / 5 * 0.9 + rng.normal(0, 0.06), 0, 1))\n",
" fire = int(rng.poisson(1 + 8 * inten))\n",
" move = float(rng.gamma(2, {\"prep\": 4, \"attack\": 12, \"defense\": 2, \"stability\": 5}[ph]))\n",
" weather = str(rng.choice([\"clear\", \"rain\", \"snow\"], p=[0.7, 0.2, 0.1]))\n",
" emerg = bool(rng.random() < 0.02)\n",
" rec = {\"unit\": uid, \"day\": d, \"phase\": ph, \"unit_size\": size, \"intensity\": round(inten, 3),\n",
" \"resistance\": round(r, 2), \"fire_missions\": fire, \"move_km\": round(move, 1),\n",
" \"weather\": weather, \"emergency\": emerg}\n",
" base = size / 100\n",
" for i, it in enumerate(ITEMS):\n",
" c = CSR[ph][i] * base\n",
" if it in AMMO:\n",
" c *= (0.5 + 0.6 * inten) * (1 + 0.04 * fire * (1.0 if it in (\"ammo_C\", \"ammo_D\") else 0.4))\n",
" elif it == \"fuel_jet\":\n",
" c *= (0.6 + 0.5 * inten) + 0.03 * fire\n",
" else:\n",
" c = c * (0.8 + 0.2 * (weather != \"clear\")) + 0.05 * move * base\n",
" c *= rng.lognormal(0, 0.12) * (2.5 if emerg else 1.0)\n",
" rec[it] = round(float(c), 1)\n",
" rows.append(rec)\n",
" return pd.DataFrame(rows)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ ์ฅ๊ณ (H1~H3) ์ ์ฅ์นธ ๋ฐฐ์น ๋ชจ๋ธ. ์ค์ ํ์ฝ ์ ์ฅ ๊ธฐ์คยท์์ ๊ฑฐ๋ฆฌ์ ๋ฌด๊ดํ ๊ฐ์ ๊ณ์๋ค.\n",
"# ์ ์ฅ์นธ: ์ ์ฅ๊ณ x ์ด(row) x ์นธ(col). ๋์ ๊ฑฐ๋ฆฌ = ๋ฐ์ถ์ฅ(์
๊ตฌ)๊น์ง์ ๊ฐ์ ๊ฑฐ๋ฆฌ(m).\n",
"# ์ ์ฅ๊ตฐ(G1~G3)์ ๊ฐ์ ํผ์ ์ ํ ๊ทธ๋ฃน์ด๋ฉฐ, ์ ์ฅ๊ณ ๋ง๋ค ๋ฐ์ ์ ์๋ ์ ์ฅ๊ตฐ์ด ์ ํด์ ธ ์๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"HOUSES = {\"H1\": {\"groups\": {\"G1\", \"G2\"}, \"rows\": 2, \"cols\": 6, \"dock\": 15.0},\n",
" \"H2\": {\"groups\": {\"G1\"}, \"rows\": 2, \"cols\": 5, \"dock\": 40.0},\n",
" \"H3\": {\"groups\": {\"G2\", \"G3\"}, \"rows\": 2, \"cols\": 6, \"dock\": 70.0}}\n",
"ITEM_GROUP = {\"ammo_A\": \"G1\", \"ammo_B\": \"G1\", \"ammo_C\": \"G2\", \"ammo_D\": \"G3\"}\n",
"SLOT_CAP = 6 # 1์ด ์ ์ฅ์นธ ๊ฐ์ ํ๋ ํธ ์์ฉ๋(2์ด์ +4)\n",
"\n",
"\n",
"def make_slots(seed=3):\n",
" \"\"\"์ ์ฅ์นธ ๋ชฉ๋ก๊ณผ ํ์ฌ ์ ์ (์ด๋ ๋ถ๊ฐ ๊ธฐ์กด ์ฌ๊ณ ) ์ฌ๋ถ๋ฅผ ๋ง๋ ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for h, c in HOUSES.items():\n",
" for r in range(c[\"rows\"]):\n",
" for k in range(c[\"cols\"]):\n",
" rows.append({\"slot\": f\"{h}-{r + 1}{k + 1:02d}\", \"house\": h, \"dist_m\": c[\"dock\"] + 6.0 * k + 4.0 * r,\n",
" \"cap\": SLOT_CAP if r == 0 else SLOT_CAP + 4, \"fixed\": bool(rng.random() < 0.15)})\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"def make_skus(demand, seed=5):\n",
" \"\"\"ํ๋ชฉ๋ณ ์์ธก ์ผ์ผ ๋ฐ์ถ๋(demand: ํ๋ชฉ->์์/์ผ)์ ๋กํธ ๋จ์ SKU๋ก ๋๋๋ค. ๋ฐ์ถ ๋น๋ = ์ผ์ผ ๋ฐ์ถ ํ์(๊ฐ์).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for it, dq in demand.items():\n",
" n_lot = 5\n",
" share = rng.dirichlet(np.ones(n_lot) * 2)\n",
" for j in range(n_lot):\n",
" rows.append({\"sku\": f\"{it}-L{j + 1}\", \"item\": it, \"group\": ITEM_GROUP[it],\n",
" \"pallets\": int(rng.integers(2, SLOT_CAP + 5)), \"freq\": round(float(dq * share[j] / 4), 2)})\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"def feasible(sku, slot):\n",
" return (sku[\"group\"] in HOUSES[slot[\"house\"]][\"groups\"]) and sku[\"pallets\"] <= slot[\"cap\"] and not slot[\"fixed\"]\n",
"\n",
"\n",
"def travel(assign, skus, slots):\n",
" \"\"\"์์
๋์ = ๋ฐ์ถ ๋น๋ x ์๋ณต ๊ฑฐ๋ฆฌ ํฉ(๊ฐ์ m/์ผ).\"\"\"\n",
" d = slots.set_index(\"slot\")[\"dist_m\"]\n",
" return float(sum(2 * skus.loc[i, \"freq\"] * d[s] for i, s in assign.items()))\n",
"\n",
"\n",
"def first_fit(skus, slots):\n",
" \"\"\"ํํ ๋ฐฉ์(๋์ ๋น๊ต ๊ธฐ์ค): ์
๊ณ ์์๋๋ก ๋น์ด ์๋ ์ฒซ ๋ฒ์งธ ์ ํฉ ์นธ์ ๋ฃ๋๋ค.\"\"\"\n",
" used, out = set(), {}\n",
" for i, k in skus.iterrows():\n",
" for _, s in slots.iterrows():\n",
" if s[\"slot\"] not in used and feasible(k, s):\n",
" out[i] = s[\"slot\"]; used.add(s[\"slot\"]); break\n",
" return out\n",
"\n",
"\n",
"def optimize(skus, slots, lam=8):\n",
" \"\"\"ํ ๋น ๋ฌธ์ (์ ์๊ณํ ํน์ํ): ๋น์ฉ = ๋ฐ์ถ ๋น๋ x ์๋ณต๊ฑฐ๋ฆฌ + lam x ๋น ๊ณต๊ฐ(๊ณต๊ฐ๋ญ๋น). ๋ถ์ ํฉ ์นธ์ ํฐ ๋น์ฉ.\"\"\"\n",
" cost = np.full((len(skus), len(slots)), 1e7)\n",
" for i, k in skus.iterrows():\n",
" for j, s in slots.iterrows():\n",
" if feasible(k, s):\n",
" cost[i, j] = 2 * k[\"freq\"] * s[\"dist_m\"] + lam * (s[\"cap\"] - k[\"pallets\"])\n",
" ri, cj = linear_sum_assignment(cost)\n",
" return {int(i): slots.loc[j, \"slot\"] for i, j in zip(ri, cj) if cost[i, j] < 1e7}, cost\n",
"\n",
"\n",
"def loc_score(assign, skus, slots):\n",
" \"\"\"์์น์ ์(0~100): ๋ฐ์ถ ๋น๋ ์์ SKU์ผ์๋ก ๋ฐ์ถ์ฅ ๊ฐ๊น์ด ์นธ์ ์๋ ์ ๋(๊ฐ์ค ํ๊ท ).\"\"\"\n",
" d = slots.set_index(\"slot\")[\"dist_m\"]; dmin, dmax = d.min(), d.max()\n",
" w = skus[\"freq\"] / skus[\"freq\"].sum()\n",
" return float(100 * sum(w[i] * (1 - (d[s] - dmin) / (dmax - dmin)) for i, s in assign.items()))\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"COST_PER_M = 0.02 # [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ง๊ฒ์ฐจ ์๋ณต 1m๋น ๊ฐ์ ์์
๋น์ฉ ์ง์\n",
"SUB_RATE = {\"ammo_C\": (\"ammo_D\", 0.8)} # ๊ฐ์ ๋์ฒด ํ์ข
๊ณผ ํ์ฐ ๊ณ์(๊ต์ก์ฉ ๊ฐ์ ๊ฐ)\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",
"# 1) ๋ฐ์ถ ์์: ์ต๊ทผ 14์ผ ํ๋ชฉ๋ณ ์ผ์ผ ํ๊ท ์๋ชจ(๋ฏธ์
5 ์์ธก๊ฐ ๋์ ์ฌ์ฉ, ๊ฐ์ ์์/์ผ)\n",
"ops = make_ops(90, seed=7)\n",
"demand = ops[ops[\"day\"] >= 76][AMMO].sum().div(14).round(1).to_dict()\n",
"skus, slots = make_skus(demand), make_slots()\n",
"print(f\"[๊ฐ์ ์ ์ฅ๊ณ ] ์ ์ฅ์นธ {len(slots)}๊ฐ(ํ์ฌ ์ ์ ยท์ด๋ ๋ถ๊ฐ {slots['fixed'].sum()}๊ฐ), SKU {len(skus)}๊ฐ, ์ผ์ผ ๋ฐ์ถ ์์ {demand}\")\n",
"\n",
"cur = first_fit(skus, slots)\n",
"opt, cost = optimize(skus, slots)\n",
"\n",
"\n",
"def kpi(a):\n",
" used = slots.set_index(\"slot\").loc[list(a.values()), \"cap\"].sum()\n",
" waste = 1 - skus.loc[list(a.keys()), \"pallets\"].sum() / used\n",
" t = travel(a, skus, slots)\n",
" return {\"์์
๋์ (m/์ผ)\": t, \"์์
๋น์ฉ ์ง์\": t * COST_PER_M, \"๊ณต๊ฐ๋ญ๋น์จ\": waste, \"์์น์ ์\": loc_score(a, skus, slots), \"๋ฐฐ์น SKU\": len(a)}\n",
"\n",
"\n",
"kp = pd.DataFrame({\"ํํ(์
๊ณ ์)\": kpi(cur), \"์ต์ ํ\": kpi(opt)}).T\n",
"print(kp.to_string(float_format=lambda x: f\"{x:.2f}\"))\n",
"cut = 1 - kp.loc[\"์ต์ ํ\", \"์์
๋น์ฉ ์ง์\"] / kp.loc[\"ํํ(์
๊ณ ์)\", \"์์
๋น์ฉ ์ง์\"]\n",
"print(f\"์์
๋น์ฉ ์ ๊ฐ๋ฅ {cut:.1%} (๋ชฉํ 15% ์ด์: {'๋ฌ์ฑ' if cut >= 0.15 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"top = skus[\"freq\"].idxmax()\n",
"alt = [(slots.loc[j, \"slot\"], cost[top, j]) for j in np.argsort(cost[top]) if cost[top, j] < 1e7][:3]\n",
"print(f\"๋์๋ฐฐ์น(๋ฐ์ถ ์ต๋ค {skus.loc[top, 'sku']}): \" + \", \".join(f\"{s}(๋น์ฉ {c:.0f})\" for s, c in alt) + \" -> 1์ ์ ์ ์ 2ยท3์ ์์ผ๋ก ์ ์\")\n",
"\n",
"# 2) ๋ฏผ๊ฐ๋: ๋๋ฐ ์์(๊ฐ์ ์ ์ ์ ํ์ผ๋ก ammo_A ๋ฐ์ถ 2๋ฐฐ)\n",
"sk2 = skus.copy(); sk2.loc[sk2[\"item\"] == \"ammo_A\", \"freq\"] *= 2\n",
"o2, _ = optimize(sk2, slots)\n",
"moved = sum(o2[i] != opt[i] for i in opt)\n",
"print(f\"๋ฏผ๊ฐ๋(ammo_A ๋ฐ์ถ 2๋ฐฐ): ์ต์ ๋ฐฐ์น ๋ณ๊ฒฝ {moved}์นธ, ์์
๋์ ํํ {travel(cur, sk2, slots):.0f} -> ์ฌ์ต์ ํ {travel(o2, sk2, slots):.0f} m/์ผ\")\n",
"\n",
"# 3) HITL ๊ฒฐ์ฌ์ : ์ ์์ ๋ชจ๋ '๊ฒฐ์ฌ ๋๊ธฐ'๋ก ์์ํ๋ฉฐ ์๋ ํ์ ํ์ง ์๋๋ค\n",
"moves = [(skus.loc[i, \"sku\"], cur.get(i), opt[i], skus.loc[i, \"freq\"]) for i in opt if cur.get(i) != opt[i]]\n",
"moves.sort(key=lambda m: -m[3])\n",
"props = [{\"id\": f\"P{n + 1:02d}\", \"type\": \"์ฌ๋ฐฐ์น\", \"what\": f\"{s} {a}->{b}\", \"evid\": f\"๋ฐ์ถ {f:.1f}ํ/์ผ, ๋์ ๊ฐ์\"} for n, (s, a, b, f) in enumerate(moves[:6])]\n",
"rng = np.random.default_rng(4)\n",
"dos = {u: round(float(rng.uniform(0.8, 4.0)), 1) for u in UNITS}\n",
"urgent = sorted([u for u in UNITS if dos[u] < 2.0], key=lambda u: dos[u])\n",
"props += [{\"id\": f\"P{len(props) + k + 1:02d}\", \"type\": \"์ฌ๋ณด๊ธ ์ฐ์ ์์\", \"what\": f\"{u} ํ์ฝ ์ฐ์ ๋ถ์ถ({k + 1}์์)\", \"evid\": f\"DOS {dos[u]}์ผ\"} for k, u in enumerate(urgent)]\n",
"src, (dst, rate) = \"ammo_C\", SUB_RATE[\"ammo_C\"]\n",
"props.append({\"id\": f\"P{len(props) + 1:02d}\", \"type\": \"๋์ฒด ํ์ข
\", \"what\": f\"{src} ๋ถ์กฑ๋ถ 40์์ -> {dst} {40 * rate:.0f}์์\", \"evid\": f\"๊ฐ์ ํ์ฐ ๊ณ์ {rate}\"})\n",
"\n",
"audit = Audit()\n",
"for p in props:\n",
" audit.add(t=\"์ ์\", pid=p[\"id\"], kind=p[\"type\"], what=p[\"what\"], evid=p[\"evid\"], status=\"๊ฒฐ์ฌ๋๊ธฐ\", by=\"AI\")\n",
"\n",
"\n",
"def staff_decision(p, k):\n",
" \"\"\"๊ฐ์ ๊ตฐ์์ฐธ๋ชจ ํ๋จ(์๋ฎฌ๋ ์ด์
): ๊ทผ๊ฑฐ ํ์ธ ํ ์น์ธยท์์ ยท๋ฐ๋ ค, ์ผ๋ถ๋ ์๊ฐ ๋ด ๋ฌด์๋ต.\"\"\"\n",
" if p[\"type\"] == \"๋์ฒด ํ์ข
\":\n",
" return \"์์ \", \"๋์ฒด ์๋ ์ ๋ฐ๋ง ์น์ธ, ์์ฌ๋ ์๊ธ๋ถ๋ ์ฒญ๊ตฌ\"\n",
" if k % 5 == 4:\n",
" return \"๋ฌด์๋ต\", \"๊ฒฐ์ฌ ์ํ ์ด๊ณผ -> ํํ ์ ์ง\"\n",
" if p[\"type\"] == \"์ฌ๋ฐฐ์น\" and k % 3 == 2:\n",
" return \"๋ฐ๋ ค\", \"ํด๋น ์ ์ฅ๊ณ ์ ๊ฒ ์ผ์ ๊ณผ ๊ฒน์นจ\"\n",
" return \"์น์ธ\", \"๊ทผ๊ฑฐ ํ์ธ\"\n",
"\n",
"\n",
"executed = []\n",
"for k, p in enumerate(props):\n",
" d, why = staff_decision(p, k)\n",
" audit.add(t=\"๊ฒฐ์ฌ\", pid=p[\"id\"], decision=d, reason=why, by=\"๊ตฐ์์ฐธ๋ชจ(๊ฐ์)\")\n",
" if d in (\"์น์ธ\", \"์์ \"):\n",
" executed.append(p[\"id\"]); audit.add(t=\"์งํ\", pid=p[\"id\"], by=\"์ ์ฅ๊ด๋ฆฌ๋ฐ(๊ฐ์)\")\n",
"tab = pd.DataFrame([{\"์ ์\": e[\"pid\"], \"๊ฒฐ์ \": e[\"decision\"], \"์ฌ์ \": e[\"reason\"]} for e in audit.log if e[\"t\"] == \"๊ฒฐ์ฌ\"])\n",
"info = pd.DataFrame(props).set_index(\"id\")\n",
"tab[\"์ ํ\"], tab[\"๋ด์ฉ\"] = tab[\"์ ์\"].map(info[\"type\"]), tab[\"์ ์\"].map(info[\"what\"])\n",
"print(f\"\\n[๊ตฐ์์ฐธ๋ชจ ๊ฒฐ์ฌ ๊ฒฐ๊ณผ] ์ ์ {len(props)}๊ฑด\")\n",
"print(tab[[\"์ ์\", \"์ ํ\", \"๋ด์ฉ\", \"๊ฒฐ์ \", \"์ฌ์ \"]].to_string(index=False))\n",
"auto = [e for e in audit.log if e[\"t\"] == \"์งํ\" and not any(x[\"t\"] == \"๊ฒฐ์ฌ\" and x[\"pid\"] == e[\"pid\"] and x[\"decision\"] in (\"์น์ธ\", \"์์ \") for x in audit.log)]\n",
"print(f\"๊ฒฐ์ฌ ์์ด ์งํ๋ ๊ฑด {len(auto)}๊ฑด (์๋ ํ์ ๊ธ์ง: {'์ค์' if not auto else '์๋ฐ'}), ๊ฒฐ์ฌ ์ฌ์ ๊ธฐ๋ก๋ฅ {tab['์ฌ์ '].astype(bool).mean():.0%}\")\n",
"print(f\"๊ฐ์ฌ ๋ก๊ทธ {len(audit.log)}๊ฑด ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ: {'์ ์' if audit.verify() is None else '๋ณ์กฐ ๋ฐ๊ฒฌ'}\")\n",
"i3 = next(e[\"seq\"] for e in audit.log if e[\"t\"] == \"๊ฒฐ์ฌ\" and e[\"decision\"] == \"๋ฐ๋ ค\")\n",
"audit.log[i3][\"decision\"] = \"์น์ธ\"\n",
"print(f\"๋ณ์กฐ ์ํ({i3}๋ฒ ๋ฐ๋ ค ๊ธฐ๋ก์ ์น์ธ์ผ๋ก ์์ ) -> {audit.verify()}๋ฒ ๊ธฐ๋ก์์ ๋ณ์กฐ ๊ฒ์ถ\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(12, 4))\n",
"for a, (nm, asg) in zip(ax, [(\"current (first-fit)\", cur), (\"optimized\", opt)]):\n",
" s = slots.set_index(\"slot\")\n",
" a.scatter(s[\"dist_m\"], [list(HOUSES).index(h) for h in s[\"house\"]], c=\"lightgray\", s=60, marker=\"s\")\n",
" for i, sl in asg.items():\n",
" a.scatter(s.loc[sl, \"dist_m\"], list(HOUSES).index(s.loc[sl, \"house\"]), s=40 + 30 * skus.loc[i, \"freq\"], alpha=0.6)\n",
" a.set_yticks(range(3)); a.set_yticklabels(list(HOUSES)); a.set_xlabel(\"distance to dock (m)\"); a.set_title(f\"Placement: {nm} (size=issue freq)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
07 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ฐ์ ์ ์ฅ๊ณ ] ์ ์ฅ์นธ 34๊ฐ(ํ์ฌ ์ ์ ยท์ด๋ ๋ถ๊ฐ 6๊ฐ), SKU 20๊ฐ, ์ผ์ผ ๋ฐ์ถ ์์ {'ammo_A': 25.8, 'ammo_B': 16.7, 'ammo_C': 10.2, 'ammo_D': 7.1}\n",
" ์์
๋์ (m/์ผ) ์์
๋น์ฉ ์ง์ ๊ณต๊ฐ๋ญ๋น์จ ์์น์ ์ ๋ฐฐ์น SKU\n",
"ํํ(์
๊ณ ์) 1506.20 30.12 0.29 60.18 20.00\n",
"์ต์ ํ 1195.08 23.90 0.29 71.88 20.00\n",
"์์
๋น์ฉ ์ ๊ฐ๋ฅ 20.7% (๋ชฉํ 15% ์ด์: ๋ฌ์ฑ)\n",
"๋์๋ฐฐ์น(๋ฐ์ถ ์ต๋ค ammo_A-L3): H1-102(๋น์ฉ 126), H1-201(๋น์ฉ 147), H1-103(๋น์ฉ 157) -> 1์ ์ ์ ์ 2ยท3์ ์์ผ๋ก ์ ์\n",
"๋ฏผ๊ฐ๋(ammo_A ๋ฐ์ถ 2๋ฐฐ): ์ต์ ๋ฐฐ์น ๋ณ๊ฒฝ 7์นธ, ์์
๋์ ํํ 1919 -> ์ฌ์ต์ ํ 1491 m/์ผ\n",
"\n",
"[๊ตฐ์์ฐธ๋ชจ ๊ฒฐ์ฌ ๊ฒฐ๊ณผ] ์ ์ 9๊ฑด\n",
" ์ ์ ์ ํ ๋ด์ฉ ๊ฒฐ์ ์ฌ์ \n",
"P01 ์ฌ๋ฐฐ์น ammo_A-L3 H1-104->H1-201 ์น์ธ ๊ทผ๊ฑฐ ํ์ธ\n",
"P02 ์ฌ๋ฐฐ์น ammo_B-L4 H1-206->H1-102 ์น์ธ ๊ทผ๊ฑฐ ํ์ธ\n",
"P03 ์ฌ๋ฐฐ์น ammo_A-L5 H1-106->H1-103 ๋ฐ๋ ค ํด๋น ์ ์ฅ๊ณ ์ ๊ฒ ์ผ์ ๊ณผ ๊ฒน์นจ\n",
"P04 ์ฌ๋ฐฐ์น ammo_A-L2 H1-103->H2-101 ์น์ธ ๊ทผ๊ฑฐ ํ์ธ\n",
"P05 ์ฌ๋ฐฐ์น ammo_B-L3 H1-205->H1-202 ๋ฌด์๋ต ๊ฒฐ์ฌ ์ํ ์ด๊ณผ -> ํํ ์ ์ง\n",
"P06 ์ฌ๋ฐฐ์น ammo_B-L5 H2-101->H2-102 ๋ฐ๋ ค ํด๋น ์ ์ฅ๊ณ ์ ๊ฒ ์ผ์ ๊ณผ ๊ฒน์นจ\n",
"P07 ์ฌ๋ณด๊ธ ์ฐ์ ์์ U04 ํ์ฝ ์ฐ์ ๋ถ์ถ(1์์) ์น์ธ ๊ทผ๊ฑฐ ํ์ธ\n",
"P08 ์ฌ๋ณด๊ธ ์ฐ์ ์์ U08 ํ์ฝ ์ฐ์ ๋ถ์ถ(2์์) ์น์ธ ๊ทผ๊ฑฐ ํ์ธ\n",
"P09 ๋์ฒด ํ์ข
ammo_C ๋ถ์กฑ๋ถ 40์์ -> ammo_D 32์์ ์์ ๋์ฒด ์๋ ์ ๋ฐ๋ง ์น์ธ, ์์ฌ๋ ์๊ธ๋ถ๋ ์ฒญ๊ตฌ\n",
"๊ฒฐ์ฌ ์์ด ์งํ๋ ๊ฑด 0๊ฑด (์๋ ํ์ ๊ธ์ง: ์ค์), ๊ฒฐ์ฌ ์ฌ์ ๊ธฐ๋ก๋ฅ 100%\n",
"๊ฐ์ฌ ๋ก๊ทธ 24๊ฑด ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ: ์ ์\n",
"๋ณ์กฐ ์ํ(13๋ฒ ๋ฐ๋ ค ๊ธฐ๋ก์ ์น์ธ์ผ๋ก ์์ ) -> 13๋ฒ ๊ธฐ๋ก์์ ๋ณ์กฐ ๊ฒ์ถ\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
08. 7์ผ๊ฐ ๊ณ ๊ฐ๋ ์ ๋ฉด์ ๋ชจ์ ํ์ฝ/์ ๋ฅ ์๋ชจ๋ ๊ทนํ ์คํธ๋ ์ค ์๋ฎฌ๋ ์ด์
\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"7์ผ๊ฐ ์ ๋ฉด์ ๋ชจ์ ํ์ฝ/์ ๋ฅ ์๋ชจ๋ ์คํธ๋ ์ค ์๋ฎฌ๋ ์ด์
์ ์ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **7์ผ๊ฐ ๊ณ ๊ฐ๋ ์ ๋ฉด์ (๊ฐ์) ์ํฉ์์ ์๋ชจ๋๊ณผ ๋ณด์ ๋์ ์ผ์ผ ๋ชจ์ํ๊ณ ์ฌ๋ณด๊ธ ๊ฒฝ๋ณด ๋์๋ณด๋์ ์ฑ๋ฅ์ ๊ทนํ ์กฐ๊ฑด์์ ๊ฒ์ฆํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ฐ์ ๋ถ๋ 8๊ฐ์ ๋ง์ง๋ง 7์ผ์ ๊ณ ๊ฐ๋ ๊ณต๊ฒฉ ๋จ๊ณ๋ก ๋ฐ๊ฟ ์๋ชจยท๋ณด์ ๋์ ๋ชจ์ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/dashboard/amm02_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": [
"\"\"\"7์ผ๊ฐ ๊ณ ๊ฐ๋ ์ ๋ฉด์ (๊ฐ์) ์ํฉ์์ ๋ถ๋๋ณ ํ์ฝยท์ ๋ฅ ์๋ชจ๋๊ณผ ๋ณด์ ๋์ ์ผ์ผ ๋ชจ์ํ๊ณ , ๋ณด๊ณ ๋๋ฝยท๋ณด๊ณ ์ง์ฐยท๊ธด๊ธ ์์ ๊ธ์ฆยท๋ถ๋ ์ 10๋ฐฐ ๋ถํ๋ฅผ ์ฃผ์
ํด ์ฌ๋ณด๊ธ ๊ฒฝ๋ณด ๋์๋ณด๋์ ๊ฐฑ์ ์๊ฐ(2์ด ์ด๋ด)๊ณผ ๊ฒฝ๋ณด ๋ฐ๋ น ์ ํ๋(95% ์ด์)๋ฅผ ๊ฒ์ฆํ๋ค.\"\"\"\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, RandomForestRegressor\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋ถ๋ยท๊ฐ์ ํ๋ชฉ ์ผ์ผ ์๋ชจ ๊ธฐ๋ก ์์ฑ๊ธฐ (์ค์ ํ์ฝ ์ ์ยท์๋ชจ์จ๊ณผ ๋ฌด๊ด)\n",
"# ๊ฐ์ ๋ถ๋ 8๊ฐ x 90์ผ. ์๋ชจ๋ = ๊ฐ์ CSR ๊ณ์ x ๋ณ๋ ฅ ๊ท๋ชจ x (๊ต์ ๊ฐ๋ยท์ ์ ํญยทํ๋ ฅ ๊ณํ ํจ๊ณผ) x ์ก์\n",
"# ---------------------------------------------------------------------------\n",
"UNITS = [f\"U{i:02d}\" for i in range(1, 9)]\n",
"ITEMS = [\"ammo_A\", \"ammo_B\", \"ammo_C\", \"ammo_D\", \"fuel_jet\", \"fuel_diesel\"]\n",
"AMMO, FUEL = ITEMS[:4], ITEMS[4:]\n",
"PHASES = [\"prep\", \"attack\", \"defense\", \"stability\"]\n",
"CSR = { # ๊ฐ์ ๊ณ์: ๋ณ๋ ฅ 100๋ช
ยท1์ผ ๊ธฐ์ค ์๋ชจ ๋จ์(ํ์ฝ=์์, ์ ๋ฅ=๋๋ผ)\n",
" \"prep\": [1.0, 0.6, 0.3, 0.2, 1.5, 6.0],\n",
" \"attack\": [6.0, 3.5, 2.0, 1.2, 5.0, 12.0],\n",
" \"defense\": [4.5, 3.0, 1.6, 0.8, 3.0, 7.0],\n",
" \"stability\": [0.8, 0.5, 0.3, 0.2, 1.0, 5.0],\n",
"}\n",
"PHASE_RESIST = {\"prep\": 1.0, \"attack\": 3.2, \"defense\": 3.6, \"stability\": 0.8}\n",
"\n",
"\n",
"def make_ops(n_days=90, seed=7, surge=0):\n",
" \"\"\"๊ฐ์ ๋ถ๋๋ณ ์ผ์ผ ์์ ๊ธฐ๋ก๊ณผ ํ๋ชฉ๋ณ ์ค์ ์๋ชจ๋(์ ๋ต)์ ๋ง๋ ๋ค. surge>0์ด๋ฉด ๋ง์ง๋ง surge์ผ์ ๊ณ ๊ฐ๋ ๊ณต๊ฒฉ ๋จ๊ณ๋ก ๋ฐ๊พผ๋ค(์คํธ๋ ์ค ์ํ์ฉ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for uid in UNITS:\n",
" size = int(rng.integers(3, 9)) * 100\n",
" lens = rng.integers(8, 20, 4)\n",
" sched = [\"prep\"] * lens[0] + [\"attack\"] * lens[1] + [\"defense\"] * lens[2] + [\"attack\"] * lens[3]\n",
" sched = (sched + [\"stability\"] * n_days)[:n_days]\n",
" if surge:\n",
" sched[-surge:] = [\"attack\"] * surge\n",
" r = 1.0\n",
" for d, ph in enumerate(sched):\n",
" r = float(np.clip(0.7 * r + 0.3 * (4.8 if surge and d >= n_days - surge else PHASE_RESIST[ph]) + rng.normal(0, 0.35), 0, 5))\n",
" inten = float(np.clip(r / 5 * 0.9 + rng.normal(0, 0.06), 0, 1))\n",
" fire = int(rng.poisson(1 + 8 * inten))\n",
" move = float(rng.gamma(2, {\"prep\": 4, \"attack\": 12, \"defense\": 2, \"stability\": 5}[ph]))\n",
" weather = str(rng.choice([\"clear\", \"rain\", \"snow\"], p=[0.7, 0.2, 0.1]))\n",
" emerg = bool(rng.random() < 0.02)\n",
" rec = {\"unit\": uid, \"day\": d, \"phase\": ph, \"unit_size\": size, \"intensity\": round(inten, 3),\n",
" \"resistance\": round(r, 2), \"fire_missions\": fire, \"move_km\": round(move, 1),\n",
" \"weather\": weather, \"emergency\": emerg}\n",
" base = size / 100\n",
" for i, it in enumerate(ITEMS):\n",
" c = CSR[ph][i] * base\n",
" if it in AMMO:\n",
" c *= (0.5 + 0.6 * inten) * (1 + 0.04 * fire * (1.0 if it in (\"ammo_C\", \"ammo_D\") else 0.4))\n",
" elif it == \"fuel_jet\":\n",
" c *= (0.6 + 0.5 * inten) + 0.03 * fire\n",
" else:\n",
" c = c * (0.8 + 0.2 * (weather != \"clear\")) + 0.05 * move * base\n",
" c *= rng.lognormal(0, 0.12) * (2.5 if emerg else 1.0)\n",
" rec[it] = round(float(c), 1)\n",
" rows.append(rec)\n",
" return pd.DataFrame(rows)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์๋ชจ๋ ์์ธก์ฉ ํ์ ํผ์ฒ: ์ ์ผ๊น์ง์ ์ค์ + ๋น์ผ ์์ ยทํ๋ ฅ ๊ณํ(์ฌ์ ์ ์ ์ ์๋ ๊ฐ๋ง) ----\n",
"FEATS = [\"lag1\", \"lag2\", \"roll3\", \"roll7\", \"csr_base\", \"size100\", \"fire_plan\", \"fire_x_engaged\", \"resist_lag1\",\n",
" \"resist_trend\", \"inten_lag1\", \"move_plan\", \"days_in_phase\", \"engaged\", \"ph_attack\", \"ph_defense\", \"ph_prep\",\n",
" \"item_idx\", \"is_fuel\"]\n",
"DERIVED = [\"lag1\", \"lag2\", \"roll3\", \"roll7\", \"csr_base\", \"fire_x_engaged\", \"resist_lag1\", \"resist_trend\",\n",
" \"inten_lag1\", \"days_in_phase\", \"engaged\", \"ph_attack\", \"ph_defense\", \"ph_prep\"]\n",
"\n",
"\n",
"def build_long(ops):\n",
" \"\"\"๋ถ๋x์ผ์ ๊ธฐ๋ก์ (๋ถ๋, ์ผ์, ํ๋ชฉ) ํ์ผ๋ก ํผ์น๊ณ ํ์ ํผ์ฒ๋ฅผ ๋ง๋ ๋ค.\n",
" ๋ชฉํ๊ฐ y = log(์ค์ ์๋ชจ๋ / CSR ๊ธฐ์ค๋): CSR ๊ธฐ์ค๋์ ๊ณฑํ '๋ณด์ ๊ณ์'๋ฅผ ์์ธกํ๋ค. lagยทroll ํผ์ฒ๋ ๊ฐ์ ๋น์จ(์ค์ /๊ธฐ์ค)์ด๋ค.\"\"\"\n",
" df = ops.sort_values([\"unit\", \"day\"]).reset_index(drop=True)\n",
" g = df.groupby(\"unit\")\n",
" df[\"resist_lag1\"] = g[\"resistance\"].shift(1)\n",
" df[\"resist_trend\"] = df[\"resist_lag1\"] - g[\"resistance\"].shift(3)\n",
" df[\"inten_lag1\"] = g[\"intensity\"].shift(1)\n",
" df[\"days_in_phase\"] = df.groupby((df[\"phase\"] != g[\"phase\"].shift()).cumsum()).cumcount()\n",
" out = []\n",
" for i, it in enumerate(ITEMS):\n",
" csr = np.array([CSR[p][i] for p in df[\"phase\"]])\n",
" ratio = df[it] / (csr * df[\"unit_size\"] / 100)\n",
" prev = ratio.groupby(df[\"unit\"]).shift(1)\n",
" d = pd.DataFrame({\"unit\": df[\"unit\"], \"day\": df[\"day\"], \"phase\": df[\"phase\"], \"item\": it, \"item_idx\": i,\n",
" \"is_fuel\": int(it in FUEL), \"lag1\": prev, \"lag2\": ratio.groupby(df[\"unit\"]).shift(2),\n",
" \"roll3\": prev.groupby(df[\"unit\"]).transform(lambda s: s.rolling(3).mean()),\n",
" \"roll7\": prev.groupby(df[\"unit\"]).transform(lambda s: s.rolling(7, min_periods=3).mean()),\n",
" \"csr_base\": csr, \"size100\": df[\"unit_size\"] / 100,\n",
" \"fire_plan\": df[\"fire_missions\"], \"fire_x_engaged\": df[\"fire_missions\"] * df[\"phase\"].isin([\"attack\", \"defense\"]),\n",
" \"resist_lag1\": df[\"resist_lag1\"], \"resist_trend\": df[\"resist_trend\"], \"inten_lag1\": df[\"inten_lag1\"],\n",
" \"move_plan\": df[\"move_km\"], \"days_in_phase\": df[\"days_in_phase\"],\n",
" \"engaged\": df[\"phase\"].isin([\"attack\", \"defense\"]).astype(int),\n",
" \"ph_attack\": (df[\"phase\"] == \"attack\").astype(int), \"ph_defense\": (df[\"phase\"] == \"defense\").astype(int),\n",
" \"ph_prep\": (df[\"phase\"] == \"prep\").astype(int), \"emergency\": df[\"emergency\"],\n",
" \"y\": np.log(ratio), \"qty\": df[it]})\n",
" out.append(d)\n",
" return pd.concat(out, ignore_index=True).dropna(subset=[\"lag2\", \"roll3\", \"resist_trend\"]).reset_index(drop=True)\n",
"\n",
"\n",
"def to_qty(d, y_pred):\n",
" \"\"\"์์ธก ๋ณด์ ๊ณ์(log)๋ฅผ ์๋ชจ๋์ผ๋ก ๋๋๋ฆฐ๋ค: ๊ธฐ์ค๋ x exp(์์ธก).\"\"\"\n",
" return np.exp(y_pred) * d[\"csr_base\"].values * d[\"size100\"].values\n",
"\n",
"\n",
"def mape(y, p):\n",
" return float(np.mean(np.abs(np.asarray(p) - np.asarray(y)) / np.asarray(y)))\n",
"\n",
"# ---- ์๋ชจ๋ ์์ธก ๋ชจ๋ธ(์์๋ธ) ํ์ตยท์์ธก ----\n",
"def fit_models(tr, seed=0):\n",
" \"\"\"GBR + ๋๋คํฌ๋ ์คํธ ๋ ๋ชจ๋ธ์ ํ์ตํ๋ค(๋ชฉํ๊ฐ์ CSR ๊ธฐ์ค๋ ๋๋น log ๋ณด์ ๊ณ์).\"\"\"\n",
" g = GradientBoostingRegressor(n_estimators=150, max_depth=3, learning_rate=0.08, subsample=0.8, random_state=seed).fit(tr[FEATS], tr[\"y\"])\n",
" r = RandomForestRegressor(n_estimators=120, min_samples_leaf=3, random_state=seed, n_jobs=1).fit(tr[FEATS], tr[\"y\"])\n",
" return g, r\n",
"\n",
"\n",
"def predict_qty(models, d, w=0.5):\n",
" \"\"\"๋ ๋ชจ๋ธ์ log ์์ธก์ ๊ฐ์ค ํ๊ท ํด ์๋ชจ๋์ผ๋ก ๋๋๋ฆฐ๋ค.\"\"\"\n",
" g, r = models\n",
" return to_qty(d, w * g.predict(d[FEATS]) + (1 - w) * r.predict(d[FEATS]))\n",
"\n",
"SURGE = 7 # ๊ณ ๊ฐ๋ ๋ชจ์ ์ผ์\n",
"DOS_ALERT = 2.0 # [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฒฝ๋ณด ๊ธฐ์ค ๋ณด๊ธ ์์ค(์ผ)\n",
"\n",
"data = build_long(make_ops(90, seed=7, surge=SURGE))\n",
"start = 90 - SURGE\n",
"models = fit_models(data[data[\"day\"] < start])\n",
"test = data[data[\"day\"] >= start].copy()\n",
"\n",
"\n",
"def inject(d, miss=0.0, delay=0.0, spike=0.0, seed=0):\n",
" \"\"\"์ค๋ฅ ์ฃผ์
: ๋ณด๊ณ ๋๋ฝ(์ ์ผ ์ค์ ๊ฒฐ์ธก -> CSR ๊ธฐ์ค 1.0 ๋์
), ๋ณด๊ณ ์ง์ฐ(์ ์ผ ๋์ 2์ผ ์ ์ค์ ), ๊ธด๊ธ ์์ ๊ธ์ฆ(๊ธด๊ธ ๋ถ์ถ ์์ฒญ ์ ์, ์ค์ ์๋ชจ x2.5).\"\"\"\n",
" rng = np.random.default_rng(seed); d = d.copy()\n",
" m = rng.random(len(d)) < miss\n",
" d.loc[m, [\"lag1\", \"lag2\", \"roll3\", \"roll7\"]] = 1.0\n",
" l = (rng.random(len(d)) < delay) & ~m\n",
" d.loc[l, \"lag1\"] = d.loc[l, \"lag2\"]\n",
" s = rng.random(len(d)) < spike\n",
" d.loc[s, \"qty\"] *= 2.5\n",
" d.loc[s, \"emergency\"] = True\n",
" return d\n",
"\n",
"\n",
"def dashboard(d, stock0, margin=1.0):\n",
" \"\"\"๋์๋ณด๋ 1ํ ๊ฐฑ์ : ์์ธก -> ๋ณด์ ๋ ๊ฒฝ๊ณผ -> DOSยท๊ฒฝ๋ณด -> ๋ถ๋๋ณ ์ง๊ณ ํ. ๋ฐํ: ๊ฒฝ๋ณดํ, ์ง๊ณํ.\"\"\"\n",
" # ๊ธด๊ธ ๋ถ์ถ ์์ฒญ์ด ์ ์๋ ๊ฑด์ ์์ฒญ ๋ฐฐ์(๊ฐ์ 2.5๋ฐฐ)๋ฅผ ์์ธก์ ๋ฐ์ํ๋ค(์์ฒญ์ ์ฌ์ ์ ์ ์ ์๋ ์ ๋ณด)\n",
" d = d.assign(pred=predict_qty(models, d) * np.where(d[\"emergency\"], 2.5, 1.0)).sort_values([\"unit\", \"item\", \"day\"])\n",
" resup = d[\"csr_base\"] * d[\"size100\"] * 1.0 # ๊ณํ ์ฌ๋ณด๊ธ๋(์์ ๋จ๊ณ CSR ๊ธฐ์ค, ๊ฐ์)\n",
" used = d.groupby([\"unit\", \"item\"])[\"qty\"].cumsum() - d[\"qty\"]\n",
" got = resup.groupby([d[\"unit\"], d[\"item\"]]).cumsum() - resup\n",
" d[\"on_hand\"] = d.set_index([\"unit\", \"item\"]).index.map(stock0).values + got.values - used.values\n",
" d[\"dos_pred\"] = d[\"on_hand\"].clip(lower=0) / d[\"pred\"]\n",
" d[\"dos_true\"] = d[\"on_hand\"].clip(lower=0) / d[\"qty\"]\n",
" d[\"alert\"], d[\"truth\"] = d[\"dos_pred\"] < DOS_ALERT * margin, d[\"dos_true\"] < DOS_ALERT\n",
" agg = d.groupby([\"day\", \"unit\"]).agg(alerts=(\"alert\", \"sum\"), ammo=(\"pred\", \"sum\"))\n",
" return d, agg\n",
"\n",
"\n",
"stock0 = (test[test[\"day\"] == start].set_index([\"unit\", \"item\"])[\"csr_base\"] * test[test[\"day\"] == start].set_index([\"unit\", \"item\"])[\"size100\"] * 3.0).to_dict()\n",
"cases = [(\"์ ์ ๋ณด๊ณ \", {}), (\"๋ณด๊ณ ๋๋ฝ 10%\", {\"miss\": 0.1}), (\"๋ณด๊ณ ์ง์ฐ 20%\", {\"delay\": 0.2}),\n",
" (\"๊ธด๊ธ ๊ธ์ฆ 5%\", {\"spike\": 0.05}), (\"๋ณตํฉ(๋๋ฝ10+์ง์ฐ20+๊ธ์ฆ5)\", {\"miss\": 0.1, \"delay\": 0.2, \"spike\": 0.05})]\n",
"rows = []\n",
"for name, kw in cases:\n",
" d = inject(test, seed=1, **kw)\n",
" t0 = time.perf_counter(); out, agg = dashboard(d, stock0); dt = time.perf_counter() - t0\n",
" tp = int((out[\"alert\"] & out[\"truth\"]).sum()); fn = int((~out[\"alert\"] & out[\"truth\"]).sum())\n",
" rows.append({\"์๋๋ฆฌ์ค\": name, \"๊ฐฑ์ (s)\": dt, \"๊ฒฝ๋ณด\": int(out[\"alert\"].sum()), \"์ค์ ๋ถ์กฑ\": int(out[\"truth\"].sum()),\n",
" \"์ ํ๋\": (out[\"alert\"] == out[\"truth\"]).mean(), \"๋ถ์กฑ ํ์ง์จ\": tp / max(tp + fn, 1)})\n",
"res = pd.DataFrame(rows)\n",
"print(f\"[7์ผ ๊ณ ๊ฐ๋ ๋ชจ์] ๋ถ๋ 8 x ํ๋ชฉ 6 x {SURGE}์ผ = {len(test)}๊ฑด, ์์ ๋ณด์ ๋ 3์ผ๋ถ(๊ฐ์)\")\n",
"print(res.to_string(index=False, float_format=lambda x: f\"{x:.3f}\"))\n",
"acc = res[\"์ ํ๋\"].min()\n",
"print(f\"๊ฒฝ๋ณด ์ ํ๋ ์ต์ {acc:.1%} (๋ชฉํ 95% ์ด์: {'๋ฌ์ฑ' if acc >= 0.95 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# ๋ถํ ์ํ: ๊ฐ์ ๋ถ๋ 10๋ฐฐ(80๊ฐ) ๋ฐ์ดํฐ๋ก ๋์๋ณด๋ ๊ฐฑ์ ์๊ฐ ์ธก์ \n",
"big = pd.concat([test.assign(unit=test[\"unit\"] + f\"_{k}\") for k in range(10)], ignore_index=True)\n",
"st_big = {(u + f\"_{k}\", it): v for (u, it), v in stock0.items() for k in range(10)}\n",
"t0 = time.perf_counter(); out_b, _ = dashboard(big, st_big); dt_b = time.perf_counter() - t0\n",
"mx = max(res[\"๊ฐฑ์ (s)\"].max(), dt_b)\n",
"print(f\"๋ถํ ์ํ(๋ถ๋ 80๊ฐ, {len(big)}๊ฑด) ๊ฐฑ์ {dt_b:.2f}s, ์ต๋ ๊ฐฑ์ {mx:.2f}s (๋ชฉํ 2์ด ์ด๋ด: {'๋ฌ์ฑ' if mx <= 2 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"base, _ = dashboard(test, stock0)\n",
"first = base[base[\"truth\"]].groupby(\"item\")[\"day\"].min().sub(start - 1).astype(int)\n",
"print(\"ํ๋ชฉ๋ณ ์ฒซ ๋ถ์กฑ ๋ฐ์(๋ชจ์ n์ผ์ฐจ):\", \", \".join(f\"{k} {v}์ผ์ฐจ\" for k, v in first.items()) or \"์์\")\n",
"ammo_tot = base[base[\"is_fuel\"] == 0].groupby(\"day\")[[\"qty\", \"pred\"]].sum()\n",
"print(f\"7์ผ ํ์ฝ ์ด์๋ชจ(๊ฐ์ ์์): ์ค์ {ammo_tot['qty'].sum():.0f}, ์์ธก {ammo_tot['pred'].sum():.0f}, ์ค์ฐจ {ammo_tot['pred'].sum() / ammo_tot['qty'].sum() - 1:+.1%}\")\n",
"tr_off = []\n",
"for mg in (1.0, 1.1, 1.2):\n",
" o, _ = dashboard(test, stock0, mg)\n",
" tr_off.append(f\"x{mg}: ์ ํ๋ {(o['alert'] == o['truth']).mean():.1%}/ํ์ง์จ {(o['alert'] & o['truth']).sum() / o['truth'].sum():.0%}\")\n",
"print(\"๊ฒฝ๋ณด ์์ ๊ณ์ ์์ถฉ๊ด๊ณ(์ ์ ๋ณด๊ณ ):\", \", \".join(tr_off), \"-> ๋ถ์กฑ ๋์นจ์ ์ค์ด๋ ค๋ฉด ์์ ๊ณ์๋ฅผ ์ฌ๋ฆฌ๊ณ ์ ํ๋ ์ ํ๋ฅผ ๊ฐ์ํ๋ค\")\n",
"print(\"๊ฒฝ๋ณด๋ ์ฌ๋ณด๊ธ ๊ฒํ ์์ฒญ์ด๋ฉฐ ๋ถ์ถยท์ฐ์ ์์ ๊ฒฐ์ ์ ๊ตฐ์์ฐธ๋ชจ ๊ฒฐ์ฌ๋ฅผ ๊ฑฐ์น๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 3, figsize=(16, 4))\n",
"ammo_tot.plot(ax=ax[0], marker=\"o\", title=\"7-day surge: total ammo (actual vs forecast)\")\n",
"piv = base.groupby([\"unit\", \"day\"])[\"alert\"].sum().unstack()\n",
"im = ax[1].imshow(piv.values, cmap=\"Reds\", aspect=\"auto\"); ax[1].set_yticks(range(len(piv))); ax[1].set_yticklabels(piv.index)\n",
"ax[1].set_xticks(range(piv.shape[1])); ax[1].set_xticklabels(range(1, piv.shape[1] + 1)); ax[1].set_xlabel(\"surge day\")\n",
"ax[1].set_title(\"Alerts per unit/day (items with DOS<2)\"); plt.colorbar(im, ax=ax[1])\n",
"ax[2].bar(range(len(res)), res[\"์ ํ๋\"]); ax[2].axhline(0.95, c=\"r\", ls=\"--\"); ax[2].set_ylim(0.8, 1.0)\n",
"ax[2].set_xticks(range(len(res))); ax[2].set_xticklabels([\"normal\", \"missing\", \"delay\", \"spike\", \"mixed\"]); ax[2].set_title(\"Alert accuracy by fault scenario\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
08 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[7์ผ ๊ณ ๊ฐ๋ ๋ชจ์] ๋ถ๋ 8 x ํ๋ชฉ 6 x 7์ผ = 336๊ฑด, ์์ ๋ณด์ ๋ 3์ผ๋ถ(๊ฐ์)\n",
" ์๋๋ฆฌ์ค ๊ฐฑ์ (s) ๊ฒฝ๋ณด ์ค์ ๋ถ์กฑ ์ ํ๋ ๋ถ์กฑ ํ์ง์จ\n",
" ์ ์ ๋ณด๊ณ 0.041 23 29 0.964 0.690\n",
" ๋ณด๊ณ ๋๋ฝ 10% 0.039 23 29 0.964 0.690\n",
" ๋ณด๊ณ ์ง์ฐ 20% 0.037 22 29 0.961 0.655\n",
" ๊ธด๊ธ ๊ธ์ฆ 5% 0.038 67 73 0.964 0.877\n",
"๋ณตํฉ(๋๋ฝ10+์ง์ฐ20+๊ธ์ฆ5) 0.041 66 73 0.961 0.863\n",
"๊ฒฝ๋ณด ์ ํ๋ ์ต์ 96.1% (๋ชฉํ 95% ์ด์: ๋ฌ์ฑ)\n",
"๋ถํ ์ํ(๋ถ๋ 80๊ฐ, 3360๊ฑด) ๊ฐฑ์ 0.07s, ์ต๋ ๊ฐฑ์ 0.07s (๋ชฉํ 2์ด ์ด๋ด: ๋ฌ์ฑ)\n",
"ํ๋ชฉ๋ณ ์ฒซ ๋ถ์กฑ ๋ฐ์(๋ชจ์ n์ผ์ฐจ): ammo_C 4์ผ์ฐจ, ammo_D 5์ผ์ฐจ, fuel_jet 4์ผ์ฐจ\n",
"7์ผ ํ์ฝ ์ด์๋ชจ(๊ฐ์ ์์): ์ค์ 4645, ์์ธก 4633, ์ค์ฐจ -0.2%\n",
"๊ฒฝ๋ณด ์์ ๊ณ์ ์์ถฉ๊ด๊ณ(์ ์ ๋ณด๊ณ ): x1.0: ์ ํ๋ 96.4%/ํ์ง์จ 69%, x1.1: ์ ํ๋ 94.3%/ํ์ง์จ 90%, x1.2: ์ ํ๋ 87.2%/ํ์ง์จ 97% -> ๋ถ์กฑ ๋์นจ์ ์ค์ด๋ ค๋ฉด ์์ ๊ณ์๋ฅผ ์ฌ๋ฆฌ๊ณ ์ ํ๋ ์ ํ๋ฅผ ๊ฐ์ํ๋ค\n",
"๊ฒฝ๋ณด๋ ์ฌ๋ณด๊ธ ๊ฒํ ์์ฒญ์ด๋ฉฐ ๋ถ์ถยท์ฐ์ ์์ ๊ฒฐ์ ์ ๊ตฐ์์ฐธ๋ชจ ๊ฒฐ์ฌ๋ฅผ ๊ฑฐ์น๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
09. AAR ๊ธฐ๋ฐ ์๋ชจ๋ ์์ธก์น์ ์ค์ฌ๊ฒฉ ๊ฒฐ๊ณผ ๊ฐ ํธ์ฐจ ๋ถ์ ๋ฐ ์๋ชจ ๊ณ์ ์ฌ๋ณด์ \n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"AAR ๊ธฐ๋ฐ ์๋ชจ๋ ์์ธก ํธ์ฐจ๋ฅผ ๋ถ์ํ๊ณ ์๋ชจ ๊ณ์๋ฅผ ์ฌ๋ณด์ ํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **AAR ์๋ฃ๋ก CSR ๊ณํ์น์ ์ค์๋ชจ ๊ฒฐ๊ณผ์ ํธ์ฐจ๋ฅผ ๋ถ์ํ๊ณ ์๋ชจ ๊ณ์๋ฅผ ์ฌ๋ณด์ ํด ๋์
ํจ๊ณผ๋ฅผ ์ฐ์ถํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์์ ๋จ๊ณยทํ๋ชฉยท๊ต์ ๊ฐ๋ ๊ตฌ๊ฐ๋ณ๋ก ์ค์ /๊ณํ ๋น์จ์ ๋ถ์ํด ํธ์ฐจ ์์ธ์ ์ฐพ๋๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `reports/amm02_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(์ฌํ๊ฒํ ) ์๋ฃ๋ก ๊ธฐ์ค ์๋ชจ์จ(CSR) ๊ณํ์น์ ์ค์ฌ๊ฒฉยท์ค์๋ชจ ๊ฒฐ๊ณผ์ ํธ์ฐจ๋ฅผ ์์ ๋จ๊ณยทํ๋ชฉยท๊ต์ ๊ฐ๋๋ณ๋ก ๋ถ์ํด ์์ธ์ ์ฐพ๊ณ , ์๋ชจ ๊ณ์๋ฅผ ์ถ์ ์ถ์ ์ผ๋ก ์ฌ๋ณด์ ํ ๋ค ์ ํ๋ จ ์๋ฃ์์ ๊ฒ์ฆํ๋ฉฐ, ๋น์ฉ ์ ๊ฐยท์์
์๊ฐ ๋จ์ถยท๊ธด๊ธ ์ฌ๋ณด๊ธ ๊ฐ์ 3๋ ํจ๊ณผ ์งํ์ ๋ฏผ๊ฐ๋๋ฅผ ์ฐ์ถํ๋ค.\"\"\"\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",
"# ๊ฐ์ ๋ถ๋ 8๊ฐ x 90์ผ. ์๋ชจ๋ = ๊ฐ์ CSR ๊ณ์ x ๋ณ๋ ฅ ๊ท๋ชจ x (๊ต์ ๊ฐ๋ยท์ ์ ํญยทํ๋ ฅ ๊ณํ ํจ๊ณผ) x ์ก์\n",
"# ---------------------------------------------------------------------------\n",
"UNITS = [f\"U{i:02d}\" for i in range(1, 9)]\n",
"ITEMS = [\"ammo_A\", \"ammo_B\", \"ammo_C\", \"ammo_D\", \"fuel_jet\", \"fuel_diesel\"]\n",
"AMMO, FUEL = ITEMS[:4], ITEMS[4:]\n",
"PHASES = [\"prep\", \"attack\", \"defense\", \"stability\"]\n",
"CSR = { # ๊ฐ์ ๊ณ์: ๋ณ๋ ฅ 100๋ช
ยท1์ผ ๊ธฐ์ค ์๋ชจ ๋จ์(ํ์ฝ=์์, ์ ๋ฅ=๋๋ผ)\n",
" \"prep\": [1.0, 0.6, 0.3, 0.2, 1.5, 6.0],\n",
" \"attack\": [6.0, 3.5, 2.0, 1.2, 5.0, 12.0],\n",
" \"defense\": [4.5, 3.0, 1.6, 0.8, 3.0, 7.0],\n",
" \"stability\": [0.8, 0.5, 0.3, 0.2, 1.0, 5.0],\n",
"}\n",
"PHASE_RESIST = {\"prep\": 1.0, \"attack\": 3.2, \"defense\": 3.6, \"stability\": 0.8}\n",
"\n",
"\n",
"def make_ops(n_days=90, seed=7, surge=0):\n",
" \"\"\"๊ฐ์ ๋ถ๋๋ณ ์ผ์ผ ์์ ๊ธฐ๋ก๊ณผ ํ๋ชฉ๋ณ ์ค์ ์๋ชจ๋(์ ๋ต)์ ๋ง๋ ๋ค. surge>0์ด๋ฉด ๋ง์ง๋ง surge์ผ์ ๊ณ ๊ฐ๋ ๊ณต๊ฒฉ ๋จ๊ณ๋ก ๋ฐ๊พผ๋ค(์คํธ๋ ์ค ์ํ์ฉ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for uid in UNITS:\n",
" size = int(rng.integers(3, 9)) * 100\n",
" lens = rng.integers(8, 20, 4)\n",
" sched = [\"prep\"] * lens[0] + [\"attack\"] * lens[1] + [\"defense\"] * lens[2] + [\"attack\"] * lens[3]\n",
" sched = (sched + [\"stability\"] * n_days)[:n_days]\n",
" if surge:\n",
" sched[-surge:] = [\"attack\"] * surge\n",
" r = 1.0\n",
" for d, ph in enumerate(sched):\n",
" r = float(np.clip(0.7 * r + 0.3 * (4.8 if surge and d >= n_days - surge else PHASE_RESIST[ph]) + rng.normal(0, 0.35), 0, 5))\n",
" inten = float(np.clip(r / 5 * 0.9 + rng.normal(0, 0.06), 0, 1))\n",
" fire = int(rng.poisson(1 + 8 * inten))\n",
" move = float(rng.gamma(2, {\"prep\": 4, \"attack\": 12, \"defense\": 2, \"stability\": 5}[ph]))\n",
" weather = str(rng.choice([\"clear\", \"rain\", \"snow\"], p=[0.7, 0.2, 0.1]))\n",
" emerg = bool(rng.random() < 0.02)\n",
" rec = {\"unit\": uid, \"day\": d, \"phase\": ph, \"unit_size\": size, \"intensity\": round(inten, 3),\n",
" \"resistance\": round(r, 2), \"fire_missions\": fire, \"move_km\": round(move, 1),\n",
" \"weather\": weather, \"emergency\": emerg}\n",
" base = size / 100\n",
" for i, it in enumerate(ITEMS):\n",
" c = CSR[ph][i] * base\n",
" if it in AMMO:\n",
" c *= (0.5 + 0.6 * inten) * (1 + 0.04 * fire * (1.0 if it in (\"ammo_C\", \"ammo_D\") else 0.4))\n",
" elif it == \"fuel_jet\":\n",
" c *= (0.6 + 0.5 * inten) + 0.03 * fire\n",
" else:\n",
" c = c * (0.8 + 0.2 * (weather != \"clear\")) + 0.05 * move * base\n",
" c *= rng.lognormal(0, 0.12) * (2.5 if emerg else 1.0)\n",
" rec[it] = round(float(c), 1)\n",
" rows.append(rec)\n",
" return pd.DataFrame(rows)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ ์ฅ๊ณ (H1~H3) ์ ์ฅ์นธ ๋ฐฐ์น ๋ชจ๋ธ. ์ค์ ํ์ฝ ์ ์ฅ ๊ธฐ์คยท์์ ๊ฑฐ๋ฆฌ์ ๋ฌด๊ดํ ๊ฐ์ ๊ณ์๋ค.\n",
"# ์ ์ฅ์นธ: ์ ์ฅ๊ณ x ์ด(row) x ์นธ(col). ๋์ ๊ฑฐ๋ฆฌ = ๋ฐ์ถ์ฅ(์
๊ตฌ)๊น์ง์ ๊ฐ์ ๊ฑฐ๋ฆฌ(m).\n",
"# ์ ์ฅ๊ตฐ(G1~G3)์ ๊ฐ์ ํผ์ ์ ํ ๊ทธ๋ฃน์ด๋ฉฐ, ์ ์ฅ๊ณ ๋ง๋ค ๋ฐ์ ์ ์๋ ์ ์ฅ๊ตฐ์ด ์ ํด์ ธ ์๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"HOUSES = {\"H1\": {\"groups\": {\"G1\", \"G2\"}, \"rows\": 2, \"cols\": 6, \"dock\": 15.0},\n",
" \"H2\": {\"groups\": {\"G1\"}, \"rows\": 2, \"cols\": 5, \"dock\": 40.0},\n",
" \"H3\": {\"groups\": {\"G2\", \"G3\"}, \"rows\": 2, \"cols\": 6, \"dock\": 70.0}}\n",
"ITEM_GROUP = {\"ammo_A\": \"G1\", \"ammo_B\": \"G1\", \"ammo_C\": \"G2\", \"ammo_D\": \"G3\"}\n",
"SLOT_CAP = 6 # 1์ด ์ ์ฅ์นธ ๊ฐ์ ํ๋ ํธ ์์ฉ๋(2์ด์ +4)\n",
"\n",
"\n",
"def make_slots(seed=3):\n",
" \"\"\"์ ์ฅ์นธ ๋ชฉ๋ก๊ณผ ํ์ฌ ์ ์ (์ด๋ ๋ถ๊ฐ ๊ธฐ์กด ์ฌ๊ณ ) ์ฌ๋ถ๋ฅผ ๋ง๋ ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for h, c in HOUSES.items():\n",
" for r in range(c[\"rows\"]):\n",
" for k in range(c[\"cols\"]):\n",
" rows.append({\"slot\": f\"{h}-{r + 1}{k + 1:02d}\", \"house\": h, \"dist_m\": c[\"dock\"] + 6.0 * k + 4.0 * r,\n",
" \"cap\": SLOT_CAP if r == 0 else SLOT_CAP + 4, \"fixed\": bool(rng.random() < 0.15)})\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"def make_skus(demand, seed=5):\n",
" \"\"\"ํ๋ชฉ๋ณ ์์ธก ์ผ์ผ ๋ฐ์ถ๋(demand: ํ๋ชฉ->์์/์ผ)์ ๋กํธ ๋จ์ SKU๋ก ๋๋๋ค. ๋ฐ์ถ ๋น๋ = ์ผ์ผ ๋ฐ์ถ ํ์(๊ฐ์).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for it, dq in demand.items():\n",
" n_lot = 5\n",
" share = rng.dirichlet(np.ones(n_lot) * 2)\n",
" for j in range(n_lot):\n",
" rows.append({\"sku\": f\"{it}-L{j + 1}\", \"item\": it, \"group\": ITEM_GROUP[it],\n",
" \"pallets\": int(rng.integers(2, SLOT_CAP + 5)), \"freq\": round(float(dq * share[j] / 4), 2)})\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"def feasible(sku, slot):\n",
" return (sku[\"group\"] in HOUSES[slot[\"house\"]][\"groups\"]) and sku[\"pallets\"] <= slot[\"cap\"] and not slot[\"fixed\"]\n",
"\n",
"\n",
"def travel(assign, skus, slots):\n",
" \"\"\"์์
๋์ = ๋ฐ์ถ ๋น๋ x ์๋ณต ๊ฑฐ๋ฆฌ ํฉ(๊ฐ์ m/์ผ).\"\"\"\n",
" d = slots.set_index(\"slot\")[\"dist_m\"]\n",
" return float(sum(2 * skus.loc[i, \"freq\"] * d[s] for i, s in assign.items()))\n",
"\n",
"\n",
"def first_fit(skus, slots):\n",
" \"\"\"ํํ ๋ฐฉ์(๋์ ๋น๊ต ๊ธฐ์ค): ์
๊ณ ์์๋๋ก ๋น์ด ์๋ ์ฒซ ๋ฒ์งธ ์ ํฉ ์นธ์ ๋ฃ๋๋ค.\"\"\"\n",
" used, out = set(), {}\n",
" for i, k in skus.iterrows():\n",
" for _, s in slots.iterrows():\n",
" if s[\"slot\"] not in used and feasible(k, s):\n",
" out[i] = s[\"slot\"]; used.add(s[\"slot\"]); break\n",
" return out\n",
"\n",
"\n",
"def optimize(skus, slots, lam=8):\n",
" \"\"\"ํ ๋น ๋ฌธ์ (์ ์๊ณํ ํน์ํ): ๋น์ฉ = ๋ฐ์ถ ๋น๋ x ์๋ณต๊ฑฐ๋ฆฌ + lam x ๋น ๊ณต๊ฐ(๊ณต๊ฐ๋ญ๋น). ๋ถ์ ํฉ ์นธ์ ํฐ ๋น์ฉ.\"\"\"\n",
" cost = np.full((len(skus), len(slots)), 1e7)\n",
" for i, k in skus.iterrows():\n",
" for j, s in slots.iterrows():\n",
" if feasible(k, s):\n",
" cost[i, j] = 2 * k[\"freq\"] * s[\"dist_m\"] + lam * (s[\"cap\"] - k[\"pallets\"])\n",
" ri, cj = linear_sum_assignment(cost)\n",
" return {int(i): slots.loc[j, \"slot\"] for i, j in zip(ri, cj) if cost[i, j] < 1e7}, cost\n",
"\n",
"\n",
"def loc_score(assign, skus, slots):\n",
" \"\"\"์์น์ ์(0~100): ๋ฐ์ถ ๋น๋ ์์ SKU์ผ์๋ก ๋ฐ์ถ์ฅ ๊ฐ๊น์ด ์นธ์ ์๋ ์ ๋(๊ฐ์ค ํ๊ท ).\"\"\"\n",
" d = slots.set_index(\"slot\")[\"dist_m\"]; dmin, dmax = d.min(), d.max()\n",
" w = skus[\"freq\"] / skus[\"freq\"].sum()\n",
" return float(100 * sum(w[i] * (1 - (d[s] - dmin) / (dmax - dmin)) for i, s in assign.items()))\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"PRICE = {\"hold\": 1.0, \"emerg\": 25.0} # [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ณผ์ 1๋จ์ ๋ณด๊ดยท์์ก ๋น์ฉ ์ง์, ๊ธด๊ธ ์ฌ๋ณด๊ธ 1ํ ๋น์ฉ ์ง์\n",
"SHRINK_K = 20 # ์ฌ๋ณด์ ์ถ์ ๊ฐ๋(ํ๋ณธ n์ด ์์์๋ก ๊ธฐ์กด ๊ณ์ ์ ์ง)\n",
"SPEED = 80.0 # ์ง๊ฒ์ฐจ ๊ฐ์ ์ด๋ ์๋(m/๋ถ)\n",
"\n",
"\n",
"def long_ratio(ops):\n",
" \"\"\"(๋ถ๋, ์ผ์, ํ๋ชฉ)๋ณ ์ค์ ์๋ชจ๋๊ณผ CSR ๊ณํ๋, ๋น์จ.\"\"\"\n",
" out = []\n",
" for i, it in enumerate(ITEMS):\n",
" plan = np.array([CSR[p][i] for p in ops[\"phase\"]]) * ops[\"unit_size\"] / 100\n",
" out.append(pd.DataFrame({\"unit\": ops[\"unit\"], \"day\": ops[\"day\"], \"phase\": ops[\"phase\"], \"item\": it,\n",
" \"intensity\": ops[\"intensity\"], \"plan\": plan, \"qty\": ops[it], \"emergency\": ops[\"emergency\"]}))\n",
" d = pd.concat(out, ignore_index=True)\n",
" return d.assign(ratio=d[\"qty\"] / d[\"plan\"])\n",
"\n",
"\n",
"aar = long_ratio(make_ops(90, seed=7)) # ์ง๋ ํ๋ จ(AAR ๋์)\n",
"new = long_ratio(make_ops(90, seed=8)) # ๋ค์ ํ๋ จ(์ฌ๋ณด์ ๊ฒ์ฆ์ฉ, ๊ฐ์ ์์ฑ ๊ท์น์ ๋ค๋ฅธ ๊ฐ์ ๋ถ๋)\n",
"\n",
"# 1) ํธ์ฐจ ๋ถ์: ๋จ๊ณ x ํ๋ชฉ๋ณ ์ค์ /๊ณํ ๋น์จ ์ค์๊ฐ(1๋ณด๋ค ํฌ๋ฉด ๊ณํ ๊ณผ์)\n",
"piv = aar[~aar[\"emergency\"]].groupby([\"phase\", \"item\"])[\"ratio\"].median().unstack()[ITEMS]\n",
"print(\"[AAR ํธ์ฐจ: ์ค์ /๊ณํ ๋น์จ ์ค์๊ฐ(๊ธด๊ธ ๋ถ์ถ ์ ์ธ)]\")\n",
"print(piv.to_string(float_format=lambda x: f\"{x:.2f}\"))\n",
"band = pd.cut(aar[\"intensity\"], [0, 0.3, 0.6, 1.0], labels=[\"low\", \"mid\", \"high\"], include_lowest=True)\n",
"amm = aar[aar[\"item\"].isin(AMMO)].groupby(band, observed=True)[\"ratio\"].median()\n",
"print(\"ํ์ฝ ๋น์จ vs ๊ต์ ๊ฐ๋:\", \", \".join(f\"{k} {v:.2f}\" for k, v in amm.items()),\n",
" \"-> ์์ธ: CSR์ ๋จ๊ณ๋ณ ๊ณ ์ ๊ฐ์ด๋ผ ๊ฐ์ ๊ณต๊ฒฉ ๋จ๊ณ๋ผ๋ ๊ฐ๋๊ฐ ๋ฎ์ ๋ ์ ๊ณผ๋, ๋์ ๋ ์ ๊ณผ์ ๊ณํ๋๋ค\")\n",
"em_share = aar.loc[aar[\"emergency\"], \"qty\"].sum() / aar[\"qty\"].sum()\n",
"print(f\"๊ธด๊ธ ๋ถ์ถ ๋น์ค {em_share:.1%} -> ๊ณ์ ์ฌ๋ณด์ ์์ ์ ์ธํ๊ณ ์๋น ์ฌ๊ณ ๋ก ๊ด๋ฆฌ\")\n",
"\n",
"# 2) ์ฌ๋ณด์ : ๋จ๊ณ x ํ๋ชฉ x ๊ณํ ๊ต์ ๊ฐ๋ ๊ตฌ๊ฐ๋ณ ๋ณด์ ๊ณ์ = ๋น์จ ์ค์๊ฐ^(n/(n+K)) (ํ๋ณธ์ด ์ ์ ์นธ์ ๊ธฐ์กด ๊ณ์ ์ ์ง ์ชฝ์ผ๋ก ์ถ์)\n",
"BANDS = [0, 0.3, 0.6, 1.0]\n",
"for d in (aar, new):\n",
" d[\"band\"] = pd.cut(d[\"intensity\"], BANDS, labels=[\"low\", \"mid\", \"high\"], include_lowest=True).astype(str)\n",
"base = aar[~aar[\"emergency\"]]\n",
"g = base.groupby([\"phase\", \"item\", \"band\"])[\"ratio\"]\n",
"FACT = (g.median() ** (g.size() / (g.size() + SHRINK_K))).to_dict()\n",
"print(f\"์ฌ๋ณด์ ๊ณ์ {len(FACT)}๊ฐ(๋จ๊ณ 4 x ํ๋ชฉ 6 x ๊ฐ๋ ๊ตฌ๊ฐ), ์: attack-ammo_A low/mid/high = \"\n",
" + \"/\".join(f\"{FACT.get(('attack', 'ammo_A', b), 1):.2f}\" for b in (\"low\", \"mid\", \"high\")))\n",
"\n",
"\n",
"def mape(y, p):\n",
" return float(np.mean(np.abs(p - y) / y))\n",
"\n",
"\n",
"def apply_csr(d):\n",
" \"\"\"์ฌ๋ณด์ ๊ณํ๋ = ๊ธฐ์กด CSR ๊ณํ๋ x ๋ณด์ ๊ณ์(ํด๋น ์นธ์ด ์์ผ๋ฉด 1.0).\"\"\"\n",
" return d[\"plan\"] * np.array([FACT.get(k, 1.0) for k in zip(d[\"phase\"], d[\"item\"], d[\"band\"])])\n",
"\n",
"\n",
"aar[\"plan2\"], new[\"plan2\"] = apply_csr(aar), apply_csr(new)\n",
"\n",
"\n",
"m1, m2 = mape(new[\"qty\"], new[\"plan\"]), mape(new[\"qty\"], new[\"plan2\"])\n",
"print(f\"\\n[์ฌ๋ณด์ ๊ฒ์ฆ(๋ค์ ํ๋ จ ์๋ฃ)] ์ผ์ผ MAPE ๊ธฐ์กด CSR {m1:.1%} -> ์ฌ๋ณด์ {m2:.1%} ({m1 - m2:+.1%}p ๊ฐ์ )\")\n",
"tot = new.groupby(\"item\")[[\"qty\", \"plan\", \"plan2\"]].sum()\n",
"tot[\"๊ธฐ์กด ์ค์ฐจ\"], tot[\"์ฌ๋ณด์ ์ค์ฐจ\"] = tot[\"plan\"] / tot[\"qty\"] - 1, tot[\"plan2\"] / tot[\"qty\"] - 1\n",
"print(\"ํ๋ชฉ๋ณ ์ด๋ ์ค์ฐจ:\", \", \".join(f\"{k} {a:+.1%}->{b:+.1%}\" for k, a, b in zip(tot.index, tot[\"๊ธฐ์กด ์ค์ฐจ\"], tot[\"์ฌ๋ณด์ ์ค์ฐจ\"])))\n",
"\n",
"\n",
"# 3) 3๋ ํจ๊ณผ ์งํ(๊ฐ์ ๋จ๊ฐ, 90์ผ x ๋ถ๋ 8 ๊ธฐ์ค). ๋ ๊ณํ ๋ชจ๋ AAR ์๋ฃ์์ 95% ์ถฉ์กฑ ์์ ๊ณ์๋ฅผ ์ ํด ๊ฐ์ ์๋น์ค ์์ค์ผ๋ก ๋น๊ตํ๋ค\n",
"SAFE = {c: float(np.quantile(aar.loc[~aar[\"emergency\"], \"qty\"] / aar.loc[~aar[\"emergency\"], c], 0.95)) for c in (\"plan\", \"plan2\")}\n",
"print(f\"95% ์ถฉ์กฑ ์์ ๊ณ์: ๊ธฐ์กด x{SAFE['plan']:.2f}, ์ฌ๋ณด์ x{SAFE['plan2']:.2f} -> ๊ฐ์ ์ถฉ์กฑ ์์ค์์ ๊ณต๊ธ ๊ณํ๋์ ๋น๊ตํ๋ค\")\n",
"\n",
"\n",
"def effect(plan_col, p_hold=PRICE[\"hold\"], p_em=PRICE[\"emerg\"]):\n",
" supply = new[plan_col] * SAFE[plan_col]\n",
" excess = (supply - new[\"qty\"]).clip(lower=0).sum()\n",
" short = int((new[\"qty\"] > supply).sum()) # ๊ณต๊ธ ๊ณํ ์ด๊ณผ ์๋ชจ -> ๊ธด๊ธ ์ฌ๋ณด๊ธ 1ํ\n",
" return excess * p_hold + short * p_em, short\n",
"\n",
"\n",
"c1, s1 = effect(\"plan\"); c2, s2 = effect(\"plan2\")\n",
"ops = make_ops(90, seed=7)\n",
"demand = ops[ops[\"day\"] >= 76][AMMO].sum().div(14).to_dict()\n",
"skus, slots = make_skus(demand), make_slots()\n",
"t_cur, t_opt = travel(first_fit(skus, slots), skus, slots), travel(optimize(skus, slots)[0], skus, slots)\n",
"kpis = pd.DataFrame([[\"๋น์ฉ(๊ณผ์ ๋ณด๊ดยท์์ก+๊ธด๊ธ ์ฌ๋ณด๊ธ) ์ง์\", c1, c2, 1 - c2 / c1],\n",
" [\"์ ์ฅ๊ณ ๋ฐ์ถ ์์
์๊ฐ(๋ถ/์ผ)\", t_cur / SPEED, t_opt / SPEED, 1 - t_opt / t_cur],\n",
" [\"๊ธด๊ธ ์ฌ๋ณด๊ธ ํ์(ํ/90์ผ)\", s1, s2, 1 - s2 / max(s1, 1)]], columns=[\"์งํ\", \"ํํ\", \"๊ฐ์ \", \"๊ฐ์ ์จ\"])\n",
"print(\"\\n[3๋ ์ ๋ ํจ๊ณผ ์งํ(๊ฐ์ ๋จ๊ฐ ๊ธฐ์ค ์๋ ๋น๊ต๊ฐ)]\")\n",
"print(kpis.to_string(index=False, float_format=lambda x: f\"{x:.2f}\"))\n",
"print(f\"ํจ๊ณผ ์งํ ์ฐ์ถ {len(kpis)}์ข
(๋ชฉํ 3๋ ์งํ: {'๋ฌ์ฑ' if len(kpis) >= 3 else '๋ฏธ๋ฌ'}) - ๊ธด๊ธ ์ฌ๋ณด๊ธ ํ์๋ ๊ฐ์ ์ถฉ์กฑ ์์ค์์ ๋น์ทํ๊ฒ ์ ์ง๋๋์ง(์ฐจ์ด {s2 - s1:+d}ํ) ํ์ธํ๋ ์งํ๋ค\")\n",
"\n",
"sens = []\n",
"for ph in (0.8, 1.0, 1.2):\n",
" for pe in (15.0, 25.0, 40.0):\n",
" a, _ = effect(\"plan\", PRICE[\"hold\"] * ph, pe); b, _ = effect(\"plan2\", PRICE[\"hold\"] * ph, pe)\n",
" sens.append((ph, pe, 1 - b / a))\n",
"s = pd.DataFrame(sens, columns=[\"๋ณด๊ด๋จ๊ฐ ๋ฐฐ์\", \"๊ธด๊ธ 1ํ ๋น์ฉ\", \"๋น์ฉ ์ ๊ฐ๋ฅ \"]).pivot(index=\"๋ณด๊ด๋จ๊ฐ ๋ฐฐ์\", columns=\"๊ธด๊ธ 1ํ ๋น์ฉ\", values=\"๋น์ฉ ์ ๊ฐ๋ฅ \")\n",
"print(\"\\n[๋ฏผ๊ฐ๋: ๋น์ฉ ์ ๊ฐ๋ฅ ]\")\n",
"print(s.to_string(float_format=lambda x: f\"{x:.1%}\"))\n",
"print(\"๋์
๋ก๋๋งต: ๋จ๊ธฐ(1~3๊ฐ์) ํ๋ จ AAR๋ก ๊ณ์ ์ฌ๋ณด์ / ์ค๊ธฐ(4~9๊ฐ์) ์์ธก ๋ชจ๋ธยท์ ์ฅ ๋ฐฐ์น ์ต์ ํ ์๋ฒ ์ด์ฉ / ์ฅ๊ธฐ(10๊ฐ์~) ๊ตฐ์ ์ ๋ณด์ฒด๊ณ ์ฐ๊ณ์ ์ ๊ธฐ ์ฌํ์ต\")\n",
"print(\"๊ธ์กยท์๊ฐ์ ๊ฐ์ ๋จ๊ฐ๋ก ๊ณ์ฐํ ์๋ ๋น๊ต๊ฐ์ด๋ฉฐ ์์ฐ ๊ทผ๊ฑฐ๋ก ์ฐ์ง ์๋๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(12, 4))\n",
"im = ax[0].imshow(piv.values, cmap=\"coolwarm\", vmin=0.5, vmax=1.5); ax[0].set_xticks(range(6)); ax[0].set_xticklabels(ITEMS, rotation=30)\n",
"ax[0].set_yticks(range(4)); ax[0].set_yticklabels(piv.index); ax[0].set_title(\"AAR actual/plan ratio (median)\"); plt.colorbar(im, ax=ax[0])\n",
"x = np.arange(len(tot)); ax[1].bar(x - 0.2, tot[\"๊ธฐ์กด ์ค์ฐจ\"], 0.4, label=\"old CSR\"); ax[1].bar(x + 0.2, tot[\"์ฌ๋ณด์ ์ค์ฐจ\"], 0.4, label=\"recalibrated\")\n",
"ax[1].set_xticks(x); ax[1].set_xticklabels(tot.index, rotation=30); ax[1].axhline(0, c=\"k\", lw=0.8); ax[1].set_title(\"Total plan error on next exercise\"); ax[1].legend()\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
09 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[AAR ํธ์ฐจ: ์ค์ /๊ณํ ๋น์จ ์ค์๊ฐ(๊ธด๊ธ ๋ถ์ถ ์ ์ธ)]\n",
"item ammo_A ammo_B ammo_C ammo_D fuel_jet fuel_diesel\n",
"phase \n",
"attack 0.90 0.89 0.99 1.02 1.04 0.94\n",
"defense 0.96 0.99 1.05 1.09 1.10 0.85\n",
"prep 0.61 0.64 0.62 0.67 0.76 0.90\n",
"stability 0.62 0.63 0.67 0.64 0.76 0.94\n",
"ํ์ฝ ๋น์จ vs ๊ต์ ๊ฐ๋: low 0.62, mid 0.90, high 1.06 -> ์์ธ: CSR์ ๋จ๊ณ๋ณ ๊ณ ์ ๊ฐ์ด๋ผ ๊ฐ์ ๊ณต๊ฒฉ ๋จ๊ณ๋ผ๋ ๊ฐ๋๊ฐ ๋ฎ์ ๋ ์ ๊ณผ๋, ๋์ ๋ ์ ๊ณผ์ ๊ณํ๋๋ค\n",
"๊ธด๊ธ ๋ถ์ถ ๋น์ค 6.0% -> ๊ณ์ ์ฌ๋ณด์ ์์ ์ ์ธํ๊ณ ์๋น ์ฌ๊ณ ๋ก ๊ด๋ฆฌ\n",
"์ฌ๋ณด์ ๊ณ์ 60๊ฐ(๋จ๊ณ 4 x ํ๋ชฉ 6 x ๊ฐ๋ ๊ตฌ๊ฐ), ์: attack-ammo_A low/mid/high = 0.89/0.87/1.02\n",
"\n",
"[์ฌ๋ณด์ ๊ฒ์ฆ(๋ค์ ํ๋ จ ์๋ฃ)] ์ผ์ผ MAPE ๊ธฐ์กด CSR 34.0% -> ์ฌ๋ณด์ 14.5% (+19.5%p ๊ฐ์ )\n",
"ํ๋ชฉ๋ณ ์ด๋ ์ค์ฐจ: ammo_A +12.3%->+0.2%, ammo_B +11.6%->+0.2%, ammo_C -0.4%->-4.4%, ammo_D +0.9%->-3.4%, fuel_diesel +2.1%->-4.0%, fuel_jet +0.1%->-2.7%\n",
"95% ์ถฉ์กฑ ์์ ๊ณ์: ๊ธฐ์กด x1.23, ์ฌ๋ณด์ x1.26 -> ๊ฐ์ ์ถฉ์กฑ ์์ค์์ ๊ณต๊ธ ๊ณํ๋์ ๋น๊ตํ๋ค\n",
"\n",
"[3๋ ์ ๋ ํจ๊ณผ ์งํ(๊ฐ์ ๋จ๊ฐ ๊ธฐ์ค ์๋ ๋น๊ต๊ฐ)]\n",
" ์งํ ํํ ๊ฐ์ ๊ฐ์ ์จ\n",
"๋น์ฉ(๊ณผ์ ๋ณด๊ดยท์์ก+๊ธด๊ธ ์ฌ๋ณด๊ธ) ์ง์ 21556.08 19063.11 0.12\n",
" ์ ์ฅ๊ณ ๋ฐ์ถ ์์
์๊ฐ(๋ถ/์ผ) 18.88 14.97 0.21\n",
" ๊ธด๊ธ ์ฌ๋ณด๊ธ ํ์(ํ/90์ผ) 283.00 284.00 -0.00\n",
"ํจ๊ณผ ์งํ ์ฐ์ถ 3์ข
(๋ชฉํ 3๋ ์งํ: ๋ฌ์ฑ) - ๊ธด๊ธ ์ฌ๋ณด๊ธ ํ์๋ ๊ฐ์ ์ถฉ์กฑ ์์ค์์ ๋น์ทํ๊ฒ ์ ์ง๋๋์ง(์ฐจ์ด +1ํ) ํ์ธํ๋ ์งํ๋ค\n",
"\n",
"[๋ฏผ๊ฐ๋: ๋น์ฉ ์ ๊ฐ๋ฅ ]\n",
"๊ธด๊ธ 1ํ ๋น์ฉ 15.0 25.0 40.0\n",
"๋ณด๊ด๋จ๊ฐ ๋ฐฐ์ \n",
"0.8 12.6% 10.7% 8.6%\n",
"1.0 13.4% 11.6% 9.6%\n",
"1.2 13.9% 12.3% 10.4%\n",
"๋์
๋ก๋๋งต: ๋จ๊ธฐ(1~3๊ฐ์) ํ๋ จ AAR๋ก ๊ณ์ ์ฌ๋ณด์ / ์ค๊ธฐ(4~9๊ฐ์) ์์ธก ๋ชจ๋ธยท์ ์ฅ ๋ฐฐ์น ์ต์ ํ ์๋ฒ ์ด์ฉ / ์ฅ๊ธฐ(10๊ฐ์~) ๊ตฐ์ ์ ๋ณด์ฒด๊ณ ์ฐ๊ณ์ ์ ๊ธฐ ์ฌํ์ต\n",
"๊ธ์กยท์๊ฐ์ ๊ฐ์ ๋จ๊ฐ๋ก ๊ณ์ฐํ ์๋ ๋น๊ต๊ฐ์ด๋ฉฐ ์์ฐ ๊ทผ๊ฑฐ๋ก ์ฐ์ง ์๋๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
10. ํ์ฝ/์ ๋ฅ ์๋ชจ๋ ์์ธก ๋ฐ ์ ์ ์ฌ๋ณด๊ธ ๊ณํ ์๋ฆฝ ์๋ฎฌ๋ ์ดํฐ ํตํฉ ์์ฑ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"ํ์ฝ/์ ๋ฅ ์๋ชจ๋ ์์ธก ๋ฐ ์ ์ ์ฌ๋ณด๊ธ ๊ณํ ์๋ฆฝ ์๋ฎฌ๋ ์ดํฐ๋ฅผ ์์ฑํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์๋ชจ๋ ์์ธก๋ถํฐ ์ฌ๋ณด๊ธ ๊ณํ, ์ ์ฅ ๋ฐฐ์น, ๊ฒฐ์ฌ ๋๊ธฐ์ด, ์ฌํ์ต๊น์ง ํ ๋ฒ์ ์คํํ๋ ํตํฉ ์๋ฎฌ๋ ์ดํฐ๋ฅผ ์์ฑํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์๋ฃ ์์ฑยท์ ํฉ์ฑ ์ ๊ฒ, ์ด์ ๋ชจ๋ธ๊ณผ ์ฌํ์ต ๋ชจ๋ธ ๋น๊ต ํ ๊ต์ฒด ๊ฒฐ์ , DOS ๊ธฐ๋ฐ ์ฌ๋ณด๊ธ ๊ณํ, ์ ์ฅ ๋ฐฐ์น ์ต์ ํ, ๊ฒฐ์ฌ ๋๊ธฐ์ด ๋ฑ๋ก์ ํ ํ๋ฆ์ผ๋ก ์คํ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `docs/amm02_operations_guide.md ๋ฐ pipeline/amm02_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": [
"\"\"\"๋ฐ์ดํฐ ์์ฑยท์ ํฉ์ฑ ์ ๊ฒ, ์๋ชจ๋ ์์ธก ๋ชจ๋ธ ํ์ต๊ณผ ์๋ ์ฌํ์ต(๊ธฐ์กดยท์ ๊ท ๋ชจ๋ธ ๋น๊ต ํ ๊ต์ฒด), DOS ๊ธฐ๋ฐ ์ ์ ์ฌ๋ณด๊ธ ๊ณํ, ๊ฐ์ ์ ์ฅ๊ณ ๋ฐฐ์น ์ต์ ํ, ๊ตฐ์์ฐธ๋ชจ ๊ฒฐ์ฌ ๋๊ธฐ์ด์ ํ ๋ฒ์ ์คํํ๋ ํตํฉ ์๋ฎฌ๋ ์ดํฐ๋ฅผ ์์ฑํ๊ณ , ์์ฒด ์ ๊ฒํยท์ด์ ๊ฐ์ด๋ ๋ชฉ์ฐจยท์ฒด๊ณ ์ฐ๊ณ ๋ฉ์์ง ์์ยท๋ธ๋ฆฌํ ์์ฝ์ ์ถ๋ ฅํ๋ค.\"\"\"\n",
"import json\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, RandomForestRegressor\n",
"from scipy.optimize import linear_sum_assignment\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋ถ๋ยท๊ฐ์ ํ๋ชฉ ์ผ์ผ ์๋ชจ ๊ธฐ๋ก ์์ฑ๊ธฐ (์ค์ ํ์ฝ ์ ์ยท์๋ชจ์จ๊ณผ ๋ฌด๊ด)\n",
"# ๊ฐ์ ๋ถ๋ 8๊ฐ x 90์ผ. ์๋ชจ๋ = ๊ฐ์ CSR ๊ณ์ x ๋ณ๋ ฅ ๊ท๋ชจ x (๊ต์ ๊ฐ๋ยท์ ์ ํญยทํ๋ ฅ ๊ณํ ํจ๊ณผ) x ์ก์\n",
"# ---------------------------------------------------------------------------\n",
"UNITS = [f\"U{i:02d}\" for i in range(1, 9)]\n",
"ITEMS = [\"ammo_A\", \"ammo_B\", \"ammo_C\", \"ammo_D\", \"fuel_jet\", \"fuel_diesel\"]\n",
"AMMO, FUEL = ITEMS[:4], ITEMS[4:]\n",
"PHASES = [\"prep\", \"attack\", \"defense\", \"stability\"]\n",
"CSR = { # ๊ฐ์ ๊ณ์: ๋ณ๋ ฅ 100๋ช
ยท1์ผ ๊ธฐ์ค ์๋ชจ ๋จ์(ํ์ฝ=์์, ์ ๋ฅ=๋๋ผ)\n",
" \"prep\": [1.0, 0.6, 0.3, 0.2, 1.5, 6.0],\n",
" \"attack\": [6.0, 3.5, 2.0, 1.2, 5.0, 12.0],\n",
" \"defense\": [4.5, 3.0, 1.6, 0.8, 3.0, 7.0],\n",
" \"stability\": [0.8, 0.5, 0.3, 0.2, 1.0, 5.0],\n",
"}\n",
"PHASE_RESIST = {\"prep\": 1.0, \"attack\": 3.2, \"defense\": 3.6, \"stability\": 0.8}\n",
"\n",
"\n",
"def make_ops(n_days=90, seed=7, surge=0):\n",
" \"\"\"๊ฐ์ ๋ถ๋๋ณ ์ผ์ผ ์์ ๊ธฐ๋ก๊ณผ ํ๋ชฉ๋ณ ์ค์ ์๋ชจ๋(์ ๋ต)์ ๋ง๋ ๋ค. surge>0์ด๋ฉด ๋ง์ง๋ง surge์ผ์ ๊ณ ๊ฐ๋ ๊ณต๊ฒฉ ๋จ๊ณ๋ก ๋ฐ๊พผ๋ค(์คํธ๋ ์ค ์ํ์ฉ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for uid in UNITS:\n",
" size = int(rng.integers(3, 9)) * 100\n",
" lens = rng.integers(8, 20, 4)\n",
" sched = [\"prep\"] * lens[0] + [\"attack\"] * lens[1] + [\"defense\"] * lens[2] + [\"attack\"] * lens[3]\n",
" sched = (sched + [\"stability\"] * n_days)[:n_days]\n",
" if surge:\n",
" sched[-surge:] = [\"attack\"] * surge\n",
" r = 1.0\n",
" for d, ph in enumerate(sched):\n",
" r = float(np.clip(0.7 * r + 0.3 * (4.8 if surge and d >= n_days - surge else PHASE_RESIST[ph]) + rng.normal(0, 0.35), 0, 5))\n",
" inten = float(np.clip(r / 5 * 0.9 + rng.normal(0, 0.06), 0, 1))\n",
" fire = int(rng.poisson(1 + 8 * inten))\n",
" move = float(rng.gamma(2, {\"prep\": 4, \"attack\": 12, \"defense\": 2, \"stability\": 5}[ph]))\n",
" weather = str(rng.choice([\"clear\", \"rain\", \"snow\"], p=[0.7, 0.2, 0.1]))\n",
" emerg = bool(rng.random() < 0.02)\n",
" rec = {\"unit\": uid, \"day\": d, \"phase\": ph, \"unit_size\": size, \"intensity\": round(inten, 3),\n",
" \"resistance\": round(r, 2), \"fire_missions\": fire, \"move_km\": round(move, 1),\n",
" \"weather\": weather, \"emergency\": emerg}\n",
" base = size / 100\n",
" for i, it in enumerate(ITEMS):\n",
" c = CSR[ph][i] * base\n",
" if it in AMMO:\n",
" c *= (0.5 + 0.6 * inten) * (1 + 0.04 * fire * (1.0 if it in (\"ammo_C\", \"ammo_D\") else 0.4))\n",
" elif it == \"fuel_jet\":\n",
" c *= (0.6 + 0.5 * inten) + 0.03 * fire\n",
" else:\n",
" c = c * (0.8 + 0.2 * (weather != \"clear\")) + 0.05 * move * base\n",
" c *= rng.lognormal(0, 0.12) * (2.5 if emerg else 1.0)\n",
" rec[it] = round(float(c), 1)\n",
" rows.append(rec)\n",
" return pd.DataFrame(rows)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์๋ชจ๋ ์์ธก์ฉ ํ์ ํผ์ฒ: ์ ์ผ๊น์ง์ ์ค์ + ๋น์ผ ์์ ยทํ๋ ฅ ๊ณํ(์ฌ์ ์ ์ ์ ์๋ ๊ฐ๋ง) ----\n",
"FEATS = [\"lag1\", \"lag2\", \"roll3\", \"roll7\", \"csr_base\", \"size100\", \"fire_plan\", \"fire_x_engaged\", \"resist_lag1\",\n",
" \"resist_trend\", \"inten_lag1\", \"move_plan\", \"days_in_phase\", \"engaged\", \"ph_attack\", \"ph_defense\", \"ph_prep\",\n",
" \"item_idx\", \"is_fuel\"]\n",
"DERIVED = [\"lag1\", \"lag2\", \"roll3\", \"roll7\", \"csr_base\", \"fire_x_engaged\", \"resist_lag1\", \"resist_trend\",\n",
" \"inten_lag1\", \"days_in_phase\", \"engaged\", \"ph_attack\", \"ph_defense\", \"ph_prep\"]\n",
"\n",
"\n",
"def build_long(ops):\n",
" \"\"\"๋ถ๋x์ผ์ ๊ธฐ๋ก์ (๋ถ๋, ์ผ์, ํ๋ชฉ) ํ์ผ๋ก ํผ์น๊ณ ํ์ ํผ์ฒ๋ฅผ ๋ง๋ ๋ค.\n",
" ๋ชฉํ๊ฐ y = log(์ค์ ์๋ชจ๋ / CSR ๊ธฐ์ค๋): CSR ๊ธฐ์ค๋์ ๊ณฑํ '๋ณด์ ๊ณ์'๋ฅผ ์์ธกํ๋ค. lagยทroll ํผ์ฒ๋ ๊ฐ์ ๋น์จ(์ค์ /๊ธฐ์ค)์ด๋ค.\"\"\"\n",
" df = ops.sort_values([\"unit\", \"day\"]).reset_index(drop=True)\n",
" g = df.groupby(\"unit\")\n",
" df[\"resist_lag1\"] = g[\"resistance\"].shift(1)\n",
" df[\"resist_trend\"] = df[\"resist_lag1\"] - g[\"resistance\"].shift(3)\n",
" df[\"inten_lag1\"] = g[\"intensity\"].shift(1)\n",
" df[\"days_in_phase\"] = df.groupby((df[\"phase\"] != g[\"phase\"].shift()).cumsum()).cumcount()\n",
" out = []\n",
" for i, it in enumerate(ITEMS):\n",
" csr = np.array([CSR[p][i] for p in df[\"phase\"]])\n",
" ratio = df[it] / (csr * df[\"unit_size\"] / 100)\n",
" prev = ratio.groupby(df[\"unit\"]).shift(1)\n",
" d = pd.DataFrame({\"unit\": df[\"unit\"], \"day\": df[\"day\"], \"phase\": df[\"phase\"], \"item\": it, \"item_idx\": i,\n",
" \"is_fuel\": int(it in FUEL), \"lag1\": prev, \"lag2\": ratio.groupby(df[\"unit\"]).shift(2),\n",
" \"roll3\": prev.groupby(df[\"unit\"]).transform(lambda s: s.rolling(3).mean()),\n",
" \"roll7\": prev.groupby(df[\"unit\"]).transform(lambda s: s.rolling(7, min_periods=3).mean()),\n",
" \"csr_base\": csr, \"size100\": df[\"unit_size\"] / 100,\n",
" \"fire_plan\": df[\"fire_missions\"], \"fire_x_engaged\": df[\"fire_missions\"] * df[\"phase\"].isin([\"attack\", \"defense\"]),\n",
" \"resist_lag1\": df[\"resist_lag1\"], \"resist_trend\": df[\"resist_trend\"], \"inten_lag1\": df[\"inten_lag1\"],\n",
" \"move_plan\": df[\"move_km\"], \"days_in_phase\": df[\"days_in_phase\"],\n",
" \"engaged\": df[\"phase\"].isin([\"attack\", \"defense\"]).astype(int),\n",
" \"ph_attack\": (df[\"phase\"] == \"attack\").astype(int), \"ph_defense\": (df[\"phase\"] == \"defense\").astype(int),\n",
" \"ph_prep\": (df[\"phase\"] == \"prep\").astype(int), \"emergency\": df[\"emergency\"],\n",
" \"y\": np.log(ratio), \"qty\": df[it]})\n",
" out.append(d)\n",
" return pd.concat(out, ignore_index=True).dropna(subset=[\"lag2\", \"roll3\", \"resist_trend\"]).reset_index(drop=True)\n",
"\n",
"\n",
"def to_qty(d, y_pred):\n",
" \"\"\"์์ธก ๋ณด์ ๊ณ์(log)๋ฅผ ์๋ชจ๋์ผ๋ก ๋๋๋ฆฐ๋ค: ๊ธฐ์ค๋ x exp(์์ธก).\"\"\"\n",
" return np.exp(y_pred) * d[\"csr_base\"].values * d[\"size100\"].values\n",
"\n",
"\n",
"def mape(y, p):\n",
" return float(np.mean(np.abs(np.asarray(p) - np.asarray(y)) / np.asarray(y)))\n",
"\n",
"# ---- ์๋ชจ๋ ์์ธก ๋ชจ๋ธ(์์๋ธ) ํ์ตยท์์ธก ----\n",
"def fit_models(tr, seed=0):\n",
" \"\"\"GBR + ๋๋คํฌ๋ ์คํธ ๋ ๋ชจ๋ธ์ ํ์ตํ๋ค(๋ชฉํ๊ฐ์ CSR ๊ธฐ์ค๋ ๋๋น log ๋ณด์ ๊ณ์).\"\"\"\n",
" g = GradientBoostingRegressor(n_estimators=150, max_depth=3, learning_rate=0.08, subsample=0.8, random_state=seed).fit(tr[FEATS], tr[\"y\"])\n",
" r = RandomForestRegressor(n_estimators=120, min_samples_leaf=3, random_state=seed, n_jobs=1).fit(tr[FEATS], tr[\"y\"])\n",
" return g, r\n",
"\n",
"\n",
"def predict_qty(models, d, w=0.5):\n",
" \"\"\"๋ ๋ชจ๋ธ์ log ์์ธก์ ๊ฐ์ค ํ๊ท ํด ์๋ชจ๋์ผ๋ก ๋๋๋ฆฐ๋ค.\"\"\"\n",
" g, r = models\n",
" return to_qty(d, w * g.predict(d[FEATS]) + (1 - w) * r.predict(d[FEATS]))\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ ์ฅ๊ณ (H1~H3) ์ ์ฅ์นธ ๋ฐฐ์น ๋ชจ๋ธ. ์ค์ ํ์ฝ ์ ์ฅ ๊ธฐ์คยท์์ ๊ฑฐ๋ฆฌ์ ๋ฌด๊ดํ ๊ฐ์ ๊ณ์๋ค.\n",
"# ์ ์ฅ์นธ: ์ ์ฅ๊ณ x ์ด(row) x ์นธ(col). ๋์ ๊ฑฐ๋ฆฌ = ๋ฐ์ถ์ฅ(์
๊ตฌ)๊น์ง์ ๊ฐ์ ๊ฑฐ๋ฆฌ(m).\n",
"# ์ ์ฅ๊ตฐ(G1~G3)์ ๊ฐ์ ํผ์ ์ ํ ๊ทธ๋ฃน์ด๋ฉฐ, ์ ์ฅ๊ณ ๋ง๋ค ๋ฐ์ ์ ์๋ ์ ์ฅ๊ตฐ์ด ์ ํด์ ธ ์๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"HOUSES = {\"H1\": {\"groups\": {\"G1\", \"G2\"}, \"rows\": 2, \"cols\": 6, \"dock\": 15.0},\n",
" \"H2\": {\"groups\": {\"G1\"}, \"rows\": 2, \"cols\": 5, \"dock\": 40.0},\n",
" \"H3\": {\"groups\": {\"G2\", \"G3\"}, \"rows\": 2, \"cols\": 6, \"dock\": 70.0}}\n",
"ITEM_GROUP = {\"ammo_A\": \"G1\", \"ammo_B\": \"G1\", \"ammo_C\": \"G2\", \"ammo_D\": \"G3\"}\n",
"SLOT_CAP = 6 # 1์ด ์ ์ฅ์นธ ๊ฐ์ ํ๋ ํธ ์์ฉ๋(2์ด์ +4)\n",
"\n",
"\n",
"def make_slots(seed=3):\n",
" \"\"\"์ ์ฅ์นธ ๋ชฉ๋ก๊ณผ ํ์ฌ ์ ์ (์ด๋ ๋ถ๊ฐ ๊ธฐ์กด ์ฌ๊ณ ) ์ฌ๋ถ๋ฅผ ๋ง๋ ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for h, c in HOUSES.items():\n",
" for r in range(c[\"rows\"]):\n",
" for k in range(c[\"cols\"]):\n",
" rows.append({\"slot\": f\"{h}-{r + 1}{k + 1:02d}\", \"house\": h, \"dist_m\": c[\"dock\"] + 6.0 * k + 4.0 * r,\n",
" \"cap\": SLOT_CAP if r == 0 else SLOT_CAP + 4, \"fixed\": bool(rng.random() < 0.15)})\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"def make_skus(demand, seed=5):\n",
" \"\"\"ํ๋ชฉ๋ณ ์์ธก ์ผ์ผ ๋ฐ์ถ๋(demand: ํ๋ชฉ->์์/์ผ)์ ๋กํธ ๋จ์ SKU๋ก ๋๋๋ค. ๋ฐ์ถ ๋น๋ = ์ผ์ผ ๋ฐ์ถ ํ์(๊ฐ์).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for it, dq in demand.items():\n",
" n_lot = 5\n",
" share = rng.dirichlet(np.ones(n_lot) * 2)\n",
" for j in range(n_lot):\n",
" rows.append({\"sku\": f\"{it}-L{j + 1}\", \"item\": it, \"group\": ITEM_GROUP[it],\n",
" \"pallets\": int(rng.integers(2, SLOT_CAP + 5)), \"freq\": round(float(dq * share[j] / 4), 2)})\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"def feasible(sku, slot):\n",
" return (sku[\"group\"] in HOUSES[slot[\"house\"]][\"groups\"]) and sku[\"pallets\"] <= slot[\"cap\"] and not slot[\"fixed\"]\n",
"\n",
"\n",
"def travel(assign, skus, slots):\n",
" \"\"\"์์
๋์ = ๋ฐ์ถ ๋น๋ x ์๋ณต ๊ฑฐ๋ฆฌ ํฉ(๊ฐ์ m/์ผ).\"\"\"\n",
" d = slots.set_index(\"slot\")[\"dist_m\"]\n",
" return float(sum(2 * skus.loc[i, \"freq\"] * d[s] for i, s in assign.items()))\n",
"\n",
"\n",
"def first_fit(skus, slots):\n",
" \"\"\"ํํ ๋ฐฉ์(๋์ ๋น๊ต ๊ธฐ์ค): ์
๊ณ ์์๋๋ก ๋น์ด ์๋ ์ฒซ ๋ฒ์งธ ์ ํฉ ์นธ์ ๋ฃ๋๋ค.\"\"\"\n",
" used, out = set(), {}\n",
" for i, k in skus.iterrows():\n",
" for _, s in slots.iterrows():\n",
" if s[\"slot\"] not in used and feasible(k, s):\n",
" out[i] = s[\"slot\"]; used.add(s[\"slot\"]); break\n",
" return out\n",
"\n",
"\n",
"def optimize(skus, slots, lam=8):\n",
" \"\"\"ํ ๋น ๋ฌธ์ (์ ์๊ณํ ํน์ํ): ๋น์ฉ = ๋ฐ์ถ ๋น๋ x ์๋ณต๊ฑฐ๋ฆฌ + lam x ๋น ๊ณต๊ฐ(๊ณต๊ฐ๋ญ๋น). ๋ถ์ ํฉ ์นธ์ ํฐ ๋น์ฉ.\"\"\"\n",
" cost = np.full((len(skus), len(slots)), 1e7)\n",
" for i, k in skus.iterrows():\n",
" for j, s in slots.iterrows():\n",
" if feasible(k, s):\n",
" cost[i, j] = 2 * k[\"freq\"] * s[\"dist_m\"] + lam * (s[\"cap\"] - k[\"pallets\"])\n",
" ri, cj = linear_sum_assignment(cost)\n",
" return {int(i): slots.loc[j, \"slot\"] for i, j in zip(ri, cj) if cost[i, j] < 1e7}, cost\n",
"\n",
"\n",
"def loc_score(assign, skus, slots):\n",
" \"\"\"์์น์ ์(0~100): ๋ฐ์ถ ๋น๋ ์์ SKU์ผ์๋ก ๋ฐ์ถ์ฅ ๊ฐ๊น์ด ์นธ์ ์๋ ์ ๋(๊ฐ์ค ํ๊ท ).\"\"\"\n",
" d = slots.set_index(\"slot\")[\"dist_m\"]; dmin, dmax = d.min(), d.max()\n",
" w = skus[\"freq\"] / skus[\"freq\"].sum()\n",
" return float(100 * sum(w[i] * (1 - (d[s] - dmin) / (dmax - dmin)) for i, s in assign.items()))\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"DOS_ALERT, DOS_TARGET, PLAN_DAY = 2.0, 3.0, 85\n",
"t_all = time.perf_counter()\n",
"checks = []\n",
"\n",
"# 1) ๋ฐ์ดํฐ ์์งยท์ ํฉ์ฑ ์ ๊ฒ\n",
"ops = make_ops(90, seed=7)\n",
"ok_schema = ops[\"intensity\"].between(0, 1).all() and (ops[ITEMS] >= 0).all().all() and not ops.duplicated([\"unit\", \"day\"]).any()\n",
"checks.append((\"์์ฒ ์๋ฃ ์ ํฉ์ฑ(๋ฒ์ยท์์ยท์ค๋ณต)\", ok_schema))\n",
"data = build_long(ops)\n",
"print(f\"[1] ๊ฐ์ ์์ ๊ธฐ๋ก {len(ops)}๊ฑด -> ํ์ต ํ {len(data)}๊ฐ, ์ ํฉ์ฑ {'ํต๊ณผ' if ok_schema else '์คํจ'}\")\n",
"\n",
"# 2) ์ด์ ๋ชจ๋ธ(v1: 60์ผ ์ด์ ํ์ต) -> 3) ์ฌํ์ต ํ์ดํ๋ผ์ธ(v2: 80์ผ ์ด์ ํ์ต), ์ต๊ทผ 10์ผ๋ก ๋น๊ต ํ ๊ต์ฒด ๊ฒฐ์ \n",
"val = data[data[\"day\"] >= 80]\n",
"v1 = fit_models(data[data[\"day\"] < 60])\n",
"v2 = fit_models(data[data[\"day\"] < 80])\n",
"e1, e2 = mape(val[\"qty\"], predict_qty(v1, val)), mape(val[\"qty\"], predict_qty(v2, val))\n",
"base = mape(val[\"qty\"], val[\"csr_base\"] * val[\"size100\"])\n",
"champ = v2 if e2 <= e1 else v1\n",
"print(f\"[2] ๊ฒ์ฆ(80~89์ผ) MAPE: CSR Baseline {base:.1%}, ์ด์ ๋ชจ๋ธ v1 {e1:.1%}, ์ฌํ์ต v2 {e2:.1%} -> {'v2๋ก ๊ต์ฒด' if e2 <= e1 else 'v1 ์ ์ง'}\")\n",
"checks += [(\"์๋ ์ฌํ์ต ํ์ดํ๋ผ์ธ ์คํยท๋น๊ต\", True), (\"์ด์ ๋ชจ๋ธ MAPE 25% ์ดํ\", min(e1, e2) <= 0.25)]\n",
"\n",
"# 4) ์ ์ ์ฌ๋ณด๊ธ ๊ณํ(85์ผ ๊ธฐ์ค)\n",
"rng = np.random.default_rng(11)\n",
"day = data[data[\"day\"] == PLAN_DAY].copy()\n",
"day[\"pred\"] = predict_qty(champ, day)\n",
"day[\"on_hand\"] = np.round(day[\"csr_base\"] * day[\"size100\"] * rng.uniform(1.0, 5.0, len(day)))\n",
"day[\"dos\"] = day[\"on_hand\"] / day[\"pred\"]\n",
"day[\"need\"] = np.maximum(0, np.ceil(DOS_TARGET * day[\"pred\"] - day[\"on_hand\"]))\n",
"urg = day[day[\"dos\"] < DOS_ALERT].sort_values(\"dos\")\n",
"print(f\"[3] {PLAN_DAY}์ผ ์ฌ๋ณด๊ธ ๊ณํ: ๊ธด๊ธ {len(urg)}๊ฑด, ์ฌ๋ณด๊ธ ์ด๋ ํ์ฝ {day.loc[day['is_fuel'] == 0, 'need'].sum():.0f}์์ / ์ ๋ฅ {day.loc[day['is_fuel'] == 1, 'need'].sum():.0f}๋๋ผ(๊ฐ์)\")\n",
"for _, r in urg.head(4).iterrows():\n",
" print(f\" {r['unit']} {r['item']}: DOS {r['dos']:.1f}์ผ, ์ฌ๋ณด๊ธ {r['need']:.0f}\")\n",
"\n",
"# 5) ์ ์ฅ ๋ฐฐ์น ์ต์ ํ(์์ธก ํ์ฝ ๋ฐ์ถ ์์ ๊ธฐ์ค)\n",
"demand = day[day[\"is_fuel\"] == 0].groupby(\"item\")[\"pred\"].sum().round(1).to_dict()\n",
"skus, slots = make_skus(demand), make_slots()\n",
"cur, (opt, _) = first_fit(skus, slots), optimize(skus, slots)\n",
"tc, to = travel(cur, skus, slots), travel(opt, skus, slots)\n",
"print(f\"[4] ์ ์ฅ ๋ฐฐ์น: ์์
๋์ {tc:.0f} -> {to:.0f} m/์ผ ({1 - to / tc:.1%} ๋จ์ถ), ์์น์ ์ {loc_score(cur, skus, slots):.1f} -> {loc_score(opt, skus, slots):.1f}\")\n",
"checks.append((\"์ ์ฅ ๋ฐฐ์น ์์
๋์ 15% ์ด์ ๋จ์ถ\", 1 - to / tc >= 0.15))\n",
"\n",
"# 6) ๊ฒฐ์ฌ ๋๊ธฐ์ด(์๋ ํ์ ๊ธ์ง)\n",
"queue = [{\"id\": f\"Q{k + 1:02d}\", \"type\": \"๊ธด๊ธ ์ฌ๋ณด๊ธ\", \"unit\": r[\"unit\"], \"item\": r[\"item\"], \"qty\": int(r[\"need\"]), \"status\": \"๊ฒฐ์ฌ๋๊ธฐ\"} for k, (_, r) in enumerate(urg.iterrows())]\n",
"queue += [{\"id\": f\"Q{len(queue) + 1:02d}\", \"type\": \"์ ์ฅ ์ฌ๋ฐฐ์น\", \"moves\": int(sum(cur.get(i) != opt[i] for i in opt)), \"status\": \"๊ฒฐ์ฌ๋๊ธฐ\"}]\n",
"auto = [q for q in queue if q[\"status\"] != \"๊ฒฐ์ฌ๋๊ธฐ\"]\n",
"print(f\"[5] ๊ตฐ์์ฐธ๋ชจ ๊ฒฐ์ฌ ๋๊ธฐ {len(queue)}๊ฑด, ์๋ ํ์ {len(auto)}๊ฑด\")\n",
"checks.append((\"์๋ ํ์ 0๊ฑด(HITL)\", not auto))\n",
"\n",
"# 7) ์ด์ ๊ฐ์ด๋ ๋ชฉ์ฐจยท์ฐ๊ณ ์ธํฐํ์ด์ค\n",
"GUIDE = {\"1. ๋ชฉ์ ยท๋ฒ์\": \"๊ฐ์ ๋ถ๋ ํ์ฝยท์ ๋ฅ ์๋ชจ ์์ธก๊ณผ ์ ์ฅ ๋ฐฐ์น ์ง์\",\n",
" \"2. ๋ฐ์ดํฐ ๊ท๊ฒฉ\": f\"์์ ๊ธฐ๋ก {ops.shape[1]}๊ฐ ํ๋, ํ๋ชฉ {len(ITEMS)}์ข
\",\n",
" \"3. ์ ์ฒ๋ฆฌ ๊ท์น\": \"๋ฒ์ยท์์ยท์ค๋ณต ์ ๊ฒ, ๊ธด๊ธ ๋ถ์ถ ๋ณด์กด\",\n",
" \"4. ์์ธก ๋ชจ๋ธ\": f\"GBR+RF ์์๋ธ, ๊ฒ์ฆ MAPE {min(e1, e2):.1%}\",\n",
" \"5. DOS ์ฌ๋ณด๊ธ ์์ง\": f\"๊ฒฝ๋ณด {DOS_ALERT:.0f}์ผ ๋ฏธ๋ง, ๋ชฉํ {DOS_TARGET:.0f}์ผ\",\n",
" \"6. ์ ์ฅ ๋ฐฐ์น\": f\"ํ ๋น ์ต์ ํ, ๋์ {1 - to / tc:.0%} ๋จ์ถ\",\n",
" \"7. ๊ฒฐ์ฌ์ ยท๊ฐ์ฌ\": \"์๋ ํ์ ๊ธ์ง, ์ฌ์ ๊ธฐ๋ก\",\n",
" \"8. ์ฌํ์ต\": \"์ต๊ทผ ์๋ฃ๋ก ์ฌํ์ต ํ ๊ฒ์ฆ MAPE๊ฐ ๋ฎ์ ๋๋ง ๊ต์ฒด\",\n",
" \"9. ์์ธ ๋์\": \"๋ณด๊ณ ๋๋ฝ ์ CSR ๊ธฐ์ค๊ฐ ๋์
, ๊ธด๊ธ ์์ฒญ์ ์ฆ์ ๊ฒฝ๋ณด\",\n",
" \"10. ์ฐ๊ณ ์ธํฐํ์ด์ค\": \"RESUPPLY_REQ ๊ฐ์ ๋ฉ์์ง ๊ท๊ฒฉ\"}\n",
"done = np.mean([bool(v) for v in GUIDE.values()])\n",
"print(\"[6] ์ด์ ๊ฐ์ด๋ ๋ชฉ์ฐจ:\", \" / \".join(GUIDE))\n",
"checks.append((\"์ด์ ๊ฐ์ด๋ 10๊ฐ ์ฅ ์๋น\", done == 1))\n",
"msg = {\"msg_type\": \"RESUPPLY_REQ\", \"version\": \"1.0\", \"unit_id\": queue[0].get(\"unit\", \"U00\") if queue else \"U00\",\n",
" \"item_code\": queue[0].get(\"item\", \"-\") if queue else \"-\", \"qty\": queue[0].get(\"qty\", 0) if queue else 0,\n",
" \"dos_days\": round(float(urg[\"dos\"].iloc[0]), 2) if len(urg) else None, \"model_ver\": \"v2\" if champ is v2 else \"v1\",\n",
" \"approval\": \"PENDING\", \"source\": \"๊ต์ก์ฉ๊ฐ์์๋ฃ\"}\n",
"print(\"[7] ์ฒด๊ณ ์ฐ๊ณ ๋ฉ์์ง ์์(๊ฐ์ ๊ท๊ฒฉ):\", json.dumps(msg, ensure_ascii=False))\n",
"elapsed = time.perf_counter() - t_all\n",
"checks.append((\"ํตํฉ ์คํ 60์ด ์ด๋ด\", elapsed < 60))\n",
"\n",
"print(\"\\n[์์ฒด ์ ๊ฒํ]\")\n",
"for name, ok in checks:\n",
" print(f\" [{'PASS' if ok else 'FAIL'}] {name}\")\n",
"print(f\"์ ๊ฒ {sum(ok for _, ok in checks)}/{len(checks)} ํต๊ณผ, ์ด์ ๊ฐ์ด๋ ์๋น์จ {done:.0%}, ์ ์ฒด ์คํ {elapsed:.1f}s\")\n",
"\n",
"print(\"\\n[๋ธ๋ฆฌํ ์์ฝ]\")\n",
"print(f\"- ์๋ชจ๋ ์์ธก MAPE {min(e1, e2):.1%}๋ก CSR ๊ณ ์ ๊ณ์({base:.1%}) ๋๋น ์ค์ฐจ๋ฅผ ์ค์๊ณ , ์ฌํ์ต ๋ชจ๋ธ์ ๋น๊ต ํ ๊ต์ฒดํ๋ค.\")\n",
"print(f\"- {PLAN_DAY}์ผ ๊ธฐ์ค ๊ธด๊ธ ์ฌ๋ณด๊ธ {len(urg)}๊ฑด์ DOS ์์ผ๋ก ์ ๋ ฌํด ๊ฒฐ์ฌ ๋๊ธฐ์ด์ ์ฌ๋ ธ๋ค.\")\n",
"print(f\"- ๊ฐ์ ์ ์ฅ๊ณ ๋ฐฐ์น ์ต์ ํ๋ก ๋ฐ์ถ ์์
๋์ ์ {1 - to / tc:.0%} ์ค์ด๋ ์์ ์ ์ํ๋ค(์ฌ๋ฐฐ์น๋ ๊ฒฐ์ฌ ํ ์ํ).\")\n",
"print(\"- ๋ชจ๋ ๋ถ์ถยท๋์ฒด ํ์ข
ยท์ฌ๋ฐฐ์น๋ ๊ตฐ์์ฐธ๋ชจ ๊ฒฐ์ฌ ํ ์งํํ๋ฉฐ, ์์น๋ ๊ต์ก์ฉ ๊ฐ์ ์๋ฃ ๊ธฐ์ค์ด๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(12, 4))\n",
"ax[0].bar([\"CSR baseline\", \"model v1\", \"model v2\"], [base, e1, e2], color=[\"gray\", \"tab:blue\", \"tab:green\"])\n",
"ax[0].axhline(0.25, c=\"r\", ls=\"--\"); ax[0].set_title(\"Validation MAPE (day 80-89)\")\n",
"ax[1].barh(day[\"unit\"] + \"-\" + day[\"item\"], day[\"dos\"].clip(upper=6), color=np.where(day[\"dos\"] < DOS_ALERT, \"tab:red\", \"tab:gray\"))\n",
"ax[1].axvline(DOS_ALERT, c=\"r\", ls=\"--\"); ax[1].set_title(f\"Days of supply on day {PLAN_DAY}\"); ax[1].tick_params(axis=\"y\", labelsize=5)\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
10 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[1] ๊ฐ์ ์์ ๊ธฐ๋ก 720๊ฑด -> ํ์ต ํ 4176๊ฐ, ์ ํฉ์ฑ ํต๊ณผ\n",
"[2] ๊ฒ์ฆ(80~89์ผ) MAPE: CSR Baseline 46.5%, ์ด์ ๋ชจ๋ธ v1 13.3%, ์ฌํ์ต v2 14.7% -> v1 ์ ์ง\n",
"[3] 85์ผ ์ฌ๋ณด๊ธ ๊ณํ: ๊ธด๊ธ 6๊ฑด, ์ฌ๋ณด๊ธ ์ด๋ ํ์ฝ 23์์ / ์ ๋ฅ 221๋๋ผ(๊ฐ์)\n",
" U08 fuel_diesel: DOS 1.1์ผ, ์ฌ๋ณด๊ธ 61\n",
" U01 fuel_diesel: DOS 1.2์ผ, ์ฌ๋ณด๊ธ 72\n",
" U04 ammo_A: DOS 1.6์ผ, ์ฌ๋ณด๊ธ 4\n",
" U06 ammo_D: DOS 1.6์ผ, ์ฌ๋ณด๊ธ 2\n",
"[4] ์ ์ฅ ๋ฐฐ์น: ์์
๋์ 1588 -> 1260 m/์ผ (20.6% ๋จ์ถ), ์์น์ ์ 60.4 -> 72.0\n",
"[5] ๊ตฐ์์ฐธ๋ชจ ๊ฒฐ์ฌ ๋๊ธฐ 7๊ฑด, ์๋ ํ์ 0๊ฑด\n",
"[6] ์ด์ ๊ฐ์ด๋ ๋ชฉ์ฐจ: 1. ๋ชฉ์ ยท๋ฒ์ / 2. ๋ฐ์ดํฐ ๊ท๊ฒฉ / 3. ์ ์ฒ๋ฆฌ ๊ท์น / 4. ์์ธก ๋ชจ๋ธ / 5. DOS ์ฌ๋ณด๊ธ ์์ง / 6. ์ ์ฅ ๋ฐฐ์น / 7. ๊ฒฐ์ฌ์ ยท๊ฐ์ฌ / 8. ์ฌํ์ต / 9. ์์ธ ๋์ / 10. ์ฐ๊ณ ์ธํฐํ์ด์ค\n",
"[7] ์ฒด๊ณ ์ฐ๊ณ ๋ฉ์์ง ์์(๊ฐ์ ๊ท๊ฒฉ): {\"msg_type\": \"RESUPPLY_REQ\", \"version\": \"1.0\", \"unit_id\": \"U08\", \"item_code\": \"fuel_diesel\", \"qty\": 61, \"dos_days\": 1.14, \"model_ver\": \"v1\", \"approval\": \"PENDING\", \"source\": \"๊ต์ก์ฉ๊ฐ์์๋ฃ\"}\n",
"\n",
"[์์ฒด ์ ๊ฒํ]\n",
" [PASS] ์์ฒ ์๋ฃ ์ ํฉ์ฑ(๋ฒ์ยท์์ยท์ค๋ณต)\n",
" [PASS] ์๋ ์ฌํ์ต ํ์ดํ๋ผ์ธ ์คํยท๋น๊ต\n",
" [PASS] ์ด์ ๋ชจ๋ธ MAPE 25% ์ดํ\n",
" [PASS] ์ ์ฅ ๋ฐฐ์น ์์
๋์ 15% ์ด์ ๋จ์ถ\n",
" [PASS] ์๋ ํ์ 0๊ฑด(HITL)\n",
" [PASS] ์ด์ ๊ฐ์ด๋ 10๊ฐ ์ฅ ์๋น\n",
" [PASS] ํตํฉ ์คํ 60์ด ์ด๋ด\n",
"์ ๊ฒ 7/7 ํต๊ณผ, ์ด์ ๊ฐ์ด๋ ์๋น์จ 100%, ์ ์ฒด ์คํ 7.1s\n",
"\n",
"[๋ธ๋ฆฌํ ์์ฝ]\n",
"- ์๋ชจ๋ ์์ธก MAPE 13.3%๋ก CSR ๊ณ ์ ๊ณ์(46.5%) ๋๋น ์ค์ฐจ๋ฅผ ์ค์๊ณ , ์ฌํ์ต ๋ชจ๋ธ์ ๋น๊ต ํ ๊ต์ฒดํ๋ค.\n",
"- 85์ผ ๊ธฐ์ค ๊ธด๊ธ ์ฌ๋ณด๊ธ 6๊ฑด์ DOS ์์ผ๋ก ์ ๋ ฌํด ๊ฒฐ์ฌ ๋๊ธฐ์ด์ ์ฌ๋ ธ๋ค.\n",
"- ๊ฐ์ ์ ์ฅ๊ณ ๋ฐฐ์น ์ต์ ํ๋ก ๋ฐ์ถ ์์
๋์ ์ 21% ์ค์ด๋ ์์ ์ ์ํ๋ค(์ฌ๋ฐฐ์น๋ ๊ฒฐ์ฌ ํ ์ํ).\n",
"- ๋ชจ๋ ๋ถ์ถยท๋์ฒด ํ์ข
ยท์ฌ๋ฐฐ์น๋ ๊ตฐ์์ฐธ๋ชจ ๊ฒฐ์ฌ ํ ์งํํ๋ฉฐ, ์์น๋ ๊ต์ก์ฉ ๊ฐ์ ์๋ฃ ๊ธฐ์ค์ด๋ค.\n",
"```\n",
" \n"
]
}
],
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}