{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# ๐ŸŽฏ [MNT02] AI ๊ธฐ๋ฐ˜ ์ง€๋Šฅํ˜• ๋ถ€ํ’ˆ ๋‹จ์ข…์œ„ํ—˜ ๋ถ„์„ยท๋Œ€์‘์ฒด๊ณ„\n", "### Google Colab ์‹ค์Šต ํ™˜๊ฒฝ (์žฅ๋น„์ •๋น„ยท์ˆ˜๋ฆฌ๋ถ€์† / Level 3)\n", "\n", "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/edunlab/milicontent2/blob/main/uploads/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 }