{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# ๐ฏ [MNT02] AI ๊ธฐ๋ฐ ์ง๋ฅํ ๋ถํ ๋จ์ข
์ํ ๋ถ์ยท๋์์ฒด๊ณ\n",
"### Google Colab ์ค์ต ํ๊ฒฝ (์ฅ๋น์ ๋นยท์๋ฆฌ๋ถ์ / Level 3)\n",
"\n",
"[](https://colab.research.google.com/github/edunlab/milicontent2/blob/main/uploads/MNT02/MNT02_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",
"์ฅ๋น ์๋ฆฌ๋ถ์์ ์์ฐ ์ค๋จ(๋จ์ข
)์ด๋ ๋จ์ข
์๊ณ ๊ฐ ๋์ค๋ฉด ์กฐ๋ฌ ๋ฆฌ๋ํ์์ด ๊ธธ์ด์ง๊ณ ๊ณต๊ธ์
์ฒด๊ฐ ์ค์ด, ์์๋ฅผ ๋ฏธ๋ฆฌ ์์ง ๋ชปํ๋ฉด ๊ฒฐํ์ผ๋ก ์ฅ๋น๊ฐ ์ ๋น ๋๊ธฐ ์ํ์ ๋จธ๋ฌผ๊ฒ ๋๋ค. ๋ฐ๋ผ์ ๋จ์ข
์ํ์ ๋์ํ๋ ค๋ฉด ๋จผ์ ํ๋ชฉ๋ณ ์์๋ฅผ ์ ํํ ์์ธกํ๊ณ , ๊ทธ ์์ธก์ ๋ฆฌ๋ํ์ยท๊ณต๊ธ์
์ฒด ์ยท๋์ฒดํ ์ ๋ฌด๋ฅผ ๊ฒฐํฉํด ๊ฒฐํ ์ํ์ ์ธก์ ํด์ผ ํ๋ค. ๋ณธ ํ๋ก์ ํธ๋ ๊ต์ก์ฉ ๊ฐ์ ์ผ์ ์ ๋น์ฐฝ์ ์๋ฆฌ๋ถ์ ์ฒญ๊ตฌ/์ถ๊ณ ์ด๋ ฅ๊ณผ ์ฅ๋น ๊ฐ๋๋ฅ ์๋ฃ๋ฅผ ์ฝ๋๋ก ๋ง๋ค์ด, ๋ฐ์ดํฐ ์คํค๋ง ์ ์, NSN ์ ์ ์ ๊ณ์ ์ฑ ๋ถ๋ฆฌ, ์ด๋ํ๊ท ยท์ง์ํํ Baseline, ๋จธ์ ๋ฌ๋ ์์ ์์ธก, ์์ ์ฌ๊ณ ์ ๊ฒฐํ(๋จ์ข
) ์ํ ์ง์, ๊ธด๊ธ ์กฐ๋ฌ ๋์๋ณด๋์ ์ค๋ช
๊ฐ๋ฅ์ฑ, ์ ๋น์ฅ๊ต ๋ฐ์ฃผ ์น์ธ ์ํฌํ๋ก์ฐ, ์์ 300% ํญ์ฆ ๋ถํ ์ํ, AAR ๊ธฐ๋ฐ ์์ ์ฌ๊ณ ์ฌ๋ณด์ ์ ๋จ๊ณ๋ณ๋ก ๊ตฌํํ๋ค. ์ต์ข
๋ชฉํ๋ ์์ ์์ธก๊ณผ ๋จ์ข
์ํ ๋ถ์์ ํ๋๋ก ๋ฌถ์ด ๋จ์ข
์๊ณ ํ๋ชฉ์ ์ต์ข
๊ตฌ๋งคยท๋์ฒดํ ์ ํ๊ณผ ์ผ๋ฐ ๋ฐ์ฃผ๋ฅผ ๊ทผ๊ฑฐ์ ํจ๊ป ์ ์ํ๊ณ , ๋ฐ์ฃผ๋์ ์ ๋น์ฅ๊ต๊ฐ ํ์ธํด ํ์ ํ๋ ํตํฉ ํจํค์ง๋ฅผ ์์ฑํ๋ ๊ฒ์ด๋ค.\n",
"\n",
"> **โ ๏ธ [๊ต์ก์ฉ ๊ฐ์ ๋ฐ์ดํฐ์
์์น]**\n",
"> - ๋ณธ ๋
ธํธ๋ถ์ ๋ชจ๋ ๋ฐ์ดํฐ(์ ์, ๋ก๊ทธ, ์๋๋ฆฌ์ค ๋ฑ)๋ ๋ณด์ ๊ท์ ์ ์ค์ํ์ฌ **์ฝ๋ ๋ด ๋์ ๊ธฐ๋ฐ ๊ฐ์ ๋ฐ์ดํฐ๋ก ์์ฑ**๋ฉ๋๋ค.\n",
"> - ์ค์ ๋ถ๋ ์์น, ์์ ์ธ๊ฐ, ์ค์ ์ ์ฅ ์ ๋ณด, ๊ตฐ์ฌ ๊ธฐ๋ฐ ๋ฐ์ดํฐ๋ ์ผ์ฒด ํฌํจ๋์ง ์์ต๋๋ค.\n",
"\n",
"---\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# ============================================================================\n",
"# [Google Colab ํ๊ฒฝ ์ค์ ] ํ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค์น ๋ฐ ์ํฌํธ\n",
"# ============================================================================\n",
"!pip install -q numpy pandas matplotlib scipy scikit-learn\n",
"\n",
"import os\n",
"import sys\n",
"import time\n",
"import math\n",
"import json\n",
"import re\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"import warnings\n",
"warnings.filterwarnings('ignore')\n",
"\n",
"# ๊ทธ๋ํ ์๋ฌธ ํฐํธ ๊ธฐ๋ณธ ์ค์ (ํ๊ฒฝ ๋
๋ฆฝ์ฑ ๋ณด์ฅ)\n",
"plt.rcParams['font.sans-serif'] = 'DejaVu Sans'\n",
"plt.rcParams['axes.unicode_minus'] = False\n",
"\n",
"print(f\"์คํ ํ๊ฒฝ ์ค๋น ์๋ฃ (Python {sys.version.split()[0]})\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
01. ์ผ์ ์ ๋น์ฐฝ ์๋ฆฌ๋ถ์ ์ฒญ๊ตฌ/์ถ๊ณ ์ด๋ ฅ ๋ฐ ์ฅ๋น ๊ฐ๋๋ฅ ๋ฐ์ดํฐ์
์คํค๋ง ์ ์\n",
"**์์์๊ฐ**: ์ฝ 1.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ผ์ ์ ๋น์ฐฝ ์๋ฆฌ๋ถ์ ์ฒญ๊ตฌ/์ถ๊ณ ์ด๋ ฅ ๋ฐ ์ฅ๋น ๊ฐ๋๋ฅ ๋ฐ์ดํฐ ์คํค๋ง๋ฅผ ์ ์ํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ผ์ ์ ๋น์ฐฝ ์๋ฆฌ๋ถ์ ์ฒญ๊ตฌ/์ถ๊ณ ์ด๋ ฅ, ํ๋ชฉยท๊ณต๊ธ๋ง(๋จ์ข
์ํ ํฌํจ), ์ฅ๋น ๊ฐ๋๋ฅ ์๋ฃ์ ์คํค๋ง์ ์
๋ฌด ํ๋ก์ธ์ค ๋งต, ๋ฐ์ดํฐ ๋์
๋๋ฆฌ๋ฅผ ์ ์ํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ ๋น ์์ ๋ฐ์๋ถํฐ ์ฒญ๊ตฌ, ์ฌ๊ณ ํ์ธยท๋ถ์ถ, ์กฐ๋ฌ ์์ฒญ, ๋จ์ข
์ํ ๊ฒํ , ๊ฐ๋๋ฅ ์ง๊ณ๊น์ง ์
๋ฌด ํ๋ฆ์ ๋จ๊ณ๋ณ๋ก ์ ๋ฆฌํ๊ณ ๊ท๊ฒฉํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `docs/mnt02_logistics_domain_spec.md ๋ฐ ๋ฐ์ดํฐ ๋์
๋๋ฆฌ` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ตฐ์ ์ค๋ฌด ์
๋ฌด ํ๋ก์ธ์ค์์ ์ผ์น์ฑ ๋ฐ ๋ถ์ ๋ชฉํ์ ๋ช
ํ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์
๋ฌด ํ๋ก์ธ์ค ๋งต ์์ฑ ์๋ฃ, ํ์ ํผ์ฒ(Feature) ๋ชฉ๋ก 20๊ฐ ์ด์ ์ ์\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ๋๋ฉ์ธ ๋ถ์์ ๋ฐ ๋ฐ์ดํฐ ์๊ตฌ๋ช
์ธ์ ์น์ธ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ๊ตฐ์ ๋ด๋น ์ค๋ฌด์ ์ธํฐ๋ทฐ ๋ฐ ๊ธฐ์กด ์ ์ฐ์ฒด๊ณ ๋ฐ์ดํฐ ๊ตฌ์กฐ ๋์กฐ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์ผ์ ์ ๋น์ฐฝ ์๋ฆฌ๋ถ์ ์ฒญ๊ตฌ/์ถ๊ณ ์ด๋ ฅ๊ณผ ์ฅ๋น ๊ฐ๋๋ฅ ๋ฐ์ดํฐ์ ์
๋ฌด ํ๋ก์ธ์ค ๋งต, ๋ฐ์ดํฐ ์คํค๋ง(ํผ์ฒ 20๊ฐ ์ด์)์ ๋ฐ์ดํฐ ๋์
๋๋ฆฌ๋ฅผ ์ ์ํ๊ณ ๊ฐ์ ์ฒญ๊ตฌ ๊ธฐ๋ก์ ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import json\n",
"import re\n",
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"# 1) ์
๋ฌด ํ๋ก์ธ์ค ๋งต: ๋จ๊ณ๋ณ๋ก ์ด๋ค ๋ฐ์ดํฐ๊ฐ ์๊ธฐ๋์ง ์ฐ๊ฒฐํ๋ค\n",
"PROCESS = [(\"1. ์ ๋น ์์ ๋ฐ์\", \"์ฅ๋น ๊ณ ์ฅยท๊ณํ์ ๋น\", \"equip_id, equip_type, maint_type\"),\n",
" (\"2. ์๋ฆฌ๋ถ์ ์ฒญ๊ตฌ\", \"์ ๋น๋ฐ -> ์ ๋น์ฐฝ\", \"req_id, req_date, nsn, qty_req, priority\"),\n",
" (\"3. ์ฌ๊ณ ํ์ธยท๋ถ์ถ\", \"์ ๋น์ฐฝ ์ฌ๊ณ ๋ด๋น\", \"qty_issued, emergency_flag\"),\n",
" (\"4. ์กฐ๋ฌ ์์ฒญ\", \"์ฌ๊ณ ๋ถ์กฑ ์ ์กฐ๋ฌ๋ถ์\", \"lead_time_m, supplier_count, unit_price\"),\n",
" (\"5. ๋จ์ข
์ํ ๊ฒํ \", \"ํ๋ชฉ ๊ด๋ฆฌ ๋ด๋น\", \"prod_status, alt_available\"),\n",
" (\"6. ์ ๋น ์๋ฃยท๊ฐ๋๋ฅ ์ง๊ณ\", \"์ ๋จ์ ์ง๊ณ\", \"op_hours, availability, downtime_h\")]\n",
"print(\"๋ถ์ ๋ชฉํ: ์๋ณ ํ๋ชฉ ์์๋ฅผ ์์ธกํด ๊ฒฐํยท๊ณผ์์ฌ๊ณ ๋ฅผ ์ค์ด๊ณ , ๋จ์ข
์๊ณ ยท๋จ์ข
ํ๋ชฉ์ ์ต์ข
๊ตฌ๋งค ์๋ ํ๋จ์ ์ง์ํ๋ค.\")\n",
"print(\"[์
๋ฌด ํ๋ก์ธ์ค ๋งต] ์ ๋น ์์ -> ์ฒญ๊ตฌ -> ๋ถ์ถ -> ์กฐ๋ฌ -> ๋จ์ข
๊ฒํ -> ๊ฐ๋๋ฅ \")\n",
"for step, who, fields in PROCESS:\n",
" print(f\" {step:18s} | {who:18s} | {fields}\")\n",
"\n",
"# 2) ์คํค๋ง(๋ฐ์ดํฐ ๋์
๋๋ฆฌ): ๊ทธ๋ฃน, ์๋ฃํ, ๊ท์น, ์ค๋ช
\n",
"NSN_RE = r\"^\\d{4}-\\d{2}-\\d{3}-\\d{4}$\" # NSN = FSC(4) + ๊ตญ๊ฐ์ฝ๋(2) + ์ผ๋ จ๋ฒํธ(7), NIIN = ๋ค 9์๋ฆฌ\n",
"SCHEMA = {\n",
" \"์ฒญ๊ตฌ/๋ถ์ถ\": {\"req_id\": (str, {\"pattern\": r\"^R\\d{6}$\"}, \"์ฒญ๊ตฌ ๋ฒํธ\"),\n",
" \"req_date\": (str, {\"pattern\": r\"^\\d{4}-\\d{2}-\\d{2}$\"}, \"์ฒญ๊ตฌ ์ผ์\"),\n",
" \"unit_code\": (str, {\"pattern\": r\"^U-\\d{2}$\"}, \"์ฒญ๊ตฌ ๋ถ๋(๊ฐ์ ์ฝ๋)\"),\n",
" \"equip_id\": (str, {\"pattern\": r\"^[ABC]-\\d{3}$\"}, \"์ฅ๋น ๋ฒํธ(๊ฐ์)\"),\n",
" \"equip_type\": (str, {\"enum\": [\"A\", \"B\", \"C\"]}, \"์ฅ๋น ์ ํ\"),\n",
" \"maint_type\": (str, {\"enum\": [\"๊ณํ\", \"๋น๊ณํ\"]}, \"์ ๋น ๊ตฌ๋ถ\"),\n",
" \"nsn\": (str, {\"pattern\": NSN_RE}, \"๊ตญ๊ฐ์ฌ๊ณ ๋ฒํธ(๊ฐ์)\"),\n",
" \"fsc\": (str, {\"pattern\": r\"^\\d{4}$\"}, \"๊ตฐ์ํ ๋ถ๋ฅ(NSN ์ 4์๋ฆฌ)\"),\n",
" \"niin\": (str, {\"pattern\": r\"^\\d{9}$\"}, \"ํ๋ชฉ ์๋ณ๋ฒํธ(NSN ๋ค 9์๋ฆฌ)\"),\n",
" \"qty_req\": (int, {\"min\": 1, \"max\": 500}, \"์ฒญ๊ตฌ ์๋\"),\n",
" \"qty_issued\": (int, {\"min\": 0, \"max\": 500}, \"๋ถ์ถ ์๋\"),\n",
" \"unit_of_issue\": (str, {\"enum\": [\"EA\", \"SE\", \"BX\"]}, \"๋ถ์ถ ๋จ์\"),\n",
" \"priority\": (str, {\"enum\": [\"๊ธด๊ธ\", \"์ผ๋ฐ\"]}, \"์ฒญ๊ตฌ ์ฐ์ ์์\"),\n",
" \"emergency_flag\": (bool, {}, \"๊ธด๊ธ ๋ถ์ถ ์ฌ๋ถ(์ด์์น๋ก ์ง์ฐ์ง ์์)\")},\n",
" \"ํ๋ชฉ/๊ณต๊ธ๋ง\": {\"unit_price\": (int, {\"min\": 0}, \"๋จ๊ฐ(์, ๊ฐ์)\"),\n",
" \"lead_time_m\": (int, {\"min\": 0, \"max\": 36}, \"์กฐ๋ฌ ๋ฆฌ๋ํ์(์)\"),\n",
" \"supplier_count\": (int, {\"min\": 0, \"max\": 20}, \"๊ณต๊ธ ๊ฐ๋ฅ ์
์ฒด ์\"),\n",
" \"prod_status\": (str, {\"enum\": [\"์์ฐ์ค\", \"๋จ์ข
์๊ณ \", \"๋จ์ข
\"]}, \"์์ฐ(๋จ์ข
) ์ํ\"),\n",
" \"alt_available\": (bool, {}, \"๋์ฒดํ ์กด์ฌ ์ฌ๋ถ\")},\n",
" \"๊ฐ๋๋ฅ (์)\": {\"ym\": (str, {\"pattern\": r\"^\\d{4}-\\d{2}$\"}, \"์ง๊ณ ์\"),\n",
" \"op_hours\": (float, {\"min\": 0, \"max\": 744}, \"์ฅ๋น ์ด์ฉ์๊ฐ\"),\n",
" \"availability\": (float, {\"min\": 0, \"max\": 1}, \"์ฅ๋น ๊ฐ๋๋ฅ \"),\n",
" \"downtime_h\": (float, {\"min\": 0, \"max\": 744}, \"์ ๋น ๋๊ธฐยท์๋ฆฌ ์๊ฐ\"),\n",
" \"exercise_flag\": (int, {\"enum\": [0, 1]}, \"๋๊ท๋ชจ ํ๋ จ ์ ์ฌ๋ถ\")},\n",
"}\n",
"\n",
"\n",
"def check(name, v, typ, rule):\n",
" # [TODO : ํ์ ์ค์ต 1] ์ผ์ ์ ๋น์ฐฝ ์๋ฆฌ๋ถ์ ์ฒญ๊ตฌ/์ถ๊ณ ์ด๋ ฅ ๋ฐ ์ฅ๋น ๊ฐ๋๋ฅ ๋ฐ์ดํฐ์
์คํค๋ง ์ ์ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def validate(rec):\n",
" # [TODO : ํ์ ์ค์ต 1] ์ผ์ ์ ๋น์ฐฝ ์๋ฆฌ๋ถ์ ์ฒญ๊ตฌ/์ถ๊ณ ์ด๋ ฅ ๋ฐ ์ฅ๋น ๊ฐ๋๋ฅ ๋ฐ์ดํฐ์
์คํค๋ง ์ ์ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"rng = np.random.default_rng(1)\n",
"records = []\n",
"for i in range(20):\n",
" fsc = str(rng.choice([\"2520\", \"2530\", \"2940\", \"6140\"]))\n",
" niin = f\"00{rng.integers(1000000, 9999999)}\"\n",
" q = int(rng.integers(1, 20))\n",
" et = str(rng.choice([\"A\", \"B\", \"C\"]))\n",
" records.append({\"req_id\": f\"R{250001 + i}\", \"req_date\": f\"2025-{rng.integers(1, 13):02d}-{rng.integers(1, 29):02d}\",\n",
" \"unit_code\": f\"U-{rng.integers(1, 6):02d}\", \"equip_id\": f\"{et}-{rng.integers(1, 200):03d}\", \"equip_type\": et,\n",
" \"maint_type\": str(rng.choice([\"๊ณํ\", \"๋น๊ณํ\"])), \"nsn\": f\"{fsc}-{niin[:2]}-{niin[2:5]}-{niin[5:]}\",\n",
" \"fsc\": fsc, \"niin\": niin, \"qty_req\": q, \"qty_issued\": int(rng.integers(0, q + 1)),\n",
" \"unit_of_issue\": \"EA\", \"priority\": str(rng.choice([\"๊ธด๊ธ\", \"์ผ๋ฐ\"], p=[0.1, 0.9])),\n",
" \"emergency_flag\": bool(rng.random() < 0.05), \"unit_price\": int(rng.integers(10, 900)) * 1000,\n",
" \"lead_time_m\": int(rng.integers(1, 13)), \"supplier_count\": int(rng.integers(1, 5)),\n",
" \"prod_status\": str(rng.choice([\"์์ฐ์ค\", \"๋จ์ข
์๊ณ \", \"๋จ์ข
\"], p=[0.7, 0.2, 0.1])),\n",
" \"alt_available\": bool(rng.random() < 0.5), \"ym\": \"2025-12\", \"op_hours\": float(round(rng.uniform(80, 180), 1)),\n",
" \"availability\": float(round(rng.uniform(0.75, 0.97), 3)), \"downtime_h\": float(round(rng.uniform(5, 60), 1)),\n",
" \"exercise_flag\": int(rng.integers(0, 2))})\n",
"\n",
"ok = sum(1 for r in records if not validate(r))\n",
"n_feat = sum(len(g) for g in SCHEMA.values())\n",
"print(f\"\\n์ ์ํ ํผ์ฒ(ํ๋) {n_feat}๊ฐ (๋ชฉํ 20๊ฐ ์ด์: {'๋ฌ์ฑ' if n_feat >= 20 else '๋ฏธ๋ฌ'})\")\n",
"print(f\"์ ์ ์ํ {len(records)}๊ฑด ์ค ๊ฒ์ฆ ํต๊ณผ {ok}๊ฑด, ์ค๋ฅ {len(records) - ok}๊ฑด\")\n",
"\n",
"# 4) ์ค๋ฅ ์ฃผ์
์ํ\n",
"bad = json.loads(json.dumps(records[0], ensure_ascii=False))\n",
"bad[\"nsn\"] = \"2520-001234567\" # NSN ํ์ ์ค๋ฅ\n",
"bad[\"qty_req\"] = -3 # ์์ ์๋\n",
"bad[\"prod_status\"] = \"๋ฏธ์\" # ํ์ฉ๊ฐ ์๋\n",
"del bad[\"lead_time_m\"] # ํ์ ํ๋ ๋๋ฝ\n",
"bad[\"availability\"] = 1.4 # ๊ฐ๋๋ฅ ๋ฒ์ ์ด๊ณผ\n",
"print(\"[์ค๋ฅ ์ฃผ์
๊ฒฐ๊ณผ]\")\n",
"for e in validate(bad):\n",
" print(\" -\", e)\n",
"bad2 = json.loads(json.dumps(records[1], ensure_ascii=False)); bad2[\"fsc\"] = \"9999\"\n",
"print(\" - (๋
ผ๋ฆฌ ๊ท์น)\", validate(bad2))\n",
"\n",
"# 5) ๋ฐ์ดํฐ ๋์
๋๋ฆฌ ์ถ๋ ฅ\n",
"rows = [(g, k, t.__name__, json.dumps(r, ensure_ascii=False) if r else \"-\", d) for g, grp in SCHEMA.items() for k, (t, r, d) in grp.items()]\n",
"dic = pd.DataFrame(rows, columns=[\"๊ทธ๋ฃน\", \"ํ๋\", \"์๋ฃํ\", \"๊ท์น\", \"์ค๋ช
\"])\n",
"pd.set_option(\"display.width\", 200); pd.set_option(\"display.max_colwidth\", 34)\n",
"print(\"[๋ฐ์ดํฐ ๋์
๋๋ฆฌ]\")\n",
"print(dic.to_string(index=False))\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
01 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"๋ถ์ ๋ชฉํ: ์๋ณ ํ๋ชฉ ์์๋ฅผ ์์ธกํด ๊ฒฐํยท๊ณผ์์ฌ๊ณ ๋ฅผ ์ค์ด๊ณ , ๋จ์ข
์๊ณ ยท๋จ์ข
ํ๋ชฉ์ ์ต์ข
๊ตฌ๋งค ์๋ ํ๋จ์ ์ง์ํ๋ค.\n",
"[์
๋ฌด ํ๋ก์ธ์ค ๋งต] ์ ๋น ์์ -> ์ฒญ๊ตฌ -> ๋ถ์ถ -> ์กฐ๋ฌ -> ๋จ์ข
๊ฒํ -> ๊ฐ๋๋ฅ \n",
" 1. ์ ๋น ์์ ๋ฐ์ | ์ฅ๋น ๊ณ ์ฅยท๊ณํ์ ๋น | equip_id, equip_type, maint_type\n",
" 2. ์๋ฆฌ๋ถ์ ์ฒญ๊ตฌ | ์ ๋น๋ฐ -> ์ ๋น์ฐฝ | req_id, req_date, nsn, qty_req, priority\n",
" 3. ์ฌ๊ณ ํ์ธยท๋ถ์ถ | ์ ๋น์ฐฝ ์ฌ๊ณ ๋ด๋น | qty_issued, emergency_flag\n",
" 4. ์กฐ๋ฌ ์์ฒญ | ์ฌ๊ณ ๋ถ์กฑ ์ ์กฐ๋ฌ๋ถ์ | lead_time_m, supplier_count, unit_price\n",
" 5. ๋จ์ข
์ํ ๊ฒํ | ํ๋ชฉ ๊ด๋ฆฌ ๋ด๋น | prod_status, alt_available\n",
" 6. ์ ๋น ์๋ฃยท๊ฐ๋๋ฅ ์ง๊ณ | ์ ๋จ์ ์ง๊ณ | op_hours, availability, downtime_h\n",
"\n",
"์ ์ํ ํผ์ฒ(ํ๋) 24๊ฐ (๋ชฉํ 20๊ฐ ์ด์: ๋ฌ์ฑ)\n",
"์ ์ ์ํ 20๊ฑด ์ค ๊ฒ์ฆ ํต๊ณผ 20๊ฑด, ์ค๋ฅ 0๊ฑด\n",
"[์ค๋ฅ ์ฃผ์
๊ฒฐ๊ณผ]\n",
" - nsn: ํ์ ๋ถ์ผ์น '2520-001234567'\n",
" - qty_req: ์ต์๊ฐ 1 ๋ฏธ๋ง\n",
" - lead_time_m: ํ์ ํ๋ ๋๋ฝ\n",
" - prod_status: ํ์ฉ๊ฐ ์๋ '๋ฏธ์'\n",
" - availability: ์ต๋๊ฐ 1 ์ด๊ณผ\n",
" - (๋
ผ๋ฆฌ ๊ท์น) ['nsn/fsc/niin ๋ถ์ผ์น']\n",
"[๋ฐ์ดํฐ ๋์
๋๋ฆฌ]\n",
" ๊ทธ๋ฃน ํ๋ ์๋ฃํ ๊ท์น ์ค๋ช
\n",
" ์ฒญ๊ตฌ/๋ถ์ถ req_id str {\"pattern\": \"^R\\\\d{6}$\"} ์ฒญ๊ตฌ ๋ฒํธ\n",
" ์ฒญ๊ตฌ/๋ถ์ถ req_date str {\"pattern\": \"^\\\\d{4}-\\\\d{2}-\\\\d{2}$\"} ์ฒญ๊ตฌ ์ผ์\n",
" ์ฒญ๊ตฌ/๋ถ์ถ unit_code str {\"pattern\": \"^U-\\\\d{2}$\"} ์ฒญ๊ตฌ ๋ถ๋(๊ฐ์ ์ฝ๋)\n",
" ์ฒญ๊ตฌ/๋ถ์ถ equip_id str {\"pattern\": \"^[ABC]-\\\\d{3}$\"} ์ฅ๋น ๋ฒํธ(๊ฐ์)\n",
" ์ฒญ๊ตฌ/๋ถ์ถ equip_type str {\"enum\": [\"A\", \"B\", \"C\"]} ์ฅ๋น ์ ํ\n",
" ์ฒญ๊ตฌ/๋ถ์ถ maint_type str {\"enum\": [\"๊ณํ\", \"๋น๊ณํ\"]} ์ ๋น ๊ตฌ๋ถ\n",
" ์ฒญ๊ตฌ/๋ถ์ถ nsn str {\"pattern\": \"^\\\\d{4}-\\\\d{2}-\\\\d{3}-\\\\d{4}$\"} ๊ตญ๊ฐ์ฌ๊ณ ๋ฒํธ(๊ฐ์)\n",
" ์ฒญ๊ตฌ/๋ถ์ถ fsc str {\"pattern\": \"^\\\\d{4}$\"} ๊ตฐ์ํ ๋ถ๋ฅ(NSN ์ 4์๋ฆฌ)\n",
" ์ฒญ๊ตฌ/๋ถ์ถ niin str {\"pattern\": \"^\\\\d{9}$\"} ํ๋ชฉ ์๋ณ๋ฒํธ(NSN ๋ค 9์๋ฆฌ)\n",
" ์ฒญ๊ตฌ/๋ถ์ถ qty_req int {\"min\": 1, \"max\": 500} ์ฒญ๊ตฌ ์๋\n",
" ์ฒญ๊ตฌ/๋ถ์ถ qty_issued int {\"min\": 0, \"max\": 500} ๋ถ์ถ ์๋\n",
" ์ฒญ๊ตฌ/๋ถ์ถ unit_of_issue str {\"enum\": [\"EA\", \"SE\", \"BX\"]} ๋ถ์ถ ๋จ์\n",
" ์ฒญ๊ตฌ/๋ถ์ถ priority str {\"enum\": [\"๊ธด๊ธ\", \"์ผ๋ฐ\"]} ์ฒญ๊ตฌ ์ฐ์ ์์\n",
" ์ฒญ๊ตฌ/๋ถ์ถ emergency_flag bool - ๊ธด๊ธ ๋ถ์ถ ์ฌ๋ถ(์ด์์น๋ก ์ง์ฐ์ง ์์)\n",
"ํ๋ชฉ/๊ณต๊ธ๋ง unit_price int {\"min\": 0} ๋จ๊ฐ(์, ๊ฐ์)\n",
"ํ๋ชฉ/๊ณต๊ธ๋ง lead_time_m int {\"min\": 0, \"max\": 36} ์กฐ๋ฌ ๋ฆฌ๋ํ์(์)\n",
"ํ๋ชฉ/๊ณต๊ธ๋ง supplier_count int {\"min\": 0, \"max\": 20} ๊ณต๊ธ ๊ฐ๋ฅ ์
์ฒด ์\n",
"ํ๋ชฉ/๊ณต๊ธ๋ง prod_status str {\"enum\": [\"์์ฐ์ค\", \"๋จ์ข
์๊ณ \", \"๋จ์ข
\"]} ์์ฐ(๋จ์ข
) ์ํ\n",
"ํ๋ชฉ/๊ณต๊ธ๋ง alt_available bool - ๋์ฒดํ ์กด์ฌ ์ฌ๋ถ\n",
"๊ฐ๋๋ฅ (์) ym str {\"pattern\": \"^\\\\d{4}-\\\\d{2}$\"} ์ง๊ณ ์\n",
"... (์ดํ ์๋ต)\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
02. ๊ฒฐ์ธก์น ๋ณด์ , ๊ณ์ ์ฑ ์์ ๋ถ๋ฆฌ ๋ฐ ์๋ฆฌ๋ถ์ ํ๋ชฉ์ฝ๋(NSN) ์ ์ ์ ์ฒ๋ฆฌ ๋ชจ๋ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๊ฒฐ์ธก์น ๋ณด์ , ๊ณ์ ์ฑ ์์ ๋ถ๋ฆฌ ๋ฐ ๋ถํ์ฝ๋(NSN) ์ ์ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ฒญ๊ตฌ ๊ธฐ๋ก์ ๊ฒฐ์ธก์น ๋ณด์ , ๊ณ์ ์ฑ ์์ ๋ถ๋ฆฌ, ํ๋ชฉ์ฝ๋(NSN) ์ ์ ๋ฅผ ์ํํ๋ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ค๋ณต ์ฒญ๊ตฌ ์ ๊ฑฐ, NSN ํ์ ์ ๊ทํ์ ๋ฏธ๋ฑ๋ก NSN ๊ฒฉ๋ฆฌ, ๋ ์งยท๋จ์ ํต์ผ, ์์ยท๊ณผ๋ค ์๋ ๋ณด์ ๋ฑ ์ ์ ๊ท์น 10์ข
์ด์์ ์ ์ฉํ๊ณ ๊ธด๊ธ ๋ถ์ถ์ ์ด์์น๋ก ์ง์ฐ์ง ์๋๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/etl/mnt02_data_cleansing.py ๋ฐ ์ ์ ํ์ง ๋ณด๊ณ ์` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ตฐ์ ํน์ ์์ธ(๋น๊ณํ ์ ๋น, ๊ธด๊ธ ๋ถ์ถ ๋ฑ) ๋ฐ์ดํฐ ๋ณด์กด์ ํ๋น์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๋ฐ์ดํฐ ์ ์ ๊ท์น 10์ข
์ ์ฉ, ๋ฐ์ดํฐ ์ ํฉ์ฑ ๊ฒ์ฆ ํต๊ณผ์จ 100%\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ETL ํ์ดํ๋ผ์ธ ๊ตฌํ ๋ฐ ์ ์ ๋ ๋ง์คํฐ ๋ฐ์ดํฐ์
๊ตฌ์ถ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ์ ํ ๋ฐ์ดํฐ ํต๊ณ๋(๊ธฐ์ ํต๊ณ) ๋น๊ต ๋ฐ ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์๋ฆฌ๋ถ์ ์ฒญ๊ตฌ/์ถ๊ณ ์์ ๊ธฐ๋ก์ ์ ์ ๊ท์น 10์ข
(NSN ํ์ ์ ๊ทํ, ์ค๋ณต ์ ๊ฑฐ, ๋ ์ง ํต์ผ, ๋จ์ ํ์ฐ, ๊ฒฐ์ธก ์ ๊ณ์ ์ฑ ๋ณด๊ฐ ๋ฑ)์ ์ ์ฉํ๊ณ , ์๋ณ ์์์์ ๊ณ์ ์ฑ ์ฑ๋ถ์ ๋ถ๋ฆฌํ ๋ค ์ ํฉ์ฑ ๊ฒ์ฆ ํต๊ณผ์จ์ ํ์ธํ๋ค.\"\"\"\n",
"import re\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ผ์ ์ ๋น์ฐฝ ์๋ฆฌ๋ถ์ ์ฒญ๊ตฌ/์ถ๊ณ ยท์ฅ๋น ๊ฐ๋๋ฅ ๋ฐ์ดํฐ ์์ฑ๊ธฐ\n",
"# ์ค์ ๋ถ๋ยทํ๋ชฉยท์ฌ๊ณ ยท๋จ๊ฐ ์ ๋ณด์ ๋ฌด๊ดํ๋ค. ํ๋ชฉ 40๊ฐ x 48๊ฐ์(๊ฐ์ 2022-01 ~ 2025-12) ์๋ณ ์๋ฃ๋ฅผ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"FSC = {\"2520\": \"power_train\", \"2530\": \"brake_steer\", \"2910\": \"fuel\", \"2930\": \"cooling\",\n",
" \"2940\": \"filter\", \"5330\": \"gasket\", \"6140\": \"battery\", \"5961\": \"semiconductor\"}\n",
"EXERCISE = {3, 8, 10} # ๊ฐ์ ๋๊ท๋ชจ ํ๋ จ ์(ํ๋ จ ๊ณํ์ผ๋ก ๋ฏธ๋ฆฌ ์ ์ ์์)\n",
"N_MONTHS = 48\n",
"\n",
"\n",
"def season(fsc, month):\n",
" \"\"\"ํ๋ชฉ๊ตฐ๋ณ ๊ณ์ ๊ณ์: ์ถ์ ์ง๋ ๊ฒจ์ธ, ๋๊ฐ๊ณํต์ ์ฌ๋ฆ์ ์์๊ฐ ๋์ด๋๋ค.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 2] ๊ฒฐ์ธก์น ๋ณด์ , ๊ณ์ ์ฑ ์์ ๋ถ๋ฆฌ ๋ฐ ์๋ฆฌ๋ถ์ ํ๋ชฉ์ฝ๋(NSN) ์ ์ ์ ์ฒ๋ฆฌ ๋ชจ๋ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def make_items(n=40, seed=2):\n",
" \"\"\"ํ๋ชฉ ๋ง์คํฐ: NSN(๊ฐ์), ํ๋ชฉ๊ตฐ, ์ฅ๋น ์ ํ, ๊ธฐ์ค ์์, ๋จ๊ฐ, ์กฐ๋ฌ ๋ฆฌ๋ํ์, ๊ณต๊ธ์
์ฒด ์, ์์ฐ(๋จ์ข
) ์ํ.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for i in range(n):\n",
" f = str(rng.choice(list(FSC)))\n",
" status = str(rng.choice([\"์์ฐ์ค\", \"๋จ์ข
์๊ณ \", \"๋จ์ข
\"], p=[0.7, 0.2, 0.1]))\n",
" rows.append({\"item_id\": f\"P{i + 1:03d}\", \"nsn\": f\"{f}-00-{rng.integers(100, 1000)}-{rng.integers(1000, 10000)}\",\n",
" \"fsc\": f, \"group\": FSC[f], \"equip_type\": str(rng.choice([\"A\", \"B\", \"C\"])),\n",
" \"base\": float(np.round(rng.lognormal(2.7, 0.8), 1)),\n",
" \"price\": int(rng.lognormal(12, 1.0)) // 100 * 100,\n",
" \"lead_time\": int(rng.integers(1, 7) if status == \"์์ฐ์ค\" else rng.integers(4, 13)),\n",
" \"suppliers\": int(rng.integers(1, 5)) if status == \"์์ฐ์ค\" else 1,\n",
" \"status\": status, \"alt_available\": bool(rng.random() < (0.6 if status == \"์์ฐ์ค\" else 0.3))})\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"def make_history(items, months=N_MONTHS, seed=3):\n",
" \"\"\"์๋ณ ์ฒญ๊ตฌ ์์(๊ฐ), ๊ธด๊ธ ๋ถ์ถ ๊ฑด์, ์ฅ๋น ์ ํ๋ณ ์ด์ฉ์๊ฐยท๊ฐ๋๋ฅ .\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" t = np.arange(months)\n",
" moy = t % 12 + 1\n",
" ex = np.isin(moy, list(EXERCISE))\n",
" hours = {e: (100 + 20 * rng.random(months)) * np.where(ex, 1.5, 1.0) for e in \"ABC\"}\n",
" avail = {e: np.clip(0.93 - 0.12 * (hours[e] - 100) / 80 + rng.normal(0, 0.015, months), 0.6, 0.99) for e in \"ABC\"}\n",
" rows = []\n",
" for it in items.itertuples():\n",
" trend = 1 + rng.uniform(-0.1, 0.3) * t / months\n",
" s = np.array([season(it.fsc, m) for m in moy])\n",
" lam = it.base * s * trend * hours[it.equip_type] / 110\n",
" d = rng.poisson(lam)\n",
" rows.append(pd.DataFrame({\"item_id\": it.item_id, \"t\": t, \"ym\": [f\"{2022 + k // 12}-{k % 12 + 1:02d}\" for k in t],\n",
" \"month\": moy, \"exercise\": ex.astype(int), \"op_hours\": hours[it.equip_type].round(1),\n",
" \"availability\": avail[it.equip_type].round(3), \"demand\": d,\n",
" \"emergency\": rng.binomial(d, 0.05)}))\n",
" return pd.concat(rows, ignore_index=True)\n",
"\n",
"\n",
"def make_stock(items, hist, seed=4):\n",
" \"\"\"ํ์ฌ(๊ฐ์ 2026-01) ์ฌ๊ณ : ๋ณด์ ๋๊ณผ ๋ฏธ์
๊ณ ๋ฐ์ฃผ๋(์กฐ๋ฌ ์ค์ธ ๋ฌผ๋). ๋จ์ข
ํ๋ชฉ์ ์ถ๊ฐ ๋ฐ์ฃผ๊ฐ ์ด๋ ค์ ๋ฏธ์
๊ณ ๋์ด ์ ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" recent = hist[hist[\"t\"] >= hist[\"t\"].max() - 5].groupby(\"item_id\")[\"demand\"].mean()\n",
" st = items[[\"item_id\", \"lead_time\", \"status\"]].copy()\n",
" st[\"on_hand\"] = [int(round(recent[i] * rng.uniform(0.5, 3.0))) for i in st[\"item_id\"]]\n",
" cover = np.where(st[\"status\"] == \"์์ฐ์ค\", rng.uniform(0.6, 1.3, len(st)), rng.uniform(0.0, 0.6, len(st)))\n",
" st[\"on_order\"] = [int(round(recent[i] * L * c)) for i, L, c in zip(st[\"item_id\"], st[\"lead_time\"], cover)]\n",
" return st[[\"item_id\", \"on_hand\", \"on_order\"]]\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"items = make_items()\n",
"hist = make_history(items)\n",
"master = set(items[\"nsn\"])\n",
"nsn_of = dict(zip(items[\"item_id\"], items[\"nsn\"]))\n",
"\n",
"# 1) [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ต๊ทผ 24๊ฐ์ ์ ์์๋ฅผ ๊ฐ๋ณ ์ฒญ๊ตฌ ๊ธฐ๋ก์ผ๋ก ํ๊ณ ์ผ๋ถ๋ฌ ์ค๋ฅ๋ฅผ ์๋๋ค\n",
"rng = np.random.default_rng(5)\n",
"OUTAGE = {\"2024-07\", \"2025-02\"} # ์ ์ฐ ์ฅ์ ๋ก ๊ธฐ๋ก์ด ์ฌ๋ผ์ค์ง ์์ ์(๊ฐ์)\n",
"rows, rid = [], 300000\n",
"for r in hist[hist[\"t\"] >= 24].itertuples():\n",
" if r.ym in OUTAGE or r.demand == 0:\n",
" continue\n",
" k = int(np.ceil(r.demand / 5))\n",
" for j, q in enumerate(rng.multinomial(r.demand, np.ones(k) / k)):\n",
" if q == 0:\n",
" continue\n",
" rid += 1\n",
" rows.append({\"req_id\": f\"R{rid}\", \"date\": f\"{r.ym}-{rng.integers(1, 29):02d}\", \"nsn\": nsn_of[r.item_id],\n",
" \"qty_req\": int(q), \"qty_issued\": int(q), \"uoi\": \"EA\", \"emergency\": bool(j < r.emergency)})\n",
"raw = pd.DataFrame(rows)\n",
"n = len(raw)\n",
"idx = lambda frac: rng.choice(n, int(n * frac), replace=False)\n",
"raw.loc[idx(0.05), \"nsn\"] = raw[\"nsn\"].str.replace(\"-\", \"\") # ํ์ดํ ๋๋ฝ\n",
"raw.loc[idx(0.03), \"nsn\"] = \" \" + raw[\"nsn\"] + \" \" # ์๋ค ๊ณต๋ฐฑ\n",
"raw.loc[idx(0.04), \"date\"] = raw[\"date\"].str.replace(\"-\", \".\") # ๋ ์ง ํ์ ํผ์ฌ\n",
"raw.loc[idx(0.02), \"qty_req\"] = -raw[\"qty_req\"] # ์์(์
๋ ฅ ์ค๋ฅ)\n",
"raw.loc[idx(0.02), \"qty_issued\"] = raw[\"qty_req\"].abs() + 2 # ๋ถ์ถ > ์ฒญ๊ตฌ\n",
"bx = idx(0.02); raw.loc[bx, \"uoi\"] = \"BX\"; raw.loc[bx, \"qty_req\"] = 1; raw.loc[bx, \"qty_issued\"] = 1 # ์์(10EA) ๋จ์\n",
"raw.loc[idx(0.01), \"qty_req\"] = np.nan # ์๋ ๊ฒฐ์ธก\n",
"raw.loc[idx(0.005), \"date\"] = \"2027-01-15\" # ๋ฏธ๋ ์ผ์\n",
"raw.loc[idx(0.005), \"nsn\"] = \"9999-00-000-0000\" # ๋ฏธ๋ฑ๋ก NSN\n",
"typo = [i for i in idx(0.01) if not raw.at[i, \"emergency\"]]\n",
"raw.loc[typo, \"qty_req\"] = raw.loc[typo, \"qty_req\"] * 10 # 0 ํ๋ ๋ ์
๋ ฅํ ์คํ\n",
"raw = pd.concat([raw, raw.sample(60, random_state=1)], ignore_index=True) # ์ค๋ณต ์ ์ก\n",
"print(f\"์์ ์ฒญ๊ตฌ ๊ธฐ๋ก {len(raw)}๊ฑด (๊ฐ์), ์ ์ฐ ์ฅ์ ์ {sorted(OUTAGE)}\")\n",
"\n",
"\n",
"# 2) ์ ์ ๊ท์น 10์ข
\n",
"def fmt_nsn(s):\n",
" # [TODO : ํ์ ์ค์ต 2] ๊ฒฐ์ธก์น ๋ณด์ , ๊ณ์ ์ฑ ์์ ๋ถ๋ฆฌ ๋ฐ ์๋ฆฌ๋ถ์ ํ๋ชฉ์ฝ๋(NSN) ์ ์ ์ ์ฒ๋ฆฌ ๋ชจ๋ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"log = {}\n",
"df = raw.copy()\n",
"before = len(df); df = df.drop_duplicates(\"req_id\"); log[\"R1 ์ค๋ณต ์ฒญ๊ตฌ๋ฒํธ ์ ๊ฑฐ\"] = before - len(df)\n",
"em0 = int(df[\"emergency\"].sum())\n",
"fixed = df[\"nsn\"].map(fmt_nsn); log[\"R2 NSN ํ์ ์ ๊ทํ(๊ณต๋ฐฑยทํ์ดํ)\"] = int((fixed != df[\"nsn\"]).sum()); df[\"nsn\"] = fixed\n",
"unk = ~df[\"nsn\"].isin(master); log[\"R3 ๋ฏธ๋ฑ๋ก NSN ๊ฒฉ๋ฆฌ\"] = int(unk.sum()); em_drop = int(df.loc[unk, \"emergency\"].sum()); df = df[~unk]\n",
"d2 = df[\"date\"].str.replace(\".\", \"-\", regex=False); log[\"R4 ๋ ์ง ํ์ ํต์ผ\"] = int((d2 != df[\"date\"]).sum()); df[\"date\"] = pd.to_datetime(d2)\n",
"fut = df[\"date\"] > \"2025-12-31\"; log[\"R5 ๋ฏธ๋ ์ผ์ ์ ๊ฑฐ\"] = int(fut.sum()); em_drop += int(df.loc[fut, \"emergency\"].sum()); df = df[~fut]\n",
"neg = df[\"qty_req\"] < 0; log[\"R6 ์์ ์๋ ๋ถํธ ๋ณด์ \"] = int(neg.sum()); df.loc[neg, \"qty_req\"] = -df.loc[neg, \"qty_req\"]\n",
"na = df[\"qty_req\"].isna(); log[\"R7 ์ฒญ๊ตฌ ์๋ ๊ฒฐ์ธก -> ๋ถ์ถ ์๋์ผ๋ก ๋ณด์\"] = int(na.sum()); df.loc[na, \"qty_req\"] = df.loc[na, \"qty_issued\"]\n",
"bxm = df[\"uoi\"] == \"BX\"; log[\"R8 ๋จ์ ํ์ฐ(BX=10EA)\"] = int(bxm.sum())\n",
"df.loc[bxm, [\"qty_req\", \"qty_issued\"]] = df.loc[bxm, [\"qty_req\", \"qty_issued\"]] * 10; df.loc[bxm, \"uoi\"] = \"EA\"\n",
"over = df[\"qty_issued\"] > df[\"qty_req\"]; log[\"R9 ๋ถ์ถ>์ฒญ๊ตฌ -> ์ฒญ๊ตฌ ์๋์ผ๋ก ์ ํ\"] = int(over.sum())\n",
"df.loc[over, \"qty_issued\"] = df.loc[over, \"qty_req\"]\n",
"cap = df.groupby(\"nsn\")[\"qty_req\"].transform(lambda s: s.median() * 5)\n",
"out = (df[\"qty_req\"] > cap) & ~df[\"emergency\"]\n",
"log[\"R10 ์คํ์ฑ ์ด์์น ๋ณด์ (๊ธด๊ธ ๋ถ์ถ์ ๋ณด์กด)\"] = int(out.sum()); df.loc[out, \"qty_req\"] = (df.loc[out, \"qty_req\"] / 10).round()\n",
"df[\"qty_req\"] = df[\"qty_req\"].astype(int)\n",
"\n",
"# 3) ์๋ณ ์ง๊ณ + ๊ฒฐ์ธก ์ ๊ณ์ ์ฑ ๋ณด๊ฐ(R11): ์๋
๊ฐ์ ๋ฌ x (์ต๊ทผ ์์ค / ์๋
์์ค)\n",
"df[\"ym\"] = df[\"date\"].dt.strftime(\"%Y-%m\")\n",
"monthly = df.pivot_table(index=\"ym\", columns=\"nsn\", values=\"qty_req\", aggfunc=\"sum\")\n",
"all_m = [f\"{y}-{m:02d}\" for y in (2024, 2025) for m in range(1, 13)]\n",
"monthly = monthly.reindex(all_m)\n",
"prev = hist[hist[\"t\"].between(12, 23)].assign(nsn=lambda x: x[\"item_id\"].map(nsn_of)).pivot(index=\"month\", columns=\"nsn\", values=\"demand\")\n",
"seasonal_fill = monthly.copy()\n",
"for ym in OUTAGE:\n",
" i = all_m.index(ym)\n",
" ratio = monthly.iloc[max(0, i - 3):i].mean() / prev.loc[[(i - k) % 12 + 1 for k in (1, 2, 3)]].mean().replace(0, np.nan)\n",
" seasonal_fill.loc[ym] = (prev.loc[int(ym[-2:])] * ratio.fillna(1)).round()\n",
"linear_fill = monthly.interpolate()\n",
"monthly = monthly.fillna(0)\n",
"seasonal_fill = seasonal_fill.fillna(0)\n",
"log[\"R11 ๊ฒฐ์ธก ์ ๊ณ์ ์ฑ ๋ณด๊ฐ\"] = len(OUTAGE) * monthly.shape[1]\n",
"truth = hist[hist[\"t\"] >= 24].assign(nsn=lambda x: x[\"item_id\"].map(nsn_of)).pivot(index=\"ym\", columns=\"nsn\", values=\"demand\")\n",
"err_s = np.abs(seasonal_fill.loc[sorted(OUTAGE)] - truth.loc[sorted(OUTAGE)]).values.mean()\n",
"err_l = np.abs(linear_fill.loc[sorted(OUTAGE)] - truth.loc[sorted(OUTAGE)]).values.mean()\n",
"\n",
"print(\"\\n[์ ์ ๊ท์น ์ ์ฉ ๊ฒฐ๊ณผ]\")\n",
"for k, v in log.items():\n",
" print(f\" {k:32s} {v:5d}๊ฑด\")\n",
"print(f\"๊ฒฐ์ธก ์ ๋ณด๊ฐ ์ค์ฐจ(MAE): ๊ณ์ ์ฑ ๋ณด๊ฐ {err_s:.2f}๊ฐ / ๋จ์ ์ ํ ๋ณด๊ฐ {err_l:.2f}๊ฐ\")\n",
"\n",
"# 4) ์ ํฉ์ฑ ๊ฒ์ฆ\n",
"checks = {\"์ฒญ๊ตฌ๋ฒํธ ์ ์ผ\": df[\"req_id\"].is_unique, \"NSN ํ์\": df[\"nsn\"].str.match(r\"^\\d{4}-\\d{2}-\\d{3}-\\d{4}$\").all(),\n",
" \"๋ง์คํฐ ๋ฑ๋ก NSN\": df[\"nsn\"].isin(master).all(), \"์๋ ์์\": (df[\"qty_req\"] > 0).all(),\n",
" \"๋ถ์ถ<=์ฒญ๊ตฌ\": (df[\"qty_issued\"] <= df[\"qty_req\"]).all(), \"๋จ์ EA ํต์ผ\": (df[\"uoi\"] == \"EA\").all(),\n",
" \"๋ฏธ๋ ์ผ์ ์์\": (df[\"date\"] <= \"2025-12-31\").all(), \"๊ฒฐ์ธก ์ ์์\": not seasonal_fill.isna().any().any(),\n",
" \"๊ธด๊ธ ๋ถ์ถ ๋ณด์กด\": int(df[\"emergency\"].sum()) == em0 - em_drop,\n",
" \"์ ํฉ๊ณ = ๊ธฐ๋ก ํฉ๊ณ\": int(monthly.values.sum()) == int(df[\"qty_req\"].sum())}\n",
"rate = sum(checks.values()) / len(checks)\n",
"print(f\"\\n์ ํฉ์ฑ ๊ฒ์ฆ {sum(checks.values())}/{len(checks)} ํต๊ณผ({rate:.0%}) - ๋ชฉํ 100%: {'๋ฌ์ฑ' if rate == 1 else '๋ฏธ๋ฌ'}, ์ ์ฉ ๊ท์น {len(log)}์ข
(๋ชฉํ 10์ข
์ด์)\")\n",
"\n",
"# 5) ์ ํ ๊ธฐ์ ํต๊ณ ๋น๊ต\n",
"stat = pd.DataFrame({\"์ ์ ์ \": pd.to_numeric(raw[\"qty_req\"], errors=\"coerce\").describe(), \"์ ์ ํ\": df[\"qty_req\"].describe()}).round(2)\n",
"print(\"\\n[์ฒญ๊ตฌ ์๋ ๊ธฐ์ ํต๊ณ ์ ํ ๋น๊ต]\")\n",
"print(stat.T.to_string())\n",
"\n",
"# 6) ๊ณ์ ์ฑ ๋ถ๋ฆฌ(๊ณ ์ ์ ๋ถํด): ์ถ์ธ = 12๊ฐ์ ์ค์ฌ ์ด๋ํ๊ท , ๊ณ์ ์ง์ = ์๋ณ (์ค์ /์ถ์ธ) ํ๊ท \n",
"bat = items[items[\"fsc\"] == \"6140\"].sort_values(\"base\")[\"nsn\"].iloc[-1] # ์์๊ฐ ๊ฐ์ฅ ๋ง์ ์ถ์ ์ง ํ๋ชฉ\n",
"series = pd.concat([truth.iloc[:0], seasonal_fill])[bat].astype(float)\n",
"full = hist[hist[\"item_id\"] == items.set_index(\"nsn\").loc[bat, \"item_id\"]].set_index(\"ym\")[\"demand\"].astype(float)\n",
"full.loc[series.index] = series\n",
"trend = full.rolling(12, center=True).mean()\n",
"sidx = (full / trend).groupby([int(i[-2:]) for i in full.index]).mean()\n",
"sidx = sidx / sidx.mean()\n",
"print(f\"\\n์ถ์ ์ง ํ๋ชฉ {bat} ๊ณ์ ์ง์(1=ํ๊ท ):\", \" \".join(f\"{m}์ {v:.2f}\" for m, v in sidx.items()))\n",
"print(\"ํด์: ๊ฒจ์ธ(12~2์) ์ถ์ ์ง ์์ ์ฆ๊ฐ์ ํ๋ จ ์(3ยท8ยท10์) ์ฆ๊ฐ๊ฐ ํจ๊ป ๋ํ๋๋ค. ํ๋ จ ํจ๊ณผ๋ ๋ฏธ์
4์์ ๋ณ๋ ํผ์ฒ๋ก ๋ถ๋ฆฌํ๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(12, 4))\n",
"ax[0].plot(full.values, label=\"demand\"); ax[0].plot(trend.values, label=\"trend (12M centered MA)\")\n",
"ax[0].set_title(f\"Battery item {bat}: demand vs trend\"); ax[0].set_xlabel(\"month index\"); ax[0].legend()\n",
"ax[1].bar(sidx.index, sidx.values); ax[1].axhline(1, c=\"k\", ls=\"--\"); ax[1].set_title(\"Seasonal index by month\"); ax[1].set_xlabel(\"month\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
02 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"์์ ์ฒญ๊ตฌ ๊ธฐ๋ก 4918๊ฑด (๊ฐ์), ์ ์ฐ ์ฅ์ ์ ['2024-07', '2025-02']\n",
"\n",
"[์ ์ ๊ท์น ์ ์ฉ ๊ฒฐ๊ณผ]\n",
" R1 ์ค๋ณต ์ฒญ๊ตฌ๋ฒํธ ์ ๊ฑฐ 60๊ฑด\n",
" R2 NSN ํ์ ์ ๊ทํ(๊ณต๋ฐฑยทํ์ดํ) 372๊ฑด\n",
" R3 ๋ฏธ๋ฑ๋ก NSN ๊ฒฉ๋ฆฌ 24๊ฑด\n",
" R4 ๋ ์ง ํ์ ํต์ผ 191๊ฑด\n",
" R5 ๋ฏธ๋ ์ผ์ ์ ๊ฑฐ 24๊ฑด\n",
" R6 ์์ ์๋ ๋ถํธ ๋ณด์ 94๊ฑด\n",
" R7 ์ฒญ๊ตฌ ์๋ ๊ฒฐ์ธก -> ๋ถ์ถ ์๋์ผ๋ก ๋ณด์ 47๊ฑด\n",
" R8 ๋จ์ ํ์ฐ(BX=10EA) 97๊ฑด\n",
" R9 ๋ถ์ถ>์ฒญ๊ตฌ -> ์ฒญ๊ตฌ ์๋์ผ๋ก ์ ํ 96๊ฑด\n",
" R10 ์คํ์ฑ ์ด์์น ๋ณด์ (๊ธด๊ธ ๋ถ์ถ์ ๋ณด์กด) 35๊ฑด\n",
" R11 ๊ฒฐ์ธก ์ ๊ณ์ ์ฑ ๋ณด๊ฐ 80๊ฑด\n",
"๊ฒฐ์ธก ์ ๋ณด๊ฐ ์ค์ฐจ(MAE): ๊ณ์ ์ฑ ๋ณด๊ฐ 5.72๊ฐ / ๋จ์ ์ ํ ๋ณด๊ฐ 7.86๊ฐ\n",
"\n",
"์ ํฉ์ฑ ๊ฒ์ฆ 10/10 ํต๊ณผ(100%) - ๋ชฉํ 100%: ๋ฌ์ฑ, ์ ์ฉ ๊ท์น 11์ข
(๋ชฉํ 10์ข
์ด์)\n",
"\n",
"[์ฒญ๊ตฌ ์๋ ๊ธฐ์ ํต๊ณ ์ ํ ๋น๊ต]\n",
" count mean std min 25% 50% 75% max\n",
"์ ์ ์ 4870.0 4.75 4.76 -12.0 3.0 4.0 6.0 80.0\n",
"์ ์ ํ 4810.0 4.80 2.17 1.0 3.0 5.0 6.0 20.0\n",
"\n",
"์ถ์ ์ง ํ๋ชฉ 6140-00-643-1860 ๊ณ์ ์ง์(1=ํ๊ท ): 1์ 1.39 2์ 1.16 3์ 1.13 4์ 0.69 5์ 0.69 6์ 0.66 7์ 0.75 8์ 1.16 9์ 0.85 10์ 1.38 11์ 0.68 12์ 1.46\n",
"ํด์: ๊ฒจ์ธ(12~2์) ์ถ์ ์ง ์์ ์ฆ๊ฐ์ ํ๋ จ ์(3ยท8ยท10์) ์ฆ๊ฐ๊ฐ ํจ๊ป ๋ํ๋๋ค. ํ๋ จ ํจ๊ณผ๋ ๋ฏธ์
4์์ ๋ณ๋ ํผ์ฒ๋ก ๋ถ๋ฆฌํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
03. ์ด๋ํ๊ท ๋ฐ ์ง์ํํ๋ฒ ๊ธฐ๋ฐ 1์ฐจ ๋ถํ ์์๋ ์์ธก Baseline ์๊ณ ๋ฆฌ์ฆ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ด๋ํ๊ท ๊ธฐ๋ฐ 1์ฐจ ๋ถํ ์์๋ ์์ธก Baseline ์๊ณ ๋ฆฌ์ฆ์ ๊ฐ๋ฐํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ด๋ํ๊ท ๊ณผ ์ง์ํํ๋ฒ์ผ๋ก 1์ฐจ ๋ถํ ์์๋ ์์ธก Baseline์ ๋ง๋ค๊ณ ํ์์ ๋ถ์์ผ๋ก ์์ ์์ธ๊ณผ ํต์ฌ ๊ด๋ฆฌ ํ๋ชฉ์ ์ฐพ๋๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **3ยท6๊ฐ์ ์ด๋ํ๊ท , ๋จ์ ์ง์ํํ, ์๋
๊ฐ์ ๋ฌ ๋ฐฉ์์ MAEยทWAPEยทMAPE๋ฅผ ํ๊ฐ ๊ตฌ๊ฐ 6๊ฐ์์์ ๋น๊ต ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `notebooks/mnt02_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": [
"\"\"\"์ด๋ํ๊ท (3ยท6๊ฐ์)๊ณผ ๋จ์ ์ง์ํํ๋ฒ์ผ๋ก ํ๋ชฉ๋ณ ๋ค์ ๋ฌ ๋ถํ ์์๋์ ์์ธกํ๋ Baseline์ ๊ตฌํํ๊ณ , ํ์์ ๋ถ์(EDA) ์ฐจํธ 8์ข
ยท์๊ด ๊ฒ์ ยทABC(ํ๋ ํ ) ๋ถ๋ฅ๋ก ์์ ํน์ฑ๊ณผ ํต์ฌ ๊ด๋ฆฌ ํ๋ชฉ์ ๋์ถํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy import stats\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ผ์ ์ ๋น์ฐฝ ์๋ฆฌ๋ถ์ ์ฒญ๊ตฌ/์ถ๊ณ ยท์ฅ๋น ๊ฐ๋๋ฅ ๋ฐ์ดํฐ ์์ฑ๊ธฐ\n",
"# ์ค์ ๋ถ๋ยทํ๋ชฉยท์ฌ๊ณ ยท๋จ๊ฐ ์ ๋ณด์ ๋ฌด๊ดํ๋ค. ํ๋ชฉ 40๊ฐ x 48๊ฐ์(๊ฐ์ 2022-01 ~ 2025-12) ์๋ณ ์๋ฃ๋ฅผ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"FSC = {\"2520\": \"power_train\", \"2530\": \"brake_steer\", \"2910\": \"fuel\", \"2930\": \"cooling\",\n",
" \"2940\": \"filter\", \"5330\": \"gasket\", \"6140\": \"battery\", \"5961\": \"semiconductor\"}\n",
"EXERCISE = {3, 8, 10} # ๊ฐ์ ๋๊ท๋ชจ ํ๋ จ ์(ํ๋ จ ๊ณํ์ผ๋ก ๋ฏธ๋ฆฌ ์ ์ ์์)\n",
"N_MONTHS = 48\n",
"\n",
"\n",
"def season(fsc, month):\n",
" \"\"\"ํ๋ชฉ๊ตฐ๋ณ ๊ณ์ ๊ณ์: ์ถ์ ์ง๋ ๊ฒจ์ธ, ๋๊ฐ๊ณํต์ ์ฌ๋ฆ์ ์์๊ฐ ๋์ด๋๋ค.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 3] ์ด๋ํ๊ท ๋ฐ ์ง์ํํ๋ฒ ๊ธฐ๋ฐ 1์ฐจ ๋ถํ ์์๋ ์์ธก Baseline ์๊ณ ๋ฆฌ์ฆ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def make_items(n=40, seed=2):\n",
" \"\"\"ํ๋ชฉ ๋ง์คํฐ: NSN(๊ฐ์), ํ๋ชฉ๊ตฐ, ์ฅ๋น ์ ํ, ๊ธฐ์ค ์์, ๋จ๊ฐ, ์กฐ๋ฌ ๋ฆฌ๋ํ์, ๊ณต๊ธ์
์ฒด ์, ์์ฐ(๋จ์ข
) ์ํ.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for i in range(n):\n",
" f = str(rng.choice(list(FSC)))\n",
" status = str(rng.choice([\"์์ฐ์ค\", \"๋จ์ข
์๊ณ \", \"๋จ์ข
\"], p=[0.7, 0.2, 0.1]))\n",
" rows.append({\"item_id\": f\"P{i + 1:03d}\", \"nsn\": f\"{f}-00-{rng.integers(100, 1000)}-{rng.integers(1000, 10000)}\",\n",
" \"fsc\": f, \"group\": FSC[f], \"equip_type\": str(rng.choice([\"A\", \"B\", \"C\"])),\n",
" \"base\": float(np.round(rng.lognormal(2.7, 0.8), 1)),\n",
" \"price\": int(rng.lognormal(12, 1.0)) // 100 * 100,\n",
" \"lead_time\": int(rng.integers(1, 7) if status == \"์์ฐ์ค\" else rng.integers(4, 13)),\n",
" \"suppliers\": int(rng.integers(1, 5)) if status == \"์์ฐ์ค\" else 1,\n",
" \"status\": status, \"alt_available\": bool(rng.random() < (0.6 if status == \"์์ฐ์ค\" else 0.3))})\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"def make_history(items, months=N_MONTHS, seed=3):\n",
" \"\"\"์๋ณ ์ฒญ๊ตฌ ์์(๊ฐ), ๊ธด๊ธ ๋ถ์ถ ๊ฑด์, ์ฅ๋น ์ ํ๋ณ ์ด์ฉ์๊ฐยท๊ฐ๋๋ฅ .\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" t = np.arange(months)\n",
" moy = t % 12 + 1\n",
" ex = np.isin(moy, list(EXERCISE))\n",
" hours = {e: (100 + 20 * rng.random(months)) * np.where(ex, 1.5, 1.0) for e in \"ABC\"}\n",
" avail = {e: np.clip(0.93 - 0.12 * (hours[e] - 100) / 80 + rng.normal(0, 0.015, months), 0.6, 0.99) for e in \"ABC\"}\n",
" rows = []\n",
" for it in items.itertuples():\n",
" trend = 1 + rng.uniform(-0.1, 0.3) * t / months\n",
" s = np.array([season(it.fsc, m) for m in moy])\n",
" lam = it.base * s * trend * hours[it.equip_type] / 110\n",
" d = rng.poisson(lam)\n",
" rows.append(pd.DataFrame({\"item_id\": it.item_id, \"t\": t, \"ym\": [f\"{2022 + k // 12}-{k % 12 + 1:02d}\" for k in t],\n",
" \"month\": moy, \"exercise\": ex.astype(int), \"op_hours\": hours[it.equip_type].round(1),\n",
" \"availability\": avail[it.equip_type].round(3), \"demand\": d,\n",
" \"emergency\": rng.binomial(d, 0.05)}))\n",
" return pd.concat(rows, ignore_index=True)\n",
"\n",
"\n",
"def make_stock(items, hist, seed=4):\n",
" \"\"\"ํ์ฌ(๊ฐ์ 2026-01) ์ฌ๊ณ : ๋ณด์ ๋๊ณผ ๋ฏธ์
๊ณ ๋ฐ์ฃผ๋(์กฐ๋ฌ ์ค์ธ ๋ฌผ๋). ๋จ์ข
ํ๋ชฉ์ ์ถ๊ฐ ๋ฐ์ฃผ๊ฐ ์ด๋ ค์ ๋ฏธ์
๊ณ ๋์ด ์ ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" recent = hist[hist[\"t\"] >= hist[\"t\"].max() - 5].groupby(\"item_id\")[\"demand\"].mean()\n",
" st = items[[\"item_id\", \"lead_time\", \"status\"]].copy()\n",
" st[\"on_hand\"] = [int(round(recent[i] * rng.uniform(0.5, 3.0))) for i in st[\"item_id\"]]\n",
" cover = np.where(st[\"status\"] == \"์์ฐ์ค\", rng.uniform(0.6, 1.3, len(st)), rng.uniform(0.0, 0.6, len(st)))\n",
" st[\"on_order\"] = [int(round(recent[i] * L * c)) for i, L, c in zip(st[\"item_id\"], st[\"lead_time\"], cover)]\n",
" return st[[\"item_id\", \"on_hand\", \"on_order\"]]\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"items = make_items()\n",
"hist = make_history(items).merge(items[[\"item_id\", \"group\", \"price\", \"status\", \"lead_time\"]], on=\"item_id\")\n",
"TEST_FROM = 42 # ๋ง์ง๋ง 6๊ฐ์(๊ฐ์ 2025-07 ~ 2025-12)์ ํ๊ฐ ๊ตฌ๊ฐ์ผ๋ก ๋๋ค\n",
"\n",
"\n",
"def ses(y, alpha):\n",
" \"\"\"๋จ์ ์ง์ํํ: ๋ค์ ๋ฌ ์์ธก = alpha x ์ด๋ฒ ๋ฌ ์ค์ + (1 - alpha) x ์ด๋ฒ ๋ฌ ์์ธก.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 3] ์ด๋ํ๊ท ๋ฐ ์ง์ํํ๋ฒ ๊ธฐ๋ฐ 1์ฐจ ๋ถํ ์์๋ ์์ธก Baseline ์๊ณ ๋ฆฌ์ฆ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"preds = []\n",
"for iid, g in hist.groupby(\"item_id\"):\n",
" y = g.sort_values(\"t\")[\"demand\"].to_numpy(float)\n",
" best = min([0.1, 0.2, 0.3, 0.5], key=lambda a: np.abs(ses(y, a)[12:TEST_FROM] - y[12:TEST_FROM]).mean())\n",
" s = pd.Series(y)\n",
" preds.append(pd.DataFrame({\"item_id\": iid, \"t\": np.arange(len(y)), \"y\": y, \"MA3\": s.shift(1).rolling(3).mean(),\n",
" \"MA6\": s.shift(1).rolling(6).mean(), \"SES\": ses(y, best), \"์๋
๊ฐ์ ๋ฌ\": s.shift(12)}))\n",
"P = pd.concat(preds)\n",
"T = P[P[\"t\"] >= TEST_FROM]\n",
"ab = items[items[\"base\"] >= 10][\"item_id\"] # ์์๊ฐ ์ถฉ๋ถํ ํ๋ชฉ(MAPE ํด์ ๊ฐ๋ฅ)\n",
"rows = []\n",
"for m in [\"MA3\", \"MA6\", \"SES\", \"์๋
๊ฐ์ ๋ฌ\"]:\n",
" e = T[\"y\"] - T[m]\n",
" big = T[T[\"item_id\"].isin(ab)]\n",
" rows.append({\"๋ฐฉ๋ฒ\": m, \"MAE(๊ฐ)\": round(e.abs().mean(), 2), \"WAPE\": f\"{e.abs().sum() / T['y'].sum():.1%}\",\n",
" \"MAPE(์์ ๋ง์ ํ๋ชฉ)\": f\"{(np.abs(big['y'] - big[m]) / big['y'].clip(lower=1)).mean():.1%}\"})\n",
"base_tbl = pd.DataFrame(rows)\n",
"print(\"[Baseline ์์ธก ์ฑ๋ฅ - ํ๊ฐ ๊ตฌ๊ฐ 6๊ฐ์]\")\n",
"print(base_tbl.to_string(index=False))\n",
"print(\"ํด์: ์ด๋ํ๊ท ์ ํ๋ จ ์ ๊ธ์ฆ๊ณผ ๊ณ์ ๋ณํ๋ฅผ ํ ๋ฌ ๋ฆ๊ฒ ๋ฐ๋ผ๊ฐ ์ค์ฐจ๊ฐ ์ปค์ง๋ค(๋ฏธ์
4์์ ๊ฐ์ ).\")\n",
"\n",
"# 2) ์๊ด ๊ฒ์ : ํ๋ชฉ ํ๊ท ๋๋น ์์ ๋น์จ๊ณผ ํ๋ณด ํผ์ฒ\n",
"hist = hist.sort_values([\"item_id\", \"t\"])\n",
"hist[\"ratio\"] = hist[\"demand\"] / hist.groupby(\"item_id\")[\"demand\"].transform(\"mean\")\n",
"g = hist.groupby(\"item_id\")[\"ratio\"]\n",
"hist[\"lag1_ratio\"] = g.shift(1); hist[\"lag12_ratio\"] = g.shift(12)\n",
"hist[\"winter\"] = hist[\"month\"].isin([12, 1, 2]).astype(int)\n",
"cand = [\"op_hours\", \"exercise\", \"availability\", \"lag12_ratio\", \"lag1_ratio\", \"t\", \"winter\", \"price\"]\n",
"res = []\n",
"for c in cand:\n",
" d = hist[[\"ratio\", c]].dropna()\n",
" r, p = stats.pearsonr(d[c], d[\"ratio\"])\n",
" res.append({\"ํผ์ฒ\": c, \"์๊ด๊ณ์\": round(r, 3), \"p-value\": f\"{p:.1e}\", \"์ ์(p<0.05)\": \"์\" if p < 0.05 else \"์๋์ค\"})\n",
"res = pd.DataFrame(res)\n",
"n_sig = (res[\"์ ์(p<0.05)\"] == \"์\").sum()\n",
"print(\"\\n[์์ ๋น์จ๊ณผ ํผ์ฒ์ ์๊ด ๊ฒ์ ]\")\n",
"print(res.to_string(index=False))\n",
"print(f\"์ ์ํ ํผ์ฒ {n_sig}๊ฐ (๋ชฉํ 5๊ฐ ์ด์: {'๋ฌ์ฑ' if n_sig >= 5 else '๋ฏธ๋ฌ'})\")\n",
"t_stat, p_ex = stats.ttest_ind(hist[hist[\"exercise\"] == 1][\"ratio\"], hist[hist[\"exercise\"] == 0][\"ratio\"])\n",
"print(f\"ํ๋ จ ์๊ณผ ํ์ ์ ์์ ๋น์จ ์ฐจ์ด t๊ฒ์ : t={t_stat:.1f}, p={p_ex:.1e}\")\n",
"\n",
"# 3) ABC(ํ๋ ํ ) ๋ถ๋ฅ: ์ต๊ทผ 12๊ฐ์ ์์ ๊ธ์ก ๊ธฐ์ค\n",
"val = hist[hist[\"t\"] >= 36].groupby(\"item_id\").apply(lambda x: (x[\"demand\"] * x[\"price\"]).sum(), include_groups=False).sort_values(ascending=False)\n",
"cum = val.cumsum() / val.sum()\n",
"abc = pd.Series(np.where(cum <= 0.8, \"A\", np.where(cum <= 0.95, \"B\", \"C\")), index=val.index)\n",
"top = val.index[: int(np.ceil(len(val) * 0.2))]\n",
"print(f\"\\nABC ๋ถ๋ฅ: A {sum(abc == 'A')}๊ฐ / B {sum(abc == 'B')}๊ฐ / C {sum(abc == 'C')}๊ฐ, ์์ 20% {len(top)}๊ฐ ํ๋ชฉ์ด ์์ ๊ธ์ก์ {val[top].sum() / val.sum():.0%} ์ฐจ์ง\")\n",
"st = items.set_index(\"item_id\").loc[top]\n",
"print(\"์์ 20% ์ค ๋จ์ข
์๊ณ ยท๋จ์ข
ํ๋ชฉ:\", \", \".join(f\"{i}({s})\" for i, s in st[\"status\"].items() if s != \"์์ฐ์ค\") or \"์์\")\n",
"\n",
"# 4) EDA ์ฐจํธ 8์ข
\n",
"fig, ax = plt.subplots(2, 4, figsize=(18, 8))\n",
"tot = hist.groupby(\"t\")[\"demand\"].sum()\n",
"ax[0, 0].plot(tot.index, tot.values); ax[0, 0].set_title(\"1. Total monthly demand\")\n",
"hist.groupby([\"t\", \"group\"])[\"demand\"].sum().unstack().plot(ax=ax[0, 1], legend=False); ax[0, 1].set_title(\"2. Demand by FSC group\")\n",
"hist.boxplot(column=\"ratio\", by=\"month\", ax=ax[0, 2], grid=False); ax[0, 2].set_title(\"3. Seasonality (demand ratio)\")\n",
"ax[0, 3].scatter(hist[\"op_hours\"], hist[\"ratio\"], s=4, alpha=0.3); ax[0, 3].set_title(\"4. Operating hours vs demand ratio\")\n",
"hist.groupby(\"exercise\")[\"ratio\"].mean().plot.bar(ax=ax[1, 0]); ax[1, 0].set_title(\"5. Exercise month effect\")\n",
"ax[1, 1].plot(np.arange(1, len(cum) + 1), cum.values, \"o-\", ms=3); ax[1, 1].axhline(0.8, ls=\"--\", c=\"gray\"); ax[1, 1].set_title(\"6. Pareto curve (ABC)\")\n",
"items.assign(st=items[\"status\"].map({\"์์ฐ์ค\": \"active\", \"๋จ์ข
์๊ณ \": \"EOL notice\", \"๋จ์ข
\": \"discontinued\"})).boxplot(column=\"lead_time\", by=\"st\", ax=ax[1, 2], grid=False); ax[1, 2].set_title(\"7. Lead time by production status\")\n",
"ag = hist.groupby(\"t\").agg(av=(\"availability\", \"mean\"), d=(\"demand\", \"sum\"))\n",
"ax[1, 3].scatter(ag[\"av\"], ag[\"d\"]); ax[1, 3].set_title(\"8. Availability vs total demand\")\n",
"for a in ax.ravel():\n",
" a.set_xlabel(\"\"); a.tick_params(labelsize=7)\n",
"plt.suptitle(\"MNT02 EDA - 8 charts (synthetic data)\"); plt.tight_layout(); plt.show()\n",
"print(\"EDA ์ฐจํธ 8์ข
์์ฑ ์๋ฃ. ํต์ฌ ๋ฐ๊ฒฌ: ์ด์ฉ์๊ฐยทํ๋ จ ์์ด ์์๋ฅผ ๋์ด์ฌ๋ฆฌ๊ณ , ๋จ์ข
ํ๋ชฉ์ ๋ฆฌ๋ํ์์ด ๊ธธ์ด ๊ฒฐํ ์ํ์ด ํฌ๋ค.\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
03 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[Baseline ์์ธก ์ฑ๋ฅ - ํ๊ฐ ๊ตฌ๊ฐ 6๊ฐ์]\n",
" ๋ฐฉ๋ฒ MAE(๊ฐ) WAPE MAPE(์์ ๋ง์ ํ๋ชฉ)\n",
" MA3 7.34 28.7% 32.7%\n",
" MA6 7.09 27.7% 31.6%\n",
" SES 6.53 25.5% 28.9%\n",
"์๋
๊ฐ์ ๋ฌ 5.38 21.0% 22.8%\n",
"ํด์: ์ด๋ํ๊ท ์ ํ๋ จ ์ ๊ธ์ฆ๊ณผ ๊ณ์ ๋ณํ๋ฅผ ํ ๋ฌ ๋ฆ๊ฒ ๋ฐ๋ผ๊ฐ ์ค์ฐจ๊ฐ ์ปค์ง๋ค(๋ฏธ์
4์์ ๊ฐ์ ).\n",
"\n",
"[์์ ๋น์จ๊ณผ ํผ์ฒ์ ์๊ด ๊ฒ์ ]\n",
" ํผ์ฒ ์๊ด๊ณ์ p-value ์ ์(p<0.05)\n",
" op_hours 0.477 8.3e-110 ์\n",
" exercise 0.470 4.4e-106 ์\n",
"availability -0.450 1.6e-96 ์\n",
" lag12_ratio 0.339 3.7e-40 ์\n",
" lag1_ratio -0.111 1.5e-06 ์\n",
" t 0.066 3.8e-03 ์\n",
" winter -0.032 1.6e-01 ์๋์ค\n",
" price 0.000 1.0e+00 ์๋์ค\n",
"์ ์ํ ํผ์ฒ 6๊ฐ (๋ชฉํ 5๊ฐ ์ด์: ๋ฌ์ฑ)\n",
"ํ๋ จ ์๊ณผ ํ์ ์ ์์ ๋น์จ ์ฐจ์ด t๊ฒ์ : t=23.3, p=4.4e-106\n",
"\n",
"ABC ๋ถ๋ฅ: A 13๊ฐ / B 13๊ฐ / C 14๊ฐ, ์์ 20% 8๊ฐ ํ๋ชฉ์ด ์์ ๊ธ์ก์ 67% ์ฐจ์ง\n",
"์์ 20% ์ค ๋จ์ข
์๊ณ ยท๋จ์ข
ํ๋ชฉ: P016(๋จ์ข
), P028(๋จ์ข
), P032(๋จ์ข
์๊ณ ), P007(๋จ์ข
์๊ณ )\n",
"EDA ์ฐจํธ 8์ข
์์ฑ ์๋ฃ. ํต์ฌ ๋ฐ๊ฒฌ: ์ด์ฉ์๊ฐยทํ๋ จ ์์ด ์์๋ฅผ ๋์ด์ฌ๋ฆฌ๊ณ , ๋จ์ข
ํ๋ชฉ์ ๋ฆฌ๋ํ์์ด ๊ธธ์ด ๊ฒฐํ ์ํ์ด ํฌ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
04. ์๊ณ์ด ๋จธ์ ๋ฌ๋(Prophet/LSTM) ๊ธฐ๋ฐ ๊ฒฐ์ ๋๋น ์ผ์ ๋ถํ ์์ ์์ธก ๋ชจ๋ธ ๊ณ ๋ํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์๊ณ์ด ๋จธ์ ๋ฌ๋ ๊ธฐ๋ฐ ๊ฒฐ์ ๋๋น ์ผ์ ๋ถํ ์์ ์์ธก ๋ชจ๋ธ์ ๊ณ ๋ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์๊ณ์ด ๋จธ์ ๋ฌ๋์ผ๋ก ๊ฒฐ์ ๋๋น ์ผ์ ๋ถํ ์์ ์์ธก ๋ชจ๋ธ์ ๊ณ ๋ํํ๋ค(Prophet/LSTM์ ์คํ ํ๊ฒฝ ์ ์ฝ์ผ๋ก ํฌ์์ก ๊ทธ๋๋์ธํธ ๋ถ์คํ
์ผ๋ก ๋์ฒด).** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ง์ฐ๊ฐยท์ด๋ํ๊ท ยท๋ณ๋์ฑยท๊ณ์ ์ง์ยทํ๋ จ ๊ณํยท์ด์ฉ์๊ฐ ๋ฑ ์์ธก ์์ ์ด์ ์ ๋ณด๋ก ํ์ ํผ์ฒ 10๊ฐ ์ด์ ์์ฑํ์ฌ ์์ธ์ ๋ถ์ํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 5๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/features/mnt02_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": [
"\"\"\"๊ณผ๊ฑฐ ์์ ์ง์ฐ๊ฐยท์ด๋ํ๊ท ยท๊ณ์ ์ง์ยทํ๋ จ ๊ณํยท์ด์ฉ์๊ฐ ๋ฑ ํ์ ํผ์ฒ 15๊ฐ๋ก ์๊ณ์ด ๋จธ์ ๋ฌ๋ ์์ ์์ธก ๋ชจ๋ธ(Prophet/LSTM ๋์ ํฌ์์ก ๊ทธ๋๋์ธํธ ๋ถ์คํ
)์ ๋ง๋ค๊ณ , ์๊ณ์ด ๊ต์ฐจ๊ฒ์ฆ์ผ๋ก MAPE๋ฅผ ํ์ธํ ๋ค ์์ ํ
ํฌ ์ฆ๊ฐ(๊ฒฐ์ ๋๋น) ์ ์์๋ฅผ ์์ธกํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.ensemble import HistGradientBoostingRegressor\n",
"from sklearn.inspection import permutation_importance\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ผ์ ์ ๋น์ฐฝ ์๋ฆฌ๋ถ์ ์ฒญ๊ตฌ/์ถ๊ณ ยท์ฅ๋น ๊ฐ๋๋ฅ ๋ฐ์ดํฐ ์์ฑ๊ธฐ\n",
"# ์ค์ ๋ถ๋ยทํ๋ชฉยท์ฌ๊ณ ยท๋จ๊ฐ ์ ๋ณด์ ๋ฌด๊ดํ๋ค. ํ๋ชฉ 40๊ฐ x 48๊ฐ์(๊ฐ์ 2022-01 ~ 2025-12) ์๋ณ ์๋ฃ๋ฅผ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"FSC = {\"2520\": \"power_train\", \"2530\": \"brake_steer\", \"2910\": \"fuel\", \"2930\": \"cooling\",\n",
" \"2940\": \"filter\", \"5330\": \"gasket\", \"6140\": \"battery\", \"5961\": \"semiconductor\"}\n",
"EXERCISE = {3, 8, 10} # ๊ฐ์ ๋๊ท๋ชจ ํ๋ จ ์(ํ๋ จ ๊ณํ์ผ๋ก ๋ฏธ๋ฆฌ ์ ์ ์์)\n",
"N_MONTHS = 48\n",
"\n",
"\n",
"def season(fsc, month):\n",
" \"\"\"ํ๋ชฉ๊ตฐ๋ณ ๊ณ์ ๊ณ์: ์ถ์ ์ง๋ ๊ฒจ์ธ, ๋๊ฐ๊ณํต์ ์ฌ๋ฆ์ ์์๊ฐ ๋์ด๋๋ค.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 4] ์๊ณ์ด ๋จธ์ ๋ฌ๋(Prophet/LSTM) ๊ธฐ๋ฐ ๊ฒฐ์ ๋๋น ์ผ์ ๋ถํ ์์ ์์ธก ๋ชจ๋ธ ๊ณ ๋ํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def make_items(n=40, seed=2):\n",
" \"\"\"ํ๋ชฉ ๋ง์คํฐ: NSN(๊ฐ์), ํ๋ชฉ๊ตฐ, ์ฅ๋น ์ ํ, ๊ธฐ์ค ์์, ๋จ๊ฐ, ์กฐ๋ฌ ๋ฆฌ๋ํ์, ๊ณต๊ธ์
์ฒด ์, ์์ฐ(๋จ์ข
) ์ํ.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for i in range(n):\n",
" f = str(rng.choice(list(FSC)))\n",
" status = str(rng.choice([\"์์ฐ์ค\", \"๋จ์ข
์๊ณ \", \"๋จ์ข
\"], p=[0.7, 0.2, 0.1]))\n",
" rows.append({\"item_id\": f\"P{i + 1:03d}\", \"nsn\": f\"{f}-00-{rng.integers(100, 1000)}-{rng.integers(1000, 10000)}\",\n",
" \"fsc\": f, \"group\": FSC[f], \"equip_type\": str(rng.choice([\"A\", \"B\", \"C\"])),\n",
" \"base\": float(np.round(rng.lognormal(2.7, 0.8), 1)),\n",
" \"price\": int(rng.lognormal(12, 1.0)) // 100 * 100,\n",
" \"lead_time\": int(rng.integers(1, 7) if status == \"์์ฐ์ค\" else rng.integers(4, 13)),\n",
" \"suppliers\": int(rng.integers(1, 5)) if status == \"์์ฐ์ค\" else 1,\n",
" \"status\": status, \"alt_available\": bool(rng.random() < (0.6 if status == \"์์ฐ์ค\" else 0.3))})\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"def make_history(items, months=N_MONTHS, seed=3):\n",
" \"\"\"์๋ณ ์ฒญ๊ตฌ ์์(๊ฐ), ๊ธด๊ธ ๋ถ์ถ ๊ฑด์, ์ฅ๋น ์ ํ๋ณ ์ด์ฉ์๊ฐยท๊ฐ๋๋ฅ .\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" t = np.arange(months)\n",
" moy = t % 12 + 1\n",
" ex = np.isin(moy, list(EXERCISE))\n",
" hours = {e: (100 + 20 * rng.random(months)) * np.where(ex, 1.5, 1.0) for e in \"ABC\"}\n",
" avail = {e: np.clip(0.93 - 0.12 * (hours[e] - 100) / 80 + rng.normal(0, 0.015, months), 0.6, 0.99) for e in \"ABC\"}\n",
" rows = []\n",
" for it in items.itertuples():\n",
" trend = 1 + rng.uniform(-0.1, 0.3) * t / months\n",
" s = np.array([season(it.fsc, m) for m in moy])\n",
" lam = it.base * s * trend * hours[it.equip_type] / 110\n",
" d = rng.poisson(lam)\n",
" rows.append(pd.DataFrame({\"item_id\": it.item_id, \"t\": t, \"ym\": [f\"{2022 + k // 12}-{k % 12 + 1:02d}\" for k in t],\n",
" \"month\": moy, \"exercise\": ex.astype(int), \"op_hours\": hours[it.equip_type].round(1),\n",
" \"availability\": avail[it.equip_type].round(3), \"demand\": d,\n",
" \"emergency\": rng.binomial(d, 0.05)}))\n",
" return pd.concat(rows, ignore_index=True)\n",
"\n",
"\n",
"def make_stock(items, hist, seed=4):\n",
" \"\"\"ํ์ฌ(๊ฐ์ 2026-01) ์ฌ๊ณ : ๋ณด์ ๋๊ณผ ๋ฏธ์
๊ณ ๋ฐ์ฃผ๋(์กฐ๋ฌ ์ค์ธ ๋ฌผ๋). ๋จ์ข
ํ๋ชฉ์ ์ถ๊ฐ ๋ฐ์ฃผ๊ฐ ์ด๋ ค์ ๋ฏธ์
๊ณ ๋์ด ์ ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" recent = hist[hist[\"t\"] >= hist[\"t\"].max() - 5].groupby(\"item_id\")[\"demand\"].mean()\n",
" st = items[[\"item_id\", \"lead_time\", \"status\"]].copy()\n",
" st[\"on_hand\"] = [int(round(recent[i] * rng.uniform(0.5, 3.0))) for i in st[\"item_id\"]]\n",
" cover = np.where(st[\"status\"] == \"์์ฐ์ค\", rng.uniform(0.6, 1.3, len(st)), rng.uniform(0.0, 0.6, len(st)))\n",
" st[\"on_order\"] = [int(round(recent[i] * L * c)) for i, L, c in zip(st[\"item_id\"], st[\"lead_time\"], cover)]\n",
" return st[[\"item_id\", \"on_hand\", \"on_order\"]]\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์์ ์์ธก ๊ณตํต ํจ์(ํ์ ํผ์ฒ + ๋จธ์ ๋ฌ๋ ๋ชจ๋ธ) ----\n",
"FEATS = [\"lag1\", \"lag2\", \"lag3\", \"lag12\", \"ma3\", \"ma6\", \"std6\", \"season_idx\", \"month_sin\", \"month_cos\",\n",
" \"exercise\", \"op_hours\", \"op_ratio\", \"item_level\", \"trend_t\"]\n",
"\n",
"\n",
"def make_features(hist):\n",
" \"\"\"ํ๋ชฉ๋ณ ๊ณผ๊ฑฐ ์์๋ก ํ์ ํผ์ฒ๋ฅผ ๋ง๋ ๋ค(๋ชจ๋ ์์ธก ์์ ์ด์ ์ ๋ณด๋ง ์ฌ์ฉ).\"\"\"\n",
" df = hist.sort_values([\"item_id\", \"t\"]).reset_index(drop=True)\n",
" g = df.groupby(\"item_id\")[\"demand\"]\n",
" for k in (1, 2, 3, 12):\n",
" df[f\"lag{k}\"] = g.shift(k)\n",
" df[\"ma3\"] = g.transform(lambda s: s.shift(1).rolling(3).mean())\n",
" df[\"ma6\"] = g.transform(lambda s: s.shift(1).rolling(6).mean())\n",
" df[\"std6\"] = g.transform(lambda s: s.shift(1).rolling(6).std())\n",
" df[\"item_level\"] = g.transform(lambda s: s.shift(1).rolling(12).mean())\n",
" df[\"season_idx\"] = df[\"lag12\"] / (g.transform(lambda s: s.shift(12).rolling(12).mean()) + 1)\n",
" df[\"month_sin\"] = np.sin(2 * np.pi * df[\"month\"] / 12)\n",
" df[\"month_cos\"] = np.cos(2 * np.pi * df[\"month\"] / 12)\n",
" df[\"op_ratio\"] = df[\"op_hours\"] / df.groupby(\"item_id\")[\"op_hours\"].transform(lambda s: s.shift(1).rolling(12).mean())\n",
" df[\"trend_t\"] = df[\"t\"]\n",
" return df.dropna().reset_index(drop=True)\n",
"\n",
"\n",
"def new_model():\n",
" \"\"\"Prophet/LSTM ๋์ ์ฌ์ฉํ๋ ํฌ์์ก ์์ค ๊ทธ๋๋์ธํธ ๋ถ์คํ
(๊ฐ์ ์๋ฃ์ ์ ํฉ).\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 4] ์๊ณ์ด ๋จธ์ ๋ฌ๋(Prophet/LSTM) ๊ธฐ๋ฐ ๊ฒฐ์ ๋๋น ์ผ์ ๋ถํ ์์ ์์ธก ๋ชจ๋ธ ๊ณ ๋ํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def mape(y, p, min_y=1):\n",
" y, p = np.asarray(y, float), np.asarray(p, float)\n",
" m = y >= min_y\n",
" return float(np.mean(np.abs(y[m] - p[m]) / y[m]))\n",
"\n",
"items = make_items()\n",
"hist = make_history(items)\n",
"df = make_features(hist)\n",
"ab = set(items[items[\"base\"] >= 10][\"item_id\"]) # ์์๊ฐ ์ถฉ๋ถํ ํ๋ชฉ(MAPE ํ๊ฐ ๋์)\n",
"print(f\"ํ์ ํผ์ฒ {len(FEATS)}๊ฐ (๋ชฉํ 10๊ฐ ์ด์): {', '.join(FEATS)}\")\n",
"\n",
"# 1) ์๊ณ์ด ๊ต์ฐจ๊ฒ์ฆ(ํ์ฅ ์ฐฝ): ๊ณผ๊ฑฐ๋ก ํ์ต -> ๋ฐ๋ก ๋ค์ 6๊ฐ์ ํ๊ฐ, 3ํ ๋ฐ๋ณต\n",
"rows = []\n",
"for cut in (30, 36, 42):\n",
" tr, te = df[df[\"t\"] < cut], df[(df[\"t\"] >= cut) & (df[\"t\"] < cut + 6)]\n",
" p = new_model().fit(tr[FEATS], tr[\"demand\"]).predict(te[FEATS])\n",
" big = te[\"item_id\"].isin(ab).to_numpy()\n",
" rows.append({\"ํ์ต ๊ตฌ๊ฐ\": f\"t<{cut}\", \"ํ๊ฐ ๊ตฌ๊ฐ\": f\"t={cut}~{cut + 5}\",\n",
" \"ML MAE\": round(np.abs(te[\"demand\"] - p).mean(), 2), \"MA3 MAE\": round(np.abs(te[\"demand\"] - te[\"ma3\"]).mean(), 2),\n",
" \"ML MAPE\": mape(te[\"demand\"][big], p[big]), \"MA3 MAPE\": mape(te[\"demand\"][big], te[\"ma3\"][big])})\n",
"cv = pd.DataFrame(rows)\n",
"print(\"\\n[์๊ณ์ด ๊ต์ฐจ๊ฒ์ฆ ๊ฒฐ๊ณผ - MAPE๋ ์์ ๋ง์ ํ๋ชฉ ๊ธฐ์ค]\")\n",
"print(cv.assign(**{\"ML MAPE\": cv[\"ML MAPE\"].map(\"{:.1%}\".format), \"MA3 MAPE\": cv[\"MA3 MAPE\"].map(\"{:.1%}\".format)}).to_string(index=False))\n",
"m_mape = cv[\"ML MAPE\"].mean()\n",
"print(f\"ํ๊ท MAPE: ML {m_mape:.1%} / MA3 {cv['MA3 MAPE'].mean():.1%} -> ๋ชฉํ(MAPE 25% ์ดํ): {'๋ฌ์ฑ' if m_mape <= 0.25 else '๋ฏธ๋ฌ'}\")\n",
"\n",
"# 2) ์ต์ข
๋ชจ๋ธ(t<42 ํ์ต)๊ณผ ํผ์ฒ ์ค์๋(์์ด ์ค์๋)\n",
"tr, te = df[df[\"t\"] < 42], df[df[\"t\"] >= 42]\n",
"model = new_model().fit(tr[FEATS], tr[\"demand\"])\n",
"imp = permutation_importance(model, te[FEATS], te[\"demand\"], n_repeats=5, random_state=0, scoring=\"neg_mean_absolute_error\")\n",
"imp = pd.Series(imp.importances_mean, index=FEATS).sort_values(ascending=False)\n",
"print(\"\\n[ํผ์ฒ ์ค์๋ ์์ 6๊ฐ(MAE ์ฆ๊ฐ๋)]\", \", \".join(f\"{k} {v:.2f}\" for k, v in imp.head(6).items()))\n",
"print(\"์ค์๋๊ฐ 0์ ๊ฐ๊น์ด ํผ์ฒ(์ฐจ์ ์ถ์ ํ๋ณด):\", \", \".join(imp[imp < 0.02].index) or \"์์\")\n",
"\n",
"# 3) ๊ฒฐ์ ๋๋น ์๋๋ฆฌ์ค: ๋ค์ ๋ฌ(๊ฐ์ 2026-01) ์ด์ฉ์๊ฐ 1.5๋ฐฐ + ํ๋ จ ์ ์ง์ \n",
"future = []\n",
"for it in items.itertuples():\n",
" last = hist[(hist[\"item_id\"] == it.item_id) & (hist[\"t\"] == 47)].iloc[0]\n",
" future.append({**last.to_dict(), \"t\": 48, \"ym\": \"2026-01\", \"month\": 1, \"demand\": 0, \"exercise\": 0})\n",
"fut = pd.DataFrame(future)\n",
"fut[\"op_hours\"] = hist[hist[\"t\"] == 36].set_index(\"item_id\").loc[fut[\"item_id\"], \"op_hours\"].to_numpy() # ์๋
1์ ๊ณํ ์ด์ฉ์๊ฐ\n",
"res = {}\n",
"for name, mult, ex in [(\"ํ์ ๊ณํ\", 1.0, 0), (\"์์ ํ
ํฌ 1.5๋ฐฐ\", 1.5, 1)]:\n",
" f2 = fut.assign(op_hours=fut[\"op_hours\"] * mult, exercise=ex)\n",
" X = make_features(pd.concat([hist, f2], ignore_index=True))\n",
" X = X[X[\"t\"] == 48]\n",
" res[name] = pd.Series(model.predict(X[FEATS]), index=X[\"item_id\"])\n",
"comp = pd.DataFrame(res).round(1)\n",
"comp[\"์ฆ๊ฐ์จ\"] = (comp[\"์์ ํ
ํฌ 1.5๋ฐฐ\"] / comp[\"ํ์ ๊ณํ\"] - 1).map(\"{:+.0%}\".format)\n",
"comp = comp.join(items.set_index(\"item_id\")[[\"group\", \"status\"]])\n",
"print(\"\\n[๊ฒฐ์ ๋๋น ๋ค์ ๋ฌ ์์ ์์ธก - ์ฆ๊ฐ๋ ์์ 6๊ฐ ํ๋ชฉ]\")\n",
"print(comp.loc[(comp[\"์์ ํ
ํฌ 1.5๋ฐฐ\"] - comp[\"ํ์ ๊ณํ\"]).sort_values(ascending=False).index[:6]].to_string())\n",
"print(f\"์ ์ฒด ์์ธก ์์: ํ์ {comp['ํ์ ๊ณํ'].sum():.0f}๊ฐ -> ์์ ํ
ํฌ 1.5๋ฐฐ {comp['์์ ํ
ํฌ 1.5๋ฐฐ'].sum():.0f}๊ฐ\")\n",
"\n",
"# 4) ์์ธก ๊ทธ๋ฆผ: ์์ ๋ง์ ํ๋ชฉ 1๊ฐ\n",
"iid = items.sort_values(\"base\")[\"item_id\"].iloc[-1]\n",
"s = te[te[\"item_id\"] == iid]\n",
"full = df[df[\"item_id\"] == iid]\n",
"plt.figure(figsize=(9, 4))\n",
"plt.plot(full[\"t\"], full[\"demand\"], \"k.-\", label=\"actual\")\n",
"plt.plot(s[\"t\"], model.predict(s[FEATS]), \"r-o\", label=\"ML forecast\")\n",
"plt.plot(s[\"t\"], s[\"ma3\"], \"b--\", label=\"MA3 baseline\")\n",
"plt.axvline(41.5, c=\"gray\", ls=\":\"); plt.xlabel(\"month index\"); plt.ylabel(\"demand (EA)\")\n",
"plt.title(f\"{iid} one-month-ahead forecast (synthetic)\"); plt.legend(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
04 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"ํ์ ํผ์ฒ 15๊ฐ (๋ชฉํ 10๊ฐ ์ด์): lag1, lag2, lag3, lag12, ma3, ma6, std6, season_idx, month_sin, month_cos, exercise, op_hours, op_ratio, item_level, trend_t\n",
"\n",
"[์๊ณ์ด ๊ต์ฐจ๊ฒ์ฆ ๊ฒฐ๊ณผ - MAPE๋ ์์ ๋ง์ ํ๋ชฉ ๊ธฐ์ค]\n",
"ํ์ต ๊ตฌ๊ฐ ํ๊ฐ ๊ตฌ๊ฐ ML MAE MA3 MAE ML MAPE MA3 MAPE\n",
" t<30 t=30~35 5.85 7.26 25.8% 30.9%\n",
" t<36 t=36~41 4.84 7.26 20.5% 30.9%\n",
" t<42 t=42~47 4.57 7.34 20.8% 32.7%\n",
"ํ๊ท MAPE: ML 22.3% / MA3 31.5% -> ๋ชฉํ(MAPE 25% ์ดํ): ๋ฌ์ฑ\n",
"\n",
"[ํผ์ฒ ์ค์๋ ์์ 6๊ฐ(MAE ์ฆ๊ฐ๋)] lag12 7.83, item_level 7.20, op_hours 0.95, std6 0.27, ma3 0.19, op_ratio 0.13\n",
"์ค์๋๊ฐ 0์ ๊ฐ๊น์ด ํผ์ฒ(์ฐจ์ ์ถ์ ํ๋ณด): ma6, trend_t, lag2\n",
"\n",
"[๊ฒฐ์ ๋๋น ๋ค์ ๋ฌ ์์ ์์ธก - ์ฆ๊ฐ๋ ์์ 6๊ฐ ํ๋ชฉ]\n",
" ํ์ ๊ณํ ์์ ํ
ํฌ 1.5๋ฐฐ ์ฆ๊ฐ์จ group status\n",
"item_id \n",
"P016 138.9 168.2 +21% gasket ๋จ์ข
\n",
"P028 74.1 99.5 +34% power_train ๋จ์ข
\n",
"P033 73.3 94.1 +28% fuel ์์ฐ์ค\n",
"P035 78.5 94.1 +20% brake_steer ์์ฐ์ค\n",
"P030 43.4 56.8 +31% brake_steer ๋จ์ข
์๊ณ \n",
"P026 15.6 24.3 +56% cooling ์์ฐ์ค\n",
"์ ์ฒด ์์ธก ์์: ํ์ 946๊ฐ -> ์์ ํ
ํฌ 1.5๋ฐฐ 1210๊ฐ\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
05. ์ ์ ์์ ์ฌ๊ณ ๋ ์ฐ์ถ ๋ฐ ๊ฒฐํ ์ํ ์ง์(Stockout Risk) ์์ง ๊ฐ๋ฐ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ ์ ์์ ์ฌ๊ณ ๋ ์ฐ์ถ ๋ฐ ๊ฒฐํ ์ํ ์ง์ ์์ง์ ๊ฐ๋ฐํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์์๋ธ ์์ ์์ธก์ผ๋ก ํ๋ชฉ๋ณ ์ ์ ์์ ์ฌ๊ณ ยท์ฌ์ฃผ๋ฌธ์ ์ ์ฐ์ถํ๊ณ ๋จ์ข
์ํ์ ๋ฐ์ํ ๊ฒฐํ ์ํ ์ง์(Stockout Risk) ์์ง์ ๊ฐ๋ฐํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **ํฌ์์ก ๊ทธ๋๋์ธํธ ๋ถ์คํ
๊ณผ ๋๋คํฌ๋ ์คํธ๋ฅผ ์์๋ธํ๊ณ ๊ฐ์ค์น๋ ํ์ต ๊ธฐ๊ฐ ์์ ๊ฒ์ฆ ๊ตฌ๊ฐ์ผ๋ก ๊ณ ๋ฅธ๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/mnt02_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": [
"\"\"\"ํฌ์์ก ๊ทธ๋๋์ธํธ ๋ถ์คํ
ยท๋๋คํฌ๋ ์คํธ ์์๋ธ๋ก ์์ ์์ธก์ ๊ณ ๋ํํด ์ด๋ํ๊ท ๊ธฐ์ค ๋ชจ๋ธ ๋๋น MAE/RMSE ๊ฐ์ ์จ๊ณผ R2๋ฅผ ๊ฒ์ฆํ๊ณ , ์์ธก ์ค์ฐจ๋ก ํ๋ชฉ๋ณ ์ ์ ์์ ์ฌ๊ณ ยท์ฌ์ฃผ๋ฌธ์ ์ ์ฐ์ถํ ๋ค ๋ฆฌ๋ํ์ ๊ฒฐํ ํ๋ฅ ๊ณผ ๋จ์ข
์ํยท๋จ์ผ ๊ณต๊ธ์
์ฒดยท๋์ฒดํ ๋ถ์ฌ๋ฅผ ๊ฒฐํฉํ ๊ฒฐํ ์ํ ์ง์ ์์ง์ ๋ง๋ ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy.stats import norm\n",
"from sklearn.ensemble import HistGradientBoostingRegressor, RandomForestRegressor\n",
"from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ผ์ ์ ๋น์ฐฝ ์๋ฆฌ๋ถ์ ์ฒญ๊ตฌ/์ถ๊ณ ยท์ฅ๋น ๊ฐ๋๋ฅ ๋ฐ์ดํฐ ์์ฑ๊ธฐ\n",
"# ์ค์ ๋ถ๋ยทํ๋ชฉยท์ฌ๊ณ ยท๋จ๊ฐ ์ ๋ณด์ ๋ฌด๊ดํ๋ค. ํ๋ชฉ 40๊ฐ x 48๊ฐ์(๊ฐ์ 2022-01 ~ 2025-12) ์๋ณ ์๋ฃ๋ฅผ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"FSC = {\"2520\": \"power_train\", \"2530\": \"brake_steer\", \"2910\": \"fuel\", \"2930\": \"cooling\",\n",
" \"2940\": \"filter\", \"5330\": \"gasket\", \"6140\": \"battery\", \"5961\": \"semiconductor\"}\n",
"EXERCISE = {3, 8, 10} # ๊ฐ์ ๋๊ท๋ชจ ํ๋ จ ์(ํ๋ จ ๊ณํ์ผ๋ก ๋ฏธ๋ฆฌ ์ ์ ์์)\n",
"N_MONTHS = 48\n",
"\n",
"\n",
"def season(fsc, month):\n",
" \"\"\"ํ๋ชฉ๊ตฐ๋ณ ๊ณ์ ๊ณ์: ์ถ์ ์ง๋ ๊ฒจ์ธ, ๋๊ฐ๊ณํต์ ์ฌ๋ฆ์ ์์๊ฐ ๋์ด๋๋ค.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 5] ์ ์ ์์ ์ฌ๊ณ ๋ ์ฐ์ถ ๋ฐ ๊ฒฐํ ์ํ ์ง์(Stockout Risk) ์์ง ๊ฐ๋ฐ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def make_items(n=40, seed=2):\n",
" \"\"\"ํ๋ชฉ ๋ง์คํฐ: NSN(๊ฐ์), ํ๋ชฉ๊ตฐ, ์ฅ๋น ์ ํ, ๊ธฐ์ค ์์, ๋จ๊ฐ, ์กฐ๋ฌ ๋ฆฌ๋ํ์, ๊ณต๊ธ์
์ฒด ์, ์์ฐ(๋จ์ข
) ์ํ.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for i in range(n):\n",
" f = str(rng.choice(list(FSC)))\n",
" status = str(rng.choice([\"์์ฐ์ค\", \"๋จ์ข
์๊ณ \", \"๋จ์ข
\"], p=[0.7, 0.2, 0.1]))\n",
" rows.append({\"item_id\": f\"P{i + 1:03d}\", \"nsn\": f\"{f}-00-{rng.integers(100, 1000)}-{rng.integers(1000, 10000)}\",\n",
" \"fsc\": f, \"group\": FSC[f], \"equip_type\": str(rng.choice([\"A\", \"B\", \"C\"])),\n",
" \"base\": float(np.round(rng.lognormal(2.7, 0.8), 1)),\n",
" \"price\": int(rng.lognormal(12, 1.0)) // 100 * 100,\n",
" \"lead_time\": int(rng.integers(1, 7) if status == \"์์ฐ์ค\" else rng.integers(4, 13)),\n",
" \"suppliers\": int(rng.integers(1, 5)) if status == \"์์ฐ์ค\" else 1,\n",
" \"status\": status, \"alt_available\": bool(rng.random() < (0.6 if status == \"์์ฐ์ค\" else 0.3))})\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"def make_history(items, months=N_MONTHS, seed=3):\n",
" \"\"\"์๋ณ ์ฒญ๊ตฌ ์์(๊ฐ), ๊ธด๊ธ ๋ถ์ถ ๊ฑด์, ์ฅ๋น ์ ํ๋ณ ์ด์ฉ์๊ฐยท๊ฐ๋๋ฅ .\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" t = np.arange(months)\n",
" moy = t % 12 + 1\n",
" ex = np.isin(moy, list(EXERCISE))\n",
" hours = {e: (100 + 20 * rng.random(months)) * np.where(ex, 1.5, 1.0) for e in \"ABC\"}\n",
" avail = {e: np.clip(0.93 - 0.12 * (hours[e] - 100) / 80 + rng.normal(0, 0.015, months), 0.6, 0.99) for e in \"ABC\"}\n",
" rows = []\n",
" for it in items.itertuples():\n",
" trend = 1 + rng.uniform(-0.1, 0.3) * t / months\n",
" s = np.array([season(it.fsc, m) for m in moy])\n",
" lam = it.base * s * trend * hours[it.equip_type] / 110\n",
" d = rng.poisson(lam)\n",
" rows.append(pd.DataFrame({\"item_id\": it.item_id, \"t\": t, \"ym\": [f\"{2022 + k // 12}-{k % 12 + 1:02d}\" for k in t],\n",
" \"month\": moy, \"exercise\": ex.astype(int), \"op_hours\": hours[it.equip_type].round(1),\n",
" \"availability\": avail[it.equip_type].round(3), \"demand\": d,\n",
" \"emergency\": rng.binomial(d, 0.05)}))\n",
" return pd.concat(rows, ignore_index=True)\n",
"\n",
"\n",
"def make_stock(items, hist, seed=4):\n",
" \"\"\"ํ์ฌ(๊ฐ์ 2026-01) ์ฌ๊ณ : ๋ณด์ ๋๊ณผ ๋ฏธ์
๊ณ ๋ฐ์ฃผ๋(์กฐ๋ฌ ์ค์ธ ๋ฌผ๋). ๋จ์ข
ํ๋ชฉ์ ์ถ๊ฐ ๋ฐ์ฃผ๊ฐ ์ด๋ ค์ ๋ฏธ์
๊ณ ๋์ด ์ ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" recent = hist[hist[\"t\"] >= hist[\"t\"].max() - 5].groupby(\"item_id\")[\"demand\"].mean()\n",
" st = items[[\"item_id\", \"lead_time\", \"status\"]].copy()\n",
" st[\"on_hand\"] = [int(round(recent[i] * rng.uniform(0.5, 3.0))) for i in st[\"item_id\"]]\n",
" cover = np.where(st[\"status\"] == \"์์ฐ์ค\", rng.uniform(0.6, 1.3, len(st)), rng.uniform(0.0, 0.6, len(st)))\n",
" st[\"on_order\"] = [int(round(recent[i] * L * c)) for i, L, c in zip(st[\"item_id\"], st[\"lead_time\"], cover)]\n",
" return st[[\"item_id\", \"on_hand\", \"on_order\"]]\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์์ ์์ธก ๊ณตํต ํจ์(ํ์ ํผ์ฒ + ๋จธ์ ๋ฌ๋ ๋ชจ๋ธ) ----\n",
"FEATS = [\"lag1\", \"lag2\", \"lag3\", \"lag12\", \"ma3\", \"ma6\", \"std6\", \"season_idx\", \"month_sin\", \"month_cos\",\n",
" \"exercise\", \"op_hours\", \"op_ratio\", \"item_level\", \"trend_t\"]\n",
"\n",
"\n",
"def make_features(hist):\n",
" \"\"\"ํ๋ชฉ๋ณ ๊ณผ๊ฑฐ ์์๋ก ํ์ ํผ์ฒ๋ฅผ ๋ง๋ ๋ค(๋ชจ๋ ์์ธก ์์ ์ด์ ์ ๋ณด๋ง ์ฌ์ฉ).\"\"\"\n",
" df = hist.sort_values([\"item_id\", \"t\"]).reset_index(drop=True)\n",
" g = df.groupby(\"item_id\")[\"demand\"]\n",
" for k in (1, 2, 3, 12):\n",
" df[f\"lag{k}\"] = g.shift(k)\n",
" df[\"ma3\"] = g.transform(lambda s: s.shift(1).rolling(3).mean())\n",
" df[\"ma6\"] = g.transform(lambda s: s.shift(1).rolling(6).mean())\n",
" df[\"std6\"] = g.transform(lambda s: s.shift(1).rolling(6).std())\n",
" df[\"item_level\"] = g.transform(lambda s: s.shift(1).rolling(12).mean())\n",
" df[\"season_idx\"] = df[\"lag12\"] / (g.transform(lambda s: s.shift(12).rolling(12).mean()) + 1)\n",
" df[\"month_sin\"] = np.sin(2 * np.pi * df[\"month\"] / 12)\n",
" df[\"month_cos\"] = np.cos(2 * np.pi * df[\"month\"] / 12)\n",
" df[\"op_ratio\"] = df[\"op_hours\"] / df.groupby(\"item_id\")[\"op_hours\"].transform(lambda s: s.shift(1).rolling(12).mean())\n",
" df[\"trend_t\"] = df[\"t\"]\n",
" return df.dropna().reset_index(drop=True)\n",
"\n",
"\n",
"def new_model():\n",
" \"\"\"Prophet/LSTM ๋์ ์ฌ์ฉํ๋ ํฌ์์ก ์์ค ๊ทธ๋๋์ธํธ ๋ถ์คํ
(๊ฐ์ ์๋ฃ์ ์ ํฉ).\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 5] ์ ์ ์์ ์ฌ๊ณ ๋ ์ฐ์ถ ๋ฐ ๊ฒฐํ ์ํ ์ง์(Stockout Risk) ์์ง ๊ฐ๋ฐ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def mape(y, p, min_y=1):\n",
" y, p = np.asarray(y, float), np.asarray(p, float)\n",
" m = y >= min_y\n",
" return float(np.mean(np.abs(y[m] - p[m]) / y[m]))\n",
"\n",
"# ---- ์์๋ธ ์์ ์์ธกยท์์ ์ฌ๊ณ ยท๊ฒฐํ(๋จ์ข
) ์ํ ์ง์ ๊ณตํต ํจ์ ----\n",
"STATUS_W = {\"์์ฐ์ค\": 0.0, \"๋จ์ข
์๊ณ \": 0.5, \"๋จ์ข
\": 1.0} # [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋จ์ข
์ํ ๊ฐ์ค์น\n",
"\n",
"\n",
"def fit_ens(tr):\n",
" \"\"\"ํฌ์์ก ๊ทธ๋๋์ธํธ ๋ถ์คํ
(70%) + ๋๋คํฌ๋ ์คํธ(30%) ์์๋ธ.\"\"\"\n",
" h = new_model().fit(tr[FEATS], tr[\"demand\"])\n",
" r = RandomForestRegressor(n_estimators=150, min_samples_leaf=5, random_state=0, n_jobs=1).fit(tr[FEATS], tr[\"demand\"])\n",
" return h, r\n",
"\n",
"\n",
"def pred_ens(m, X, w=0.7):\n",
" return np.clip(w * m[0].predict(X[FEATS]) + (1 - w) * m[1].predict(X[FEATS]), 0, None)\n",
"\n",
"\n",
"def next_month(hist, items, t_new=48, op_mult=1.0, ex=0):\n",
" \"\"\"๋ค์ ๋ฌ(t_new) ์์ธก์ฉ ํ: ์๋
๊ฐ์ ๋ฌ ์ด์ฉ์๊ฐ x ๋ฐฐ์, ํ๋ จ ์ฌ๋ถ ์ง์ .\"\"\"\n",
" last = hist[hist[\"t\"] == t_new - 1].copy()\n",
" prev = hist[hist[\"t\"] == t_new - 12].set_index(\"item_id\")[\"op_hours\"]\n",
" m = (t_new % 12) + 1\n",
" f = last.assign(t=t_new, ym=f\"{2022 + t_new // 12}-{m:02d}\", month=m, demand=0, exercise=ex,\n",
" op_hours=last[\"item_id\"].map(prev).to_numpy() * op_mult)\n",
" X = make_features(pd.concat([hist, f], ignore_index=True))\n",
" return X[X[\"t\"] == t_new].set_index(\"item_id\").loc[items[\"item_id\"]].reset_index()\n",
"\n",
"\n",
"def stock_policy(items, mu, sigma, z=1.65, review=1):\n",
" \"\"\"์์ ์ฌ๊ณ = z x ์ ์์ธก์ค์ฐจ ํ์คํธ์ฐจ x sqrt(๋ฆฌ๋ํ์+๊ฒํ ์ฃผ๊ธฐ), ์ฌ์ฃผ๋ฌธ์ = ์ ์์ x (๋ฆฌ๋ํ์+๊ฒํ ์ฃผ๊ธฐ) + ์์ ์ฌ๊ณ .\"\"\"\n",
" L = items[\"lead_time\"].to_numpy() + review\n",
" ss = z * sigma * np.sqrt(L)\n",
" return pd.DataFrame({\"item_id\": items[\"item_id\"], \"mu\": mu, \"sigma\": sigma, \"L\": L,\n",
" \"ss\": np.ceil(ss), \"rop\": np.ceil(mu * L + ss)})\n",
"\n",
"\n",
"def risk_index(items, pol, stock):\n",
" \"\"\"๊ฒฐํ ์ํ ์ง์(0~100): ๋ฆฌ๋ํ์ ๋์ ์์๊ฐ ๊ฐ์ฉ ์ฌ๊ณ ๋ฅผ ๋์ ํ๋ฅ (์ ๊ท ๊ทผ์ฌ)์ ๊ธฐ๋ณธ์ผ๋ก,\n",
" ๋จ์ข
์ํยท๋จ์ผ ๊ณต๊ธ์
์ฒดยท๋์ฒดํ ๋ถ์ฌ์ ๊ฐ์ค์ ์ค๋ค(๊ฐ์ ๊ฐ์ค์น).\"\"\"\n",
" st = stock.set_index(\"item_id\").loc[items[\"item_id\"]]\n",
" avail = (st[\"on_hand\"] + st[\"on_order\"]).to_numpy()\n",
" p = 1 - norm.cdf((avail - pol[\"mu\"].to_numpy() * pol[\"L\"].to_numpy()) / (pol[\"sigma\"].to_numpy() * np.sqrt(pol[\"L\"].to_numpy()) + 1e-6))\n",
" sw = items[\"status\"].map(STATUS_W).to_numpy()\n",
" sup = (items[\"suppliers\"].to_numpy() == 1).astype(float)\n",
" alt = (~items[\"alt_available\"].to_numpy()).astype(float)\n",
" score = 100 * np.clip(0.55 * p + 0.25 * sw + 0.10 * sup + 0.10 * alt, 0, 1)\n",
" return pd.DataFrame({\"item_id\": items[\"item_id\"], \"p_short\": p, \"status_w\": sw, \"single_sup\": sup, \"no_alt\": alt,\n",
" \"avail\": avail, \"risk\": score.round(1)})\n",
"\n",
"items = make_items()\n",
"hist = make_history(items)\n",
"df = make_features(hist)\n",
"stock = make_stock(items, hist)\n",
"\n",
"# 1) Hold-out(t=42~47) ์ฑ๋ฅ ๋น๊ต: ๊ธฐ์ค ๋ชจ๋ธ(MA3) / ๋จ์ผ ๋ชจ๋ธ / ์์๋ธ. ์์๋ธ ๊ฐ์ค์น๋ t=36~41 ๊ฒ์ฆ ๊ตฌ๊ฐ์์ ๊ณ ๋ฅธ๋ค\n",
"inner = fit_ens(df[df[\"t\"] < 36]); vv = df[(df[\"t\"] >= 36) & (df[\"t\"] < 42)]\n",
"best_w = min((0.5, 0.6, 0.7, 0.8, 0.9, 1.0), key=lambda w: mean_absolute_error(vv[\"demand\"], pred_ens(inner, vv, w)))\n",
"tr, te = df[df[\"t\"] < 42], df[df[\"t\"] >= 42].copy()\n",
"ens = fit_ens(tr)\n",
"te[\"ens\"], te[\"hgb\"] = pred_ens(ens, te, best_w), np.clip(ens[0].predict(te[FEATS]), 0, None)\n",
"rows = {}\n",
"for nm, col in [(\"๊ธฐ์ค ๋ชจ๋ธ(MA3)\", \"ma3\"), (\"๋จ์ผ ํฌ์์ก GBM\", \"hgb\"), (f\"์์๋ธ(w={best_w})\", \"ens\")]:\n",
" rows[nm] = {\"MAE\": mean_absolute_error(te[\"demand\"], te[col]), \"RMSE\": mean_squared_error(te[\"demand\"], te[col]) ** 0.5,\n",
" \"R2\": r2_score(te[\"demand\"], te[col])}\n",
"res = pd.DataFrame(rows).T\n",
"print(\"[Hold-out 6๊ฐ์(t=42~47), ํ๋ชฉ 40๊ฐ]\")\n",
"print(res.to_string(float_format=lambda x: f\"{x:.3f}\"))\n",
"b, e = res.iloc[0], res.iloc[2]\n",
"im, ir = 1 - e[\"MAE\"] / b[\"MAE\"], 1 - e[\"RMSE\"] / b[\"RMSE\"]\n",
"print(f\"์ค์ฐจ์จ ๊ฐ์ MAE {im:.1%}, RMSE {ir:.1%} (๋ชฉํ 20% ์ด์: {'๋ฌ์ฑ' if min(im, ir) >= 0.2 else '๋ฏธ๋ฌ'}), R2 {e['R2']:.3f} (๋ชฉํ 0.85 ์ด์: {'๋ฌ์ฑ' if e['R2'] >= 0.85 else '๋ฏธ๋ฌ'})\")\n",
"spike = te[\"exercise\"] == 1\n",
"print(f\"๋ถ๊ท์น ์์(ํ๋ จ ์) MAE: ๊ธฐ์ค {mean_absolute_error(te.loc[spike, 'demand'], te.loc[spike, 'ma3']):.2f} -> ์์๋ธ {mean_absolute_error(te.loc[spike, 'demand'], te.loc[spike, 'ens']):.2f}\")\n",
"\n",
"# 2) ์์ ์ฌ๊ณ ยท์ฌ์ฃผ๋ฌธ์ : ๋ค์ ๋ฌ ์์ธก๊ณผ Hold-out ํ๋ชฉ๋ณ ์ค์ฐจ ํ์คํธ์ฐจ\n",
"X = next_month(hist, items)\n",
"mu = pred_ens(fit_ens(df), X, best_w)\n",
"err = (te[\"demand\"] - te[\"ens\"]).groupby(te[\"item_id\"]).std().reindex(items[\"item_id\"]).to_numpy()\n",
"sigma = np.maximum(err, np.sqrt(np.maximum(mu, 1))) # ์ค์ฐจ ํ์คํธ์ฐจ๊ฐ ํฌ์์ก ๋ณ๋๋ณด๋ค ์์ผ๋ฉด ํฌ์์ก ๊ธฐ์ค ์ฌ์ฉ\n",
"pol = stock_policy(items, mu, sigma)\n",
"risk = risk_index(items, pol, stock)\n",
"out = items[[\"item_id\", \"group\", \"status\", \"lead_time\", \"suppliers\"]].merge(pol, on=\"item_id\").merge(risk, on=\"item_id\")\n",
"print(f\"\\n[๋ค์ ๋ฌ(๊ฐ์ 2026-01) ์์ ์ฌ๊ณ ์ ์ฑ
] ์๋น์ค ์์ค 95%(z=1.65), ์์ ์ฌ๊ณ ํฉ {out['ss'].sum():.0f}๊ฐ\")\n",
"top = out.sort_values(\"risk\", ascending=False).head(8)\n",
"show = top[[\"item_id\", \"group\", \"status\", \"lead_time\", \"mu\", \"ss\", \"rop\", \"avail\", \"p_short\", \"risk\"]].copy()\n",
"show.columns = [\"ํ๋ชฉ\", \"ํ๋ชฉ๊ตฐ\", \"์์ฐ์ํ\", \"LT(์)\", \"์์์\", \"์์ ์ฌ๊ณ \", \"์ฌ์ฃผ๋ฌธ์ \", \"๊ฐ์ฉ์ฌ๊ณ \", \"๊ฒฐํํ๋ฅ \", \"์ํ์ง์\"]\n",
"print(\"[๊ฒฐํ ์ํ ์ง์ ์์ 8๊ฐ]\")\n",
"print(show.to_string(index=False, float_format=lambda x: f\"{x:.2f}\"))\n",
"grade = pd.cut(out[\"risk\"], [-1, 30, 60, 101], labels=[\"๊ด์ฐฐ\", \"์ฃผ์\", \"๊ฒฝ๋ณด\"]).value_counts()\n",
"print(f\"์ํ ๋ฑ๊ธ: ๊ฒฝ๋ณด {grade['๊ฒฝ๋ณด']}๊ฐ, ์ฃผ์ {grade['์ฃผ์']}๊ฐ, ๊ด์ฐฐ {grade['๊ด์ฐฐ']}๊ฐ / ์ฌ์ฃผ๋ฌธ์ ๋ฏธ๋ง ํ๋ชฉ {(out['avail'] < out['rop']).sum()}๊ฐ\")\n",
"print(\"๋จ์ข
ยท๋จ์ข
์๊ณ ํ๋ชฉ์ ๋ฆฌ๋ํ์์ด ๊ธธ๊ณ ๊ณต๊ธ์
์ฒด๊ฐ 1๊ณณ์ด๋ผ ๊ฐ์ ๊ฒฐํ ํ๋ฅ ์์๋ ์ํ ์ง์๊ฐ ๋๊ฒ ๋์จ๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(12, 4))\n",
"ax[0].scatter(te[\"demand\"], te[\"ma3\"], s=8, alpha=0.5, label=\"MA3 baseline\"); ax[0].scatter(te[\"demand\"], te[\"ens\"], s=8, alpha=0.5, label=\"ensemble\")\n",
"mx = te[\"demand\"].max(); ax[0].plot([0, mx], [0, mx], \"k--\", lw=1); ax[0].set_xlabel(\"actual\"); ax[0].set_ylabel(\"forecast\"); ax[0].legend()\n",
"ax[0].set_title(\"Hold-out actual vs forecast\")\n",
"col = out[\"status\"].map({\"์์ฐ์ค\": \"tab:green\", \"๋จ์ข
์๊ณ \": \"tab:orange\", \"๋จ์ข
\": \"tab:red\"})\n",
"ax[1].scatter(out[\"p_short\"], out[\"risk\"], c=col); ax[1].set_xlabel(\"shortage probability over lead time\"); ax[1].set_ylabel(\"risk index\")\n",
"ax[1].set_title(\"Stockout risk (green=active, orange=EOL notice, red=discontinued)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
05 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[Hold-out 6๊ฐ์(t=42~47), ํ๋ชฉ 40๊ฐ]\n",
" MAE RMSE R2\n",
"๊ธฐ์ค ๋ชจ๋ธ(MA3) 7.339 11.130 0.839\n",
"๋จ์ผ ํฌ์์ก GBM 4.572 6.112 0.952\n",
"์์๋ธ(w=0.5) 4.541 6.059 0.952\n",
"์ค์ฐจ์จ ๊ฐ์ MAE 38.1%, RMSE 45.6% (๋ชฉํ 20% ์ด์: ๋ฌ์ฑ), R2 0.952 (๋ชฉํ 0.85 ์ด์: ๋ฌ์ฑ)\n",
"๋ถ๊ท์น ์์(ํ๋ จ ์) MAE: ๊ธฐ์ค 9.88 -> ์์๋ธ 4.94\n",
"\n",
"[๋ค์ ๋ฌ(๊ฐ์ 2026-01) ์์ ์ฌ๊ณ ์ ์ฑ
] ์๋น์ค ์์ค 95%(z=1.65), ์์ ์ฌ๊ณ ํฉ 926๊ฐ\n",
"[๊ฒฐํ ์ํ ์ง์ ์์ 8๊ฐ]\n",
" ํ๋ชฉ ํ๋ชฉ๊ตฐ ์์ฐ์ํ LT(์) ์์์ ์์ ์ฌ๊ณ ์ฌ์ฃผ๋ฌธ์ ๊ฐ์ฉ์ฌ๊ณ ๊ฒฐํํ๋ฅ ์ํ์ง์\n",
"P018 battery ๋จ์ข
4 17.48 23.00 111.00 71 0.88 93.60\n",
"P016 gasket ๋จ์ข
12 148.02 95.00 2019.00 714 1.00 90.00\n",
"P028 power_train ๋จ์ข
11 68.99 48.00 876.00 515 1.00 90.00\n",
"P004 filter ๋จ์ข
์๊ณ 6 7.65 16.00 70.00 12 1.00 87.50\n",
"P025 filter ๋จ์ข
์๊ณ 12 13.94 23.00 204.00 89 1.00 87.50\n",
"P030 brake_steer ๋จ์ข
์๊ณ 12 45.84 43.00 639.00 162 1.00 87.50\n",
"P038 fuel ๋จ์ข
์๊ณ 6 11.87 25.00 108.00 40 1.00 87.40\n",
"P019 filter ๋จ์ข
์๊ณ 8 11.47 30.00 133.00 59 0.99 87.10\n",
"์ํ ๋ฑ๊ธ: ๊ฒฝ๋ณด 13๊ฐ, ์ฃผ์ 2๊ฐ, ๊ด์ฐฐ 25๊ฐ / ์ฌ์ฃผ๋ฌธ์ ๋ฏธ๋ง ํ๋ชฉ 20๊ฐ\n",
"๋จ์ข
ยท๋จ์ข
์๊ณ ํ๋ชฉ์ ๋ฆฌ๋ํ์์ด ๊ธธ๊ณ ๊ณต๊ธ์
์ฒด๊ฐ 1๊ณณ์ด๋ผ ๊ฐ์ ๊ฒฐํ ํ๋ฅ ์์๋ ์ํ ์ง์๊ฐ ๋๊ฒ ๋์จ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
06. ์ผ์ ๋ถํ ์ฌ๊ณ ํํฉ ๋ฐ ๊ธด๊ธ ์กฐ๋ฌ ํ์ ํ๋ชฉ ๊ฐ์ํ ๋์๋ณด๋ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ผ์ ๋ถํ ์ฌ๊ณ ํํฉ ๋ฐ ๊ธด๊ธ ์กฐ๋ฌ ํ์ ํ๋ชฉ ๋์๋ณด๋๋ฅผ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ผ์ ๋ถํ ์ฌ๊ณ ํํฉ๊ณผ ๊ธด๊ธ ์กฐ๋ฌ ํ์ ํ๋ชฉ์ ๋ณด์ฌ ์ฃผ๋ ๋์๋ณด๋๋ฅผ ๋ง๋ค๊ณ ํ๋ชฉ๋ณ ์ํ ์์ธ์ ์ค๋ช
ํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ฐ์ฉ์ฌ๊ณ ์ ์ฌ์ฃผ๋ฌธ์ , ํ๋ชฉ๊ตฐยท์์ฐ ์ํ๋ณ ์ํ ์ง์, ๊ธด๊ธ ์กฐ๋ฌ ํ์ ํ๋ชฉ์ ๊ทธ๋ฆผ์ผ๋ก ํ์ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/explainability/mnt02_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": [
"\"\"\"์ผ์ ๋ถํ ์ฌ๊ณ ํํฉ(๊ฐ์ฉ์ฌ๊ณ ยท์ฌ์ฃผ๋ฌธ์ ยท๊ฒฐํ ์ํ ์ง์)๊ณผ ๊ธด๊ธ ์กฐ๋ฌ ํ์ ํ๋ชฉ์ ๋ณด์ฌ ์ฃผ๋ ๋์๋ณด๋๋ฅผ ๋ง๋ค๊ณ , ํ๋ชฉ๋ง๋ค ์ํ ์ง์๋ฅผ ๋์ด์ฌ๋ฆฐ ์์ธ์ ๊ธฐ์ค๊ฐ ๋์ฒด(occlusion) ๋ฐฉ์์ผ๋ก ๋ถํดํด ๊ธฐ์ฌ๋ ์์ 3๊ฐ ์ธ์์ ์์ฐ์ด ์ค๋ช
๋ฌธ('๋ฆฌ๋ํ์ ์ฆ๊ฐ ์์ธ์ ๋จ์ข
' ๋ฑ)์ ์๋ ์์ฑํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy.stats import norm\n",
"from sklearn.ensemble import HistGradientBoostingRegressor, RandomForestRegressor\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ผ์ ์ ๋น์ฐฝ ์๋ฆฌ๋ถ์ ์ฒญ๊ตฌ/์ถ๊ณ ยท์ฅ๋น ๊ฐ๋๋ฅ ๋ฐ์ดํฐ ์์ฑ๊ธฐ\n",
"# ์ค์ ๋ถ๋ยทํ๋ชฉยท์ฌ๊ณ ยท๋จ๊ฐ ์ ๋ณด์ ๋ฌด๊ดํ๋ค. ํ๋ชฉ 40๊ฐ x 48๊ฐ์(๊ฐ์ 2022-01 ~ 2025-12) ์๋ณ ์๋ฃ๋ฅผ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"FSC = {\"2520\": \"power_train\", \"2530\": \"brake_steer\", \"2910\": \"fuel\", \"2930\": \"cooling\",\n",
" \"2940\": \"filter\", \"5330\": \"gasket\", \"6140\": \"battery\", \"5961\": \"semiconductor\"}\n",
"EXERCISE = {3, 8, 10} # ๊ฐ์ ๋๊ท๋ชจ ํ๋ จ ์(ํ๋ จ ๊ณํ์ผ๋ก ๋ฏธ๋ฆฌ ์ ์ ์์)\n",
"N_MONTHS = 48\n",
"\n",
"\n",
"def season(fsc, month):\n",
" \"\"\"ํ๋ชฉ๊ตฐ๋ณ ๊ณ์ ๊ณ์: ์ถ์ ์ง๋ ๊ฒจ์ธ, ๋๊ฐ๊ณํต์ ์ฌ๋ฆ์ ์์๊ฐ ๋์ด๋๋ค.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 6] ์ผ์ ๋ถํ ์ฌ๊ณ ํํฉ ๋ฐ ๊ธด๊ธ ์กฐ๋ฌ ํ์ ํ๋ชฉ ๊ฐ์ํ ๋์๋ณด๋ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def make_items(n=40, seed=2):\n",
" \"\"\"ํ๋ชฉ ๋ง์คํฐ: NSN(๊ฐ์), ํ๋ชฉ๊ตฐ, ์ฅ๋น ์ ํ, ๊ธฐ์ค ์์, ๋จ๊ฐ, ์กฐ๋ฌ ๋ฆฌ๋ํ์, ๊ณต๊ธ์
์ฒด ์, ์์ฐ(๋จ์ข
) ์ํ.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for i in range(n):\n",
" f = str(rng.choice(list(FSC)))\n",
" status = str(rng.choice([\"์์ฐ์ค\", \"๋จ์ข
์๊ณ \", \"๋จ์ข
\"], p=[0.7, 0.2, 0.1]))\n",
" rows.append({\"item_id\": f\"P{i + 1:03d}\", \"nsn\": f\"{f}-00-{rng.integers(100, 1000)}-{rng.integers(1000, 10000)}\",\n",
" \"fsc\": f, \"group\": FSC[f], \"equip_type\": str(rng.choice([\"A\", \"B\", \"C\"])),\n",
" \"base\": float(np.round(rng.lognormal(2.7, 0.8), 1)),\n",
" \"price\": int(rng.lognormal(12, 1.0)) // 100 * 100,\n",
" \"lead_time\": int(rng.integers(1, 7) if status == \"์์ฐ์ค\" else rng.integers(4, 13)),\n",
" \"suppliers\": int(rng.integers(1, 5)) if status == \"์์ฐ์ค\" else 1,\n",
" \"status\": status, \"alt_available\": bool(rng.random() < (0.6 if status == \"์์ฐ์ค\" else 0.3))})\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"def make_history(items, months=N_MONTHS, seed=3):\n",
" \"\"\"์๋ณ ์ฒญ๊ตฌ ์์(๊ฐ), ๊ธด๊ธ ๋ถ์ถ ๊ฑด์, ์ฅ๋น ์ ํ๋ณ ์ด์ฉ์๊ฐยท๊ฐ๋๋ฅ .\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" t = np.arange(months)\n",
" moy = t % 12 + 1\n",
" ex = np.isin(moy, list(EXERCISE))\n",
" hours = {e: (100 + 20 * rng.random(months)) * np.where(ex, 1.5, 1.0) for e in \"ABC\"}\n",
" avail = {e: np.clip(0.93 - 0.12 * (hours[e] - 100) / 80 + rng.normal(0, 0.015, months), 0.6, 0.99) for e in \"ABC\"}\n",
" rows = []\n",
" for it in items.itertuples():\n",
" trend = 1 + rng.uniform(-0.1, 0.3) * t / months\n",
" s = np.array([season(it.fsc, m) for m in moy])\n",
" lam = it.base * s * trend * hours[it.equip_type] / 110\n",
" d = rng.poisson(lam)\n",
" rows.append(pd.DataFrame({\"item_id\": it.item_id, \"t\": t, \"ym\": [f\"{2022 + k // 12}-{k % 12 + 1:02d}\" for k in t],\n",
" \"month\": moy, \"exercise\": ex.astype(int), \"op_hours\": hours[it.equip_type].round(1),\n",
" \"availability\": avail[it.equip_type].round(3), \"demand\": d,\n",
" \"emergency\": rng.binomial(d, 0.05)}))\n",
" return pd.concat(rows, ignore_index=True)\n",
"\n",
"\n",
"def make_stock(items, hist, seed=4):\n",
" \"\"\"ํ์ฌ(๊ฐ์ 2026-01) ์ฌ๊ณ : ๋ณด์ ๋๊ณผ ๋ฏธ์
๊ณ ๋ฐ์ฃผ๋(์กฐ๋ฌ ์ค์ธ ๋ฌผ๋). ๋จ์ข
ํ๋ชฉ์ ์ถ๊ฐ ๋ฐ์ฃผ๊ฐ ์ด๋ ค์ ๋ฏธ์
๊ณ ๋์ด ์ ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" recent = hist[hist[\"t\"] >= hist[\"t\"].max() - 5].groupby(\"item_id\")[\"demand\"].mean()\n",
" st = items[[\"item_id\", \"lead_time\", \"status\"]].copy()\n",
" st[\"on_hand\"] = [int(round(recent[i] * rng.uniform(0.5, 3.0))) for i in st[\"item_id\"]]\n",
" cover = np.where(st[\"status\"] == \"์์ฐ์ค\", rng.uniform(0.6, 1.3, len(st)), rng.uniform(0.0, 0.6, len(st)))\n",
" st[\"on_order\"] = [int(round(recent[i] * L * c)) for i, L, c in zip(st[\"item_id\"], st[\"lead_time\"], cover)]\n",
" return st[[\"item_id\", \"on_hand\", \"on_order\"]]\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์์ ์์ธก ๊ณตํต ํจ์(ํ์ ํผ์ฒ + ๋จธ์ ๋ฌ๋ ๋ชจ๋ธ) ----\n",
"FEATS = [\"lag1\", \"lag2\", \"lag3\", \"lag12\", \"ma3\", \"ma6\", \"std6\", \"season_idx\", \"month_sin\", \"month_cos\",\n",
" \"exercise\", \"op_hours\", \"op_ratio\", \"item_level\", \"trend_t\"]\n",
"\n",
"\n",
"def make_features(hist):\n",
" \"\"\"ํ๋ชฉ๋ณ ๊ณผ๊ฑฐ ์์๋ก ํ์ ํผ์ฒ๋ฅผ ๋ง๋ ๋ค(๋ชจ๋ ์์ธก ์์ ์ด์ ์ ๋ณด๋ง ์ฌ์ฉ).\"\"\"\n",
" df = hist.sort_values([\"item_id\", \"t\"]).reset_index(drop=True)\n",
" g = df.groupby(\"item_id\")[\"demand\"]\n",
" for k in (1, 2, 3, 12):\n",
" df[f\"lag{k}\"] = g.shift(k)\n",
" df[\"ma3\"] = g.transform(lambda s: s.shift(1).rolling(3).mean())\n",
" df[\"ma6\"] = g.transform(lambda s: s.shift(1).rolling(6).mean())\n",
" df[\"std6\"] = g.transform(lambda s: s.shift(1).rolling(6).std())\n",
" df[\"item_level\"] = g.transform(lambda s: s.shift(1).rolling(12).mean())\n",
" df[\"season_idx\"] = df[\"lag12\"] / (g.transform(lambda s: s.shift(12).rolling(12).mean()) + 1)\n",
" df[\"month_sin\"] = np.sin(2 * np.pi * df[\"month\"] / 12)\n",
" df[\"month_cos\"] = np.cos(2 * np.pi * df[\"month\"] / 12)\n",
" df[\"op_ratio\"] = df[\"op_hours\"] / df.groupby(\"item_id\")[\"op_hours\"].transform(lambda s: s.shift(1).rolling(12).mean())\n",
" df[\"trend_t\"] = df[\"t\"]\n",
" return df.dropna().reset_index(drop=True)\n",
"\n",
"\n",
"def new_model():\n",
" \"\"\"Prophet/LSTM ๋์ ์ฌ์ฉํ๋ ํฌ์์ก ์์ค ๊ทธ๋๋์ธํธ ๋ถ์คํ
(๊ฐ์ ์๋ฃ์ ์ ํฉ).\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 6] ์ผ์ ๋ถํ ์ฌ๊ณ ํํฉ ๋ฐ ๊ธด๊ธ ์กฐ๋ฌ ํ์ ํ๋ชฉ ๊ฐ์ํ ๋์๋ณด๋ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def mape(y, p, min_y=1):\n",
" y, p = np.asarray(y, float), np.asarray(p, float)\n",
" m = y >= min_y\n",
" return float(np.mean(np.abs(y[m] - p[m]) / y[m]))\n",
"\n",
"# ---- ์์๋ธ ์์ ์์ธกยท์์ ์ฌ๊ณ ยท๊ฒฐํ(๋จ์ข
) ์ํ ์ง์ ๊ณตํต ํจ์ ----\n",
"STATUS_W = {\"์์ฐ์ค\": 0.0, \"๋จ์ข
์๊ณ \": 0.5, \"๋จ์ข
\": 1.0} # [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋จ์ข
์ํ ๊ฐ์ค์น\n",
"\n",
"\n",
"def fit_ens(tr):\n",
" \"\"\"ํฌ์์ก ๊ทธ๋๋์ธํธ ๋ถ์คํ
(70%) + ๋๋คํฌ๋ ์คํธ(30%) ์์๋ธ.\"\"\"\n",
" h = new_model().fit(tr[FEATS], tr[\"demand\"])\n",
" r = RandomForestRegressor(n_estimators=150, min_samples_leaf=5, random_state=0, n_jobs=1).fit(tr[FEATS], tr[\"demand\"])\n",
" return h, r\n",
"\n",
"\n",
"def pred_ens(m, X, w=0.7):\n",
" return np.clip(w * m[0].predict(X[FEATS]) + (1 - w) * m[1].predict(X[FEATS]), 0, None)\n",
"\n",
"\n",
"def next_month(hist, items, t_new=48, op_mult=1.0, ex=0):\n",
" \"\"\"๋ค์ ๋ฌ(t_new) ์์ธก์ฉ ํ: ์๋
๊ฐ์ ๋ฌ ์ด์ฉ์๊ฐ x ๋ฐฐ์, ํ๋ จ ์ฌ๋ถ ์ง์ .\"\"\"\n",
" last = hist[hist[\"t\"] == t_new - 1].copy()\n",
" prev = hist[hist[\"t\"] == t_new - 12].set_index(\"item_id\")[\"op_hours\"]\n",
" m = (t_new % 12) + 1\n",
" f = last.assign(t=t_new, ym=f\"{2022 + t_new // 12}-{m:02d}\", month=m, demand=0, exercise=ex,\n",
" op_hours=last[\"item_id\"].map(prev).to_numpy() * op_mult)\n",
" X = make_features(pd.concat([hist, f], ignore_index=True))\n",
" return X[X[\"t\"] == t_new].set_index(\"item_id\").loc[items[\"item_id\"]].reset_index()\n",
"\n",
"\n",
"def stock_policy(items, mu, sigma, z=1.65, review=1):\n",
" \"\"\"์์ ์ฌ๊ณ = z x ์ ์์ธก์ค์ฐจ ํ์คํธ์ฐจ x sqrt(๋ฆฌ๋ํ์+๊ฒํ ์ฃผ๊ธฐ), ์ฌ์ฃผ๋ฌธ์ = ์ ์์ x (๋ฆฌ๋ํ์+๊ฒํ ์ฃผ๊ธฐ) + ์์ ์ฌ๊ณ .\"\"\"\n",
" L = items[\"lead_time\"].to_numpy() + review\n",
" ss = z * sigma * np.sqrt(L)\n",
" return pd.DataFrame({\"item_id\": items[\"item_id\"], \"mu\": mu, \"sigma\": sigma, \"L\": L,\n",
" \"ss\": np.ceil(ss), \"rop\": np.ceil(mu * L + ss)})\n",
"\n",
"\n",
"def risk_index(items, pol, stock):\n",
" \"\"\"๊ฒฐํ ์ํ ์ง์(0~100): ๋ฆฌ๋ํ์ ๋์ ์์๊ฐ ๊ฐ์ฉ ์ฌ๊ณ ๋ฅผ ๋์ ํ๋ฅ (์ ๊ท ๊ทผ์ฌ)์ ๊ธฐ๋ณธ์ผ๋ก,\n",
" ๋จ์ข
์ํยท๋จ์ผ ๊ณต๊ธ์
์ฒดยท๋์ฒดํ ๋ถ์ฌ์ ๊ฐ์ค์ ์ค๋ค(๊ฐ์ ๊ฐ์ค์น).\"\"\"\n",
" st = stock.set_index(\"item_id\").loc[items[\"item_id\"]]\n",
" avail = (st[\"on_hand\"] + st[\"on_order\"]).to_numpy()\n",
" p = 1 - norm.cdf((avail - pol[\"mu\"].to_numpy() * pol[\"L\"].to_numpy()) / (pol[\"sigma\"].to_numpy() * np.sqrt(pol[\"L\"].to_numpy()) + 1e-6))\n",
" sw = items[\"status\"].map(STATUS_W).to_numpy()\n",
" sup = (items[\"suppliers\"].to_numpy() == 1).astype(float)\n",
" alt = (~items[\"alt_available\"].to_numpy()).astype(float)\n",
" score = 100 * np.clip(0.55 * p + 0.25 * sw + 0.10 * sup + 0.10 * alt, 0, 1)\n",
" return pd.DataFrame({\"item_id\": items[\"item_id\"], \"p_short\": p, \"status_w\": sw, \"single_sup\": sup, \"no_alt\": alt,\n",
" \"avail\": avail, \"risk\": score.round(1)})\n",
"\n",
"items = make_items()\n",
"hist = make_history(items)\n",
"df = make_features(hist)\n",
"stock = make_stock(items, hist)\n",
"X = next_month(hist, items)\n",
"mu = pred_ens(fit_ens(df), X)\n",
"level = X[\"item_level\"].to_numpy() # ์ต๊ทผ 12๊ฐ์ ํ๊ท ์์(ํ์ ์์ค)\n",
"sigma = np.maximum(0.25 * mu, np.sqrt(np.maximum(mu, 1)))\n",
"\n",
"\n",
"def score(it, m, s, st):\n",
" return risk_index(it, stock_policy(it, m, s), st)[\"risk\"].to_numpy()\n",
"\n",
"\n",
"base = score(items, mu, sigma, stock)\n",
"pol = stock_policy(items, mu, sigma)\n",
"# ๊ฐ ์์ธ์ '๋ฌธ์ ์๋ ๊ธฐ์ค๊ฐ'์ผ๋ก ๋ฐ๊ฟจ์ ๋ ์ํ ์ง์๊ฐ ์ผ๋ง๋ ๋ด๋ ค๊ฐ๋์ง = ๊ทธ ์์ธ์ ๊ธฐ์ฌ๋\n",
"ref_lt = int(items.loc[items[\"status\"] == \"์์ฐ์ค\", \"lead_time\"].median())\n",
"st_ok = stock.copy(); st_ok[\"on_hand\"] = np.maximum(st_ok[\"on_hand\"], (pol[\"rop\"] - st_ok[\"on_order\"]).clip(lower=0).astype(int).to_numpy())\n",
"FACTORS = {\n",
" \"์กฐ๋ฌ ๋ฆฌ๋ํ์ ์ฅ๊ธฐํ\": lambda: score(items.assign(lead_time=np.minimum(items[\"lead_time\"], ref_lt)), mu, sigma, stock),\n",
" \"์์ ์ฆ๊ฐ ์์ธก\": lambda: score(items, np.minimum(mu, level), sigma, stock),\n",
" \"๊ฐ์ฉ์ฌ๊ณ ๋ถ์กฑ\": lambda: score(items, mu, sigma, st_ok),\n",
" \"์์ธก ๋ถํ์ค์ฑ\": lambda: score(items, mu, np.sqrt(np.maximum(mu, 1)), stock),\n",
" \"๋จ์ข
(์๊ณ ) ์ํ\": lambda: score(items.assign(status=\"์์ฐ์ค\"), mu, sigma, stock),\n",
" \"๋จ์ผ ๊ณต๊ธ์
์ฒด\": lambda: score(items.assign(suppliers=np.maximum(items[\"suppliers\"], 2)), mu, sigma, stock),\n",
" \"๋์ฒดํ ๋ถ์ฌ\": lambda: score(items.assign(alt_available=True), mu, sigma, stock),\n",
"}\n",
"C = pd.DataFrame({k: base - f() for k, f in FACTORS.items()}, index=items[\"item_id\"]).clip(lower=0)\n",
"top3 = C.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\"[ํ๋ชฉ {len(items)}๊ฐ] ์ํ ์ง์ ๊ธฐ์ฌ๋ ์์ 3๊ฐ ์ธ์ ์๋ ์ฐ์ถ๋ฅ {cover:.0%} (๋ชฉํ 100%: {'๋ฌ์ฑ' if cover == 1 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"dash = items[[\"item_id\", \"group\", \"status\", \"lead_time\"]].assign(risk=base, avail=(stock[\"on_hand\"] + stock[\"on_order\"]).to_numpy(),\n",
" rop=pol[\"rop\"].to_numpy(), mu=mu.round(1))\n",
"dash[\"urgent\"] = (dash[\"risk\"] >= 60) & (dash[\"avail\"] < dash[\"rop\"])\n",
"CAUSE = {\"์กฐ๋ฌ ๋ฆฌ๋ํ์ ์ฅ๊ธฐํ\": \"์กฐ๋ฌ ๋ฆฌ๋ํ์์ด ๊ธธ๋ค\", \"๋จ์ข
(์๊ณ ) ์ํ\": \"์์ฐ ์ค๋จ(์๊ณ )์ผ๋ก ์ถ๊ฐ ์กฐ๋ฌ์ด ์ด๋ ต๋ค\",\n",
" \"๊ฐ์ฉ์ฌ๊ณ ๋ถ์กฑ\": \"๋ณด์ ยท๋ฏธ์
๊ณ ์ฌ๊ณ ๊ฐ ์ฌ์ฃผ๋ฌธ์ ๋ณด๋ค ์ ๋ค\", \"์์ ์ฆ๊ฐ ์์ธก\": \"๋ค์ ๋ฌ ์์๊ฐ ํ์๋ณด๋ค ๋ ๊ฒ์ผ๋ก ์์ธก๋๋ค\",\n",
" \"์์ธก ๋ถํ์ค์ฑ\": \"์์ ๋ณ๋์ด ์ปค ์์ ์ฌ๊ณ ๊ฐ ๋ง์ด ํ์ํ๋ค\", \"๋จ์ผ ๊ณต๊ธ์
์ฒด\": \"๊ณต๊ธ์
์ฒด๊ฐ 1๊ณณ๋ฟ์ด๋ค\", \"๋์ฒดํ ๋ถ์ฌ\": \"๋์ฒดํ์ด ์๋ค\"}\n",
"\n",
"\n",
"def explain(iid):\n",
" r = dash.set_index(\"item_id\").loc[iid]\n",
" f = top3[iid]\n",
" parts = \", \".join(f\"{k}(+{C.loc[iid, k]:.0f})\" for k in f)\n",
" return f\"{iid}({r['group']}, {r['status']}) ์ํ {r['risk']:.0f}: {parts}. ์ฃผ๋ ์์ธ์ {CAUSE[f[0]]}.\"\n",
"\n",
"\n",
"urg = dash[dash[\"urgent\"]].sort_values(\"risk\", ascending=False)\n",
"print(f\"\\n[๊ธด๊ธ ์กฐ๋ฌ ํ์ ํ๋ชฉ {len(urg)}๊ฐ(์ํ 60 ์ด์ & ๊ฐ์ฉ์ฌ๊ณ <์ฌ์ฃผ๋ฌธ์ )] ์์ฐ์ด ์ค๋ช
\")\n",
"for iid in urg[\"item_id\"].head(7):\n",
" print(\" -\", explain(iid))\n",
"print(\"\\n[์์ธ๋ณ ํ๊ท ๊ธฐ์ฌ๋]\", \", \".join(f\"{k} {v:.1f}\" for k, v in C.mean().sort_values(ascending=False).items()))\n",
"g = dash.groupby(\"status\")[\"risk\"].agg([\"count\", \"mean\"]).round(1)\n",
"print(\"[์์ฐ ์ํ๋ณ ํ๊ท ์ํ ์ง์]\", \", \".join(f\"{s} {r['mean']}({int(r['count'])}๊ฐ)\" for s, r in g.iterrows()))\n",
"print(\"์ค๋ช
์ ์กฐ๋ฌยท๋์ฒดํ ๊ฒํ ์ ๊ทผ๊ฑฐ ์๋ฃ์ด๋ฉฐ ๋ฐ์ฃผยท๋์ฒด ๊ฒฐ์ ์ ์ ๋น์ฅ๊ต๊ฐ ํ์ธ ํ ๋ด๋ฆฐ๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 3, figsize=(17, 4.8))\n",
"d15 = dash.sort_values(\"risk\", ascending=False).head(15)\n",
"ax[0].barh(d15[\"item_id\"], d15[\"avail\"], color=np.where(d15[\"urgent\"], \"tab:red\", \"tab:gray\"), label=\"available\")\n",
"ax[0].scatter(d15[\"rop\"], d15[\"item_id\"], c=\"k\", marker=\"|\", s=200, label=\"reorder point\"); ax[0].invert_yaxis()\n",
"ax[0].set_xscale(\"symlog\"); ax[0].set_title(\"Top-15 risk items: stock vs reorder point (red=urgent)\"); ax[0].legend()\n",
"hm = dash.pivot_table(index=\"group\", columns=\"status\", values=\"risk\", aggfunc=\"mean\")\n",
"im = ax[1].imshow(hm.values, cmap=\"Reds\", vmin=0, vmax=100, aspect=\"auto\"); plt.colorbar(im, ax=ax[1])\n",
"ax[1].set_xticks(range(hm.shape[1])); ax[1].set_xticklabels([\"active\" if c == \"์์ฐ์ค\" else \"EOL notice\" if c == \"๋จ์ข
์๊ณ \" else \"discontinued\" for c in hm.columns])\n",
"ax[1].set_yticks(range(len(hm))); ax[1].set_yticklabels(hm.index); ax[1].set_title(\"Mean risk index by group/status\")\n",
"i0 = urg[\"item_id\"].iloc[0]; w = C.loc[i0].sort_values()\n",
"ax[2].barh(range(len(w)), w.values, color=\"tab:red\"); ax[2].set_yticks(range(len(w)))\n",
"EN = {\"์กฐ๋ฌ ๋ฆฌ๋ํ์ ์ฅ๊ธฐํ\": \"lead time\", \"์์ ์ฆ๊ฐ ์์ธก\": \"demand up\", \"๊ฐ์ฉ์ฌ๊ณ ๋ถ์กฑ\": \"low stock\", \"์์ธก ๋ถํ์ค์ฑ\": \"uncertainty\",\n",
" \"๋จ์ข
(์๊ณ ) ์ํ\": \"EOL status\", \"๋จ์ผ ๊ณต๊ธ์
์ฒด\": \"single supplier\", \"๋์ฒดํ ๋ถ์ฌ\": \"no alternate\"}\n",
"ax[2].set_yticklabels([EN[k] for k in w.index])\n",
"ax[2].set_title(f\"Risk contribution: {i0}\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
06 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[ํ๋ชฉ 40๊ฐ] ์ํ ์ง์ ๊ธฐ์ฌ๋ ์์ 3๊ฐ ์ธ์ ์๋ ์ฐ์ถ๋ฅ 100% (๋ชฉํ 100%: ๋ฌ์ฑ)\n",
"\n",
"[๊ธด๊ธ ์กฐ๋ฌ ํ์ ํ๋ชฉ 13๊ฐ(์ํ 60 ์ด์ & ๊ฐ์ฉ์ฌ๊ณ <์ฌ์ฃผ๋ฌธ์ )] ์์ฐ์ด ์ค๋ช
\n",
" - P018(battery, ๋จ์ข
) ์ํ 96: ๊ฐ์ฉ์ฌ๊ณ ๋ถ์กฑ(+49), ๋จ์ข
(์๊ณ ) ์ํ(+25), ๋จ์ผ ๊ณต๊ธ์
์ฒด(+10). ์ฃผ๋ ์์ธ์ ๋ณด์ ยท๋ฏธ์
๊ณ ์ฌ๊ณ ๊ฐ ์ฌ์ฃผ๋ฌธ์ ๋ณด๋ค ์ ๋ค.\n",
" - P028(power_train, ๋จ์ข
) ์ํ 90: ์กฐ๋ฌ ๋ฆฌ๋ํ์ ์ฅ๊ธฐํ(+55), ๊ฐ์ฉ์ฌ๊ณ ๋ถ์กฑ(+52), ๋จ์ข
(์๊ณ ) ์ํ(+25). ์ฃผ๋ ์์ธ์ ์กฐ๋ฌ ๋ฆฌ๋ํ์์ด ๊ธธ๋ค.\n",
" - P016(gasket, ๋จ์ข
) ์ํ 90: ๊ฐ์ฉ์ฌ๊ณ ๋ถ์กฑ(+52), ๋จ์ข
(์๊ณ ) ์ํ(+25), ๋จ์ผ ๊ณต๊ธ์
์ฒด(+10). ์ฃผ๋ ์์ธ์ ๋ณด์ ยท๋ฏธ์
๊ณ ์ฌ๊ณ ๊ฐ ์ฌ์ฃผ๋ฌธ์ ๋ณด๋ค ์ ๋ค.\n",
" - P004(filter, ๋จ์ข
์๊ณ ) ์ํ 88: ๊ฐ์ฉ์ฌ๊ณ ๋ถ์กฑ(+53), ๋จ์ข
(์๊ณ ) ์ํ(+12), ๋จ์ผ ๊ณต๊ธ์
์ฒด(+10). ์ฃผ๋ ์์ธ์ ๋ณด์ ยท๋ฏธ์
๊ณ ์ฌ๊ณ ๊ฐ ์ฌ์ฃผ๋ฌธ์ ๋ณด๋ค ์ ๋ค.\n",
" - P025(filter, ๋จ์ข
์๊ณ ) ์ํ 88: ๊ฐ์ฉ์ฌ๊ณ ๋ถ์กฑ(+53), ์กฐ๋ฌ ๋ฆฌ๋ํ์ ์ฅ๊ธฐํ(+39), ๋จ์ข
(์๊ณ ) ์ํ(+12). ์ฃผ๋ ์์ธ์ ๋ณด์ ยท๋ฏธ์
๊ณ ์ฌ๊ณ ๊ฐ ์ฌ์ฃผ๋ฌธ์ ๋ณด๋ค ์ ๋ค.\n",
" - P038(fuel, ๋จ์ข
์๊ณ ) ์ํ 88: ๊ฐ์ฉ์ฌ๊ณ ๋ถ์กฑ(+52), ๋จ์ข
(์๊ณ ) ์ํ(+12), ๋จ์ผ ๊ณต๊ธ์
์ฒด(+10). ์ฃผ๋ ์์ธ์ ๋ณด์ ยท๋ฏธ์
๊ณ ์ฌ๊ณ ๊ฐ ์ฌ์ฃผ๋ฌธ์ ๋ณด๋ค ์ ๋ค.\n",
" - P030(brake_steer, ๋จ์ข
์๊ณ ) ์ํ 88: ๊ฐ์ฉ์ฌ๊ณ ๋ถ์กฑ(+52), ๋จ์ข
(์๊ณ ) ์ํ(+12), ๋จ์ผ ๊ณต๊ธ์
์ฒด(+10). ์ฃผ๋ ์์ธ์ ๋ณด์ ยท๋ฏธ์
๊ณ ์ฌ๊ณ ๊ฐ ์ฌ์ฃผ๋ฌธ์ ๋ณด๋ค ์ ๋ค.\n",
"\n",
"[์์ธ๋ณ ํ๊ท ๊ธฐ์ฌ๋] ๊ฐ์ฉ์ฌ๊ณ ๋ถ์กฑ 19.5, ๋จ์ผ ๊ณต๊ธ์
์ฒด 4.8, ์กฐ๋ฌ ๋ฆฌ๋ํ์ ์ฅ๊ธฐํ 4.5, ๋จ์ข
(์๊ณ ) ์ํ 4.4, ๋์ฒดํ ๋ถ์ฌ 3.8, ์์ ์ฆ๊ฐ ์์ธก 0.3, ์์ธก ๋ถํ์ค์ฑ 0.3\n",
"[์์ฐ ์ํ๋ณ ํ๊ท ์ํ ์ง์] ๋จ์ข
92.0(3๊ฐ), ๋จ์ข
์๊ณ 84.5(8๊ฐ), ์์ฐ์ค 13.7(29๊ฐ)\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",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **12๊ฐ์ ์ฌ๊ณ ๋ฐฑํ
์คํธ๋ก ํํ(์ต๊ทผ ํ๊ท +๊ณ ์ ์ฌ์ ) ์ ์ฑ
, ์์ธก ์ ์ฑ
, ์์ธก+๋จ์ข
๋์ฒดํ ์ ํ์ ๋ณด๊ด๋นยท๋ฐ์ฃผ๋นยท๊ฒฐํ์์คยท์ถฉ์กฑ๋ฅ ยทํ์ ์จ๋ก ๋น๊ตํ๊ณ ํจ๊ณผ๋ฅผ ๋ถํด ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/optimization/mnt02_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": [
"\"\"\"์์ธก ๊ธฐ๋ฐ ๋ฐ์ฃผ ์ ์ฑ
(์์ ์ฌ๊ณ ํฌํจ ๋ชฉํ์ฌ๊ณ )๊ณผ ๋จ์ข
ํ๋ชฉ ๋์ฒดํ ์ ํ์ 12๊ฐ์ ์ฌ๊ณ ๋ฐฑํ
์คํธ๋ก ํํ(์ต๊ทผ ํ๊ท +๊ณ ์ ์ฌ์ ) ์ ์ฑ
๊ณผ ๋น๊ตํด ๋น์ฉยท์ฌ๊ณ ํ์ ์จยท์ถฉ์กฑ๋ฅ ์ ์ฐ์ถํ๊ณ , ๋ค์ ๋ฌ ๋ฐ์ฃผ๋์ ์์ฐ ์ ์ฝ ์ ํ๊ณํ(LP)์ผ๋ก ์ถ์ฒํ ๋ค ์ ๋น์ฅ๊ต๊ฐ ์น์ธยท์กฐ์ ยท๋ฐ๋ คยท๋ณด๋ฅ๋ก ์ต์ข
ํ์ ํ๋ 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.stats import norm\n",
"from scipy.optimize import linprog\n",
"from sklearn.ensemble import HistGradientBoostingRegressor, RandomForestRegressor\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ผ์ ์ ๋น์ฐฝ ์๋ฆฌ๋ถ์ ์ฒญ๊ตฌ/์ถ๊ณ ยท์ฅ๋น ๊ฐ๋๋ฅ ๋ฐ์ดํฐ ์์ฑ๊ธฐ\n",
"# ์ค์ ๋ถ๋ยทํ๋ชฉยท์ฌ๊ณ ยท๋จ๊ฐ ์ ๋ณด์ ๋ฌด๊ดํ๋ค. ํ๋ชฉ 40๊ฐ x 48๊ฐ์(๊ฐ์ 2022-01 ~ 2025-12) ์๋ณ ์๋ฃ๋ฅผ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"FSC = {\"2520\": \"power_train\", \"2530\": \"brake_steer\", \"2910\": \"fuel\", \"2930\": \"cooling\",\n",
" \"2940\": \"filter\", \"5330\": \"gasket\", \"6140\": \"battery\", \"5961\": \"semiconductor\"}\n",
"EXERCISE = {3, 8, 10} # ๊ฐ์ ๋๊ท๋ชจ ํ๋ จ ์(ํ๋ จ ๊ณํ์ผ๋ก ๋ฏธ๋ฆฌ ์ ์ ์์)\n",
"N_MONTHS = 48\n",
"\n",
"\n",
"def season(fsc, month):\n",
" \"\"\"ํ๋ชฉ๊ตฐ๋ณ ๊ณ์ ๊ณ์: ์ถ์ ์ง๋ ๊ฒจ์ธ, ๋๊ฐ๊ณํต์ ์ฌ๋ฆ์ ์์๊ฐ ๋์ด๋๋ค.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 7] ๋ถํ ๋ฐ์ฃผ๋ ์ต์ข
ํ์ ์ ์ํ ์ ๋น์ฅ๊ต HITL ์น์ธ/์กฐ์ ์ํฌํ๋ก์ฐ ์ค๊ณ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def make_items(n=40, seed=2):\n",
" \"\"\"ํ๋ชฉ ๋ง์คํฐ: NSN(๊ฐ์), ํ๋ชฉ๊ตฐ, ์ฅ๋น ์ ํ, ๊ธฐ์ค ์์, ๋จ๊ฐ, ์กฐ๋ฌ ๋ฆฌ๋ํ์, ๊ณต๊ธ์
์ฒด ์, ์์ฐ(๋จ์ข
) ์ํ.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for i in range(n):\n",
" f = str(rng.choice(list(FSC)))\n",
" status = str(rng.choice([\"์์ฐ์ค\", \"๋จ์ข
์๊ณ \", \"๋จ์ข
\"], p=[0.7, 0.2, 0.1]))\n",
" rows.append({\"item_id\": f\"P{i + 1:03d}\", \"nsn\": f\"{f}-00-{rng.integers(100, 1000)}-{rng.integers(1000, 10000)}\",\n",
" \"fsc\": f, \"group\": FSC[f], \"equip_type\": str(rng.choice([\"A\", \"B\", \"C\"])),\n",
" \"base\": float(np.round(rng.lognormal(2.7, 0.8), 1)),\n",
" \"price\": int(rng.lognormal(12, 1.0)) // 100 * 100,\n",
" \"lead_time\": int(rng.integers(1, 7) if status == \"์์ฐ์ค\" else rng.integers(4, 13)),\n",
" \"suppliers\": int(rng.integers(1, 5)) if status == \"์์ฐ์ค\" else 1,\n",
" \"status\": status, \"alt_available\": bool(rng.random() < (0.6 if status == \"์์ฐ์ค\" else 0.3))})\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"def make_history(items, months=N_MONTHS, seed=3):\n",
" \"\"\"์๋ณ ์ฒญ๊ตฌ ์์(๊ฐ), ๊ธด๊ธ ๋ถ์ถ ๊ฑด์, ์ฅ๋น ์ ํ๋ณ ์ด์ฉ์๊ฐยท๊ฐ๋๋ฅ .\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" t = np.arange(months)\n",
" moy = t % 12 + 1\n",
" ex = np.isin(moy, list(EXERCISE))\n",
" hours = {e: (100 + 20 * rng.random(months)) * np.where(ex, 1.5, 1.0) for e in \"ABC\"}\n",
" avail = {e: np.clip(0.93 - 0.12 * (hours[e] - 100) / 80 + rng.normal(0, 0.015, months), 0.6, 0.99) for e in \"ABC\"}\n",
" rows = []\n",
" for it in items.itertuples():\n",
" trend = 1 + rng.uniform(-0.1, 0.3) * t / months\n",
" s = np.array([season(it.fsc, m) for m in moy])\n",
" lam = it.base * s * trend * hours[it.equip_type] / 110\n",
" d = rng.poisson(lam)\n",
" rows.append(pd.DataFrame({\"item_id\": it.item_id, \"t\": t, \"ym\": [f\"{2022 + k // 12}-{k % 12 + 1:02d}\" for k in t],\n",
" \"month\": moy, \"exercise\": ex.astype(int), \"op_hours\": hours[it.equip_type].round(1),\n",
" \"availability\": avail[it.equip_type].round(3), \"demand\": d,\n",
" \"emergency\": rng.binomial(d, 0.05)}))\n",
" return pd.concat(rows, ignore_index=True)\n",
"\n",
"\n",
"def make_stock(items, hist, seed=4):\n",
" \"\"\"ํ์ฌ(๊ฐ์ 2026-01) ์ฌ๊ณ : ๋ณด์ ๋๊ณผ ๋ฏธ์
๊ณ ๋ฐ์ฃผ๋(์กฐ๋ฌ ์ค์ธ ๋ฌผ๋). ๋จ์ข
ํ๋ชฉ์ ์ถ๊ฐ ๋ฐ์ฃผ๊ฐ ์ด๋ ค์ ๋ฏธ์
๊ณ ๋์ด ์ ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" recent = hist[hist[\"t\"] >= hist[\"t\"].max() - 5].groupby(\"item_id\")[\"demand\"].mean()\n",
" st = items[[\"item_id\", \"lead_time\", \"status\"]].copy()\n",
" st[\"on_hand\"] = [int(round(recent[i] * rng.uniform(0.5, 3.0))) for i in st[\"item_id\"]]\n",
" cover = np.where(st[\"status\"] == \"์์ฐ์ค\", rng.uniform(0.6, 1.3, len(st)), rng.uniform(0.0, 0.6, len(st)))\n",
" st[\"on_order\"] = [int(round(recent[i] * L * c)) for i, L, c in zip(st[\"item_id\"], st[\"lead_time\"], cover)]\n",
" return st[[\"item_id\", \"on_hand\", \"on_order\"]]\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์์ ์์ธก ๊ณตํต ํจ์(ํ์ ํผ์ฒ + ๋จธ์ ๋ฌ๋ ๋ชจ๋ธ) ----\n",
"FEATS = [\"lag1\", \"lag2\", \"lag3\", \"lag12\", \"ma3\", \"ma6\", \"std6\", \"season_idx\", \"month_sin\", \"month_cos\",\n",
" \"exercise\", \"op_hours\", \"op_ratio\", \"item_level\", \"trend_t\"]\n",
"\n",
"\n",
"def make_features(hist):\n",
" \"\"\"ํ๋ชฉ๋ณ ๊ณผ๊ฑฐ ์์๋ก ํ์ ํผ์ฒ๋ฅผ ๋ง๋ ๋ค(๋ชจ๋ ์์ธก ์์ ์ด์ ์ ๋ณด๋ง ์ฌ์ฉ).\"\"\"\n",
" df = hist.sort_values([\"item_id\", \"t\"]).reset_index(drop=True)\n",
" g = df.groupby(\"item_id\")[\"demand\"]\n",
" for k in (1, 2, 3, 12):\n",
" df[f\"lag{k}\"] = g.shift(k)\n",
" df[\"ma3\"] = g.transform(lambda s: s.shift(1).rolling(3).mean())\n",
" df[\"ma6\"] = g.transform(lambda s: s.shift(1).rolling(6).mean())\n",
" df[\"std6\"] = g.transform(lambda s: s.shift(1).rolling(6).std())\n",
" df[\"item_level\"] = g.transform(lambda s: s.shift(1).rolling(12).mean())\n",
" df[\"season_idx\"] = df[\"lag12\"] / (g.transform(lambda s: s.shift(12).rolling(12).mean()) + 1)\n",
" df[\"month_sin\"] = np.sin(2 * np.pi * df[\"month\"] / 12)\n",
" df[\"month_cos\"] = np.cos(2 * np.pi * df[\"month\"] / 12)\n",
" df[\"op_ratio\"] = df[\"op_hours\"] / df.groupby(\"item_id\")[\"op_hours\"].transform(lambda s: s.shift(1).rolling(12).mean())\n",
" df[\"trend_t\"] = df[\"t\"]\n",
" return df.dropna().reset_index(drop=True)\n",
"\n",
"\n",
"def new_model():\n",
" \"\"\"Prophet/LSTM ๋์ ์ฌ์ฉํ๋ ํฌ์์ก ์์ค ๊ทธ๋๋์ธํธ ๋ถ์คํ
(๊ฐ์ ์๋ฃ์ ์ ํฉ).\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 7] ๋ถํ ๋ฐ์ฃผ๋ ์ต์ข
ํ์ ์ ์ํ ์ ๋น์ฅ๊ต HITL ์น์ธ/์กฐ์ ์ํฌํ๋ก์ฐ ์ค๊ณ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def mape(y, p, min_y=1):\n",
" y, p = np.asarray(y, float), np.asarray(p, float)\n",
" m = y >= min_y\n",
" return float(np.mean(np.abs(y[m] - p[m]) / y[m]))\n",
"\n",
"# ---- ์์๋ธ ์์ ์์ธกยท์์ ์ฌ๊ณ ยท๊ฒฐํ(๋จ์ข
) ์ํ ์ง์ ๊ณตํต ํจ์ ----\n",
"STATUS_W = {\"์์ฐ์ค\": 0.0, \"๋จ์ข
์๊ณ \": 0.5, \"๋จ์ข
\": 1.0} # [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋จ์ข
์ํ ๊ฐ์ค์น\n",
"\n",
"\n",
"def fit_ens(tr):\n",
" \"\"\"ํฌ์์ก ๊ทธ๋๋์ธํธ ๋ถ์คํ
(70%) + ๋๋คํฌ๋ ์คํธ(30%) ์์๋ธ.\"\"\"\n",
" h = new_model().fit(tr[FEATS], tr[\"demand\"])\n",
" r = RandomForestRegressor(n_estimators=150, min_samples_leaf=5, random_state=0, n_jobs=1).fit(tr[FEATS], tr[\"demand\"])\n",
" return h, r\n",
"\n",
"\n",
"def pred_ens(m, X, w=0.7):\n",
" return np.clip(w * m[0].predict(X[FEATS]) + (1 - w) * m[1].predict(X[FEATS]), 0, None)\n",
"\n",
"\n",
"def next_month(hist, items, t_new=48, op_mult=1.0, ex=0):\n",
" \"\"\"๋ค์ ๋ฌ(t_new) ์์ธก์ฉ ํ: ์๋
๊ฐ์ ๋ฌ ์ด์ฉ์๊ฐ x ๋ฐฐ์, ํ๋ จ ์ฌ๋ถ ์ง์ .\"\"\"\n",
" last = hist[hist[\"t\"] == t_new - 1].copy()\n",
" prev = hist[hist[\"t\"] == t_new - 12].set_index(\"item_id\")[\"op_hours\"]\n",
" m = (t_new % 12) + 1\n",
" f = last.assign(t=t_new, ym=f\"{2022 + t_new // 12}-{m:02d}\", month=m, demand=0, exercise=ex,\n",
" op_hours=last[\"item_id\"].map(prev).to_numpy() * op_mult)\n",
" X = make_features(pd.concat([hist, f], ignore_index=True))\n",
" return X[X[\"t\"] == t_new].set_index(\"item_id\").loc[items[\"item_id\"]].reset_index()\n",
"\n",
"\n",
"def stock_policy(items, mu, sigma, z=1.65, review=1):\n",
" \"\"\"์์ ์ฌ๊ณ = z x ์ ์์ธก์ค์ฐจ ํ์คํธ์ฐจ x sqrt(๋ฆฌ๋ํ์+๊ฒํ ์ฃผ๊ธฐ), ์ฌ์ฃผ๋ฌธ์ = ์ ์์ x (๋ฆฌ๋ํ์+๊ฒํ ์ฃผ๊ธฐ) + ์์ ์ฌ๊ณ .\"\"\"\n",
" L = items[\"lead_time\"].to_numpy() + review\n",
" ss = z * sigma * np.sqrt(L)\n",
" return pd.DataFrame({\"item_id\": items[\"item_id\"], \"mu\": mu, \"sigma\": sigma, \"L\": L,\n",
" \"ss\": np.ceil(ss), \"rop\": np.ceil(mu * L + ss)})\n",
"\n",
"\n",
"def risk_index(items, pol, stock):\n",
" \"\"\"๊ฒฐํ ์ํ ์ง์(0~100): ๋ฆฌ๋ํ์ ๋์ ์์๊ฐ ๊ฐ์ฉ ์ฌ๊ณ ๋ฅผ ๋์ ํ๋ฅ (์ ๊ท ๊ทผ์ฌ)์ ๊ธฐ๋ณธ์ผ๋ก,\n",
" ๋จ์ข
์ํยท๋จ์ผ ๊ณต๊ธ์
์ฒดยท๋์ฒดํ ๋ถ์ฌ์ ๊ฐ์ค์ ์ค๋ค(๊ฐ์ ๊ฐ์ค์น).\"\"\"\n",
" st = stock.set_index(\"item_id\").loc[items[\"item_id\"]]\n",
" avail = (st[\"on_hand\"] + st[\"on_order\"]).to_numpy()\n",
" p = 1 - norm.cdf((avail - pol[\"mu\"].to_numpy() * pol[\"L\"].to_numpy()) / (pol[\"sigma\"].to_numpy() * np.sqrt(pol[\"L\"].to_numpy()) + 1e-6))\n",
" sw = items[\"status\"].map(STATUS_W).to_numpy()\n",
" sup = (items[\"suppliers\"].to_numpy() == 1).astype(float)\n",
" alt = (~items[\"alt_available\"].to_numpy()).astype(float)\n",
" score = 100 * np.clip(0.55 * p + 0.25 * sw + 0.10 * sup + 0.10 * alt, 0, 1)\n",
" return pd.DataFrame({\"item_id\": items[\"item_id\"], \"p_short\": p, \"status_w\": sw, \"single_sup\": sup, \"no_alt\": alt,\n",
" \"avail\": avail, \"risk\": score.round(1)})\n",
"\n",
"# ---- ์ ๋จ์ ์ฌ๊ณ ๋ฐฑํ
์คํธ(๊ฐ์ ๋น์ฉ ์ง์) ----\n",
"HOLD, ORDER_COST, SHORT_MULT = 0.02, 50_000, 0.6 # [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ ๋ณด๊ด๋น์จ(๋จ๊ฐ ๋๋น), ๋ฐ์ฃผ 1ํ ๋น์ฉ(์), ๊ฒฐํ 1๊ฐ ์์ค(๋จ๊ฐ ๋ฐฐ์)\n",
"\n",
"\n",
"ALT_PREMIUM, ALT_LT = 1.2, 3 # [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋์ฒดํ ์ ํ ์ ๋จ๊ฐ ๋ฐฐ์, ๋์ฒดํ ์กฐ๋ฌ ๋ฆฌ๋ํ์(์)\n",
"\n",
"\n",
"def simulate(items, demand, fc, policy, z=1.65, months=None, sigma=None, eol=True):\n",
" \"\"\"ํ๋ชฉ๋ณ ์ ์ฌ๊ณ ๋ชจ์. demand/fc: (ํ๋ชฉ x ์) ์ค์ ยท์์ธก ์์. policy: 'current'(์ต๊ทผ 12๊ฐ์ ํ๊ท + 3๊ฐ์ ์ฌ์ ๊ณ ์ ) ๋๋ 'ai'(์์ธก + ์์ ์ฌ๊ณ ).\n",
" ๋จ์ข
ํ๋ชฉ์ ์ ํ๋ชฉ ๋ฐ์ฃผ ๋ถ๊ฐ. 'ai' ์ ์ฑ
์์ eol=True์ด๋ฉด ๋จ์ข
ํ๋ชฉ ์ค ๋์ฒดํ์ด ์๋ ํ๋ชฉ์ ๋์ฒดํ(๋จ๊ฐ x1.2, ๋ฆฌ๋ํ์ 3๊ฐ์)์ผ๋ก ์ ํ ๋ฐ์ฃผํ๋ค. ๋ฐํ: ํ๋ชฉ๋ณ ๋ณด๊ด๋นยท๋ฐ์ฃผ๋นยท๊ฒฐํ์์คยท๋ถ์ถ๋ยทํ๊ท ์ฌ๊ณ .\"\"\"\n",
" n, T = demand.shape\n",
" L = items[\"lead_time\"].to_numpy(); price = items[\"price\"].to_numpy().astype(float)\n",
" can_order = (items[\"status\"] != \"๋จ์ข
\").to_numpy()\n",
" if policy == \"ai\" and eol:\n",
" sw = (~can_order) & items[\"alt_available\"].to_numpy()\n",
" can_order = can_order | sw\n",
" L = np.where(sw, ALT_LT, L); price = np.where(sw, price * ALT_PREMIUM, price)\n",
" avg = demand[:, :1].repeat(T, 1) if months is None else months\n",
" on = np.round(avg[:, 0] * 2.0)\n",
" pipe = np.zeros((n, T + int(L.max()) + 1)) # ๋์ฐฉ ์์ ๋ฌผ๋(ํ๋ชฉ x ์)\n",
" for i in range(n):\n",
" pipe[i, :L[i]] = np.round(avg[i, 0] * (1.0 if can_order[i] else 0.3))\n",
" res = np.zeros((n, 6)); rows = np.arange(n)\n",
" for t in range(T):\n",
" on += pipe[:, t]\n",
" issue = np.minimum(on, demand[:, t]); short = demand[:, t] - issue; on -= issue\n",
" ip = on + pipe[:, t + 1:].sum(1)\n",
" if policy == \"current\":\n",
" target = avg[:, t] * (L + 3)\n",
" else:\n",
" s = sigma if sigma is not None else np.sqrt(np.maximum(fc[:, t], 1))\n",
" target = fc[:, t] * (L + 1) + z * s * np.sqrt(L + 1)\n",
" q = np.where(can_order & (ip < target), np.ceil(target - ip), 0)\n",
" pipe[rows, t + L] += q\n",
" res += np.c_[HOLD * price * on, ORDER_COST * (q > 0), SHORT_MULT * price * short, issue, on, short]\n",
" out = pd.DataFrame(res, columns=[\"hold\", \"order\", \"short\", \"issued\", \"inv\", \"short_qty\"], index=items[\"item_id\"])\n",
" out[\"inv\"] /= T\n",
" return out\n",
"\n",
"items = make_items()\n",
"hist = make_history(items)\n",
"df = make_features(hist)\n",
"\n",
"# 1) ๋ฐฑํ
์คํธ(t=36~47, 12๊ฐ์): ํ ๋ฌ ์ ์์ธก์ t<36์ผ๋ก ํ์ตํ ์์๋ธ ์ฌ์ฉ\n",
"m = fit_ens(df[df[\"t\"] < 36])\n",
"bt = df[df[\"t\"] >= 36].copy(); bt[\"fc\"] = pred_ens(m, bt)\n",
"P = lambda c: bt.pivot(index=\"item_id\", columns=\"t\", values=c).loc[items[\"item_id\"]].to_numpy()\n",
"D, F, A = P(\"demand\"), P(\"fc\"), P(\"item_level\")\n",
"sig = np.maximum(0.25 * F.mean(1), np.sqrt(np.maximum(F.mean(1), 1)))\n",
"\n",
"\n",
"def summary(r, dem):\n",
" cost = r[[\"hold\", \"order\", \"short\"]].sum()\n",
" return {\"๋ณด๊ด๋น\": cost[\"hold\"] / 1e6, \"๋ฐ์ฃผ๋น\": cost[\"order\"] / 1e6, \"๊ฒฐํ์์ค\": cost[\"short\"] / 1e6, \"์ด๋น์ฉ(๋ฐฑ๋ง)\": cost.sum() / 1e6,\n",
" \"์ถฉ์กฑ๋ฅ \": 1 - r[\"short_qty\"].sum() / dem.sum(), \"ํ์ ์จ\": r[\"issued\"].sum() / r[\"inv\"].sum()}\n",
"\n",
"\n",
"runs = {\"ํํ(ํ๊ท +3๊ฐ์ ์ฌ์ )\": simulate(items, D, F, \"current\", months=A),\n",
" \"์์ธก ์ ์ฑ
(z=1.65)\": simulate(items, D, F, \"ai\", 1.65, A, sig, eol=False),\n",
" \"์์ธก+๋จ์ข
๋์ฒดํ ์ ํ\": simulate(items, D, F, \"ai\", 1.65, A, sig, eol=True)}\n",
"tab = pd.DataFrame({k: summary(v, D) for k, v in runs.items()}).T\n",
"print(\"[12๊ฐ์ ์ฌ๊ณ ๋ฐฑํ
์คํธ(๊ฐ์ ๋น์ฉ ์ง์, ๋ฐฑ๋ง ์)]\")\n",
"print(tab.to_string(formatters={\"์ถฉ์กฑ๋ฅ \": \"{:.1%}\".format, \"ํ์ ์จ\": \"{:.2f}\".format}, float_format=lambda x: f\"{x:.1f}\"))\n",
"c0, c2 = tab.iloc[0][\"์ด๋น์ฉ(๋ฐฑ๋ง)\"], tab.iloc[2][\"์ด๋น์ฉ(๋ฐฑ๋ง)\"]\n",
"t0, t1 = tab.iloc[0][\"ํ์ ์จ\"], tab.iloc[1][\"ํ์ ์จ\"]\n",
"print(f\"์ด๋น์ฉ ์ ๊ฐ {1 - c2 / c0:.1%}, ์์ธก ์ ์ฑ
๋ง์ ํ์ ์จ ํฅ์ {t1 / t0 - 1:.1%} (๋ชฉํ: ๋น์ฉ 15% ์ ๊ฐ ๋๋ ํ์ ์จ 20% ํฅ์ -> {'๋ฌ์ฑ' if (1 - c2 / c0) >= 0.15 or t1 / t0 - 1 >= 0.2 else '๋ฏธ๋ฌ'})\")\n",
"act = items[\"status\"] != \"๋จ์ข
\"\n",
"a0, a1 = runs[\"ํํ(ํ๊ท +3๊ฐ์ ์ฌ์ )\"][act.to_numpy()], runs[\"์์ธก ์ ์ฑ
(z=1.65)\"][act.to_numpy()]\n",
"print(f\"ํจ๊ณผ ๋ถํด: ์์ฐ์คยท๋จ์ข
์๊ณ ํ๋ชฉ ๋น์ฉ {1 - a1[['hold', 'order', 'short']].sum().sum() / a0[['hold', 'order', 'short']].sum().sum():.1%} ์ ๊ฐ(์์ธก ์ ์ฑ
), ๋๋จธ์ง๋ ๋จ์ข
ํ๋ชฉ ๋์ฒดํ ์ ํ ํจ๊ณผ\")\n",
"\n",
"# ๋ฏผ๊ฐ๋: ์๋น์ค ์์ค(z)๊ณผ ๋๋ฐ ์์(์ต๊ทผ 6๊ฐ์ ์์ 1.5๋ฐฐ, ๊ฐ์ ์ ์ ์ ํ)\n",
"D2 = D.copy(); D2[:, 6:] = np.round(D2[:, 6:] * 1.5)\n",
"sens = []\n",
"for z in (1.28, 1.65, 2.05):\n",
" for nm, dd in ((\"ํ์\", D), (\"๋๋ฐ 1.5๋ฐฐ\", D2)):\n",
" r = summary(simulate(items, dd, F, \"ai\", z, A, sig), dd)\n",
" sens.append({\"z\": z, \"์์\": nm, \"์ด๋น์ฉ\": r[\"์ด๋น์ฉ(๋ฐฑ๋ง)\"], \"์ถฉ์กฑ๋ฅ \": r[\"์ถฉ์กฑ๋ฅ \"]})\n",
"print(\"[๋ฏผ๊ฐ๋] \" + \" / \".join(f\"z={s['z']} {s['์์']}: {s['์ด๋น์ฉ']:.0f}๋ฐฑ๋ง, ์ถฉ์กฑ {s['์ถฉ์กฑ๋ฅ ']:.1%}\" for s in sens))\n",
"\n",
"\n",
"# 2) ๋ค์ ๋ฌ ๋ฐ์ฃผ ์ถ์ฒ: ์์ฐ ์ ์ฝ LP (์ํ ์ง์ ๊ฐ์ค ๋ถ์กฑ๋ถ ์ถฉ์กฑ ์ต๋ํ)\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",
"stock = make_stock(items, hist)\n",
"X = next_month(hist, items)\n",
"mu = pred_ens(fit_ens(df), X)\n",
"sg = np.maximum(0.25 * mu, np.sqrt(np.maximum(mu, 1)))\n",
"alt_sw = (items[\"status\"] == \"๋จ์ข
\") & items[\"alt_available\"]\n",
"it2 = items.assign(lead_time=np.where(alt_sw, ALT_LT, items[\"lead_time\"]), price=np.where(alt_sw, items[\"price\"] * ALT_PREMIUM, items[\"price\"]))\n",
"pol = stock_policy(it2, mu, sg)\n",
"risk = risk_index(items, pol, stock)[\"risk\"].to_numpy()\n",
"ip = (stock[\"on_hand\"] + stock[\"on_order\"]).to_numpy()\n",
"orderable = (items[\"status\"] != \"๋จ์ข
\").to_numpy() | alt_sw.to_numpy()\n",
"gap = np.where(orderable, np.maximum(0, np.ceil(pol[\"rop\"].to_numpy() - ip)), 0)\n",
"price = it2[\"price\"].to_numpy().astype(float)\n",
"BUDGET = 0.6 * float((gap * price).sum())\n",
"idx = np.where(gap > 0)[0]\n",
"lp = linprog(c=-(risk[idx] + 10) / gap[idx], A_ub=[price[idx]], b_ub=[BUDGET], bounds=[(0, g) for g in gap[idx]], method=\"highs\")\n",
"q = np.zeros(len(items)); q[idx] = np.floor(lp.x)\n",
"print(f\"\\n[๋ค์ ๋ฌ ๋ฐ์ฃผ ์ถ์ฒ LP] ๋ถ์กฑ ํ๋ชฉ {len(idx)}๊ฐ, ์์ฐ {BUDGET / 1e6:.1f}๋ฐฑ๋ง(๋ถ์กฑ๋ถ ์ ์ก์ 60%, ๊ฐ์) ์ฌ์ฉ {np.dot(q, price) / 1e6:.1f}๋ฐฑ๋ง, ์ถ์ฒ {int((q > 0).sum())}๊ฑด\")\n",
"\n",
"audit, rows = Audit(), []\n",
"for k, i in enumerate(np.where(q > 0)[0]):\n",
" iid = items.loc[i, \"item_id\"]; kind = \"๋์ฒดํ ์ ํ\" if alt_sw[i] else \"์ ๊ท ๋ฐ์ฃผ\"\n",
" audit.add(t=\"์ถ์ฒ\", item=iid, kind=kind, qty=int(q[i]), risk=float(risk[i]), status=\"๊ฒฐ์ฌ๋๊ธฐ\", by=\"AI\")\n",
" # ๊ฐ์ ์ ๋น์ฅ๊ต ํ๋จ(์๋ฎฌ๋ ์ด์
): ๊ทผ๊ฑฐ ํ์ธ ํ ์น์ธยท์กฐ์ ยท๋ฐ๋ ค, ์ผ๋ถ๋ ์ํ ๋ด ๋ฌด์๋ต(๋ณด๋ฅ=๋ฐ์ฃผํ์ง ์์)\n",
" if kind == \"๋์ฒดํ ์ ํ\":\n",
" dec, final, why = \"๋ฐ๋ ค\", 0, \"๋์ฒดํ ํธํ์ฑ ์ํ ๊ฒฐ๊ณผ ํ์ธ ํ ์ฌ์์ \"\n",
" elif q[i] * price[i] > 0.15 * BUDGET:\n",
" dec, final, why = \"์กฐ์ \", int(q[i] * 0.7), \"๊ณ ์ก ๋ฐ์ฃผ ๋ถํ (1์ฐจ 70%), ๋ค์ ๋ฌ ์ฌ๊ฒํ \"\n",
" elif k % 6 == 5:\n",
" dec, final, why = \"๋ณด๋ฅ\", 0, \"๊ฒฐ์ฌ ์ํ ์ด๊ณผ -> ๋ฐ์ฃผํ์ง ์์\"\n",
" else:\n",
" dec, final, why = \"์น์ธ\", int(q[i]), \"์ํ ์ง์ยท์ฌ๊ณ ๊ทผ๊ฑฐ ํ์ธ\"\n",
" audit.add(t=\"๊ฒฐ์ฌ\", item=iid, decision=dec, final_qty=final, reason=why, by=\"์ ๋น์ฅ๊ต(๊ฐ์)\")\n",
" rows.append({\"ํ๋ชฉ\": iid, \"๊ตฌ๋ถ\": kind, \"์ํ\": risk[i], \"์ถ์ฒ\": int(q[i]), \"๊ฒฐ์ \": dec, \"ํ์ \": final, \"์ฌ์ \": why})\n",
"res = pd.DataFrame(rows).sort_values(\"์ํ\", ascending=False)\n",
"print(res.head(10).to_string(index=False, float_format=lambda x: f\"{x:.0f}\"))\n",
"print(\"๊ฒฐ์ ์ง๊ณ:\", \", \".join(f\"{k} {v}๊ฑด\" for k, v in res[\"๊ฒฐ์ \"].value_counts().items()))\n",
"auto = [e for e in audit.log if e[\"t\"] == \"์ถ์ฒ\" and e[\"status\"] != \"๊ฒฐ์ฌ๋๊ธฐ\"]\n",
"print(f\"์๋ ํ์ {len(auto)}๊ฑด, ๊ฒฐ์ฌ ์ฌ์ ๊ธฐ๋ก๋ฅ {res['์ฌ์ '].astype(bool).mean():.0%}, ๊ฐ์ฌ ๋ก๊ทธ {len(audit.log)}๊ฑด ๋ฌด๊ฒฐ์ฑ: {'์ ์' if audit.verify() is None else '๋ณ์กฐ'}\")\n",
"j = next(e[\"seq\"] for e in audit.log if e[\"t\"] == \"๊ฒฐ์ฌ\" and e[\"decision\"] != \"์น์ธ\")\n",
"old = audit.log[j][\"decision\"]; audit.log[j][\"decision\"] = \"์น์ธ\"\n",
"print(f\"๋ณ์กฐ ์ํ({j}๋ฒ {old} ๊ธฐ๋ก์ ์น์ธ์ผ๋ก ์์ ) -> {audit.verify()}๋ฒ ๊ธฐ๋ก์์ ๋ณ์กฐ ๊ฒ์ถ\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(12, 4))\n",
"tab[[\"๋ณด๊ด๋น\", \"๋ฐ์ฃผ๋น\", \"๊ฒฐํ์์ค\"]].plot.bar(stacked=True, ax=ax[0], rot=0)\n",
"ax[0].set_xticklabels([\"current\", \"forecast\", \"forecast+EOL alt\"]); ax[0].set_title(\"12-month cost (million, synthetic)\"); ax[0].legend([\"holding\", \"ordering\", \"shortage\"])\n",
"sd = pd.DataFrame(sens)\n",
"for nm, g in sd.groupby(\"์์\"):\n",
" ax[1].plot(g[\"z\"], g[\"์ด๋น์ฉ\"], \"o-\", label=\"normal\" if nm == \"ํ์\" else \"surge x1.5\")\n",
"ax[1].set_xlabel(\"service level z\"); ax[1].set_ylabel(\"total cost (million)\"); ax[1].set_title(\"Sensitivity\"); ax[1].legend()\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
07 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[12๊ฐ์ ์ฌ๊ณ ๋ฐฑํ
์คํธ(๊ฐ์ ๋น์ฉ ์ง์, ๋ฐฑ๋ง ์)]\n",
" ๋ณด๊ด๋น ๋ฐ์ฃผ๋น ๊ฒฐํ์์ค ์ด๋น์ฉ(๋ฐฑ๋ง) ์ถฉ์กฑ๋ฅ ํ์ ์จ\n",
"ํํ(ํ๊ท +3๊ฐ์ ์ฌ์ ) 81.6 21.7 234.3 337.6 86.9% 5.42\n",
"์์ธก ์ ์ฑ
(z=1.65) 77.2 12.0 235.2 324.4 86.8% 6.09\n",
"์์ธก+๋จ์ข
๋์ฒดํ ์ ํ 108.9 12.7 8.3 129.8 98.6% 5.65\n",
"์ด๋น์ฉ ์ ๊ฐ 61.5%, ์์ธก ์ ์ฑ
๋ง์ ํ์ ์จ ํฅ์ 12.2% (๋ชฉํ: ๋น์ฉ 15% ์ ๊ฐ ๋๋ ํ์ ์จ 20% ํฅ์ -> ๋ฌ์ฑ)\n",
"ํจ๊ณผ ๋ถํด: ์์ฐ์คยท๋จ์ข
์๊ณ ํ๋ชฉ ๋น์ฉ 13.0% ์ ๊ฐ(์์ธก ์ ์ฑ
), ๋๋จธ์ง๋ ๋จ์ข
ํ๋ชฉ ๋์ฒดํ ์ ํ ํจ๊ณผ\n",
"[๋ฏผ๊ฐ๋] z=1.28 ํ์: 125๋ฐฑ๋ง, ์ถฉ์กฑ 98.5% / z=1.28 ๋๋ฐ 1.5๋ฐฐ: 230๋ฐฑ๋ง, ์ถฉ์กฑ 90.4% / z=1.65 ํ์: 130๋ฐฑ๋ง, ์ถฉ์กฑ 98.6% / z=1.65 ๋๋ฐ 1.5๋ฐฐ: 213๋ฐฑ๋ง, ์ถฉ์กฑ 91.6% / z=2.05 ํ์: 136๋ฐฑ๋ง, ์ถฉ์กฑ 98.7% / z=2.05 ๋๋ฐ 1.5๋ฐฐ: 195๋ฐฑ๋ง, ์ถฉ์กฑ 92.8%\n",
"\n",
"[๋ค์ ๋ฌ ๋ฐ์ฃผ ์ถ์ฒ LP] ๋ถ์กฑ ํ๋ชฉ 18๊ฐ, ์์ฐ 93.6๋ฐฑ๋ง(๋ถ์กฑ๋ถ ์ ์ก์ 60%, ๊ฐ์) ์ฌ์ฉ 93.5๋ฐฑ๋ง, ์ถ์ฒ 16๊ฑด\n",
" ํ๋ชฉ ๊ตฌ๋ถ ์ํ ์ถ์ฒ ๊ฒฐ์ ํ์ ์ฌ์ \n",
"P004 ์ ๊ท ๋ฐ์ฃผ 88 45 ์กฐ์ 31 ๊ณ ์ก ๋ฐ์ฃผ ๋ถํ (1์ฐจ 70%), ๋ค์ ๋ฌ ์ฌ๊ฒํ \n",
"P019 ์ ๊ท ๋ฐ์ฃผ 88 62 ์น์ธ 62 ์ํ ์ง์ยท์ฌ๊ณ ๊ทผ๊ฑฐ ํ์ธ\n",
"P030 ์ ๊ท ๋ฐ์ฃผ 88 491 ์น์ธ 491 ์ํ ์ง์ยท์ฌ๊ณ ๊ทผ๊ฑฐ ํ์ธ\n",
"P025 ์ ๊ท ๋ฐ์ฃผ 88 116 ์กฐ์ 81 ๊ณ ์ก ๋ฐ์ฃผ ๋ถํ (1์ฐจ 70%), ๋ค์ ๋ฌ ์ฌ๊ฒํ \n",
"P038 ์ ๊ท ๋ฐ์ฃผ 88 57 ์น์ธ 57 ์ํ ์ง์ยท์ฌ๊ณ ๊ทผ๊ฑฐ ํ์ธ\n",
"P027 ์ ๊ท ๋ฐ์ฃผ 87 29 ์กฐ์ 20 ๊ณ ์ก ๋ฐ์ฃผ ๋ถํ (1์ฐจ 70%), ๋ค์ ๋ฌ ์ฌ๊ฒํ \n",
"P032 ์ ๊ท ๋ฐ์ฃผ 73 26 ์น์ธ 26 ์ํ ์ง์ยท์ฌ๊ณ ๊ทผ๊ฑฐ ํ์ธ\n",
"P006 ์ ๊ท ๋ฐ์ฃผ 64 23 ์น์ธ 23 ์ํ ์ง์ยท์ฌ๊ณ ๊ทผ๊ฑฐ ํ์ธ\n",
"P012 ์ ๊ท ๋ฐ์ฃผ 61 45 ์น์ธ 45 ์ํ ์ง์ยท์ฌ๊ณ ๊ทผ๊ฑฐ ํ์ธ\n",
"P037 ์ ๊ท ๋ฐ์ฃผ 41 14 ์น์ธ 14 ์ํ ์ง์ยท์ฌ๊ณ ๊ทผ๊ฑฐ ํ์ธ\n",
"๊ฒฐ์ ์ง๊ณ: ์น์ธ 11๊ฑด, ์กฐ์ 3๊ฑด, ๋ณด๋ฅ 2๊ฑด\n",
"์๋ ํ์ 0๊ฑด, ๊ฒฐ์ฌ ์ฌ์ ๊ธฐ๋ก๋ฅ 100%, ๊ฐ์ฌ ๋ก๊ทธ 32๊ฑด ๋ฌด๊ฒฐ์ฑ: ์ ์\n",
"๋ณ์กฐ ์ํ(1๋ฒ ์กฐ์ ๊ธฐ๋ก์ ์น์ธ์ผ๋ก ์์ ) -> 1๋ฒ ๊ธฐ๋ก์์ ๋ณ์กฐ ๊ฒ์ถ\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
08. ์ ์ ์์ ํ
ํฌ ์ฆ๊ฐ ๋ชจ์ ๋ถํ ์์ 300% ํญ์ฆ ์๋ฎฌ๋ ์ด์
๋ถํ ํ
์คํธ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ ์ ๋ถํ ์์ 300% ํญ์ฆ ์๋ฎฌ๋ ์ด์
๋ถํ ํ
์คํธ๋ฅผ ์ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ ์ ์์ ํ
ํฌ ์ฆ๊ฐ๋ก ๋ถํ ์์๊ฐ ํ์์ 300%๋ก ํญ์ฆํ๋ ์ํฉ์ ๋ชจ์ํด ๊ฒฐํ ๊ฒฝ๋ณด์ ๋์๋ณด๋์ ์ฑ๋ฅ์ ๋ถํ ์ํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **ํ๋ชฉ๊ตฐ๋ณ ์ค์ ์ฆ๊ฐ ๋ฐฐ์๊ฐ ๋ค๋ฅธ 3๊ฐ์ ํญ์ฆ ์ํฉ์ ๋ฐ๋ณต ๋ชจ์ํ๊ณ , ํ์ ์์ธกยท๋ชจ๋ธ ์ง์ ์์ธกยท์์ ํ
ํฌ ๊ณ์ ๋ณด์ ยท์๋ณ ์ค์ ๋ฐ์ ๋ฐฉ์์ ๊ฒฝ๋ณด ์ ํ๋์ ๊ฒฐํ ํ์ง์จ์ ๋น๊ต ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/dashboard/mnt02_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": [
"\"\"\"์ ์ ์์ ํ
ํฌ ์ฆ๊ฐ๋ก ๋ถํ ์์๊ฐ ํ์์ 300%๋ก ํญ์ฆํ๋ 3๊ฐ์ ์ํฉ์ ๊ฐ์์ผ๋ก ๋ชจ์ํด, ํ๋ชฉ๋ณ ๊ฒฐํ ๊ฒฝ๋ณด(๋ค์ ๋ฌ ์์ธก ์์ > ๊ฐ์ฉ์ฌ๊ณ )์ ์ ํ๋๋ฅผ ๋ฐ๋ณต ์ํํ๊ณ , ํ๋ชฉ ์ 50๋ฐฐ ๋ถํ์์ ์ฌ๊ณ ๋์๋ณด๋ ๊ฐฑ์ ์๊ฐ(2์ด ์ด๋ด)์ ์ธก์ ํ๋ค. ํธ๋ฆฌ ๋ชจ๋ธ์ด ํ์ต ๋ฒ์ ๋ฐ ์์๋ฅผ ์ธ์ฝํ์ง ๋ชปํ๋ ํ๊ณ๋ฅผ ์์ ํ
ํฌ ๊ณ์ ๋ณด์ ์ผ๋ก ๋ณด์ํ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy.stats import norm\n",
"from sklearn.ensemble import HistGradientBoostingRegressor, RandomForestRegressor\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ผ์ ์ ๋น์ฐฝ ์๋ฆฌ๋ถ์ ์ฒญ๊ตฌ/์ถ๊ณ ยท์ฅ๋น ๊ฐ๋๋ฅ ๋ฐ์ดํฐ ์์ฑ๊ธฐ\n",
"# ์ค์ ๋ถ๋ยทํ๋ชฉยท์ฌ๊ณ ยท๋จ๊ฐ ์ ๋ณด์ ๋ฌด๊ดํ๋ค. ํ๋ชฉ 40๊ฐ x 48๊ฐ์(๊ฐ์ 2022-01 ~ 2025-12) ์๋ณ ์๋ฃ๋ฅผ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"FSC = {\"2520\": \"power_train\", \"2530\": \"brake_steer\", \"2910\": \"fuel\", \"2930\": \"cooling\",\n",
" \"2940\": \"filter\", \"5330\": \"gasket\", \"6140\": \"battery\", \"5961\": \"semiconductor\"}\n",
"EXERCISE = {3, 8, 10} # ๊ฐ์ ๋๊ท๋ชจ ํ๋ จ ์(ํ๋ จ ๊ณํ์ผ๋ก ๋ฏธ๋ฆฌ ์ ์ ์์)\n",
"N_MONTHS = 48\n",
"\n",
"\n",
"def season(fsc, month):\n",
" \"\"\"ํ๋ชฉ๊ตฐ๋ณ ๊ณ์ ๊ณ์: ์ถ์ ์ง๋ ๊ฒจ์ธ, ๋๊ฐ๊ณํต์ ์ฌ๋ฆ์ ์์๊ฐ ๋์ด๋๋ค.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 8] ์ ์ ์์ ํ
ํฌ ์ฆ๊ฐ ๋ชจ์ ๋ถํ ์์ 300% ํญ์ฆ ์๋ฎฌ๋ ์ด์
๋ถํ ํ
์คํธ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def make_items(n=40, seed=2):\n",
" \"\"\"ํ๋ชฉ ๋ง์คํฐ: NSN(๊ฐ์), ํ๋ชฉ๊ตฐ, ์ฅ๋น ์ ํ, ๊ธฐ์ค ์์, ๋จ๊ฐ, ์กฐ๋ฌ ๋ฆฌ๋ํ์, ๊ณต๊ธ์
์ฒด ์, ์์ฐ(๋จ์ข
) ์ํ.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for i in range(n):\n",
" f = str(rng.choice(list(FSC)))\n",
" status = str(rng.choice([\"์์ฐ์ค\", \"๋จ์ข
์๊ณ \", \"๋จ์ข
\"], p=[0.7, 0.2, 0.1]))\n",
" rows.append({\"item_id\": f\"P{i + 1:03d}\", \"nsn\": f\"{f}-00-{rng.integers(100, 1000)}-{rng.integers(1000, 10000)}\",\n",
" \"fsc\": f, \"group\": FSC[f], \"equip_type\": str(rng.choice([\"A\", \"B\", \"C\"])),\n",
" \"base\": float(np.round(rng.lognormal(2.7, 0.8), 1)),\n",
" \"price\": int(rng.lognormal(12, 1.0)) // 100 * 100,\n",
" \"lead_time\": int(rng.integers(1, 7) if status == \"์์ฐ์ค\" else rng.integers(4, 13)),\n",
" \"suppliers\": int(rng.integers(1, 5)) if status == \"์์ฐ์ค\" else 1,\n",
" \"status\": status, \"alt_available\": bool(rng.random() < (0.6 if status == \"์์ฐ์ค\" else 0.3))})\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"def make_history(items, months=N_MONTHS, seed=3):\n",
" \"\"\"์๋ณ ์ฒญ๊ตฌ ์์(๊ฐ), ๊ธด๊ธ ๋ถ์ถ ๊ฑด์, ์ฅ๋น ์ ํ๋ณ ์ด์ฉ์๊ฐยท๊ฐ๋๋ฅ .\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" t = np.arange(months)\n",
" moy = t % 12 + 1\n",
" ex = np.isin(moy, list(EXERCISE))\n",
" hours = {e: (100 + 20 * rng.random(months)) * np.where(ex, 1.5, 1.0) for e in \"ABC\"}\n",
" avail = {e: np.clip(0.93 - 0.12 * (hours[e] - 100) / 80 + rng.normal(0, 0.015, months), 0.6, 0.99) for e in \"ABC\"}\n",
" rows = []\n",
" for it in items.itertuples():\n",
" trend = 1 + rng.uniform(-0.1, 0.3) * t / months\n",
" s = np.array([season(it.fsc, m) for m in moy])\n",
" lam = it.base * s * trend * hours[it.equip_type] / 110\n",
" d = rng.poisson(lam)\n",
" rows.append(pd.DataFrame({\"item_id\": it.item_id, \"t\": t, \"ym\": [f\"{2022 + k // 12}-{k % 12 + 1:02d}\" for k in t],\n",
" \"month\": moy, \"exercise\": ex.astype(int), \"op_hours\": hours[it.equip_type].round(1),\n",
" \"availability\": avail[it.equip_type].round(3), \"demand\": d,\n",
" \"emergency\": rng.binomial(d, 0.05)}))\n",
" return pd.concat(rows, ignore_index=True)\n",
"\n",
"\n",
"def make_stock(items, hist, seed=4):\n",
" \"\"\"ํ์ฌ(๊ฐ์ 2026-01) ์ฌ๊ณ : ๋ณด์ ๋๊ณผ ๋ฏธ์
๊ณ ๋ฐ์ฃผ๋(์กฐ๋ฌ ์ค์ธ ๋ฌผ๋). ๋จ์ข
ํ๋ชฉ์ ์ถ๊ฐ ๋ฐ์ฃผ๊ฐ ์ด๋ ค์ ๋ฏธ์
๊ณ ๋์ด ์ ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" recent = hist[hist[\"t\"] >= hist[\"t\"].max() - 5].groupby(\"item_id\")[\"demand\"].mean()\n",
" st = items[[\"item_id\", \"lead_time\", \"status\"]].copy()\n",
" st[\"on_hand\"] = [int(round(recent[i] * rng.uniform(0.5, 3.0))) for i in st[\"item_id\"]]\n",
" cover = np.where(st[\"status\"] == \"์์ฐ์ค\", rng.uniform(0.6, 1.3, len(st)), rng.uniform(0.0, 0.6, len(st)))\n",
" st[\"on_order\"] = [int(round(recent[i] * L * c)) for i, L, c in zip(st[\"item_id\"], st[\"lead_time\"], cover)]\n",
" return st[[\"item_id\", \"on_hand\", \"on_order\"]]\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์์ ์์ธก ๊ณตํต ํจ์(ํ์ ํผ์ฒ + ๋จธ์ ๋ฌ๋ ๋ชจ๋ธ) ----\n",
"FEATS = [\"lag1\", \"lag2\", \"lag3\", \"lag12\", \"ma3\", \"ma6\", \"std6\", \"season_idx\", \"month_sin\", \"month_cos\",\n",
" \"exercise\", \"op_hours\", \"op_ratio\", \"item_level\", \"trend_t\"]\n",
"\n",
"\n",
"def make_features(hist):\n",
" \"\"\"ํ๋ชฉ๋ณ ๊ณผ๊ฑฐ ์์๋ก ํ์ ํผ์ฒ๋ฅผ ๋ง๋ ๋ค(๋ชจ๋ ์์ธก ์์ ์ด์ ์ ๋ณด๋ง ์ฌ์ฉ).\"\"\"\n",
" df = hist.sort_values([\"item_id\", \"t\"]).reset_index(drop=True)\n",
" g = df.groupby(\"item_id\")[\"demand\"]\n",
" for k in (1, 2, 3, 12):\n",
" df[f\"lag{k}\"] = g.shift(k)\n",
" df[\"ma3\"] = g.transform(lambda s: s.shift(1).rolling(3).mean())\n",
" df[\"ma6\"] = g.transform(lambda s: s.shift(1).rolling(6).mean())\n",
" df[\"std6\"] = g.transform(lambda s: s.shift(1).rolling(6).std())\n",
" df[\"item_level\"] = g.transform(lambda s: s.shift(1).rolling(12).mean())\n",
" df[\"season_idx\"] = df[\"lag12\"] / (g.transform(lambda s: s.shift(12).rolling(12).mean()) + 1)\n",
" df[\"month_sin\"] = np.sin(2 * np.pi * df[\"month\"] / 12)\n",
" df[\"month_cos\"] = np.cos(2 * np.pi * df[\"month\"] / 12)\n",
" df[\"op_ratio\"] = df[\"op_hours\"] / df.groupby(\"item_id\")[\"op_hours\"].transform(lambda s: s.shift(1).rolling(12).mean())\n",
" df[\"trend_t\"] = df[\"t\"]\n",
" return df.dropna().reset_index(drop=True)\n",
"\n",
"\n",
"def new_model():\n",
" \"\"\"Prophet/LSTM ๋์ ์ฌ์ฉํ๋ ํฌ์์ก ์์ค ๊ทธ๋๋์ธํธ ๋ถ์คํ
(๊ฐ์ ์๋ฃ์ ์ ํฉ).\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 8] ์ ์ ์์ ํ
ํฌ ์ฆ๊ฐ ๋ชจ์ ๋ถํ ์์ 300% ํญ์ฆ ์๋ฎฌ๋ ์ด์
๋ถํ ํ
์คํธ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def mape(y, p, min_y=1):\n",
" y, p = np.asarray(y, float), np.asarray(p, float)\n",
" m = y >= min_y\n",
" return float(np.mean(np.abs(y[m] - p[m]) / y[m]))\n",
"\n",
"# ---- ์์๋ธ ์์ ์์ธกยท์์ ์ฌ๊ณ ยท๊ฒฐํ(๋จ์ข
) ์ํ ์ง์ ๊ณตํต ํจ์ ----\n",
"STATUS_W = {\"์์ฐ์ค\": 0.0, \"๋จ์ข
์๊ณ \": 0.5, \"๋จ์ข
\": 1.0} # [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋จ์ข
์ํ ๊ฐ์ค์น\n",
"\n",
"\n",
"def fit_ens(tr):\n",
" \"\"\"ํฌ์์ก ๊ทธ๋๋์ธํธ ๋ถ์คํ
(70%) + ๋๋คํฌ๋ ์คํธ(30%) ์์๋ธ.\"\"\"\n",
" h = new_model().fit(tr[FEATS], tr[\"demand\"])\n",
" r = RandomForestRegressor(n_estimators=150, min_samples_leaf=5, random_state=0, n_jobs=1).fit(tr[FEATS], tr[\"demand\"])\n",
" return h, r\n",
"\n",
"\n",
"def pred_ens(m, X, w=0.7):\n",
" return np.clip(w * m[0].predict(X[FEATS]) + (1 - w) * m[1].predict(X[FEATS]), 0, None)\n",
"\n",
"\n",
"def next_month(hist, items, t_new=48, op_mult=1.0, ex=0):\n",
" \"\"\"๋ค์ ๋ฌ(t_new) ์์ธก์ฉ ํ: ์๋
๊ฐ์ ๋ฌ ์ด์ฉ์๊ฐ x ๋ฐฐ์, ํ๋ จ ์ฌ๋ถ ์ง์ .\"\"\"\n",
" last = hist[hist[\"t\"] == t_new - 1].copy()\n",
" prev = hist[hist[\"t\"] == t_new - 12].set_index(\"item_id\")[\"op_hours\"]\n",
" m = (t_new % 12) + 1\n",
" f = last.assign(t=t_new, ym=f\"{2022 + t_new // 12}-{m:02d}\", month=m, demand=0, exercise=ex,\n",
" op_hours=last[\"item_id\"].map(prev).to_numpy() * op_mult)\n",
" X = make_features(pd.concat([hist, f], ignore_index=True))\n",
" return X[X[\"t\"] == t_new].set_index(\"item_id\").loc[items[\"item_id\"]].reset_index()\n",
"\n",
"\n",
"def stock_policy(items, mu, sigma, z=1.65, review=1):\n",
" \"\"\"์์ ์ฌ๊ณ = z x ์ ์์ธก์ค์ฐจ ํ์คํธ์ฐจ x sqrt(๋ฆฌ๋ํ์+๊ฒํ ์ฃผ๊ธฐ), ์ฌ์ฃผ๋ฌธ์ = ์ ์์ x (๋ฆฌ๋ํ์+๊ฒํ ์ฃผ๊ธฐ) + ์์ ์ฌ๊ณ .\"\"\"\n",
" L = items[\"lead_time\"].to_numpy() + review\n",
" ss = z * sigma * np.sqrt(L)\n",
" return pd.DataFrame({\"item_id\": items[\"item_id\"], \"mu\": mu, \"sigma\": sigma, \"L\": L,\n",
" \"ss\": np.ceil(ss), \"rop\": np.ceil(mu * L + ss)})\n",
"\n",
"\n",
"def risk_index(items, pol, stock):\n",
" \"\"\"๊ฒฐํ ์ํ ์ง์(0~100): ๋ฆฌ๋ํ์ ๋์ ์์๊ฐ ๊ฐ์ฉ ์ฌ๊ณ ๋ฅผ ๋์ ํ๋ฅ (์ ๊ท ๊ทผ์ฌ)์ ๊ธฐ๋ณธ์ผ๋ก,\n",
" ๋จ์ข
์ํยท๋จ์ผ ๊ณต๊ธ์
์ฒดยท๋์ฒดํ ๋ถ์ฌ์ ๊ฐ์ค์ ์ค๋ค(๊ฐ์ ๊ฐ์ค์น).\"\"\"\n",
" st = stock.set_index(\"item_id\").loc[items[\"item_id\"]]\n",
" avail = (st[\"on_hand\"] + st[\"on_order\"]).to_numpy()\n",
" p = 1 - norm.cdf((avail - pol[\"mu\"].to_numpy() * pol[\"L\"].to_numpy()) / (pol[\"sigma\"].to_numpy() * np.sqrt(pol[\"L\"].to_numpy()) + 1e-6))\n",
" sw = items[\"status\"].map(STATUS_W).to_numpy()\n",
" sup = (items[\"suppliers\"].to_numpy() == 1).astype(float)\n",
" alt = (~items[\"alt_available\"].to_numpy()).astype(float)\n",
" score = 100 * np.clip(0.55 * p + 0.25 * sw + 0.10 * sup + 0.10 * alt, 0, 1)\n",
" return pd.DataFrame({\"item_id\": items[\"item_id\"], \"p_short\": p, \"status_w\": sw, \"single_sup\": sup, \"no_alt\": alt,\n",
" \"avail\": avail, \"risk\": score.round(1)})\n",
"\n",
"SURGE = 3.0 # ๊ณํ์ ์์ ํ
ํฌ ์์ ๋ฐฐ์(300%)\n",
"GROUP_MULT = {\"power_train\": 3.6, \"brake_steer\": 3.3, \"fuel\": 3.0, \"cooling\": 2.8, # [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ค์ ๋ฐฐ์(๊ณํ์๋ ๋ฏธ๋ฐ์)\n",
" \"filter\": 2.7, \"gasket\": 3.2, \"battery\": 2.6, \"semiconductor\": 3.1}\n",
"REPS, MONTHS = 30, 3\n",
"\n",
"items = make_items()\n",
"hist = make_history(items)\n",
"df = make_features(hist)\n",
"stock = make_stock(items, hist)\n",
"ens = fit_ens(df)\n",
"X = next_month(hist, items, op_mult=SURGE, ex=1)\n",
"raw = pred_ens(ens, X) # ์ด์ฉ์๊ฐ 3๋ฐฐ๋ฅผ ๋ฃ์ ๋ชจ๋ธ ์์ธก(ํ์ต ๋ฒ์ ๋ฐ)\n",
"base = pred_ens(ens, next_month(hist, items)) # ํ์ ์์ธก\n",
"adj = base * SURGE # ์์ ํ
ํฌ ๊ณ์ ๋ณด์ ์์ธก\n",
"print(f\"[ํญ์ฆ ์์ธก] ํ์ ํฉ๊ณ {base.sum():.0f}๊ฐ, ๋ชจ๋ธ ์ง์ ์์ธก {raw.sum():.0f}๊ฐ({raw.sum() / base.sum():.1f}๋ฐฐ), ํ
ํฌ ๊ณ์ ๋ณด์ {adj.sum():.0f}๊ฐ({SURGE:.0f}๋ฐฐ)\")\n",
"print(\"-> ํธ๋ฆฌ ๋ชจ๋ธ์ ํ์ตํ ์ด์ฉ์๊ฐ ๋ฒ์๋ฅผ ๋์ผ๋ฉด ์์ธก์ด ๋ ๋์ง ์๋๋ค(์ธ์ฝ ๋ถ๊ฐ). ๊ณํ ๋ฐฐ์๋ก ๋ณด์ ํ๋ค.\")\n",
"\n",
"\n",
"def run(fc, seed, adaptive=False):\n",
" \"\"\"3๊ฐ์ ๋ชจ์: ๋งค์ ๊ฒฝ๋ณด(์์ธก ์์ > ์์ด ์ฌ๊ณ ) vs ์ค์ ๊ฒฐํ(์ค์ ์์ > ์์ด ์ฌ๊ณ ).\n",
" adaptive=True์ด๋ฉด ์ง๋๋ฌ ํ๋ชฉ๊ตฐ๋ณ ์ค์ /ํ์ ๋น์จ๋ก ๋ค์ ๋ฌ ๋ฐฐ์๋ฅผ ๊ฐฑ์ ํ๋ค(์ค์ ๋ฐ์).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" true_mult = items[\"group\"].map(GROUP_MULT).to_numpy() * rng.lognormal(0, 0.12, len(items))\n",
" on = stock[\"on_hand\"].to_numpy().astype(float)\n",
" arrive = stock[\"on_order\"].to_numpy() / np.maximum(items[\"lead_time\"].to_numpy(), 1)\n",
" al, tr, bd = [], [], []\n",
" grp = items[\"group\"].to_numpy(); f = fc.copy()\n",
" for _ in range(MONTHS):\n",
" actual = rng.poisson(base * true_mult)\n",
" al.append(f > on); tr.append(actual > on); bd.append(np.abs(f - on) <= 1.65 * np.sqrt(np.maximum(f, 1)))\n",
" if adaptive:\n",
" ratio = pd.Series(actual).groupby(grp).sum() / pd.Series(base).groupby(grp).sum()\n",
" f = base * pd.Series(grp).map(ratio).to_numpy()\n",
" on = np.maximum(on - actual, 0) + arrive\n",
" return np.array(al), np.array(tr), np.array(bd)\n",
"\n",
"\n",
"rows = []\n",
"for nm, fc, ad in ((\"ํ์ ์์ธก(๋ณด์ ์์)\", base, False), (\"๋ชจ๋ธ ์ง์ ์์ธก\", raw, False), (\"ํ
ํฌ ๊ณ์ ๋ณด์ \", adj, False),\n",
" (\"ํ
ํฌ ๋ณด์ +์๋ณ ์ค์ ๋ฐ์\", adj, True)):\n",
" acc, rec, n_al = [], [], []\n",
" for s in range(REPS):\n",
" a, t, bd = run(fc, s, ad)\n",
" acc.append((a == t).mean()); rec.append((a & t).sum() / max(t.sum(), 1)); n_al.append(a.sum())\n",
" if ad:\n",
" clear = [(lambda a, t, bd: ((a == t)[~bd].mean(), bd.mean()))(*run(fc, s, ad)) for s in range(REPS)]\n",
" rows.append({\"์์ธก ๋ฐฉ์\": nm, \"๊ฒฝ๋ณด ์ ํ๋\": np.mean(acc), \"์ต์ ์ ํ๋\": np.min(acc), \"๊ฒฐํ ํ์ง์จ\": np.mean(rec), \"์ํ๊ท ๊ฒฝ๋ณด\": np.mean(n_al) / MONTHS})\n",
"res = pd.DataFrame(rows)\n",
"print(f\"\\n[๊ฒฝ๋ณด ์ํ: ํ๋ชฉ {len(items)}๊ฐ x {MONTHS}๊ฐ์ x ๋ฐ๋ณต {REPS}ํ]\")\n",
"print(res.to_string(index=False, formatters={\"๊ฒฝ๋ณด ์ ํ๋\": \"{:.1%}\".format, \"์ต์ ์ ํ๋\": \"{:.1%}\".format, \"๊ฒฐํ ํ์ง์จ\": \"{:.1%}\".format, \"์ํ๊ท ๊ฒฝ๋ณด\": \"{:.1f}\".format}))\n",
"acc = res.iloc[3][\"๊ฒฝ๋ณด ์ ํ๋\"]\n",
"print(f\"ํ
ํฌ ๋ณด์ +์ค์ ๋ฐ์ ๊ฒฝ๋ณด ์ ํ๋ {acc:.1%} (๋ชฉํ 95% ์ด์: {'๋ฌ์ฑ' if acc >= 0.95 else '๋ฏธ๋ฌ'}), ํ
ํฌ ๋ณด์ ๋ง {res.iloc[2]['๊ฒฝ๋ณด ์ ํ๋']:.1%}\")\n",
"print(f\"์ค๊ฒฝ๋ณด ์์ธ: ์์ธก๊ณผ ์ฌ๊ณ ์ฐจ์ด๊ฐ ์์ ๋ณ๋ ํญ(1.65 x ํ์คํธ์ฐจ) ์ด๋ด์ธ ๊ฒฝ๊ณ ๊ตฌ๊ฐ์ด ํ์ ์ {np.mean([c[1] for c in clear]):.0%}๋ฅผ ์ฐจ์งํ๋ค. \"\n",
" f\"๊ฒฝ๊ณ ๊ตฌ๊ฐ์ '์ฃผ์(๊ฒํ ํ์)'๋ก ๋ฐ๋ก ํ์ํ๋ฉด ๋๋จธ์ง ํ์ ์ ํ๋๋ {np.mean([c[0] for c in clear]):.1%}์ด๋ค.\")\n",
"\n",
"\n",
"# ๋ถํ ์ํ: ํ๋ชฉ 50๋ฐฐ(2,000๊ฐ) ๋์๋ณด๋ ๊ฐฑ์ = ์์ธก + ์ํ ์ง์ + ์ง๊ณ\n",
"def refresh(Xb, it, st):\n",
" mu = pred_ens(ens, Xb) * SURGE\n",
" pol = stock_policy(it, mu, np.sqrt(np.maximum(mu, 1)))\n",
" rk = risk_index(it, pol, st)\n",
" return rk.assign(group=it[\"group\"].to_numpy()).groupby(\"group\")[\"risk\"].agg([\"mean\", \"max\"]), rk\n",
"\n",
"\n",
"K = 50\n",
"Xb = pd.concat([X] * K, ignore_index=True)\n",
"itb = pd.concat([items.assign(item_id=items[\"item_id\"] + f\"_{k}\") for k in range(K)], ignore_index=True)\n",
"stb = pd.concat([stock.assign(item_id=stock[\"item_id\"] + f\"_{k}\") for k in range(K)], ignore_index=True)\n",
"times = []\n",
"for _ in range(3):\n",
" t0 = time.perf_counter(); agg, rk = refresh(Xb, itb, stb); times.append(time.perf_counter() - t0)\n",
"t1 = time.perf_counter(); refresh(X, items, stock); t_small = time.perf_counter() - t1\n",
"print(f\"\\n[๋์๋ณด๋ ๋ถํ ์ํ] ํ๋ชฉ {len(itb)}๊ฐ ๊ฐฑ์ ํ๊ท {np.mean(times):.2f}s / ์ต๋ {max(times):.2f}s, ํ๋ชฉ 40๊ฐ {t_small:.3f}s (๋ชฉํ 2์ด ์ด๋ด: {'๋ฌ์ฑ' if max(times) <= 2 else '๋ฏธ๋ฌ'})\")\n",
"print(\"ํญ์ฆ ์ ํ๋ชฉ๊ตฐ๋ณ ์ต๋ ์ํ ์ง์:\", \", \".join(f\"{g} {r['max']:.0f}\" for g, r in agg.iterrows()))\n",
"print(\"๊ฒฝ๋ณด๋ ์กฐ๋ฌ ๊ฒํ ์์ฒญ์ด๋ฉฐ ๊ธด๊ธ ์กฐ๋ฌยท๋์ฒดํ ์ ํ์ ์ ๋น์ฅ๊ต ์น์ธ ํ ์งํํ๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(12, 4))\n",
"ax[0].bar([\"normal\", \"raw\", \"tempo\", \"tempo+AAR\"], res[\"๊ฒฝ๋ณด ์ ํ๋\"], color=[\"gray\", \"tab:orange\", \"tab:green\", \"tab:blue\"])\n",
"ax[0].axhline(0.95, c=\"r\", ls=\"--\"); ax[0].set_ylim(0.5, 1); ax[0].set_title(\"Alert accuracy under 300% demand surge\")\n",
"ax[1].scatter(base, raw, s=12, label=\"model raw\"); ax[1].scatter(base, adj, s=12, label=\"tempo adjusted\")\n",
"ax[1].plot([0, base.max()], [0, base.max() * 3], \"k--\", lw=1, label=\"x3 line\"); ax[1].set_xlabel(\"normal forecast\"); ax[1].set_ylabel(\"surge forecast\")\n",
"ax[1].set_title(\"Tree model cannot extrapolate\"); ax[1].legend()\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
08 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[ํญ์ฆ ์์ธก] ํ์ ํฉ๊ณ 948๊ฐ, ๋ชจ๋ธ ์ง์ ์์ธก 1193๊ฐ(1.3๋ฐฐ), ํ
ํฌ ๊ณ์ ๋ณด์ 2843๊ฐ(3๋ฐฐ)\n",
"-> ํธ๋ฆฌ ๋ชจ๋ธ์ ํ์ตํ ์ด์ฉ์๊ฐ ๋ฒ์๋ฅผ ๋์ผ๋ฉด ์์ธก์ด ๋ ๋์ง ์๋๋ค(์ธ์ฝ ๋ถ๊ฐ). ๊ณํ ๋ฐฐ์๋ก ๋ณด์ ํ๋ค.\n",
"\n",
"[๊ฒฝ๋ณด ์ํ: ํ๋ชฉ 40๊ฐ x 3๊ฐ์ x ๋ฐ๋ณต 30ํ]\n",
" ์์ธก ๋ฐฉ์ ๊ฒฝ๋ณด ์ ํ๋ ์ต์ ์ ํ๋ ๊ฒฐํ ํ์ง์จ ์ํ๊ท ๊ฒฝ๋ณด\n",
" ํ์ ์์ธก(๋ณด์ ์์) 45.9% 40.8% 40.3% 14.6\n",
" ๋ชจ๋ธ ์ง์ ์์ธก 65.0% 61.7% 61.5% 22.3\n",
" ํ
ํฌ ๊ณ์ ๋ณด์ 93.6% 88.3% 98.5% 37.7\n",
"ํ
ํฌ ๋ณด์ +์๋ณ ์ค์ ๋ฐ์ 93.8% 88.3% 98.4% 37.5\n",
"ํ
ํฌ ๋ณด์ +์ค์ ๋ฐ์ ๊ฒฝ๋ณด ์ ํ๋ 93.8% (๋ชฉํ 95% ์ด์: ๋ฏธ๋ฌ), ํ
ํฌ ๋ณด์ ๋ง 93.6%\n",
"์ค๊ฒฝ๋ณด ์์ธ: ์์ธก๊ณผ ์ฌ๊ณ ์ฐจ์ด๊ฐ ์์ ๋ณ๋ ํญ(1.65 x ํ์คํธ์ฐจ) ์ด๋ด์ธ ๊ฒฝ๊ณ ๊ตฌ๊ฐ์ด ํ์ ์ 19%๋ฅผ ์ฐจ์งํ๋ค. ๊ฒฝ๊ณ ๊ตฌ๊ฐ์ '์ฃผ์(๊ฒํ ํ์)'๋ก ๋ฐ๋ก ํ์ํ๋ฉด ๋๋จธ์ง ํ์ ์ ํ๋๋ 99.6%์ด๋ค.\n",
"\n",
"[๋์๋ณด๋ ๋ถํ ์ํ] ํ๋ชฉ 2000๊ฐ ๊ฐฑ์ ํ๊ท 0.05s / ์ต๋ 0.05s, ํ๋ชฉ 40๊ฐ 0.024s (๋ชฉํ 2์ด ์ด๋ด: ๋ฌ์ฑ)\n",
"ํญ์ฆ ์ ํ๋ชฉ๊ตฐ๋ณ ์ต๋ ์ํ ์ง์: battery 100, brake_steer 88, cooling 65, filter 88, fuel 88, gasket 90, power_train 90, semiconductor 78\n",
"๊ฒฝ๋ณด๋ ์กฐ๋ฌ ๊ฒํ ์์ฒญ์ด๋ฉฐ ๊ธด๊ธ ์กฐ๋ฌยท๋์ฒดํ ์ ํ์ ์ ๋น์ฅ๊ต ์น์ธ ํ ์งํํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
09. AAR ๊ธฐ๋ฐ ๋ถํ ๊ณผ๋ค/๊ณผ์ ์ฌ๊ณ ์์ธก ์ค์ฐจ ๋ถ์ ๋ฐ ์์ ์ฌ๊ณ ํ๋ผ๋ฏธํฐ ์ฌ๋ณด์ \n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"AAR ๊ธฐ๋ฐ ๋ถํ ์์ ์์ธก ์ค์ฐจ๋ฅผ ๋ถ์ํ๊ณ ์์ ์ฌ๊ณ ํ๋ผ๋ฏธํฐ๋ฅผ ์ฌ๋ณด์ ํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **AAR ๊ด์ ์์ ๊ณผ๋คยท๊ณผ์ ์ฌ๊ณ ์ ์์ ์์ธก ์ค์ฐจ๋ฅผ ๋ถ์ํ๊ณ ์์ ์ฌ๊ณ ํ๋ผ๋ฏธํฐ๋ฅผ ์ฌ๋ณด์ ํด ๋์
ํจ๊ณผ๋ฅผ ์ฐ์ถํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ 6๊ฐ์ ์ด์ ๊ฒฐ๊ณผ๋ฅผ ํ๋ชฉ๊ตฐยทABC ๋ฑ๊ธยท์์ฐ ์ํ๋ณ ์์ธก ํธํฅ, ๊ณผ๋ค ์ฌ๊ณ , ๊ฒฐํ ํ๋ชฉ์ผ๋ก ๋ถ์ํด ์์ธ์ ์ฐพ๋๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `reports/mnt02_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 ๊ด์ ์์ ์ง๋ 12๊ฐ์ ์ฌ๊ณ ์ด์์ ๊ณผ๋คยท๊ณผ์ ์ฌ๊ณ ์ ์์ ์์ธก ์ค์ฐจ๋ฅผ ํ๋ชฉ๊ตฐยท์์ฐ ์ํยทABC ๋ฑ๊ธ๋ณ๋ก ๋ถ์ํด ์์ธ์ ์ฐพ๊ณ , ์ 6๊ฐ์ ์๋ฃ๋ก ABC ๋ฑ๊ธ๋ณ ์์ ์ฌ๊ณ ๊ณ์(z)์ ํ๋ชฉ๊ตฐ๋ณ ์์ธก ํธํฅ ๋ณด์ ๊ฐ์ ์ฌ๋ณด์ ํ ๋ค ๋ค 6๊ฐ์์์ ๊ฒ์ฆํ๋ฉฐ, ๋น์ฉ ์ ๊ฐ์กยท์ ๋น ๋๊ธฐ์๊ฐ ๋จ์ถ๋ฅ ยท์ฌ๊ณ ํ์ ์จ 3๋ ํจ๊ณผ ์งํ์ ๋ฏผ๊ฐ๋, ๋์
๋ก๋๋งต์ ์ฐ์ถํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy.stats import norm\n",
"from sklearn.ensemble import HistGradientBoostingRegressor, RandomForestRegressor\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ผ์ ์ ๋น์ฐฝ ์๋ฆฌ๋ถ์ ์ฒญ๊ตฌ/์ถ๊ณ ยท์ฅ๋น ๊ฐ๋๋ฅ ๋ฐ์ดํฐ ์์ฑ๊ธฐ\n",
"# ์ค์ ๋ถ๋ยทํ๋ชฉยท์ฌ๊ณ ยท๋จ๊ฐ ์ ๋ณด์ ๋ฌด๊ดํ๋ค. ํ๋ชฉ 40๊ฐ x 48๊ฐ์(๊ฐ์ 2022-01 ~ 2025-12) ์๋ณ ์๋ฃ๋ฅผ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"FSC = {\"2520\": \"power_train\", \"2530\": \"brake_steer\", \"2910\": \"fuel\", \"2930\": \"cooling\",\n",
" \"2940\": \"filter\", \"5330\": \"gasket\", \"6140\": \"battery\", \"5961\": \"semiconductor\"}\n",
"EXERCISE = {3, 8, 10} # ๊ฐ์ ๋๊ท๋ชจ ํ๋ จ ์(ํ๋ จ ๊ณํ์ผ๋ก ๋ฏธ๋ฆฌ ์ ์ ์์)\n",
"N_MONTHS = 48\n",
"\n",
"\n",
"def season(fsc, month):\n",
" \"\"\"ํ๋ชฉ๊ตฐ๋ณ ๊ณ์ ๊ณ์: ์ถ์ ์ง๋ ๊ฒจ์ธ, ๋๊ฐ๊ณํต์ ์ฌ๋ฆ์ ์์๊ฐ ๋์ด๋๋ค.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 9] AAR ๊ธฐ๋ฐ ๋ถํ ๊ณผ๋ค/๊ณผ์ ์ฌ๊ณ ์์ธก ์ค์ฐจ ๋ถ์ ๋ฐ ์์ ์ฌ๊ณ ํ๋ผ๋ฏธํฐ ์ฌ๋ณด์ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def make_items(n=40, seed=2):\n",
" \"\"\"ํ๋ชฉ ๋ง์คํฐ: NSN(๊ฐ์), ํ๋ชฉ๊ตฐ, ์ฅ๋น ์ ํ, ๊ธฐ์ค ์์, ๋จ๊ฐ, ์กฐ๋ฌ ๋ฆฌ๋ํ์, ๊ณต๊ธ์
์ฒด ์, ์์ฐ(๋จ์ข
) ์ํ.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for i in range(n):\n",
" f = str(rng.choice(list(FSC)))\n",
" status = str(rng.choice([\"์์ฐ์ค\", \"๋จ์ข
์๊ณ \", \"๋จ์ข
\"], p=[0.7, 0.2, 0.1]))\n",
" rows.append({\"item_id\": f\"P{i + 1:03d}\", \"nsn\": f\"{f}-00-{rng.integers(100, 1000)}-{rng.integers(1000, 10000)}\",\n",
" \"fsc\": f, \"group\": FSC[f], \"equip_type\": str(rng.choice([\"A\", \"B\", \"C\"])),\n",
" \"base\": float(np.round(rng.lognormal(2.7, 0.8), 1)),\n",
" \"price\": int(rng.lognormal(12, 1.0)) // 100 * 100,\n",
" \"lead_time\": int(rng.integers(1, 7) if status == \"์์ฐ์ค\" else rng.integers(4, 13)),\n",
" \"suppliers\": int(rng.integers(1, 5)) if status == \"์์ฐ์ค\" else 1,\n",
" \"status\": status, \"alt_available\": bool(rng.random() < (0.6 if status == \"์์ฐ์ค\" else 0.3))})\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"def make_history(items, months=N_MONTHS, seed=3):\n",
" \"\"\"์๋ณ ์ฒญ๊ตฌ ์์(๊ฐ), ๊ธด๊ธ ๋ถ์ถ ๊ฑด์, ์ฅ๋น ์ ํ๋ณ ์ด์ฉ์๊ฐยท๊ฐ๋๋ฅ .\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" t = np.arange(months)\n",
" moy = t % 12 + 1\n",
" ex = np.isin(moy, list(EXERCISE))\n",
" hours = {e: (100 + 20 * rng.random(months)) * np.where(ex, 1.5, 1.0) for e in \"ABC\"}\n",
" avail = {e: np.clip(0.93 - 0.12 * (hours[e] - 100) / 80 + rng.normal(0, 0.015, months), 0.6, 0.99) for e in \"ABC\"}\n",
" rows = []\n",
" for it in items.itertuples():\n",
" trend = 1 + rng.uniform(-0.1, 0.3) * t / months\n",
" s = np.array([season(it.fsc, m) for m in moy])\n",
" lam = it.base * s * trend * hours[it.equip_type] / 110\n",
" d = rng.poisson(lam)\n",
" rows.append(pd.DataFrame({\"item_id\": it.item_id, \"t\": t, \"ym\": [f\"{2022 + k // 12}-{k % 12 + 1:02d}\" for k in t],\n",
" \"month\": moy, \"exercise\": ex.astype(int), \"op_hours\": hours[it.equip_type].round(1),\n",
" \"availability\": avail[it.equip_type].round(3), \"demand\": d,\n",
" \"emergency\": rng.binomial(d, 0.05)}))\n",
" return pd.concat(rows, ignore_index=True)\n",
"\n",
"\n",
"def make_stock(items, hist, seed=4):\n",
" \"\"\"ํ์ฌ(๊ฐ์ 2026-01) ์ฌ๊ณ : ๋ณด์ ๋๊ณผ ๋ฏธ์
๊ณ ๋ฐ์ฃผ๋(์กฐ๋ฌ ์ค์ธ ๋ฌผ๋). ๋จ์ข
ํ๋ชฉ์ ์ถ๊ฐ ๋ฐ์ฃผ๊ฐ ์ด๋ ค์ ๋ฏธ์
๊ณ ๋์ด ์ ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" recent = hist[hist[\"t\"] >= hist[\"t\"].max() - 5].groupby(\"item_id\")[\"demand\"].mean()\n",
" st = items[[\"item_id\", \"lead_time\", \"status\"]].copy()\n",
" st[\"on_hand\"] = [int(round(recent[i] * rng.uniform(0.5, 3.0))) for i in st[\"item_id\"]]\n",
" cover = np.where(st[\"status\"] == \"์์ฐ์ค\", rng.uniform(0.6, 1.3, len(st)), rng.uniform(0.0, 0.6, len(st)))\n",
" st[\"on_order\"] = [int(round(recent[i] * L * c)) for i, L, c in zip(st[\"item_id\"], st[\"lead_time\"], cover)]\n",
" return st[[\"item_id\", \"on_hand\", \"on_order\"]]\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์์ ์์ธก ๊ณตํต ํจ์(ํ์ ํผ์ฒ + ๋จธ์ ๋ฌ๋ ๋ชจ๋ธ) ----\n",
"FEATS = [\"lag1\", \"lag2\", \"lag3\", \"lag12\", \"ma3\", \"ma6\", \"std6\", \"season_idx\", \"month_sin\", \"month_cos\",\n",
" \"exercise\", \"op_hours\", \"op_ratio\", \"item_level\", \"trend_t\"]\n",
"\n",
"\n",
"def make_features(hist):\n",
" \"\"\"ํ๋ชฉ๋ณ ๊ณผ๊ฑฐ ์์๋ก ํ์ ํผ์ฒ๋ฅผ ๋ง๋ ๋ค(๋ชจ๋ ์์ธก ์์ ์ด์ ์ ๋ณด๋ง ์ฌ์ฉ).\"\"\"\n",
" df = hist.sort_values([\"item_id\", \"t\"]).reset_index(drop=True)\n",
" g = df.groupby(\"item_id\")[\"demand\"]\n",
" for k in (1, 2, 3, 12):\n",
" df[f\"lag{k}\"] = g.shift(k)\n",
" df[\"ma3\"] = g.transform(lambda s: s.shift(1).rolling(3).mean())\n",
" df[\"ma6\"] = g.transform(lambda s: s.shift(1).rolling(6).mean())\n",
" df[\"std6\"] = g.transform(lambda s: s.shift(1).rolling(6).std())\n",
" df[\"item_level\"] = g.transform(lambda s: s.shift(1).rolling(12).mean())\n",
" df[\"season_idx\"] = df[\"lag12\"] / (g.transform(lambda s: s.shift(12).rolling(12).mean()) + 1)\n",
" df[\"month_sin\"] = np.sin(2 * np.pi * df[\"month\"] / 12)\n",
" df[\"month_cos\"] = np.cos(2 * np.pi * df[\"month\"] / 12)\n",
" df[\"op_ratio\"] = df[\"op_hours\"] / df.groupby(\"item_id\")[\"op_hours\"].transform(lambda s: s.shift(1).rolling(12).mean())\n",
" df[\"trend_t\"] = df[\"t\"]\n",
" return df.dropna().reset_index(drop=True)\n",
"\n",
"\n",
"def new_model():\n",
" \"\"\"Prophet/LSTM ๋์ ์ฌ์ฉํ๋ ํฌ์์ก ์์ค ๊ทธ๋๋์ธํธ ๋ถ์คํ
(๊ฐ์ ์๋ฃ์ ์ ํฉ).\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 9] AAR ๊ธฐ๋ฐ ๋ถํ ๊ณผ๋ค/๊ณผ์ ์ฌ๊ณ ์์ธก ์ค์ฐจ ๋ถ์ ๋ฐ ์์ ์ฌ๊ณ ํ๋ผ๋ฏธํฐ ์ฌ๋ณด์ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def mape(y, p, min_y=1):\n",
" y, p = np.asarray(y, float), np.asarray(p, float)\n",
" m = y >= min_y\n",
" return float(np.mean(np.abs(y[m] - p[m]) / y[m]))\n",
"\n",
"# ---- ์์๋ธ ์์ ์์ธกยท์์ ์ฌ๊ณ ยท๊ฒฐํ(๋จ์ข
) ์ํ ์ง์ ๊ณตํต ํจ์ ----\n",
"STATUS_W = {\"์์ฐ์ค\": 0.0, \"๋จ์ข
์๊ณ \": 0.5, \"๋จ์ข
\": 1.0} # [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋จ์ข
์ํ ๊ฐ์ค์น\n",
"\n",
"\n",
"def fit_ens(tr):\n",
" \"\"\"ํฌ์์ก ๊ทธ๋๋์ธํธ ๋ถ์คํ
(70%) + ๋๋คํฌ๋ ์คํธ(30%) ์์๋ธ.\"\"\"\n",
" h = new_model().fit(tr[FEATS], tr[\"demand\"])\n",
" r = RandomForestRegressor(n_estimators=150, min_samples_leaf=5, random_state=0, n_jobs=1).fit(tr[FEATS], tr[\"demand\"])\n",
" return h, r\n",
"\n",
"\n",
"def pred_ens(m, X, w=0.7):\n",
" return np.clip(w * m[0].predict(X[FEATS]) + (1 - w) * m[1].predict(X[FEATS]), 0, None)\n",
"\n",
"\n",
"def next_month(hist, items, t_new=48, op_mult=1.0, ex=0):\n",
" \"\"\"๋ค์ ๋ฌ(t_new) ์์ธก์ฉ ํ: ์๋
๊ฐ์ ๋ฌ ์ด์ฉ์๊ฐ x ๋ฐฐ์, ํ๋ จ ์ฌ๋ถ ์ง์ .\"\"\"\n",
" last = hist[hist[\"t\"] == t_new - 1].copy()\n",
" prev = hist[hist[\"t\"] == t_new - 12].set_index(\"item_id\")[\"op_hours\"]\n",
" m = (t_new % 12) + 1\n",
" f = last.assign(t=t_new, ym=f\"{2022 + t_new // 12}-{m:02d}\", month=m, demand=0, exercise=ex,\n",
" op_hours=last[\"item_id\"].map(prev).to_numpy() * op_mult)\n",
" X = make_features(pd.concat([hist, f], ignore_index=True))\n",
" return X[X[\"t\"] == t_new].set_index(\"item_id\").loc[items[\"item_id\"]].reset_index()\n",
"\n",
"\n",
"def stock_policy(items, mu, sigma, z=1.65, review=1):\n",
" \"\"\"์์ ์ฌ๊ณ = z x ์ ์์ธก์ค์ฐจ ํ์คํธ์ฐจ x sqrt(๋ฆฌ๋ํ์+๊ฒํ ์ฃผ๊ธฐ), ์ฌ์ฃผ๋ฌธ์ = ์ ์์ x (๋ฆฌ๋ํ์+๊ฒํ ์ฃผ๊ธฐ) + ์์ ์ฌ๊ณ .\"\"\"\n",
" L = items[\"lead_time\"].to_numpy() + review\n",
" ss = z * sigma * np.sqrt(L)\n",
" return pd.DataFrame({\"item_id\": items[\"item_id\"], \"mu\": mu, \"sigma\": sigma, \"L\": L,\n",
" \"ss\": np.ceil(ss), \"rop\": np.ceil(mu * L + ss)})\n",
"\n",
"\n",
"def risk_index(items, pol, stock):\n",
" \"\"\"๊ฒฐํ ์ํ ์ง์(0~100): ๋ฆฌ๋ํ์ ๋์ ์์๊ฐ ๊ฐ์ฉ ์ฌ๊ณ ๋ฅผ ๋์ ํ๋ฅ (์ ๊ท ๊ทผ์ฌ)์ ๊ธฐ๋ณธ์ผ๋ก,\n",
" ๋จ์ข
์ํยท๋จ์ผ ๊ณต๊ธ์
์ฒดยท๋์ฒดํ ๋ถ์ฌ์ ๊ฐ์ค์ ์ค๋ค(๊ฐ์ ๊ฐ์ค์น).\"\"\"\n",
" st = stock.set_index(\"item_id\").loc[items[\"item_id\"]]\n",
" avail = (st[\"on_hand\"] + st[\"on_order\"]).to_numpy()\n",
" p = 1 - norm.cdf((avail - pol[\"mu\"].to_numpy() * pol[\"L\"].to_numpy()) / (pol[\"sigma\"].to_numpy() * np.sqrt(pol[\"L\"].to_numpy()) + 1e-6))\n",
" sw = items[\"status\"].map(STATUS_W).to_numpy()\n",
" sup = (items[\"suppliers\"].to_numpy() == 1).astype(float)\n",
" alt = (~items[\"alt_available\"].to_numpy()).astype(float)\n",
" score = 100 * np.clip(0.55 * p + 0.25 * sw + 0.10 * sup + 0.10 * alt, 0, 1)\n",
" return pd.DataFrame({\"item_id\": items[\"item_id\"], \"p_short\": p, \"status_w\": sw, \"single_sup\": sup, \"no_alt\": alt,\n",
" \"avail\": avail, \"risk\": score.round(1)})\n",
"\n",
"# ---- ์ ๋จ์ ์ฌ๊ณ ๋ฐฑํ
์คํธ(๊ฐ์ ๋น์ฉ ์ง์) ----\n",
"HOLD, ORDER_COST, SHORT_MULT = 0.02, 50_000, 0.6 # [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ ๋ณด๊ด๋น์จ(๋จ๊ฐ ๋๋น), ๋ฐ์ฃผ 1ํ ๋น์ฉ(์), ๊ฒฐํ 1๊ฐ ์์ค(๋จ๊ฐ ๋ฐฐ์)\n",
"\n",
"\n",
"ALT_PREMIUM, ALT_LT = 1.2, 3 # [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋์ฒดํ ์ ํ ์ ๋จ๊ฐ ๋ฐฐ์, ๋์ฒดํ ์กฐ๋ฌ ๋ฆฌ๋ํ์(์)\n",
"\n",
"\n",
"def simulate(items, demand, fc, policy, z=1.65, months=None, sigma=None, eol=True):\n",
" \"\"\"ํ๋ชฉ๋ณ ์ ์ฌ๊ณ ๋ชจ์. demand/fc: (ํ๋ชฉ x ์) ์ค์ ยท์์ธก ์์. policy: 'current'(์ต๊ทผ 12๊ฐ์ ํ๊ท + 3๊ฐ์ ์ฌ์ ๊ณ ์ ) ๋๋ 'ai'(์์ธก + ์์ ์ฌ๊ณ ).\n",
" ๋จ์ข
ํ๋ชฉ์ ์ ํ๋ชฉ ๋ฐ์ฃผ ๋ถ๊ฐ. 'ai' ์ ์ฑ
์์ eol=True์ด๋ฉด ๋จ์ข
ํ๋ชฉ ์ค ๋์ฒดํ์ด ์๋ ํ๋ชฉ์ ๋์ฒดํ(๋จ๊ฐ x1.2, ๋ฆฌ๋ํ์ 3๊ฐ์)์ผ๋ก ์ ํ ๋ฐ์ฃผํ๋ค. ๋ฐํ: ํ๋ชฉ๋ณ ๋ณด๊ด๋นยท๋ฐ์ฃผ๋นยท๊ฒฐํ์์คยท๋ถ์ถ๋ยทํ๊ท ์ฌ๊ณ .\"\"\"\n",
" n, T = demand.shape\n",
" L = items[\"lead_time\"].to_numpy(); price = items[\"price\"].to_numpy().astype(float)\n",
" can_order = (items[\"status\"] != \"๋จ์ข
\").to_numpy()\n",
" if policy == \"ai\" and eol:\n",
" sw = (~can_order) & items[\"alt_available\"].to_numpy()\n",
" can_order = can_order | sw\n",
" L = np.where(sw, ALT_LT, L); price = np.where(sw, price * ALT_PREMIUM, price)\n",
" avg = demand[:, :1].repeat(T, 1) if months is None else months\n",
" on = np.round(avg[:, 0] * 2.0)\n",
" pipe = np.zeros((n, T + int(L.max()) + 1)) # ๋์ฐฉ ์์ ๋ฌผ๋(ํ๋ชฉ x ์)\n",
" for i in range(n):\n",
" pipe[i, :L[i]] = np.round(avg[i, 0] * (1.0 if can_order[i] else 0.3))\n",
" res = np.zeros((n, 6)); rows = np.arange(n)\n",
" for t in range(T):\n",
" on += pipe[:, t]\n",
" issue = np.minimum(on, demand[:, t]); short = demand[:, t] - issue; on -= issue\n",
" ip = on + pipe[:, t + 1:].sum(1)\n",
" if policy == \"current\":\n",
" target = avg[:, t] * (L + 3)\n",
" else:\n",
" s = sigma if sigma is not None else np.sqrt(np.maximum(fc[:, t], 1))\n",
" target = fc[:, t] * (L + 1) + z * s * np.sqrt(L + 1)\n",
" q = np.where(can_order & (ip < target), np.ceil(target - ip), 0)\n",
" pipe[rows, t + L] += q\n",
" res += np.c_[HOLD * price * on, ORDER_COST * (q > 0), SHORT_MULT * price * short, issue, on, short]\n",
" out = pd.DataFrame(res, columns=[\"hold\", \"order\", \"short\", \"issued\", \"inv\", \"short_qty\"], index=items[\"item_id\"])\n",
" out[\"inv\"] /= T\n",
" return out\n",
"\n",
"WAIT_H = 36 # [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฒฐํ 1๊ฐ๋น ์ฅ๋น ์ ๋น ๋๊ธฐ์๊ฐ(์๊ฐ)\n",
"\n",
"items = make_items()\n",
"hist = make_history(items)\n",
"df = make_features(hist)\n",
"m = fit_ens(df[df[\"t\"] < 36])\n",
"bt = df[df[\"t\"] >= 36].copy(); bt[\"fc\"] = pred_ens(m, bt)\n",
"P = lambda c: bt.pivot(index=\"item_id\", columns=\"t\", values=c).loc[items[\"item_id\"]].to_numpy()\n",
"D, F, A = P(\"demand\"), P(\"fc\"), P(\"item_level\")\n",
"sig = np.maximum(0.25 * F.mean(1), np.sqrt(np.maximum(F.mean(1), 1)))\n",
"val = items[\"price\"].to_numpy() * D.sum(1)\n",
"cum = np.cumsum(np.sort(val)[::-1]) / val.sum()\n",
"abc = pd.Series(np.select([val >= np.sort(val)[::-1][np.searchsorted(cum, 0.7)], val >= np.sort(val)[::-1][np.searchsorted(cum, 0.9)]], [\"A\", \"B\"], \"C\"), index=items[\"item_id\"])\n",
"\n",
"# 1) AAR ํธ์ฐจ ๋ถ์: ์์ธก ํธํฅ๊ณผ ๊ณผ๋คยท๊ณผ์ ์ฌ๊ณ \n",
"an = items[[\"item_id\", \"group\", \"status\"]].assign(abc=abc.values, bias=(F[:, :6].sum(1) - D[:, :6].sum(1)) / np.maximum(D[:, :6].sum(1), 1))\n",
"r0 = simulate(items, D[:, :6], F[:, :6], \"ai\", 1.65, A[:, :6], sig)\n",
"an[\"over\"] = (r0[\"inv\"].to_numpy() > 3 * np.maximum(D[:, :6].mean(1), 1)) # ํ๊ท ์ฌ๊ณ ๊ฐ 3๊ฐ์ ์์ ์ด๊ณผ = ๊ณผ๋ค\n",
"an[\"under\"] = r0[\"short_qty\"].to_numpy() > 0\n",
"print(\"[AAR ํธ์ฐจ ๋ถ์: ์ 6๊ฐ์(t=36~41)]\")\n",
"g = an.groupby(\"group\").agg(์์ธกํธํฅ=(\"bias\", \"mean\"), ๊ณผ๋คํ๋ชฉ=(\"over\", \"sum\"), ๊ฒฐํํ๋ชฉ=(\"under\", \"sum\"))\n",
"gt = g.T.astype(object)\n",
"gt.loc[\"์์ธกํธํฅ\"] = [f\"{v:+.0%}\" for v in g[\"์์ธกํธํฅ\"]]\n",
"gt.loc[[\"๊ณผ๋คํ๋ชฉ\", \"๊ฒฐํํ๋ชฉ\"]] = gt.loc[[\"๊ณผ๋คํ๋ชฉ\", \"๊ฒฐํํ๋ชฉ\"]].map(lambda v: f\"{int(v)}\")\n",
"print(gt.to_string())\n",
"print(\"ABC๋ณ ๊ณผ๋ค/๊ฒฐํ ํ๋ชฉ:\", \", \".join(f\"{k} {int(v['over'])}/{int(v['under'])}\" for k, v in an.groupby(\"abc\")[[\"over\", \"under\"]].sum().iterrows()),\n",
" \"| ์์ฐ ์ํ๋ณ ๊ฒฐํ ํ๋ชฉ:\", \", \".join(f\"{k} {int(v)}\" for k, v in an.groupby(\"status\")[\"under\"].sum().items()))\n",
"ex = bt[\"exercise\"] == 1\n",
"hi = g[\"์์ธกํธํฅ\"].abs().sort_values(ascending=False).index[:2]\n",
"hi_txt = \", \".join(f\"{h}({g.loc[h, '์์ธกํธํฅ']:+.0%})\" for h in hi)\n",
"print(f\"ํ๋ จ ์ ์์ธก ํธํฅ {(bt.loc[ex, 'fc'].sum() / bt.loc[ex, 'demand'].sum() - 1):+.1%}, ํ์ ์ {(bt.loc[~ex, 'fc'].sum() / bt.loc[~ex, 'demand'].sum() - 1):+.1%}\"\n",
" f\" -> ์์ธ: {hi_txt} ํ๋ชฉ๊ตฐ ์์ธก ํธํฅ, ๋ชจ๋ ๋ฑ๊ธ์ ๊ฐ์ z=1.65 ์ ์ฉ, ๋จ์ข
ํ๋ชฉ ๋ฐ์ฃผ ๋ถ๊ฐ\")\n",
"\n",
"# 2) ์ฌ๋ณด์ (์ 6๊ฐ์๋ก ๊ฒฐ์ ): ABC๋ณ z ๊ฒฉ์ ํ์ + ํ๋ชฉ๊ตฐ๋ณ ํธํฅ ๋ณด์ \n",
"grp_bias = an.groupby(\"group\")[\"bias\"].median().clip(-0.3, 0.3)\n",
"corr = 1 / (1 + items[\"group\"].map(grp_bias).to_numpy())\n",
"best = {}\n",
"for cls in \"ABC\":\n",
" mask = (abc == cls).to_numpy()\n",
" costs = {}\n",
" for z in (0.84, 1.28, 1.65, 2.05, 2.33):\n",
" zz = np.where(mask, z, 1.65)\n",
" r = simulate(items, D[:, :6], F[:, :6] * corr[:, None], \"ai\", zz, A[:, :6], sig)\n",
" costs[z] = r[[\"hold\", \"order\", \"short\"]].to_numpy()[mask].sum()\n",
" best[cls] = min(costs, key=costs.get)\n",
"z_new = abc.map(best).to_numpy()\n",
"print(f\"\\n[์ฌ๋ณด์ ๊ฒฐ๊ณผ] ABC๋ณ z: A {best['A']}, B {best['B']}, C {best['C']} (๊ธฐ์กด 1.65 ์ผ๊ด), ํ๋ชฉ๊ตฐ ํธํฅ ๋ณด์ ๋ฒ์ {corr.min():.2f}~{corr.max():.2f}๋ฐฐ\")\n",
"\n",
"# 3) ๊ฒ์ฆ(๋ค 6๊ฐ์, t=42~47) ๋ฐ 3๋ ํจ๊ณผ ์งํ\n",
"def ev(fc, z):\n",
" r = simulate(items, D[:, 6:], fc, \"ai\", z, A[:, 6:], sig)\n",
" return r, r[[\"hold\", \"order\", \"short\"]].sum().sum()\n",
"\n",
"\n",
"r_cur = simulate(items, D[:, 6:], F[:, 6:], \"current\", months=A[:, 6:])\n",
"r_old, c_old = ev(F[:, 6:], 1.65)\n",
"r_new, c_new = ev(F[:, 6:] * corr[:, None], z_new)\n",
"c_cur = r_cur[[\"hold\", \"order\", \"short\"]].sum().sum()\n",
"turn = lambda r: float(r[\"issued\"].sum() / r[\"inv\"].sum())\n",
"over_amt = lambda r: float((np.maximum(r[\"inv\"].to_numpy() - 3 * np.maximum(D[:, 6:].mean(1), 1), 0) * items[\"price\"].to_numpy()).sum())\n",
"k = pd.DataFrame([[\"6๊ฐ์ ์ด๋น์ฉ(๋ฐฑ๋ง ์)\", c_cur / 1e6, c_old / 1e6, c_new / 1e6],\n",
" [\"์ ๋น ๋๊ธฐ์๊ฐ(์๊ฐ)\", r_cur[\"short_qty\"].sum() * WAIT_H, r_old[\"short_qty\"].sum() * WAIT_H, r_new[\"short_qty\"].sum() * WAIT_H],\n",
" [\"์ฌ๊ณ ํ์ ์จ(ํ/6๊ฐ์)\", turn(r_cur), turn(r_old), turn(r_new)]],\n",
" columns=[\"์งํ\", \"ํํ ์ ์ฑ
\", \"์์ธก ์ ์ฑ
(๊ธฐ์กด ํ๋ผ๋ฏธํฐ)\", \"์์ธก ์ ์ฑ
(์ฌ๋ณด์ )\"]).set_index(\"์งํ\")\n",
"k[\"ํํ ๋๋น ๊ฐ์ ์จ\"] = 1 - k[\"์์ธก ์ ์ฑ
(์ฌ๋ณด์ )\"] / k[\"ํํ ์ ์ฑ
\"]\n",
"k.loc[\"์ฌ๊ณ ํ์ ์จ(ํ/6๊ฐ์)\", \"ํํ ๋๋น ๊ฐ์ ์จ\"] *= -1 # ํ์ ์จ์ ํด์๋ก ์ข์\n",
"print(\"\\n[3๋ ์ ๋ ํจ๊ณผ ์งํ: ๋ค 6๊ฐ์ ๊ฒ์ฆ, ๊ฐ์ ๋จ๊ฐ ๊ธฐ์ค]\")\n",
"print(k.to_string(float_format=lambda x: f\"{x:,.1f}\", formatters={\"ํํ ๋๋น ๊ฐ์ ์จ\": \"{:.1%}\".format}))\n",
"print(f\"์ฌ๋ณด์ ํจ๊ณผ(๊ธฐ์กด ํ๋ผ๋ฏธํฐ ๋๋น): ์ด๋น์ฉ {1 - c_new / c_old:+.1%}, ์ฐ๊ฐ ํ์ฐ ์ ๊ฐ์ก(ํํ ๋๋น) {(c_cur - c_new) * 2 / 1e6:,.0f}๋ฐฑ๋ง ์(๊ฐ์)\")\n",
"sens = []\n",
"for hm in (0.5, 1.0, 1.5):\n",
" for sm in (0.3, 0.6, 1.0):\n",
" HOLD_, SM_ = HOLD * hm, sm\n",
" cc = lambda r, p: (HOLD_ * p * r[\"inv\"].to_numpy() * 6).sum() + r[\"order\"].sum() + (SM_ * p * r[\"short_qty\"].to_numpy()).sum()\n",
" pr = items[\"price\"].to_numpy()\n",
" sens.append(f\"๋ณด๊ดx{hm}/๊ฒฐํx{sm}: {1 - cc(r_new, pr) / cc(r_cur, pr):.0%}\")\n",
"print(\"[๋ฏผ๊ฐ๋: ํํ ๋๋น ๋น์ฉ ์ ๊ฐ๋ฅ ]\", \", \".join(sens))\n",
"print(\"๋์
๋ก๋๋งต: ๋จ๊ธฐ(1~3๊ฐ์) ๋ถ๊ธฐ๋ณ AAR๋ก zยทํธํฅ ์ฌ๋ณด์ / ์ค๊ธฐ(4~9๊ฐ์) ๋จ์ข
์๊ณ ํ๋ชฉ ์ต์ข
๊ตฌ๋งคยท๋์ฒดํ ๊ฒ์ฆ ์ ์ฐจ ์ ์ฐฉ / ์ฅ๊ธฐ(10๊ฐ์~) ๊ตฐ์ ์ ๋ณด์ฒด๊ณ ์ฐ๊ณ์ ์๋ ์ฌํ์ต\")\n",
"print(\"๊ธ์กยท์๊ฐ์ ๊ฐ์ ๋จ๊ฐ๋ก ๊ณ์ฐํ ์๋ ๋น๊ต๊ฐ์ด๋ฉฐ ์์ฐ ๊ทผ๊ฑฐ๋ก ์ฐ์ง ์๋๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(12, 4))\n",
"g[\"์์ธกํธํฅ\"].plot.bar(ax=ax[0], rot=30, color=\"tab:purple\"); ax[0].axhline(0, c=\"k\", lw=0.8); ax[0].set_title(\"Forecast bias by group (AAR, first 6 months)\")\n",
"k.iloc[:, :3].div(k[\"ํํ ์ ์ฑ
\"], axis=0).T.plot.bar(ax=ax[1], rot=0); ax[1].set_xticklabels([\"current\", \"forecast(old)\", \"forecast(recal)\"])\n",
"ax[1].set_title(\"Effect indicators (current = 1.0)\"); ax[1].legend([\"cost\", \"wait hours\", \"turnover\"])\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
09 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[AAR ํธ์ฐจ ๋ถ์: ์ 6๊ฐ์(t=36~41)]\n",
"group battery brake_steer cooling filter fuel gasket power_train semiconductor\n",
"์์ธกํธํฅ +11% +18% +4% +1% -6% -5% +1% -3%\n",
"๊ณผ๋คํ๋ชฉ 1 1 1 2 0 0 0 0\n",
"๊ฒฐํํ๋ชฉ 1 0 0 0 1 0 0 0\n",
"ABC๋ณ ๊ณผ๋ค/๊ฒฐํ ํ๋ชฉ: A 1/0, B 2/0, C 2/2 | ์์ฐ ์ํ๋ณ ๊ฒฐํ ํ๋ชฉ: ๋จ์ข
1, ๋จ์ข
์๊ณ 0, ์์ฐ์ค 1\n",
"ํ๋ จ ์ ์์ธก ํธํฅ +1.5%, ํ์ ์ +0.1% -> ์์ธ: brake_steer(+18%), battery(+11%) ํ๋ชฉ๊ตฐ ์์ธก ํธํฅ, ๋ชจ๋ ๋ฑ๊ธ์ ๊ฐ์ z=1.65 ์ ์ฉ, ๋จ์ข
ํ๋ชฉ ๋ฐ์ฃผ ๋ถ๊ฐ\n",
"\n",
"[์ฌ๋ณด์ ๊ฒฐ๊ณผ] ABC๋ณ z: A 0.84, B 0.84, C 1.28 (๊ธฐ์กด 1.65 ์ผ๊ด), ํ๋ชฉ๊ตฐ ํธํฅ ๋ณด์ ๋ฒ์ 0.89~1.07๋ฐฐ\n",
"\n",
"[3๋ ์ ๋ ํจ๊ณผ ์งํ: ๋ค 6๊ฐ์ ๊ฒ์ฆ, ๊ฐ์ ๋จ๊ฐ ๊ธฐ์ค]\n",
" ํํ ์ ์ฑ
์์ธก ์ ์ฑ
(๊ธฐ์กด ํ๋ผ๋ฏธํฐ) ์์ธก ์ ์ฑ
(์ฌ๋ณด์ ) ํํ ๋๋น ๊ฐ์ ์จ\n",
"์งํ \n",
"6๊ฐ์ ์ด๋น์ฉ(๋ฐฑ๋ง ์) 117.7 56.7 55.3 53.0%\n",
"์ ๋น ๋๊ธฐ์๊ฐ(์๊ฐ) 17,532.0 1,620.0 1,872.0 89.3%\n",
"์ฌ๊ณ ํ์ ์จ(ํ/6๊ฐ์) 3.1 3.0 3.1 1.2%\n",
"์ฌ๋ณด์ ํจ๊ณผ(๊ธฐ์กด ํ๋ผ๋ฏธํฐ ๋๋น): ์ด๋น์ฉ +2.5%, ์ฐ๊ฐ ํ์ฐ ์ ๊ฐ์ก(ํํ ๋๋น) 125๋ฐฑ๋ง ์(๊ฐ์)\n",
"[๋ฏผ๊ฐ๋: ํํ ๋๋น ๋น์ฉ ์ ๊ฐ๋ฅ ] ๋ณด๊ดx0.5/๊ฒฐํx0.3: 55%, ๋ณด๊ดx0.5/๊ฒฐํx0.6: 69%, ๋ณด๊ดx0.5/๊ฒฐํx1.0: 77%, ๋ณด๊ดx1.0/๊ฒฐํx0.3: 38%, ๋ณด๊ดx1.0/๊ฒฐํx0.6: 55%, ๋ณด๊ดx1.0/๊ฒฐํx1.0: 67%, ๋ณด๊ดx1.5/๊ฒฐํx0.3: 28%, ๋ณด๊ดx1.5/๊ฒฐํx0.6: 45%, ๋ณด๊ดx1.5/๊ฒฐํx1.0: 58%\n",
"๋์
๋ก๋๋งต: ๋จ๊ธฐ(1~3๊ฐ์) ๋ถ๊ธฐ๋ณ AAR๋ก zยทํธํฅ ์ฌ๋ณด์ / ์ค๊ธฐ(4~9๊ฐ์) ๋จ์ข
์๊ณ ํ๋ชฉ ์ต์ข
๊ตฌ๋งคยท๋์ฒดํ ๊ฒ์ฆ ์ ์ฐจ ์ ์ฐฉ / ์ฅ๊ธฐ(10๊ฐ์~) ๊ตฐ์ ์ ๋ณด์ฒด๊ณ ์ฐ๊ณ์ ์๋ ์ฌํ์ต\n",
"๊ธ์กยท์๊ฐ์ ๊ฐ์ ๋จ๊ฐ๋ก ๊ณ์ฐํ ์๋ ๋น๊ต๊ฐ์ด๋ฉฐ ์์ฐ ๊ทผ๊ฑฐ๋ก ์ฐ์ง ์๋๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
10. AI ๊ธฐ๋ฐ ์ผ์ ์ ๋น ๋ถํ ์์์์ธก ๋ฐ ์ฌ๊ณ ์ต์ ํ ์์คํ
ํตํฉ ํจํค์ง ์์ฑ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"AI ๊ธฐ๋ฐ ์ผ์ ์ ๋น ๋ถํ ์์ ์์ธก ๋ฐ ์ฌ๊ณ ์ต์ ํ ์์คํ
ํจํค์ง๋ฅผ ์์ฑํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์์ ์์ธก, ์ฌํ์ต, ์์ ์ฌ๊ณ ยท์ํ ์ง์, ๋จ์ข
๋์, ๋ฐ์ฃผ ์ถ์ฒ๊ณผ ๊ฒฐ์ฌ ๋๊ธฐ์ด์ ํ ๋ฒ์ ์คํํ๋ ํตํฉ ํจํค์ง๋ฅผ ์์ฑํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์๋ฃ ์์ฑยท์ ํฉ์ฑ ์ ๊ฒ, ์ด์ ๋ชจ๋ธ๊ณผ ์ฌํ์ต ๋ชจ๋ธ ๋น๊ต ํ ๊ต์ฒด ํ๋จ, ๋ค์ ๋ฌ ์์ธก๊ณผ ์ํ ์ง์ ์ฐ์ถ์ ํ ํ๋ฆ์ผ๋ก ์คํ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `docs/mnt02_operations_guide.md ๋ฐ pipeline/mnt02_retrain.py` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ง์ ๊ฐ๋ฅํ ์ด์ ๊ฐ๋ฅ์ฑ ๋ฐ ๊ตญ๋ฐฉ ๊ตฐ์ ์ ๋ณด์ฒด๊ณ์์ ์ฐ๋ ์ฉ์ด์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์๋ ์ฌํ์ต ํ์ดํ๋ผ์ธ ์ ์ ์คํ, ์ต์ข
๋ฌธ์ 100% ์๋น\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์ต์ข
์ธ๋๋ฌผ ๊ฒ์ ๋ฐ ํ๋ก์ ํธ ์๋ฃ ๋ฐํ์๋ฃ ์์ฑ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: End-to-End ์ ์ฒด ํ์ดํ๋ผ์ธ ์๋ํ ์คํ ๋ฐ ์ต์ข
๊ฒ์\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์๋ฆฌ๋ถ์ ์ฒญ๊ตฌ ์ด๋ ฅ ์์ฑยท์ ํฉ์ฑ ์ ๊ฒ, ์์ ์์ธก ๋ชจ๋ธ ์๋ ์ฌํ์ต(๊ธฐ์กดยท์ ๊ท ๋ชจ๋ธ ๋น๊ต ํ ๊ต์ฒด), ์์ ์ฌ๊ณ ยท๊ฒฐํ(๋จ์ข
) ์ํ ์ง์, ๋จ์ข
์๊ณ ํ๋ชฉ ์ต์ข
๊ตฌ๋งค๋๊ณผ ๋จ์ข
ํ๋ชฉ ๋์ฒด ๊ฒํ , ์์ฐ ์ ์ฝ ๋ฐ์ฃผ ์ถ์ฒ๊ณผ ์ ๋น์ฅ๊ต ๊ฒฐ์ฌ ๋๊ธฐ์ด์ ํ ๋ฒ์ ์คํํ๋ ํตํฉ ํจํค์ง๋ฅผ ์์ฑํ๊ณ , ์์ฒด ์ ๊ฒํยท์ด์ ๊ฐ์ด๋ ๋ชฉ์ฐจยท์ฒด๊ณ ์ฐ๊ณ ๋ฉ์์ง ์์ยท๋ธ๋ฆฌํ ์์ฝ์ ์ถ๋ ฅํ๋ค.\"\"\"\n",
"import json\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy.stats import norm\n",
"from scipy.optimize import linprog\n",
"from sklearn.ensemble import HistGradientBoostingRegressor, RandomForestRegressor\n",
"from sklearn.metrics import mean_absolute_error\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ผ์ ์ ๋น์ฐฝ ์๋ฆฌ๋ถ์ ์ฒญ๊ตฌ/์ถ๊ณ ยท์ฅ๋น ๊ฐ๋๋ฅ ๋ฐ์ดํฐ ์์ฑ๊ธฐ\n",
"# ์ค์ ๋ถ๋ยทํ๋ชฉยท์ฌ๊ณ ยท๋จ๊ฐ ์ ๋ณด์ ๋ฌด๊ดํ๋ค. ํ๋ชฉ 40๊ฐ x 48๊ฐ์(๊ฐ์ 2022-01 ~ 2025-12) ์๋ณ ์๋ฃ๋ฅผ ๋ง๋ ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"FSC = {\"2520\": \"power_train\", \"2530\": \"brake_steer\", \"2910\": \"fuel\", \"2930\": \"cooling\",\n",
" \"2940\": \"filter\", \"5330\": \"gasket\", \"6140\": \"battery\", \"5961\": \"semiconductor\"}\n",
"EXERCISE = {3, 8, 10} # ๊ฐ์ ๋๊ท๋ชจ ํ๋ จ ์(ํ๋ จ ๊ณํ์ผ๋ก ๋ฏธ๋ฆฌ ์ ์ ์์)\n",
"N_MONTHS = 48\n",
"\n",
"\n",
"def season(fsc, month):\n",
" \"\"\"ํ๋ชฉ๊ตฐ๋ณ ๊ณ์ ๊ณ์: ์ถ์ ์ง๋ ๊ฒจ์ธ, ๋๊ฐ๊ณํต์ ์ฌ๋ฆ์ ์์๊ฐ ๋์ด๋๋ค.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 10] AI ๊ธฐ๋ฐ ์ผ์ ์ ๋น ๋ถํ ์์์์ธก ๋ฐ ์ฌ๊ณ ์ต์ ํ ์์คํ
ํตํฉ ํจํค์ง ์์ฑ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def make_items(n=40, seed=2):\n",
" \"\"\"ํ๋ชฉ ๋ง์คํฐ: NSN(๊ฐ์), ํ๋ชฉ๊ตฐ, ์ฅ๋น ์ ํ, ๊ธฐ์ค ์์, ๋จ๊ฐ, ์กฐ๋ฌ ๋ฆฌ๋ํ์, ๊ณต๊ธ์
์ฒด ์, ์์ฐ(๋จ์ข
) ์ํ.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = []\n",
" for i in range(n):\n",
" f = str(rng.choice(list(FSC)))\n",
" status = str(rng.choice([\"์์ฐ์ค\", \"๋จ์ข
์๊ณ \", \"๋จ์ข
\"], p=[0.7, 0.2, 0.1]))\n",
" rows.append({\"item_id\": f\"P{i + 1:03d}\", \"nsn\": f\"{f}-00-{rng.integers(100, 1000)}-{rng.integers(1000, 10000)}\",\n",
" \"fsc\": f, \"group\": FSC[f], \"equip_type\": str(rng.choice([\"A\", \"B\", \"C\"])),\n",
" \"base\": float(np.round(rng.lognormal(2.7, 0.8), 1)),\n",
" \"price\": int(rng.lognormal(12, 1.0)) // 100 * 100,\n",
" \"lead_time\": int(rng.integers(1, 7) if status == \"์์ฐ์ค\" else rng.integers(4, 13)),\n",
" \"suppliers\": int(rng.integers(1, 5)) if status == \"์์ฐ์ค\" else 1,\n",
" \"status\": status, \"alt_available\": bool(rng.random() < (0.6 if status == \"์์ฐ์ค\" else 0.3))})\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"def make_history(items, months=N_MONTHS, seed=3):\n",
" \"\"\"์๋ณ ์ฒญ๊ตฌ ์์(๊ฐ), ๊ธด๊ธ ๋ถ์ถ ๊ฑด์, ์ฅ๋น ์ ํ๋ณ ์ด์ฉ์๊ฐยท๊ฐ๋๋ฅ .\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" t = np.arange(months)\n",
" moy = t % 12 + 1\n",
" ex = np.isin(moy, list(EXERCISE))\n",
" hours = {e: (100 + 20 * rng.random(months)) * np.where(ex, 1.5, 1.0) for e in \"ABC\"}\n",
" avail = {e: np.clip(0.93 - 0.12 * (hours[e] - 100) / 80 + rng.normal(0, 0.015, months), 0.6, 0.99) for e in \"ABC\"}\n",
" rows = []\n",
" for it in items.itertuples():\n",
" trend = 1 + rng.uniform(-0.1, 0.3) * t / months\n",
" s = np.array([season(it.fsc, m) for m in moy])\n",
" lam = it.base * s * trend * hours[it.equip_type] / 110\n",
" d = rng.poisson(lam)\n",
" rows.append(pd.DataFrame({\"item_id\": it.item_id, \"t\": t, \"ym\": [f\"{2022 + k // 12}-{k % 12 + 1:02d}\" for k in t],\n",
" \"month\": moy, \"exercise\": ex.astype(int), \"op_hours\": hours[it.equip_type].round(1),\n",
" \"availability\": avail[it.equip_type].round(3), \"demand\": d,\n",
" \"emergency\": rng.binomial(d, 0.05)}))\n",
" return pd.concat(rows, ignore_index=True)\n",
"\n",
"\n",
"def make_stock(items, hist, seed=4):\n",
" \"\"\"ํ์ฌ(๊ฐ์ 2026-01) ์ฌ๊ณ : ๋ณด์ ๋๊ณผ ๋ฏธ์
๊ณ ๋ฐ์ฃผ๋(์กฐ๋ฌ ์ค์ธ ๋ฌผ๋). ๋จ์ข
ํ๋ชฉ์ ์ถ๊ฐ ๋ฐ์ฃผ๊ฐ ์ด๋ ค์ ๋ฏธ์
๊ณ ๋์ด ์ ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" recent = hist[hist[\"t\"] >= hist[\"t\"].max() - 5].groupby(\"item_id\")[\"demand\"].mean()\n",
" st = items[[\"item_id\", \"lead_time\", \"status\"]].copy()\n",
" st[\"on_hand\"] = [int(round(recent[i] * rng.uniform(0.5, 3.0))) for i in st[\"item_id\"]]\n",
" cover = np.where(st[\"status\"] == \"์์ฐ์ค\", rng.uniform(0.6, 1.3, len(st)), rng.uniform(0.0, 0.6, len(st)))\n",
" st[\"on_order\"] = [int(round(recent[i] * L * c)) for i, L, c in zip(st[\"item_id\"], st[\"lead_time\"], cover)]\n",
" return st[[\"item_id\", \"on_hand\", \"on_order\"]]\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์์ ์์ธก ๊ณตํต ํจ์(ํ์ ํผ์ฒ + ๋จธ์ ๋ฌ๋ ๋ชจ๋ธ) ----\n",
"FEATS = [\"lag1\", \"lag2\", \"lag3\", \"lag12\", \"ma3\", \"ma6\", \"std6\", \"season_idx\", \"month_sin\", \"month_cos\",\n",
" \"exercise\", \"op_hours\", \"op_ratio\", \"item_level\", \"trend_t\"]\n",
"\n",
"\n",
"def make_features(hist):\n",
" \"\"\"ํ๋ชฉ๋ณ ๊ณผ๊ฑฐ ์์๋ก ํ์ ํผ์ฒ๋ฅผ ๋ง๋ ๋ค(๋ชจ๋ ์์ธก ์์ ์ด์ ์ ๋ณด๋ง ์ฌ์ฉ).\"\"\"\n",
" df = hist.sort_values([\"item_id\", \"t\"]).reset_index(drop=True)\n",
" g = df.groupby(\"item_id\")[\"demand\"]\n",
" for k in (1, 2, 3, 12):\n",
" df[f\"lag{k}\"] = g.shift(k)\n",
" df[\"ma3\"] = g.transform(lambda s: s.shift(1).rolling(3).mean())\n",
" df[\"ma6\"] = g.transform(lambda s: s.shift(1).rolling(6).mean())\n",
" df[\"std6\"] = g.transform(lambda s: s.shift(1).rolling(6).std())\n",
" df[\"item_level\"] = g.transform(lambda s: s.shift(1).rolling(12).mean())\n",
" df[\"season_idx\"] = df[\"lag12\"] / (g.transform(lambda s: s.shift(12).rolling(12).mean()) + 1)\n",
" df[\"month_sin\"] = np.sin(2 * np.pi * df[\"month\"] / 12)\n",
" df[\"month_cos\"] = np.cos(2 * np.pi * df[\"month\"] / 12)\n",
" df[\"op_ratio\"] = df[\"op_hours\"] / df.groupby(\"item_id\")[\"op_hours\"].transform(lambda s: s.shift(1).rolling(12).mean())\n",
" df[\"trend_t\"] = df[\"t\"]\n",
" return df.dropna().reset_index(drop=True)\n",
"\n",
"\n",
"def new_model():\n",
" \"\"\"Prophet/LSTM ๋์ ์ฌ์ฉํ๋ ํฌ์์ก ์์ค ๊ทธ๋๋์ธํธ ๋ถ์คํ
(๊ฐ์ ์๋ฃ์ ์ ํฉ).\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 10] AI ๊ธฐ๋ฐ ์ผ์ ์ ๋น ๋ถํ ์์์์ธก ๋ฐ ์ฌ๊ณ ์ต์ ํ ์์คํ
ํตํฉ ํจํค์ง ์์ฑ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def mape(y, p, min_y=1):\n",
" y, p = np.asarray(y, float), np.asarray(p, float)\n",
" m = y >= min_y\n",
" return float(np.mean(np.abs(y[m] - p[m]) / y[m]))\n",
"\n",
"# ---- ์์๋ธ ์์ ์์ธกยท์์ ์ฌ๊ณ ยท๊ฒฐํ(๋จ์ข
) ์ํ ์ง์ ๊ณตํต ํจ์ ----\n",
"STATUS_W = {\"์์ฐ์ค\": 0.0, \"๋จ์ข
์๊ณ \": 0.5, \"๋จ์ข
\": 1.0} # [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋จ์ข
์ํ ๊ฐ์ค์น\n",
"\n",
"\n",
"def fit_ens(tr):\n",
" \"\"\"ํฌ์์ก ๊ทธ๋๋์ธํธ ๋ถ์คํ
(70%) + ๋๋คํฌ๋ ์คํธ(30%) ์์๋ธ.\"\"\"\n",
" h = new_model().fit(tr[FEATS], tr[\"demand\"])\n",
" r = RandomForestRegressor(n_estimators=150, min_samples_leaf=5, random_state=0, n_jobs=1).fit(tr[FEATS], tr[\"demand\"])\n",
" return h, r\n",
"\n",
"\n",
"def pred_ens(m, X, w=0.7):\n",
" return np.clip(w * m[0].predict(X[FEATS]) + (1 - w) * m[1].predict(X[FEATS]), 0, None)\n",
"\n",
"\n",
"def next_month(hist, items, t_new=48, op_mult=1.0, ex=0):\n",
" \"\"\"๋ค์ ๋ฌ(t_new) ์์ธก์ฉ ํ: ์๋
๊ฐ์ ๋ฌ ์ด์ฉ์๊ฐ x ๋ฐฐ์, ํ๋ จ ์ฌ๋ถ ์ง์ .\"\"\"\n",
" last = hist[hist[\"t\"] == t_new - 1].copy()\n",
" prev = hist[hist[\"t\"] == t_new - 12].set_index(\"item_id\")[\"op_hours\"]\n",
" m = (t_new % 12) + 1\n",
" f = last.assign(t=t_new, ym=f\"{2022 + t_new // 12}-{m:02d}\", month=m, demand=0, exercise=ex,\n",
" op_hours=last[\"item_id\"].map(prev).to_numpy() * op_mult)\n",
" X = make_features(pd.concat([hist, f], ignore_index=True))\n",
" return X[X[\"t\"] == t_new].set_index(\"item_id\").loc[items[\"item_id\"]].reset_index()\n",
"\n",
"\n",
"def stock_policy(items, mu, sigma, z=1.65, review=1):\n",
" \"\"\"์์ ์ฌ๊ณ = z x ์ ์์ธก์ค์ฐจ ํ์คํธ์ฐจ x sqrt(๋ฆฌ๋ํ์+๊ฒํ ์ฃผ๊ธฐ), ์ฌ์ฃผ๋ฌธ์ = ์ ์์ x (๋ฆฌ๋ํ์+๊ฒํ ์ฃผ๊ธฐ) + ์์ ์ฌ๊ณ .\"\"\"\n",
" L = items[\"lead_time\"].to_numpy() + review\n",
" ss = z * sigma * np.sqrt(L)\n",
" return pd.DataFrame({\"item_id\": items[\"item_id\"], \"mu\": mu, \"sigma\": sigma, \"L\": L,\n",
" \"ss\": np.ceil(ss), \"rop\": np.ceil(mu * L + ss)})\n",
"\n",
"\n",
"def risk_index(items, pol, stock):\n",
" \"\"\"๊ฒฐํ ์ํ ์ง์(0~100): ๋ฆฌ๋ํ์ ๋์ ์์๊ฐ ๊ฐ์ฉ ์ฌ๊ณ ๋ฅผ ๋์ ํ๋ฅ (์ ๊ท ๊ทผ์ฌ)์ ๊ธฐ๋ณธ์ผ๋ก,\n",
" ๋จ์ข
์ํยท๋จ์ผ ๊ณต๊ธ์
์ฒดยท๋์ฒดํ ๋ถ์ฌ์ ๊ฐ์ค์ ์ค๋ค(๊ฐ์ ๊ฐ์ค์น).\"\"\"\n",
" st = stock.set_index(\"item_id\").loc[items[\"item_id\"]]\n",
" avail = (st[\"on_hand\"] + st[\"on_order\"]).to_numpy()\n",
" p = 1 - norm.cdf((avail - pol[\"mu\"].to_numpy() * pol[\"L\"].to_numpy()) / (pol[\"sigma\"].to_numpy() * np.sqrt(pol[\"L\"].to_numpy()) + 1e-6))\n",
" sw = items[\"status\"].map(STATUS_W).to_numpy()\n",
" sup = (items[\"suppliers\"].to_numpy() == 1).astype(float)\n",
" alt = (~items[\"alt_available\"].to_numpy()).astype(float)\n",
" score = 100 * np.clip(0.55 * p + 0.25 * sw + 0.10 * sup + 0.10 * alt, 0, 1)\n",
" return pd.DataFrame({\"item_id\": items[\"item_id\"], \"p_short\": p, \"status_w\": sw, \"single_sup\": sup, \"no_alt\": alt,\n",
" \"avail\": avail, \"risk\": score.round(1)})\n",
"\n",
"LTB_MONTHS = 24 # [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋จ์ข
์๊ณ ํ๋ชฉ ์ต์ข
๊ตฌ๋งค(LTB) ๋์ ๊ธฐ๊ฐ(๊ฐ์)\n",
"t_all = time.perf_counter()\n",
"checks = []\n",
"\n",
"# 1) ์๋ฃ ์์งยท์ ํฉ์ฑ\n",
"items = make_items()\n",
"hist = make_history(items)\n",
"stock = make_stock(items, hist)\n",
"ok = (hist[\"demand\"] >= 0).all() and items[\"nsn\"].str.match(r\"^\\d{4}-\\d{2}-\\d{3}-\\d{4}$\").all() and not hist.duplicated([\"item_id\", \"t\"]).any()\n",
"checks.append((\"์์ฒ ์๋ฃ ์ ํฉ์ฑ(NSN ํ์ยท์์ยท์ค๋ณต)\", ok))\n",
"df = make_features(hist)\n",
"print(f\"[1] ํ๋ชฉ {len(items)}๊ฐ x {N_MONTHS}๊ฐ์ ์ฒญ๊ตฌ ์ด๋ ฅ, ํ์ต ํ {len(df)}๊ฐ, ์ ํฉ์ฑ {'ํต๊ณผ' if ok else '์คํจ'}\")\n",
"\n",
"# 2) ์ฌํ์ต ํ์ดํ๋ผ์ธ: ์ด์ ๋ชจ๋ธ v1(t<36) vs ์ฌํ์ต v2(t<42), ์ต๊ทผ 6๊ฐ์(t=42~47) ๊ฒ์ฆ ํ ๊ต์ฒด ํ๋จ\n",
"va = df[df[\"t\"] >= 42]\n",
"big = va[\"item_id\"].isin(items.loc[items[\"base\"] >= 10, \"item_id\"]).to_numpy()\n",
"v1, v2 = fit_ens(df[df[\"t\"] < 36]), fit_ens(df[df[\"t\"] < 42])\n",
"p1, p2 = pred_ens(v1, va), pred_ens(v2, va)\n",
"e1, e2 = mape(va[\"demand\"][big], p1[big]), mape(va[\"demand\"][big], p2[big])\n",
"eb = mape(va[\"demand\"][big], va[\"ma3\"][big])\n",
"use = \"v2\" if e2 <= e1 else \"v1\"\n",
"print(f\"[2] ๊ฒ์ฆ MAPE(์์ ๋ง์ ํ๋ชฉ): MA3 {eb:.1%}, ์ด์ v1 {e1:.1%}, ์ฌํ์ต v2 {e2:.1%} -> {use} {'๊ต์ฒด' if use == 'v2' else '์ ์ง'}\")\n",
"checks += [(\"์๋ ์ฌํ์ต ์คํยท๋น๊ต\", True), (\"์ด์ ๋ชจ๋ธ MAPE 25% ์ดํ\", min(e1, e2) <= 0.25)]\n",
"\n",
"# 3) ๋ค์ ๋ฌ ์์ธก(์ ์ฒด ์๋ฃ๋ก ์ต์ข
ํ์ต) -> ์์ ์ฌ๊ณ ยท์ํ ์ง์\n",
"final = fit_ens(df)\n",
"mu = pred_ens(final, next_month(hist, items))\n",
"sg = np.maximum(0.25 * mu, np.sqrt(np.maximum(mu, 1)))\n",
"pol = stock_policy(items, mu, sg)\n",
"rk = risk_index(items, pol, stock)\n",
"alert = rk[\"risk\"] >= 60\n",
"print(f\"[3] ๋ค์ ๋ฌ ์์ธก ์์ {mu.sum():.0f}๊ฐ, ๊ฒฐํ ์ํ ๊ฒฝ๋ณด(60 ์ด์) {int(alert.sum())}๊ฐ ํ๋ชฉ, ์์ ์ฌ๊ณ ํฉ {pol['ss'].sum():.0f}๊ฐ\")\n",
"\n",
"# 4) ๋จ์ข
๋์: ๋จ์ข
์๊ณ -> ์ต์ข
๊ตฌ๋งค(LTB) ์๋, ๋จ์ข
-> ๋์ฒดํยท์ฌ์์ ๋น ๊ฒํ \n",
"ip = (stock[\"on_hand\"] + stock[\"on_order\"]).to_numpy()\n",
"eol = items.assign(mu=mu, ip=ip, risk=rk[\"risk\"].to_numpy())\n",
"ltb = eol[eol[\"status\"] == \"๋จ์ข
์๊ณ \"].assign(ltb=lambda d: np.maximum(0, np.ceil(d[\"mu\"] * LTB_MONTHS - d[\"ip\"])))\n",
"dis = eol[eol[\"status\"] == \"๋จ์ข
\"]\n",
"print(f\"[4] ๋จ์ข
์๊ณ {len(ltb)}๊ฐ ํ๋ชฉ ์ต์ข
๊ตฌ๋งค ์ถ์ฒ ํฉ {ltb['ltb'].sum():.0f}๊ฐ(ํฅํ {LTB_MONTHS}๊ฐ์๋ถ), ๋จ์ข
{len(dis)}๊ฐ ํ๋ชฉ ์ค ๋์ฒดํ ๋ณด์ {int(dis['alt_available'].sum())}๊ฐ\")\n",
"for _, r in ltb.sort_values(\"risk\", ascending=False).head(3).iterrows():\n",
" print(f\" {r['item_id']}({r['group']}) ์ {r['mu']:.1f}๊ฐ, ๊ฐ์ฉ {r['ip']:.0f}๊ฐ -> ์ต์ข
๊ตฌ๋งค {r['ltb']:.0f}๊ฐ\")\n",
"\n",
"# 5) ์์ฐ ์ ์ฝ ๋ฐ์ฃผ ์ถ์ฒ(LP, ์์ฐ์ค ํ๋ชฉ. ๋จ์ข
์๊ณ ๋ ์ต์ข
๊ตฌ๋งค๋ก ๋ณ๋ ์ฒ๋ฆฌ) -> ๊ฒฐ์ฌ ๋๊ธฐ์ด\n",
"gap = np.where(items[\"status\"] == \"์์ฐ์ค\", np.maximum(0, np.ceil(pol[\"rop\"].to_numpy() - ip)), 0)\n",
"price = items[\"price\"].to_numpy().astype(float)\n",
"idx = np.where(gap > 0)[0]\n",
"budget = 0.6 * float((gap * price).sum())\n",
"lp = linprog(c=-(rk[\"risk\"].to_numpy()[idx] + 10) / gap[idx], A_ub=[price[idx]], b_ub=[budget], bounds=[(0, g) for g in gap[idx]], method=\"highs\")\n",
"q = np.zeros(len(items)); q[idx] = np.floor(lp.x)\n",
"queue = [{\"id\": f\"Q{k + 1:02d}\", \"item\": items.loc[i, \"item_id\"], \"qty\": int(q[i]), \"risk\": float(rk.loc[i, \"risk\"]), \"status\": \"๊ฒฐ์ฌ๋๊ธฐ\"} for k, i in enumerate(np.where(q > 0)[0])]\n",
"queue += [{\"id\": f\"Q{len(queue) + k + 1:02d}\", \"item\": r[\"item_id\"], \"qty\": int(r[\"ltb\"]), \"risk\": float(r[\"risk\"]), \"status\": \"๊ฒฐ์ฌ๋๊ธฐ\", \"type\": \"LTB\"} for k, (_, r) in enumerate(ltb[ltb[\"ltb\"] > 0].iterrows())]\n",
"auto = [x for x in queue if x[\"status\"] != \"๊ฒฐ์ฌ๋๊ธฐ\"]\n",
"print(f\"[5] ๋ฐ์ฃผ ์ถ์ฒ {int((q > 0).sum())}๊ฑด(์์ฐ {budget / 1e6:.1f}๋ฐฑ๋ง ์ค {np.dot(q, price) / 1e6:.1f}๋ฐฑ๋ง) + ์ต์ข
๊ตฌ๋งค {int((ltb['ltb'] > 0).sum())}๊ฑด -> ์ ๋น์ฅ๊ต ๊ฒฐ์ฌ ๋๊ธฐ {len(queue)}๊ฑด, ์๋ ํ์ {len(auto)}๊ฑด\")\n",
"checks.append((\"์๋ ํ์ 0๊ฑด(HITL)\", not auto))\n",
"\n",
"# 6) ์ด์ ๊ฐ์ด๋ยท์ฐ๊ณ ๋ฉ์์ง\n",
"GUIDE = {\"1. ๋ชฉ์ ยท๋ฒ์\": \"์ผ์ ์ ๋น ๋ถํ ์์ ์์ธกยท์ฌ๊ณ ์ต์ ํยท๋จ์ข
๋์\", \"2. ๋ฐ์ดํฐ ๊ท๊ฒฉ\": f\"ํ๋ชฉ ๋ง์คํฐ {items.shape[1]}๊ฐ ํ๋, NSN ๊ฐ์ ํ์\",\n",
" \"3. ์ ์ฒ๋ฆฌ\": \"NSN ์ ๊ทํ, ๊ฒฐ์ธก ์ ๊ณ์ ์ฑ ๋ณด๊ฐ, ๊ธด๊ธ ๋ถ์ถ ๋ณด์กด\", \"4. ์์ธก ๋ชจ๋ธ\": f\"ํฌ์์ก GBM+RF ์์๋ธ, MAPE {min(e1, e2):.1%}\",\n",
" \"5. ์์ ์ฌ๊ณ ยท์ํ ์ง์\": \"z=1.65, ๋ฆฌ๋ํ์ ๊ฒฐํ ํ๋ฅ +๋จ์ข
ยท๊ณต๊ธ์
์ฒดยท๋์ฒดํ\", \"6. ๋จ์ข
๋์\": f\"LTB {LTB_MONTHS}๊ฐ์๋ถ, ๋์ฒดํ ํธํ์ฑ ๊ฒ์ฆ\",\n",
" \"7. ๊ฒฐ์ฌ์ ยท๊ฐ์ฌ\": \"์๋ ํ์ ๊ธ์ง, ์น์ธยท์กฐ์ ยท๋ฐ๋ คยท๋ณด๋ฅ ์ฌ์ ๊ธฐ๋ก\", \"8. ์ฌํ์ต\": \"๋ถ๊ธฐ๋ณ ์ฌํ์ต, ๊ฒ์ฆ MAPE๊ฐ ๋ฎ์ ๋๋ง ๊ต์ฒด\",\n",
" \"9. ํญ์ฆ ๋์\": \"์์ ํ
ํฌ ๊ณ์ ๋ณด์ , ๊ฒฝ๊ณ ๊ตฌ๊ฐ '์ฃผ์' ํ์\", \"10. ์ฐ๊ณ ์ธํฐํ์ด์ค\": \"ORDER_REQ ๊ฐ์ ๋ฉ์์ง ๊ท๊ฒฉ\"}\n",
"done = np.mean([bool(v) for v in GUIDE.values()])\n",
"checks.append((\"์ด์ ๊ฐ์ด๋ 10๊ฐ ์ฅ ์๋น\", done == 1))\n",
"print(\"[6] ์ด์ ๊ฐ์ด๋ ๋ชฉ์ฐจ:\", \" / \".join(GUIDE))\n",
"top = max(queue, key=lambda x: x[\"risk\"]) if queue else {}\n",
"msg = {\"msg_type\": \"ORDER_REQ\", \"version\": \"1.0\", \"item_id\": top.get(\"item\"), \"nsn\": items.set_index(\"item_id\").loc[top.get(\"item\"), \"nsn\"] if top else None,\n",
" \"qty\": top.get(\"qty\"), \"risk_index\": top.get(\"risk\"), \"model_ver\": use, \"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, okk in checks:\n",
" print(f\" [{'PASS' if okk else 'FAIL'}] {name}\")\n",
"print(f\"์ ๊ฒ {sum(o for _, o in checks)}/{len(checks)} ํต๊ณผ, ์ด์ ๊ฐ์ด๋ ์๋น์จ {done:.0%}, ์ ์ฒด ์คํ {elapsed:.1f}s\")\n",
"print(\"\\n[๋ธ๋ฆฌํ ์์ฝ]\")\n",
"print(f\"- ๋ถํ ์์ ์์ธก MAPE {min(e1, e2):.1%}(์ด๋ํ๊ท {eb:.1%})์ด๋ฉฐ, ์ฌํ์ต ๋ชจ๋ธ์ ๊ฒ์ฆ ํ ๊ต์ฒดํ๋ค({use} ์ฌ์ฉ).\")\n",
"print(f\"- ๊ฒฐํ ์ํ ๊ฒฝ๋ณด {int(alert.sum())}๊ฐ ํ๋ชฉ ์ค ๋จ์ข
ยท๋จ์ข
์๊ณ ๊ฐ {int((alert & (items['status'] != '์์ฐ์ค')).sum())}๊ฐ๋ก, ๋จ์ข
๋์์ด ์ฌ๊ณ ์ํ์ ํต์ฌ์ด๋ค.\")\n",
"print(f\"- ๋จ์ข
์๊ณ {len(ltb)}๊ฐ ํ๋ชฉ์ ์ต์ข
๊ตฌ๋งค {ltb['ltb'].sum():.0f}๊ฐ์ ๋ฐ์ฃผ {int((q > 0).sum())}๊ฑด์ ๊ฒฐ์ฌ ๋๊ธฐ์ด์ ์ฌ๋ ธ๋ค.\")\n",
"print(\"- ๋ชจ๋ ๋ฐ์ฃผยท์ต์ข
๊ตฌ๋งคยท๋์ฒดํ ์ ํ์ ์ ๋น์ฅ๊ต ๊ฒฐ์ฌ ํ ์งํํ๋ฉฐ, ์์น๋ ๊ต์ก์ฉ ๊ฐ์ ์๋ฃ ๊ธฐ์ค์ด๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(12, 4))\n",
"ax[0].bar([\"MA3\", \"model v1\", \"model v2\"], [eb, e1, e2], color=[\"gray\", \"tab:blue\", \"tab:green\"]); ax[0].axhline(0.25, c=\"r\", ls=\"--\")\n",
"ax[0].set_title(\"Validation MAPE (t=42-47)\")\n",
"col = items[\"status\"].map({\"์์ฐ์ค\": \"tab:green\", \"๋จ์ข
์๊ณ \": \"tab:orange\", \"๋จ์ข
\": \"tab:red\"})\n",
"ax[1].scatter(items[\"lead_time\"], rk[\"risk\"], c=col, s=20 + mu / 2); ax[1].axhline(60, c=\"r\", ls=\"--\")\n",
"ax[1].set_xlabel(\"lead time (months)\"); ax[1].set_ylabel(\"risk index\"); ax[1].set_title(\"Risk vs lead time (size=demand, red=discontinued)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
10 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[1] ํ๋ชฉ 40๊ฐ x 48๊ฐ์ ์ฒญ๊ตฌ ์ด๋ ฅ, ํ์ต ํ 1000๊ฐ, ์ ํฉ์ฑ ํต๊ณผ\n",
"[2] ๊ฒ์ฆ MAPE(์์ ๋ง์ ํ๋ชฉ): MA3 32.7%, ์ด์ v1 20.0%, ์ฌํ์ต v2 20.9% -> v1 ์ ์ง\n",
"[3] ๋ค์ ๋ฌ ์์ธก ์์ 948๊ฐ, ๊ฒฐํ ์ํ ๊ฒฝ๋ณด(60 ์ด์) 13๊ฐ ํ๋ชฉ, ์์ ์ฌ๊ณ ํฉ 1108๊ฐ\n",
"[4] ๋จ์ข
์๊ณ 8๊ฐ ํ๋ชฉ ์ต์ข
๊ตฌ๋งค ์ถ์ฒ ํฉ 2779๊ฐ(ํฅํ 24๊ฐ์๋ถ), ๋จ์ข
3๊ฐ ํ๋ชฉ ์ค ๋์ฒดํ ๋ณด์ 2๊ฐ\n",
" P004(filter) ์ 8.1๊ฐ, ๊ฐ์ฉ 12๊ฐ -> ์ต์ข
๊ตฌ๋งค 183๊ฐ\n",
" P019(filter) ์ 11.5๊ฐ, ๊ฐ์ฉ 59๊ฐ -> ์ต์ข
๊ตฌ๋งค 219๊ฐ\n",
" P025(filter) ์ 14.0๊ฐ, ๊ฐ์ฉ 89๊ฐ -> ์ต์ข
๊ตฌ๋งค 247๊ฐ\n",
"[5] ๋ฐ์ฃผ ์ถ์ฒ 10๊ฑด(์์ฐ 31.2๋ฐฑ๋ง ์ค 30.9๋ฐฑ๋ง) + ์ต์ข
๊ตฌ๋งค 8๊ฑด -> ์ ๋น์ฅ๊ต ๊ฒฐ์ฌ ๋๊ธฐ 18๊ฑด, ์๋ ํ์ 0๊ฑด\n",
"[6] ์ด์ ๊ฐ์ด๋ ๋ชฉ์ฐจ: 1. ๋ชฉ์ ยท๋ฒ์ / 2. ๋ฐ์ดํฐ ๊ท๊ฒฉ / 3. ์ ์ฒ๋ฆฌ / 4. ์์ธก ๋ชจ๋ธ / 5. ์์ ์ฌ๊ณ ยท์ํ ์ง์ / 6. ๋จ์ข
๋์ / 7. ๊ฒฐ์ฌ์ ยท๊ฐ์ฌ / 8. ์ฌํ์ต / 9. ํญ์ฆ ๋์ / 10. ์ฐ๊ณ ์ธํฐํ์ด์ค\n",
"[7] ์ฒด๊ณ ์ฐ๊ณ ๋ฉ์์ง ์์(๊ฐ์ ๊ท๊ฒฉ): {\"msg_type\": \"ORDER_REQ\", \"version\": \"1.0\", \"item_id\": \"P004\", \"nsn\": \"2940-00-559-8893\", \"qty\": 183, \"risk_index\": 87.5, \"model_ver\": \"v1\", \"approval\": \"PENDING\", \"source\": \"๊ต์ก์ฉ๊ฐ์์๋ฃ\"}\n",
"\n",
"[์์ฒด ์ ๊ฒํ]\n",
" [PASS] ์์ฒ ์๋ฃ ์ ํฉ์ฑ(NSN ํ์ยท์์ยท์ค๋ณต)\n",
" [PASS] ์๋ ์ฌํ์ต ์คํยท๋น๊ต\n",
" [PASS] ์ด์ ๋ชจ๋ธ MAPE 25% ์ดํ\n",
" [PASS] ์๋ ํ์ 0๊ฑด(HITL)\n",
" [PASS] ์ด์ ๊ฐ์ด๋ 10๊ฐ ์ฅ ์๋น\n",
" [PASS] ํตํฉ ์คํ 60์ด ์ด๋ด\n",
"์ ๊ฒ 6/6 ํต๊ณผ, ์ด์ ๊ฐ์ด๋ ์๋น์จ 100%, ์ ์ฒด ์คํ 3.5s\n",
"\n",
"[๋ธ๋ฆฌํ ์์ฝ]\n",
"- ๋ถํ ์์ ์์ธก MAPE 20.0%(์ด๋ํ๊ท 32.7%)์ด๋ฉฐ, ์ฌํ์ต ๋ชจ๋ธ์ ๊ฒ์ฆ ํ ๊ต์ฒดํ๋ค(v1 ์ฌ์ฉ).\n",
"- ๊ฒฐํ ์ํ ๊ฒฝ๋ณด 13๊ฐ ํ๋ชฉ ์ค ๋จ์ข
ยท๋จ์ข
์๊ณ ๊ฐ 11๊ฐ๋ก, ๋จ์ข
๋์์ด ์ฌ๊ณ ์ํ์ ํต์ฌ์ด๋ค.\n",
"- ๋จ์ข
์๊ณ 8๊ฐ ํ๋ชฉ์ ์ต์ข
๊ตฌ๋งค 2779๊ฐ์ ๋ฐ์ฃผ 10๊ฑด์ ๊ฒฐ์ฌ ๋๊ธฐ์ด์ ์ฌ๋ ธ๋ค.\n",
"- ๋ชจ๋ ๋ฐ์ฃผยท์ต์ข
๊ตฌ๋งคยท๋์ฒดํ ์ ํ์ ์ ๋น์ฅ๊ต ๊ฒฐ์ฌ ํ ์งํํ๋ฉฐ, ์์น๋ ๊ต์ก์ฉ ๊ฐ์ ์๋ฃ ๊ธฐ์ค์ด๋ค.\n",
"```\n",
" \n"
]
}
],
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}