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