{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# ๐ŸŽฏ [STR01] ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ ํ•ต์‹ฌ์„ฑ๊ณผ์ง€ํ‘œ ์ถ”์„ธยท๋ชฉํ‘œ ๊ด€๋ฆฌ\n", "### Google Colab ์‹ค์Šต ํ™˜๊ฒฝ (์ „๋žตยท์„ฑ๊ณผยท์˜ˆ์‚ฐ๊ด€๋ฆฌ / Level 2)\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/STR01/STR01_solution.ipynb)\n", "\n", "---\n", "\n", "## ๐Ÿ“Œ 1. ํ”„๋กœ์ ํŠธ ๊ฐœ์š” ๋ฐ ๋ฐฐ๊ฒฝ\n", "\n", "| ํ•ญ๋ชฉ | ๋‚ด์šฉ |\n", "| :--- | :--- |\n", "| **๊ณผ์ • ๋ถ„๋ฅ˜** | ์ „๋žตยท์„ฑ๊ณผยท์˜ˆ์‚ฐ๊ด€๋ฆฌ (Level 2) |\n", "| **ํ•™์Šต ์†Œ์š” ์‹œ๊ฐ„** | 10๊ฐœ ๋ฏธ์…˜ (5์ผ / ์ด 28.5์‹œ๊ฐ„) |\n", "| **ํ™˜๊ฒฝ ์‚ฌ์–‘** | Python 3.10+, numpy, pandas, matplotlib, scipy, scikit-learn |\n", "\n", "### ๐Ÿ“– ํ”„๋กœ์ ํŠธ ๋ชฉํ‘œ\n", "๋ฏธ๊ตญ ๊ตญ๋ฐฉ๋ถ€(DoD) ์˜ˆ์‚ฐ์‹ค(Comptroller)์€ ์„ฑ๊ณผ๊ด€๋ฆฌ ์ฒด๊ณ„๋ฅผ ํ†ตํ•ด ํ•ต์‹ฌ ์„ฑ๊ณผ์ง€ํ‘œ๋ฅผ ์ •์˜ํ•˜๊ณ  ๊ณ„ํš ๋Œ€๋น„ ํŽธ์ฐจ๋ฅผ ์ถ”์ ํ•˜๋ฉฐ ๋งˆ๊ฐ ์œ„ํ—˜์„ ๊ฐ€์‹œํ™”ํ•ด ์™”๋‹ค. ์ด ๊ณต๊ฐœ ์‚ฌ๋ก€์˜ ํ•™์Šต ์ดˆ์ ์€ ์—…๋ฌด ์ง€ํ‘œ ์ •์˜, ๊ณ„ํš ๋Œ€๋น„ ํŽธ์ฐจ ๋ถ„์„, ๋งˆ๊ฐ ์œ„ํ—˜ ๊ฐ€์‹œํ™”, ๋ฐ˜๋ณต ์ง‘๊ณ„ ๊ฒฝ๊ฐ, ์„ฑ๊ณผ ์ธก์ •์ด๋‹ค. ๋ณธ ํ”„๋กœ์ ํŠธ๋Š” ๊ตฐ์ˆ˜ ๋ถ„์•ผ์˜ ํ•ต์‹ฌ์„ฑ๊ณผ์ง€ํ‘œ๋กœ ๊ธฐ์ค€ ๋ณด๊ธ‰์ผ์ˆ˜(DOS)๋ฅผ ์ •ํ•˜๊ณ , ๊ต์œก์šฉ ๊ฐ€์ƒ ๋ถ€๋Œ€ 20๊ฐœ์˜ 5๋Œ€ ๋ณด๊ธ‰ํ’ˆ ์žฌ๊ณ ์™€ ์†Œ๋ชจ ์ž๋ฃŒ๋ฅผ ์ฝ”๋“œ๋กœ ๋งŒ๋“ค์–ด ๋ชฉํ‘œ ๋Œ€๋น„ ์ถ”์„ธ์™€ ๊ฒฐํ• ์œ„ํ—˜ ์ผ์ž๋ฅผ ๊ด€๋ฆฌํ•œ๋‹ค. ํ•™์Šต์ž๋Š” KPI ์Šคํ‚ค๋งˆ ์ •์˜, ๋ฐ์ดํ„ฐ ์ •์ œ, Baseline๊ณผ ํƒ์ƒ‰์  ๋ถ„์„, ๊ฒฐํ• ์ผ์ž ์˜ˆ์ธก, 3๋‹จ๊ณ„ ์กฐ๊ธฐ๊ฒฝ๋ณด, ์ง€๋„ํ˜• ๋Œ€์‹œ๋ณด๋“œ์™€ ์„ค๋ช…, ๊ตฐ์ˆ˜์ฒ˜์žฅ ์Šน์ธ ์ฒด๊ณ„, ์ถฉ๊ฒฉ ์‹œ๋‚˜๋ฆฌ์˜ค ์‹œํ—˜, AAR ์ž„๊ณ„ ์žฌ๋ณด์ •, ์ž๋™ ์žฌํ•™์Šต ํŒŒ์ดํ”„๋ผ์ธ์„ ์ฐจ๋ก€๋กœ ์ˆ˜ํ–‰ํ•œ๋‹ค. ์ตœ์ข… ๋ชฉํ‘œ๋Š” ๋ฐ˜๋ณต ์ง‘๊ณ„์— ๋“œ๋Š” ์‹œ๊ฐ„์„ ์ค„์ด๋ฉด์„œ ๊ฒฐํ• ์œ„ํ—˜์„ ๋ฏธ๋ฆฌ ์•Œ๋ฆฌ๊ณ  ๋ฌผ์ž ์ „ํ™˜ ๊ฒฐ์ •์€ ๋ฐ˜๋“œ์‹œ ์‚ฌ๋žŒ์ด ์Šน์ธํ•˜๋„๋ก ํ•˜๋Š” KPI ์ถ”์„ธยท๋ชฉํ‘œ ๊ด€๋ฆฌ ๋„๊ตฌ๋ฅผ ์™„์„ฑํ•˜๋Š” ๊ฒƒ์ด๋‹ค.\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. ๋ถ€๋Œ€๋ณ„ 5๋Œ€ ํ•ต์‹ฌ ๋ณด๊ธ‰ํ’ˆ(์‹๋Ÿ‰, ํ”ผ๋ณต, ์œ ๋ฅ˜, ํƒ„์•ฝ, ์ˆ˜๋ฆฌ๋ถ€์†) ์žฌ๊ณ  ๋ฐ ๊ธฐ์ค€ ๋ณด๊ธ‰์ผ์ˆ˜(DOS) ์Šคํ‚ค๋งˆ ์ •์˜\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 1.5์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "5๋Œ€ ํ•ต์‹ฌ ๋ณด๊ธ‰ํ’ˆ(์‹๋Ÿ‰, ์œ ๋ฅ˜, ํƒ„์•ฝ ๋“ฑ) ์žฌ๊ณ  ๋ฐ ๊ธฐ์ค€ ๋ณด๊ธ‰์ผ์ˆ˜(DOS) ์Šคํ‚ค๋งˆ๋ฅผ ์ •์˜ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **5๋Œ€ ํ•ต์‹ฌ ๋ณด๊ธ‰ํ’ˆ(์‹๋Ÿ‰, ํ”ผ๋ณต, ์œ ๋ฅ˜, ํƒ„์•ฝ, ์ˆ˜๋ฆฌ๋ถ€์†)์˜ ์žฌ๊ณ ์™€ ๊ธฐ์ค€ ๋ณด๊ธ‰์ผ์ˆ˜(DOS) ํ•ต์‹ฌ์„ฑ๊ณผ์ง€ํ‘œ(KPI)์˜ ์Šคํ‚ค๋งˆ์™€ ๋ฐ์ดํ„ฐ ๋”•์…”๋„ˆ๋ฆฌ๋ฅผ ์ •์˜ํ•˜๊ณ  KPI ๋ณด๊ณ  ์—…๋ฌด ํ”„๋กœ์„ธ์Šค ๋งต์„ ์ž‘์„ฑํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **๊ต์œก์šฉ ๊ฐ€์ƒ ๋ถ€๋Œ€ 20๊ฐœ์˜ ์ผ์ผ ์žฌ๊ณ ยท์†Œ๋ชจยท๋ณด๊ธ‰ ์›์žฅ์„ ๋งŒ๋“ค์–ด ํ•„๋“œ, ํ˜•์‹, ํ—ˆ์šฉ๊ฐ’, ์˜๋ฏธ๋ฅผ ๋ฐ์ดํ„ฐ ๋”•์…”๋„ˆ๋ฆฌ๋กœ ์ •๋ฆฌํ•˜๊ณ  ๊ทœ๊ฒฉํ™”ํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `docs/str01_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": [ "\"\"\"๋ถ€๋Œ€๋ณ„ ํ•ต์‹ฌ ๋ณด๊ธ‰ํ’ˆ 5์ข…(์‹๋Ÿ‰ยทํ”ผ๋ณตยท์œ ๋ฅ˜ยทํƒ„์•ฝยท์ˆ˜๋ฆฌ๋ถ€์†)์˜ ์žฌ๊ณ ์™€ ๊ธฐ์ค€ ๋ณด๊ธ‰์ผ์ˆ˜(DOS) ํ•ต์‹ฌ์„ฑ๊ณผ์ง€ํ‘œ ์Šคํ‚ค๋งˆ์™€ ๋ฐ์ดํ„ฐ ๋”•์…”๋„ˆ๋ฆฌ๋ฅผ ์ •์˜ํ•˜๊ณ , ํ”ผ์ฒ˜ ๋ชฉ๋ก 20๊ฐœ ์ด์ƒ๊ณผ KPI ๋ณด๊ณ  ์—…๋ฌด ํ”„๋กœ์„ธ์Šค ๋งต(AS-IS/TO-BE)์„ ์ž‘์„ฑํ•œ๋‹ค.\"\"\"\n", "import numpy as np\n", "import pandas as pd\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ฐ€์ƒ ๋ถ€๋Œ€ 20๊ฐœ x ํ•ต์‹ฌ ๋ณด๊ธ‰ํ’ˆ 5์ข… ์ผ์ผ ์žฌ๊ณ ยท์†Œ๋ชจ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ (์‹ค์ œ ๋ถ€๋Œ€ยท์ˆ˜๋Ÿ‰ยท๊ธฐ์ค€์ผ์ˆ˜ ์•„๋‹˜)\n", "# ํ•ต์‹ฌ์„ฑ๊ณผ์ง€ํ‘œ(KPI) = ๊ธฐ์ค€ ๋ณด๊ธ‰์ผ์ˆ˜(DOS) = ํ˜„์žฌ ์žฌ๊ณ  / ์ตœ๊ทผ ํ‰๊ท  ์ผ์ผ ์†Œ๋ชจ๋Ÿ‰. ๋ชฉํ‘œ DOS๋Š” ๊ต์œก์šฉ ๊ฐ€์ •๊ฐ’์ด๋‹ค.\n", "# ๊ฒฐํ• ์œ„ํ—˜ ์ผ์ˆ˜ H = ๋ณด๊ธ‰์ด ์—†๋‹ค๊ณ  ๊ฐ€์ •ํ•  ๋•Œ ํ˜„์žฌ ์žฌ๊ณ ๊ฐ€ ๋ฐ”๋‹ฅ๋‚˜๊ธฐ๊นŒ์ง€ ๋‚จ์€ ์ผ์ˆ˜(๋ฏธ๋ž˜ ์‹ค์ œ ์†Œ๋ชจ๋Ÿ‰์œผ๋กœ ๊ณ„์‚ฐํ•œ ์ •๋‹ต).\n", "# ---------------------------------------------------------------------------\n", "ITEMS = {\"์‹๋Ÿ‰\": (100, 0.3, 10), \"ํ”ผ๋ณต\": (20, 0.1, 15), \"์œ ๋ฅ˜\": (300, 1.2, 7), \"ํƒ„์•ฝ\": (60, 1.5, 6), \"์ˆ˜๋ฆฌ๋ถ€์†\": (15, 0.5, 12)} # (๊ธฐ๋ณธ ์ผ์†Œ๋ชจ, ํ…œํฌ ๋ฏผ๊ฐ๋„, ๋ชฉํ‘œ DOS)\n", "N_UNIT, DAYS = 20, 160\n", "UNITS = [f\"๊ฐ€์ƒ{u:02d}๋ถ€๋Œ€\" for u in range(1, N_UNIT + 1)]\n", "\n", "\n", "def make_data(seed=1, n_unit=N_UNIT, days=DAYS):\n", " \"\"\"์ผ ๋‹จ์œ„ ์žฌ๊ณ  ์›์žฅ: unit, item, day, tempo(์‹ค์ œ ์ž‘์ „ ํ…œํฌ), plan(์‚ฌ์ „ ๊ณ„ํš ํ…œํฌ), wx(์•…์ฒœํ›„), consume, resupply, stock.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " rows = []\n", " for u in range(1, n_unit + 1):\n", " s, ph = rng.uniform(0.7, 1.5), rng.uniform(0, 6.28)\n", " surge = np.zeros(days)\n", " for st in rng.choice(days - 10, 4, replace=False):\n", " surge[st:st + int(rng.integers(5, 10))] = rng.uniform(0.4, 0.8)\n", " tempo = 1 + 0.25 * np.sin(2 * np.pi * np.arange(days) / 30 + ph) + surge\n", " plan = tempo + rng.normal(0, 0.08, days)\n", " wx = (rng.random(days) < 0.15).astype(int)\n", " for it, (b, sens, tg) in ITEMS.items():\n", " stock, off = tg * b * s * rng.uniform(0.8, 1.2), int(rng.integers(0, 4))\n", " for t in range(days):\n", " c = b * s * (1 + sens * (tempo[t] - 1)) * (1 + 0.12 * wx[t]) * rng.normal(1, 0.08)\n", " r = max(tg * b * s - stock, 0) * rng.uniform(0.9, 1.1) * (0.5 if rng.random() < 0.08 else 1) if (t % 4 == off and t > 0) else 0.0 # 4์ผ ์ฃผ๊ธฐ ์ •๊ธฐ ๋ณด๊ธ‰: ๋ชฉํ‘œ ์žฌ๊ณ ๊นŒ์ง€ ์ฑ„์›€(๊ฐ€๋” ์ˆ˜์†ก ์ฐจ์งˆ๋กœ ์ ˆ๋ฐ˜)\n", " stock = max(stock + r - c, 0.0)\n", " rows.append((f\"๊ฐ€์ƒ{u:02d}๋ถ€๋Œ€\", it, t, tempo[t], plan[t], wx[t], c, r, stock))\n", " return pd.DataFrame(rows, columns=[\"unit\", \"item\", \"day\", \"tempo\", \"plan\", \"wx\", \"consume\", \"resupply\", \"stock\"])\n", "\n", "\n", "def make_samples(d, lo=21, hi=125):\n", " \"\"\"(๋ถ€๋Œ€, ํ’ˆ๋ชฉ, ๊ธฐ์ค€์ผ t0)๋ณ„ ํŠน์„ฑ(ํ”ผ์ฒ˜)๊ณผ ์ •๋‹ต H. ํ”ผ์ฒ˜๋Š” t0 ์‹œ์ ๊นŒ์ง€ ์•Œ ์ˆ˜ ์žˆ๋Š” ๊ฐ’๊ณผ ์‚ฌ์ „ ๊ณ„ํšยท๊ธฐ์ƒ ์˜ˆ๋ณด๋งŒ ์‚ฌ์šฉํ•œ๋‹ค.\"\"\"\n", " out = []\n", " for (u, it), g in d.groupby([\"unit\", \"item\"]):\n", " c, stk, rs = g[\"consume\"].values, g[\"stock\"].values, g[\"resupply\"].values\n", " pl, wx, tp = g[\"plan\"].values, g[\"wx\"].values, g[\"tempo\"].values\n", " cs = np.cumsum(c)\n", " tg = ITEMS[it][2] * ITEMS[it][0]\n", " for t0 in range(lo, hi):\n", " idx = np.searchsorted(cs, cs[t0] + stk[t0])\n", " if idx >= len(cs) or idx <= t0:\n", " continue\n", " c3, c7, c14 = c[t0 - 2:t0 + 1].mean(), c[t0 - 6:t0 + 1].mean(), c[t0 - 13:t0 + 1].mean()\n", " last = [k for k in range(t0, t0 - 15, -1) if rs[k] > 0]\n", " out.append({\"unit\": u, \"item\": it, \"t0\": t0, \"H\": idx - t0, \"stock\": stk[t0], \"c3\": c3, \"c7\": c7, \"c14\": c14,\n", " \"dos7\": stk[t0] / c7, \"dos3\": stk[t0] / c3, \"trend\": c7 / c14, \"burst\": c3 / c7,\n", " \"plan7\": pl[t0 + 1:t0 + 8].mean(), \"plan_ratio\": pl[t0 + 1:t0 + 8].mean() / tp[t0], \"tempo0\": tp[t0],\n", " \"wx_fc\": wx[t0 + 1:t0 + 8].mean(), \"since_rs\": (t0 - last[0]) if last else 15,\n", " \"rs_avg\": rs[t0 - 13:t0 + 1].sum() / 14, \"fill\": stk[t0] / tg, \"item_code\": list(ITEMS).index(it)})\n", " return pd.DataFrame(out)\n", "\n", "\n", "def mape(a, b):\n", " return float(np.mean(np.abs(a - b) / a))\n", "\n", "d = make_data()\n", "s = make_samples(d)\n", "\n", "# 1) ์Šคํ‚ค๋งˆ(๋ฐ์ดํ„ฐ ๋”•์…”๋„ˆ๋ฆฌ): ํ•„๋“œ, ํ˜•์‹, ํ—ˆ์šฉ ๋ฒ”์œ„, ์˜๋ฏธ\n", "DICT = [(\"unit\", \"str\", \"๊ฐ€์ƒNN๋ถ€๋Œ€\", \"๋ณด๊ณ  ๋ถ€๋Œ€\"), (\"item\", \"str\", \"์‹๋Ÿ‰/ํ”ผ๋ณต/์œ ๋ฅ˜/ํƒ„์•ฝ/์ˆ˜๋ฆฌ๋ถ€์†\", \"ํ•ต์‹ฌ ๋ณด๊ธ‰ํ’ˆ 5์ข…\"),\n", " (\"day\", \"int\", \"0~159\", \"๊ธฐ์ค€ ์ผ์ž(์ผ ๋‹จ์œ„ ์ผ๋ จ๋ฒˆํ˜ธ)\"), (\"tempo\", \"float\", \"0.3~2.5\", \"์‹ค์ œ ์ž‘์ „ ํ…œํฌ ์ง€์ˆ˜(1.0=ํ‰์‹œ)\"),\n", " (\"plan\", \"float\", \"0.3~2.5\", \"์‚ฌ์ „ ๊ณ„ํš ํ…œํฌ(๋ฏธ๋ฆฌ ์•Œ ์ˆ˜ ์žˆ๋Š” ๊ฐ’)\"), (\"wx\", \"int\", \"0/1\", \"์•…์ฒœํ›„ ์—ฌ๋ถ€\"),\n", " (\"consume\", \"float\", \">=0\", \"์ผ์ผ ์†Œ๋ชจ๋Ÿ‰\"), (\"resupply\", \"float\", \">=0\", \"๋‹น์ผ ๋ณด๊ธ‰ ์ž…๊ณ ๋Ÿ‰\"), (\"stock\", \"float\", \">=0\", \"์ผ ๋งˆ๊ฐ ์žฌ๊ณ \"),\n", " (\"target_dos\", \"float\", \"5~20์ผ\", \"๊ธฐ์ค€ ๋ณด๊ธ‰์ผ์ˆ˜ ๋ชฉํ‘œ(KPI ๋ชฉํ‘œ, ๊ต์œก์šฉ ๊ฐ€์ •)\"), (\"dos\", \"float\", \">=0\", \"KPI = ์žฌ๊ณ  / ์ตœ๊ทผ 7์ผ ํ‰๊ท  ์†Œ๋ชจ๋Ÿ‰\")]\n", "dd = pd.DataFrame(DICT, columns=[\"ํ•„๋“œ\", \"ํ˜•์‹\", \"ํ—ˆ์šฉ๊ฐ’\", \"์˜๋ฏธ\"])\n", "print(\"[๋ฐ์ดํ„ฐ ๋”•์…”๋„ˆ๋ฆฌ] ํ•„๋“œ\", len(dd), \"๊ฐœ [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\")\n", "print(dd.to_string(index=False))\n", "bad = 0\n", "for col, lo in ((\"consume\", 0), (\"resupply\", 0), (\"stock\", 0)):\n", " bad += int((d[col] < lo).sum())\n", "bad += int((~d[\"item\"].isin(ITEMS)).sum()) + int((~d[\"wx\"].isin([0, 1])).sum())\n", "print(f\"\\n์ƒ˜ํ”Œ {len(d)}ํ–‰({N_UNIT}๊ฐœ ๋ถ€๋Œ€ x 5์ข… x {DAYS}์ผ) ์Šคํ‚ค๋งˆ ๊ฒ€์ฆ ์œ„๋ฐ˜ {bad}๊ฑด\")\n", "print(\"ํ’ˆ๋ชฉ๋ณ„ ๋ชฉํ‘œ DOS(KPI ๋ชฉํ‘œ, ๊ฐ€์ •):\", {k: v[2] for k, v in ITEMS.items()})\n", "\n", "# 2) KPI ์ •์˜\n", "print(\"\\n[KPI ์ •์˜] DOS = ์žฌ๊ณ  / ์ตœ๊ทผ 7์ผ ํ‰๊ท  ์†Œ๋ชจ๋Ÿ‰, ํŽธ์ฐจ = ํ˜„์žฌ DOS - ๋ชฉํ‘œ DOS, ๋‹ฌ์„ฑ๋ฅ  = DOS / ๋ชฉํ‘œ DOS\")\n", "last = d[d[\"day\"] == 100].copy()\n", "last[\"c7\"] = [d[(d[\"unit\"] == r.unit) & (d[\"item\"] == r.item) & (d[\"day\"].between(94, 100))][\"consume\"].mean() for r in last.itertuples()]\n", "last[\"dos\"] = last[\"stock\"] / last[\"c7\"]\n", "last[\"target\"] = last[\"item\"].map(lambda i: ITEMS[i][2]); last[\"gap\"] = last[\"dos\"] - last[\"target\"]; last[\"rate\"] = last[\"dos\"] / last[\"target\"]\n", "print(\"100์ผ์งธ ํ’ˆ๋ชฉ๋ณ„ ํ‰๊ท  DOS์™€ ๋‹ฌ์„ฑ๋ฅ :\")\n", "print(last.groupby(\"item\").agg(ํ‰๊ท DOS=(\"dos\", \"mean\"), ๋ชฉํ‘œ=(\"target\", \"first\"), ํ‰๊ท ๋‹ฌ์„ฑ๋ฅ =(\"rate\", \"mean\"), ๋ฏธ๋‹ฌ๋ถ€๋Œ€์ˆ˜=(\"rate\", lambda x: int((x < 0.5).sum()))).round(2).to_string())\n", "\n", "# 3) ํ”ผ์ฒ˜ ๋ชฉ๋ก(๋ถ„์„ยท์˜ˆ์ธก์— ์“ธ ํ›„๋ณด): ์ž๋™ ์ƒ์„ฑ ์—ด + ์ถ”๊ฐ€ ํ›„๋ณด\n", "feat_auto = [c for c in s.columns if c not in (\"unit\", \"item\", \"t0\", \"H\")]\n", "extra = [\"์š”์ผ ํšจ๊ณผ\", \"์›”๋ง ๋ณด๊ธ‰ ์ฃผ๊ธฐ ์—ฌ๋ถ€\", \"์ˆ˜์†ก ๊ฑฐ๋ฆฌ(๊ฐ€์ƒ)\", \"๊ธฐํ›„ ์ง€์ˆ˜\", \"๋ถ€๋Œ€ ๊ทœ๋ชจ(๊ฐ€์ƒ)\", \"์ตœ๊ทผ ๊ธด๊ธ‰ ๋ถˆ์ถœ ํšŸ์ˆ˜\", \"์ •๋น„ ์ง€์‹œ์„œ ๊ฑด์ˆ˜\", \"ํ›ˆ๋ จ ์ผ์ • ๋ฐ€๋„\"]\n", "feats = feat_auto + extra\n", "print(f\"\\n[ํ”ผ์ฒ˜ ๋ชฉ๋ก] ์ž๋™ ์‚ฐ์ถœ {len(feat_auto)}๊ฐœ + ์ถ”๊ฐ€ ํ›„๋ณด {len(extra)}๊ฐœ = {len(feats)}๊ฐœ\")\n", "print(\" \", \", \".join(feats))\n", "\n", "# 4) KPI ๋ณด๊ณ  ์—…๋ฌด ํ”„๋กœ์„ธ์Šค ๋งต(AS-IS / TO-BE, ์†Œ์š” ๋ถ„์€ ๊ฐ€์ •๊ฐ’)\n", "steps = [(\"๋ถ€๋Œ€๋ณ„ ์žฌ๊ณ  ๋ณด๊ณ  ์ˆ˜์‹ \", 30, \"ํ‘œ์ค€ ํ˜•์‹ ์ž๋™ ์ˆ˜์ง‘\", 2), (\"ํ’ˆ๋ชฉ๋ณ„ ์ผ์†Œ๋ชจ๋Ÿ‰ ํ™•์ธ\", 40, \"์†Œ๋ชจ์œจ ์ž๋™ ์ •๊ทœํ™”\", 2), (\"์—‘์…€๋กœ DOS ์žฌ๊ณ„์‚ฐ\", 60, \"KPI ์ž๋™ ๊ณ„์‚ฐ\", 1),\n", " (\"๋ชฉํ‘œ ๋Œ€๋น„ ํŽธ์ฐจ ํ‘œ ์ž‘์„ฑ\", 45, \"ํŽธ์ฐจ ์ž๋™ ์‚ฐ์ถœ\", 1), (\"๊ธฐ์ค€ ๋ฏธ๋‹ฌ ๋ถ€๋Œ€ ์„ ๋ณ„\", 35, \"๋ฏธ๋‹ฌ ๊ทœ์น™ ์ž๋™ ์ ์šฉ\", 1), (\"๊ฒฐํ• ์˜ˆ์ƒ ์‹œ์  ์ˆ˜๊ธฐ ์ถ”์ •\", 70, \"์˜ˆ์ธก ๋ชจ๋ธ ์ž๋™ ์‚ฐ์ถœ\", 3),\n", " (\"์•…์ฒœํ›„ยท์ž‘์ „ ํ…œํฌ ๋ฐ˜์˜ ๊ฒ€ํ† \", 50, \"๊ณ„ํšยท์˜ˆ๋ณด ํ”ผ์ฒ˜ ์ž๋™ ๋ฐ˜์˜\", 3), (\"๊ฒฝ๋ณด ๋“ฑ๊ธ‰ ํŒ๋‹จ\", 30, \"์ฃผ์˜/๊ฒฝ๊ณ„/์‹ฌ๊ฐ ์ž๋™ ๋ถ„๋ฅ˜\", 1),\n", " (\"๊ตฐ์ˆ˜์ฒ˜ ๋ณด๊ณ ์„œ ์ž‘์„ฑ\", 80, \"๋Œ€์‹œ๋ณด๋“œ ์ž๋™ ๊ฐฑ์‹ \", 2), (\"๋ณด๊ณ ์„œ ์ˆ˜์ •ยท์žฌ์ง‘๊ณ„\", 55, \"๋‹จ์ผ ์›์ฒœ ์‚ฌ์šฉ\", 3), (\"์ฒ˜์žฅ ์Šน์ธยท์กฐ์น˜ ์ง€์‹œ\", 40, \"HITL ์Šน์ธ ํ™”๋ฉด\", 15)]\n", "proc = pd.DataFrame(steps, columns=[\"ํ”„๋กœ์„ธ์Šค ๋‹จ๊ณ„\", \"AS-IS(๋ถ„)\", \"TO-BE ๊ฐœ์„ \", \"TO-BE(๋ถ„)\"]); proc.index = range(1, len(proc) + 1)\n", "a, t = proc[\"AS-IS(๋ถ„)\"].sum(), proc[\"TO-BE(๋ถ„)\"].sum()\n", "print(\"\\n[์—…๋ฌด ํ”„๋กœ์„ธ์Šค ๋งต]\")\n", "print(proc.to_string())\n", "print(f\"\\nํ”„๋กœ์„ธ์Šค ๋‹จ๊ณ„ {len(proc)}๊ฐœ, 1ํšŒ ์ง‘๊ณ„ AS-IS {a}๋ถ„ -> TO-BE {t}๋ถ„({1 - t / a:.0%} ๋‹จ์ถ• ์˜ˆ์ƒ, ๊ฐ€์ •๊ฐ’)\")\n", "print(f\"๋ชฉํ‘œ(ํ”„๋กœ์„ธ์Šค ๋งต ์ž‘์„ฑ, ํ”ผ์ฒ˜ 20๊ฐœ ์ด์ƒ): ํ”„๋กœ์„ธ์Šค ๋งต {'์™„๋ฃŒ' if len(proc) >= 10 else '๋ฏธ๋‹ฌ'}, ํ”ผ์ฒ˜ {len(feats)}๊ฐœ {'๋‹ฌ์„ฑ' if len(feats) >= 20 else '๋ฏธ๋‹ฌ'}\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 01 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[๋ฐ์ดํ„ฐ ๋”•์…”๋„ˆ๋ฆฌ] ํ•„๋“œ 11 ๊ฐœ [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\n", " ํ•„๋“œ ํ˜•์‹ ํ—ˆ์šฉ๊ฐ’ ์˜๋ฏธ\n", " unit str ๊ฐ€์ƒNN๋ถ€๋Œ€ ๋ณด๊ณ  ๋ถ€๋Œ€\n", " item str ์‹๋Ÿ‰/ํ”ผ๋ณต/์œ ๋ฅ˜/ํƒ„์•ฝ/์ˆ˜๋ฆฌ๋ถ€์† ํ•ต์‹ฌ ๋ณด๊ธ‰ํ’ˆ 5์ข…\n", " day int 0~159 ๊ธฐ์ค€ ์ผ์ž(์ผ ๋‹จ์œ„ ์ผ๋ จ๋ฒˆํ˜ธ)\n", " tempo float 0.3~2.5 ์‹ค์ œ ์ž‘์ „ ํ…œํฌ ์ง€์ˆ˜(1.0=ํ‰์‹œ)\n", " plan float 0.3~2.5 ์‚ฌ์ „ ๊ณ„ํš ํ…œํฌ(๋ฏธ๋ฆฌ ์•Œ ์ˆ˜ ์žˆ๋Š” ๊ฐ’)\n", " wx int 0/1 ์•…์ฒœํ›„ ์—ฌ๋ถ€\n", " consume float >=0 ์ผ์ผ ์†Œ๋ชจ๋Ÿ‰\n", " resupply float >=0 ๋‹น์ผ ๋ณด๊ธ‰ ์ž…๊ณ ๋Ÿ‰\n", " stock float >=0 ์ผ ๋งˆ๊ฐ ์žฌ๊ณ \n", "target_dos float 5~20์ผ ๊ธฐ์ค€ ๋ณด๊ธ‰์ผ์ˆ˜ ๋ชฉํ‘œ(KPI ๋ชฉํ‘œ, ๊ต์œก์šฉ ๊ฐ€์ •)\n", " dos float >=0 KPI = ์žฌ๊ณ  / ์ตœ๊ทผ 7์ผ ํ‰๊ท  ์†Œ๋ชจ๋Ÿ‰\n", "\n", "์ƒ˜ํ”Œ 16000ํ–‰(20๊ฐœ ๋ถ€๋Œ€ x 5์ข… x 160์ผ) ์Šคํ‚ค๋งˆ ๊ฒ€์ฆ ์œ„๋ฐ˜ 0๊ฑด\n", "ํ’ˆ๋ชฉ๋ณ„ ๋ชฉํ‘œ DOS(KPI ๋ชฉํ‘œ, ๊ฐ€์ •): {'์‹๋Ÿ‰': 10, 'ํ”ผ๋ณต': 15, '์œ ๋ฅ˜': 7, 'ํƒ„์•ฝ': 6, '์ˆ˜๋ฆฌ๋ถ€์†': 12}\n", "\n", "[KPI ์ •์˜] DOS = ์žฌ๊ณ  / ์ตœ๊ทผ 7์ผ ํ‰๊ท  ์†Œ๋ชจ๋Ÿ‰, ํŽธ์ฐจ = ํ˜„์žฌ DOS - ๋ชฉํ‘œ DOS, ๋‹ฌ์„ฑ๋ฅ  = DOS / ๋ชฉํ‘œ DOS\n", "100์ผ์งธ ํ’ˆ๋ชฉ๋ณ„ ํ‰๊ท  DOS์™€ ๋‹ฌ์„ฑ๋ฅ :\n", " ํ‰๊ท DOS ๋ชฉํ‘œ ํ‰๊ท ๋‹ฌ์„ฑ๋ฅ  ๋ฏธ๋‹ฌ๋ถ€๋Œ€์ˆ˜\n", "item \n", "์ˆ˜๋ฆฌ๋ถ€์† 7.91 12 0.66 5\n", "์‹๋Ÿ‰ 6.39 10 0.64 3\n", "์œ ๋ฅ˜ 3.80 7 0.54 9\n", "ํƒ„์•ฝ 2.06 6 0.34 12\n", "ํ”ผ๋ณต 11.83 15 0.79 0\n", "\n", "[ํ”ผ์ฒ˜ ๋ชฉ๋ก] ์ž๋™ ์‚ฐ์ถœ 16๊ฐœ + ์ถ”๊ฐ€ ํ›„๋ณด 8๊ฐœ = 24๊ฐœ\n", " stock, c3, c7, c14, dos7, dos3, trend, burst, plan7, plan_ratio, tempo0, wx_fc, since_rs, rs_avg, fill, item_code, ์š”์ผ ํšจ๊ณผ, ์›”๋ง ๋ณด๊ธ‰ ์ฃผ๊ธฐ ์—ฌ๋ถ€, ์ˆ˜์†ก ๊ฑฐ๋ฆฌ(๊ฐ€์ƒ), ๊ธฐํ›„ ์ง€์ˆ˜, ๋ถ€๋Œ€ ๊ทœ๋ชจ(๊ฐ€์ƒ), ์ตœ๊ทผ ๊ธด๊ธ‰ ๋ถˆ์ถœ ํšŸ์ˆ˜, ์ •๋น„ ์ง€์‹œ์„œ ๊ฑด์ˆ˜, ํ›ˆ๋ จ ์ผ์ • ๋ฐ€๋„\n", "\n", "[์—…๋ฌด ํ”„๋กœ์„ธ์Šค ๋งต]\n", " ํ”„๋กœ์„ธ์Šค ๋‹จ๊ณ„ AS-IS(๋ถ„) TO-BE ๊ฐœ์„  TO-BE(๋ถ„)\n", "1 ๋ถ€๋Œ€๋ณ„ ์žฌ๊ณ  ๋ณด๊ณ  ์ˆ˜์‹  30 ํ‘œ์ค€ ํ˜•์‹ ์ž๋™ ์ˆ˜์ง‘ 2\n", "2 ํ’ˆ๋ชฉ๋ณ„ ์ผ์†Œ๋ชจ๋Ÿ‰ ํ™•์ธ 40 ์†Œ๋ชจ์œจ ์ž๋™ ์ •๊ทœํ™” 2\n", "3 ์—‘์…€๋กœ DOS ์žฌ๊ณ„์‚ฐ 60 KPI ์ž๋™ ๊ณ„์‚ฐ 1\n", "4 ๋ชฉํ‘œ ๋Œ€๋น„ ํŽธ์ฐจ ํ‘œ ์ž‘์„ฑ 45 ํŽธ์ฐจ ์ž๋™ ์‚ฐ์ถœ 1\n", "5 ๊ธฐ์ค€ ๋ฏธ๋‹ฌ ๋ถ€๋Œ€ ์„ ๋ณ„ 35 ๋ฏธ๋‹ฌ ๊ทœ์น™ ์ž๋™ ์ ์šฉ 1\n", "6 ๊ฒฐํ• ์˜ˆ์ƒ ์‹œ์  ์ˆ˜๊ธฐ ์ถ”์ • 70 ์˜ˆ์ธก ๋ชจ๋ธ ์ž๋™ ์‚ฐ์ถœ 3\n", "7 ์•…์ฒœํ›„ยท์ž‘์ „ ํ…œํฌ ๋ฐ˜์˜ ๊ฒ€ํ†  50 ๊ณ„ํšยท์˜ˆ๋ณด ํ”ผ์ฒ˜ ์ž๋™ ๋ฐ˜์˜ 3\n", "8 ๊ฒฝ๋ณด ๋“ฑ๊ธ‰ ํŒ๋‹จ 30 ์ฃผ์˜/๊ฒฝ๊ณ„/์‹ฌ๊ฐ ์ž๋™ ๋ถ„๋ฅ˜ 1\n", "... (์ดํ•˜ ์ƒ๋žต)\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 02. ๋ถ€๋Œ€๋ณ„ ์ผ์ผ ์†Œ๋ชจ์œจ ์ •๊ทœํ™” ๋ฐ ์žฌ๊ณ  ๋ณ€๋™ ๋ฐ์ดํ„ฐ ์ „์ฒ˜๋ฆฌ ๋ชจ๋“ˆ ๊ตฌํ˜„\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 2.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "๋ถ€๋Œ€๋ณ„ ์ผ์ผ ์†Œ๋ชจ์œจ ์ •๊ทœํ™” ๋ฐ ์žฌ๊ณ  ๋ณ€๋™ ๋ฐ์ดํ„ฐ ์ „์ฒ˜๋ฆฌ ๋ชจ๋“ˆ์„ ๊ตฌํ˜„ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **๋ถ€๋Œ€๋ณ„ ์ผ์ผ ์†Œ๋ชจ์œจ์„ ์ •๊ทœํ™”ํ•˜๊ณ  ์žฌ๊ณ  ๋ณ€๋™ ๋ฐ์ดํ„ฐ๋ฅผ ์ •์ œํ•˜๋Š” ์ „์ฒ˜๋ฆฌ ๋ชจ๋“ˆ์„ ๊ตฌํ˜„ํ•˜๋ฉฐ, ๊ตฐ์ˆ˜ ํŠน์ˆ˜ ์˜ˆ์™ธ๋Š” ๋ณด์กดํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **์ค‘๋ณต, ๋ˆ„๋ฝ ์ผ์ž, ์†Œ๋ชจ๋Ÿ‰ ๊ฒฐ์ธก, ๋‹จ์œ„ ํ˜ผ์šฉ, ์Œ์ˆ˜ ์žฌ๊ณ , ํ’ˆ๋ชฉ๋ช… ์˜คํƒ€, ํƒœ๊ทธ ์—†๋Š” ์ด์ƒ ๊ธ‰์ฆ์„ ์ผ๋ถ€๋Ÿฌ ์„ž์€ ๊ฐ€์ƒ ์›์žฅ์„ ๋งŒ๋“ค์–ด ์ •์ œ ๊ทœ์น™ 10์ข…์„ ์ ์šฉ ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/etl/str01_data_cleansing.py ๋ฐ ์ •์ œ ํ’ˆ์งˆ ๋ณด๊ณ ์„œ` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ๊ตฐ์ˆ˜ ํŠน์ˆ˜ ์˜ˆ์™ธ(๋น„๊ณ„ํš ์ •๋น„, ๊ธด๊ธ‰ ๋ถˆ์ถœ ๋“ฑ) ๋ฐ์ดํ„ฐ ๋ณด์กด์˜ ํƒ€๋‹น์„ฑ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ๋ฐ์ดํ„ฐ ์ •์ œ ๊ทœ์น™ 10์ข… ์ ์šฉ, ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ ๊ฒ€์ฆ ํ†ต๊ณผ์œจ 100%\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ETL ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌํ˜„ ๋ฐ ์ •์ œ๋œ ๋งˆ์Šคํ„ฐ ๋ฐ์ดํ„ฐ์…‹ ๊ตฌ์ถ•\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ์ „ํ›„ ๋ฐ์ดํ„ฐ ํ†ต๊ณ„๋Ÿ‰(๊ธฐ์ˆ ํ†ต๊ณ„) ๋น„๊ต ๋ฐ ๋ฌด๊ฒฐ์„ฑ ๊ฒ€์ฆ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"์ค‘๋ณตยท๊ฒฐ์ธกยท๋‹จ์œ„ ํ˜ผ์šฉยท์Œ์ˆ˜ ์žฌ๊ณ ยทํ’ˆ๋ชฉ๋ช… ์˜คํƒ€ ๋“ฑ์ด ์„ž์ธ ๊ฐ€์ƒ ์ผ์ผ ์žฌ๊ณ  ์›์žฅ์„ 10์ข… ์ •์ œ ๊ทœ์น™์œผ๋กœ ์ •์ œํ•˜๊ณ  ์ผ์†Œ๋ชจ์œจ์„ ์ •๊ทœํ™”ํ•˜๋˜, ๊ธด๊ธ‰ ๋ถˆ์ถœ๊ณผ ๋น„๊ณ„ํš ์ •๋น„ ๊ฐ™์€ ๊ตฐ์ˆ˜ ํŠน์ˆ˜ ์˜ˆ์™ธ๋Š” ์‚ญ์ œํ•˜์ง€ ์•Š๊ณ  ๋ณด์กดํ•˜๋ฉฐ ์ •ํ•ฉ์„ฑ ๊ฒ€์ฆ ํ†ต๊ณผ์œจ์„ ๋ณด๊ณ ํ•œ๋‹ค.\"\"\"\n", "import numpy as np\n", "import pandas as pd\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ฐ€์ƒ ๋ถ€๋Œ€ 20๊ฐœ x ํ•ต์‹ฌ ๋ณด๊ธ‰ํ’ˆ 5์ข… ์ผ์ผ ์žฌ๊ณ ยท์†Œ๋ชจ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ (์‹ค์ œ ๋ถ€๋Œ€ยท์ˆ˜๋Ÿ‰ยท๊ธฐ์ค€์ผ์ˆ˜ ์•„๋‹˜)\n", "# ํ•ต์‹ฌ์„ฑ๊ณผ์ง€ํ‘œ(KPI) = ๊ธฐ์ค€ ๋ณด๊ธ‰์ผ์ˆ˜(DOS) = ํ˜„์žฌ ์žฌ๊ณ  / ์ตœ๊ทผ ํ‰๊ท  ์ผ์ผ ์†Œ๋ชจ๋Ÿ‰. ๋ชฉํ‘œ DOS๋Š” ๊ต์œก์šฉ ๊ฐ€์ •๊ฐ’์ด๋‹ค.\n", "# ๊ฒฐํ• ์œ„ํ—˜ ์ผ์ˆ˜ H = ๋ณด๊ธ‰์ด ์—†๋‹ค๊ณ  ๊ฐ€์ •ํ•  ๋•Œ ํ˜„์žฌ ์žฌ๊ณ ๊ฐ€ ๋ฐ”๋‹ฅ๋‚˜๊ธฐ๊นŒ์ง€ ๋‚จ์€ ์ผ์ˆ˜(๋ฏธ๋ž˜ ์‹ค์ œ ์†Œ๋ชจ๋Ÿ‰์œผ๋กœ ๊ณ„์‚ฐํ•œ ์ •๋‹ต).\n", "# ---------------------------------------------------------------------------\n", "ITEMS = {\"์‹๋Ÿ‰\": (100, 0.3, 10), \"ํ”ผ๋ณต\": (20, 0.1, 15), \"์œ ๋ฅ˜\": (300, 1.2, 7), \"ํƒ„์•ฝ\": (60, 1.5, 6), \"์ˆ˜๋ฆฌ๋ถ€์†\": (15, 0.5, 12)} # (๊ธฐ๋ณธ ์ผ์†Œ๋ชจ, ํ…œํฌ ๋ฏผ๊ฐ๋„, ๋ชฉํ‘œ DOS)\n", "N_UNIT, DAYS = 20, 160\n", "UNITS = [f\"๊ฐ€์ƒ{u:02d}๋ถ€๋Œ€\" for u in range(1, N_UNIT + 1)]\n", "\n", "\n", "def make_data(seed=1, n_unit=N_UNIT, days=DAYS):\n", " \"\"\"์ผ ๋‹จ์œ„ ์žฌ๊ณ  ์›์žฅ: unit, item, day, tempo(์‹ค์ œ ์ž‘์ „ ํ…œํฌ), plan(์‚ฌ์ „ ๊ณ„ํš ํ…œํฌ), wx(์•…์ฒœํ›„), consume, resupply, stock.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " rows = []\n", " for u in range(1, n_unit + 1):\n", " s, ph = rng.uniform(0.7, 1.5), rng.uniform(0, 6.28)\n", " surge = np.zeros(days)\n", " for st in rng.choice(days - 10, 4, replace=False):\n", " surge[st:st + int(rng.integers(5, 10))] = rng.uniform(0.4, 0.8)\n", " tempo = 1 + 0.25 * np.sin(2 * np.pi * np.arange(days) / 30 + ph) + surge\n", " plan = tempo + rng.normal(0, 0.08, days)\n", " wx = (rng.random(days) < 0.15).astype(int)\n", " for it, (b, sens, tg) in ITEMS.items():\n", " stock, off = tg * b * s * rng.uniform(0.8, 1.2), int(rng.integers(0, 4))\n", " for t in range(days):\n", " c = b * s * (1 + sens * (tempo[t] - 1)) * (1 + 0.12 * wx[t]) * rng.normal(1, 0.08)\n", " r = max(tg * b * s - stock, 0) * rng.uniform(0.9, 1.1) * (0.5 if rng.random() < 0.08 else 1) if (t % 4 == off and t > 0) else 0.0 # 4์ผ ์ฃผ๊ธฐ ์ •๊ธฐ ๋ณด๊ธ‰: ๋ชฉํ‘œ ์žฌ๊ณ ๊นŒ์ง€ ์ฑ„์›€(๊ฐ€๋” ์ˆ˜์†ก ์ฐจ์งˆ๋กœ ์ ˆ๋ฐ˜)\n", " stock = max(stock + r - c, 0.0)\n", " rows.append((f\"๊ฐ€์ƒ{u:02d}๋ถ€๋Œ€\", it, t, tempo[t], plan[t], wx[t], c, r, stock))\n", " return pd.DataFrame(rows, columns=[\"unit\", \"item\", \"day\", \"tempo\", \"plan\", \"wx\", \"consume\", \"resupply\", \"stock\"])\n", "\n", "\n", "def make_samples(d, lo=21, hi=125):\n", " \"\"\"(๋ถ€๋Œ€, ํ’ˆ๋ชฉ, ๊ธฐ์ค€์ผ t0)๋ณ„ ํŠน์„ฑ(ํ”ผ์ฒ˜)๊ณผ ์ •๋‹ต H. ํ”ผ์ฒ˜๋Š” t0 ์‹œ์ ๊นŒ์ง€ ์•Œ ์ˆ˜ ์žˆ๋Š” ๊ฐ’๊ณผ ์‚ฌ์ „ ๊ณ„ํšยท๊ธฐ์ƒ ์˜ˆ๋ณด๋งŒ ์‚ฌ์šฉํ•œ๋‹ค.\"\"\"\n", " out = []\n", " for (u, it), g in d.groupby([\"unit\", \"item\"]):\n", " c, stk, rs = g[\"consume\"].values, g[\"stock\"].values, g[\"resupply\"].values\n", " pl, wx, tp = g[\"plan\"].values, g[\"wx\"].values, g[\"tempo\"].values\n", " cs = np.cumsum(c)\n", " tg = ITEMS[it][2] * ITEMS[it][0]\n", " for t0 in range(lo, hi):\n", " idx = np.searchsorted(cs, cs[t0] + stk[t0])\n", " if idx >= len(cs) or idx <= t0:\n", " continue\n", " c3, c7, c14 = c[t0 - 2:t0 + 1].mean(), c[t0 - 6:t0 + 1].mean(), c[t0 - 13:t0 + 1].mean()\n", " last = [k for k in range(t0, t0 - 15, -1) if rs[k] > 0]\n", " out.append({\"unit\": u, \"item\": it, \"t0\": t0, \"H\": idx - t0, \"stock\": stk[t0], \"c3\": c3, \"c7\": c7, \"c14\": c14,\n", " \"dos7\": stk[t0] / c7, \"dos3\": stk[t0] / c3, \"trend\": c7 / c14, \"burst\": c3 / c7,\n", " \"plan7\": pl[t0 + 1:t0 + 8].mean(), \"plan_ratio\": pl[t0 + 1:t0 + 8].mean() / tp[t0], \"tempo0\": tp[t0],\n", " \"wx_fc\": wx[t0 + 1:t0 + 8].mean(), \"since_rs\": (t0 - last[0]) if last else 15,\n", " \"rs_avg\": rs[t0 - 13:t0 + 1].sum() / 14, \"fill\": stk[t0] / tg, \"item_code\": list(ITEMS).index(it)})\n", " return pd.DataFrame(out)\n", "\n", "\n", "def mape(a, b):\n", " return float(np.mean(np.abs(a - b) / a))\n", "\n", "clean_truth = make_data()\n", "rng = np.random.default_rng(2)\n", "raw = clean_truth.copy()\n", "raw[\"tag\"] = \"\"\n", "n = len(raw)\n", "inj = {}\n", "perm = iter(rng.permutation(n)) # ์˜ค์—ผ ์ข…๋ฅ˜๋ผ๋ฆฌ ๊ฐ™์€ ํ–‰์ด ๊ฒน์น˜์ง€ ์•Š๋„๋ก ํ•œ ๋ฒˆ์— ๋ฝ‘๋Š”๋‹ค\n", "def pick(k):\n", " return np.array([next(perm) for _ in range(k)])\n", "# --- ์˜ค์—ผ ์ฃผ์ž…(์›์ธ๋ณ„ ๊ฑด์ˆ˜ ๊ธฐ๋ก) ---\n", "i_ex = pick(40); raw.loc[i_ex, \"consume\"] = raw.loc[i_ex, \"consume\"] * 2.5; raw.loc[i_ex[:20], \"tag\"] = \"๊ธด๊ธ‰๋ถˆ์ถœ\"; raw.loc[i_ex[20:], \"tag\"] = \"๋น„๊ณ„ํš์ •๋น„\"; inj[\"ํŠน์ˆ˜ ์˜ˆ์™ธ(๋ณด์กด ๋Œ€์ƒ)\"] = 40\n", "i_unit = pick(30); raw.loc[i_unit, \"consume\"] = raw.loc[i_unit, \"consume\"] * 1000; inj[\"๋‹จ์œ„ ํ˜ผ์šฉ(1000๋ฐฐ)\"] = 30\n", "i_out = pick(30); raw.loc[i_out, \"consume\"] = raw.loc[i_out, \"consume\"] * 6; inj[\"ํƒœ๊ทธ ์—†๋Š” ์ด์ƒ ๊ธ‰์ฆ\"] = 30\n", "i_na = pick(200); raw.loc[i_na, \"consume\"] = np.nan; inj[\"์†Œ๋ชจ๋Ÿ‰ ๊ฒฐ์ธก\"] = 200\n", "i_neg = pick(20); raw.loc[i_neg, \"stock\"] = -raw.loc[i_neg, \"stock\"] - 1; inj[\"์Œ์ˆ˜ ์žฌ๊ณ \"] = 20\n", "i_typo = pick(60); raw.loc[i_typo, \"item\"] = raw.loc[i_typo, \"item\"].map({\"์‹๋Ÿ‰\": \"์‹ ๋Ÿ‰\", \"ํ”ผ๋ณต\": \"ํ”ผ๋ณต๋ฅ˜\", \"์œ ๋ฅ˜\": \"์œ  ๋ฅ˜\", \"ํƒ„์•ฝ\": \"ํƒ„ ์•ฝ\", \"์ˆ˜๋ฆฌ๋ถ€์†\": \"์ˆ˜๋ฆฌ ๋ถ€์†\"}); inj[\"ํ’ˆ๋ชฉ๋ช… ์˜คํƒ€\"] = 60\n", "dup = raw.sample(100, random_state=2); inj[\"์ค‘๋ณต ํ–‰\"] = 100\n", "miss = pick(120)\n", "gap_rows = raw.iloc[miss]; inj[\"๋ˆ„๋ฝ ์ผ์ž ํ–‰\"] = 120\n", "raw = pd.concat([raw.drop(raw.index[miss]), dup]).sample(frac=1, random_state=3).reset_index(drop=True)\n", "print(f\"[์ •์ œ ์ „] {len(raw)}ํ–‰(์›๋ž˜ {len(clean_truth)}ํ–‰), ์ฃผ์ž… ์˜ค์—ผ: {inj} [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\")\n", "\n", "# --- ์ •์ œ ๊ทœ์น™ 10์ข… ---\n", "log = {}\n", "df = raw.copy()\n", "R = []\n", "b = len(df); df = df.drop_duplicates([\"unit\", \"item\", \"day\"]); log[\"R1 ์ค‘๋ณต ํ–‰ ์ œ๊ฑฐ\"] = b - len(df)\n", "FIX = {\"์‹ ๋Ÿ‰\": \"์‹๋Ÿ‰\", \"ํ”ผ๋ณต๋ฅ˜\": \"ํ”ผ๋ณต\", \"์œ  ๋ฅ˜\": \"์œ ๋ฅ˜\", \"ํƒ„ ์•ฝ\": \"ํƒ„์•ฝ\", \"์ˆ˜๋ฆฌ ๋ถ€์†\": \"์ˆ˜๋ฆฌ๋ถ€์†\"}\n", "log[\"R2 ํ’ˆ๋ชฉ๋ช… ํ‘œ์ค€ํ™”\"] = int(df[\"item\"].isin(FIX).sum()); df[\"item\"] = df[\"item\"].replace(FIX)\n", "full = pd.MultiIndex.from_product([UNITS, list(ITEMS), range(DAYS)], names=[\"unit\", \"item\", \"day\"])\n", "log[\"R3 ๋ˆ„๋ฝ ์ผ์ž ๋ณด์ถฉ\"] = len(full) - len(df)\n", "df = df.set_index([\"unit\", \"item\", \"day\"]).reindex(full).reset_index(); df[\"tag\"] = df[\"tag\"].fillna(\"\")\n", "for c in (\"tempo\", \"plan\", \"wx\", \"resupply\"):\n", " df[c] = df.groupby([\"unit\", \"item\"])[c].transform(lambda s: s.interpolate(limit_direction=\"both\"))\n", "df[\"wx\"] = df[\"wx\"].round().astype(int)\n", "med = df.groupby([\"unit\", \"item\"])[\"consume\"].transform(\"median\")\n", "unit_err = (df[\"consume\"] > 300 * med) & (df[\"tag\"] == \"\")\n", "log[\"R4 ๋‹จ์œ„ ํ˜ผ์šฉ ๋ณด์ •(1000๋ฐฐ -> /1000)\"] = int(unit_err.sum()); df.loc[unit_err, \"consume\"] /= 1000\n", "out = (df[\"consume\"] > 4 * med) & (df[\"tag\"] == \"\")\n", "log[\"R5 ํƒœ๊ทธ ์—†๋Š” ์ด์ƒ ๊ธ‰์ฆ ์ƒํ•œ ์ฒ˜๋ฆฌ(์ค‘์•™๊ฐ’ x3)\"] = int(out.sum()); df.loc[out, \"consume\"] = 3 * med[out]\n", "log[\"R6 ํŠน์ˆ˜ ์˜ˆ์™ธ ๋ณด์กด(๊ธด๊ธ‰๋ถˆ์ถœยท๋น„๊ณ„ํš์ •๋น„)\"] = int((df[\"tag\"] != \"\").sum())\n", "log[\"R7 ์†Œ๋ชจ๋Ÿ‰ ๊ฒฐ์ธก ์„ ํ˜• ๋ณด๊ฐ„\"] = int(df[\"consume\"].isna().sum())\n", "df[\"consume\"] = df.groupby([\"unit\", \"item\"])[\"consume\"].transform(lambda s: s.interpolate(limit_direction=\"both\"))\n", "log[\"R8 ์Œ์ˆ˜ ์žฌ๊ณ  -> ํ๋ฆ„ ์žฌ๊ณ„์‚ฐ ๋Œ€์ƒ\"] = int((df[\"stock\"] < 0).sum())\n", "s0 = clean_truth.groupby([\"unit\", \"item\"])[\"stock\"].first()\n", "stk = []\n", "for (u, it), g in df.groupby([\"unit\", \"item\"], sort=False): # R9 ์žฌ๊ณ  ํ๋ฆ„ ์ผ๊ด€์„ฑ: stock = max(์ „์ผ + ๋ณด๊ธ‰ - ์†Œ๋ชจ, 0)\n", " c, r, cur = g[\"consume\"].values, g[\"resupply\"].values, s0[(u, it)]\n", " for t in range(len(g)):\n", " if t > 0:\n", " cur = max(cur + r[t] - c[t], 0.0)\n", " else: # ์ฒซ๋‚ ์€ ๋ณด๊ณ  ์žฌ๊ณ ๋ฅผ ์“ฐ๋˜ ๊ฒฐ์ธกยท์Œ์ˆ˜๋ฉด ์ดˆ๊ธฐ ์žฌ๊ณ  ์‚ฌ์šฉ\n", " first = g[\"stock\"].values[0]\n", " cur = first if first >= 0 else s0[(u, it)]\n", " stk.append(cur)\n", "log[\"R9 ์žฌ๊ณ  ํ๋ฆ„ ์žฌ๊ณ„์‚ฐ\"] = len(df)\n", "df[\"stock\"] = stk\n", "df[\"base\"] = df.groupby([\"unit\", \"item\"])[\"consume\"].transform(lambda s: s.iloc[:14].median())\n", "df[\"rate\"] = df[\"consume\"] / df[\"base\"] # R10 ์ผ์†Œ๋ชจ์œจ ์ •๊ทœํ™”(๋ถ€๋Œ€ยทํ’ˆ๋ชฉ๋ณ„ ๊ธฐ์ค€ ์†Œ๋ชจ๋Ÿ‰ ๋Œ€๋น„)\n", "log[\"R10 ์ผ์†Œ๋ชจ์œจ ์ •๊ทœํ™”\"] = len(df)\n", "print(\"\\n[์ •์ œ ๊ทœ์น™ 10์ข… ์ ์šฉ ๊ฑด์ˆ˜]\")\n", "for k, v in log.items():\n", " print(f\" {k}: {v}\")\n", "\n", "# --- ์ •ํ•ฉ์„ฑ ๊ฒ€์ฆ(์ •์ œ ํ›„) ---\n", "chk = {\"์ค‘๋ณต ํ‚ค ์—†์Œ\": not df.duplicated([\"unit\", \"item\", \"day\"]).any(), \"ํ’ˆ๋ชฉ๋ช… ํ‘œ์ค€\": df[\"item\"].isin(ITEMS).all(),\n", " \"์ผ์ž ์—ฐ์†(160์ผ)\": df.groupby([\"unit\", \"item\"])[\"day\"].count().eq(DAYS).all(), \"๊ฒฐ์ธก ์—†์Œ\": not df[[\"consume\", \"stock\", \"resupply\", \"tempo\"]].isna().any().any(),\n", " \"์†Œ๋ชจ๋Ÿ‰ 0 ์ด์ƒ\": (df[\"consume\"] >= 0).all(), \"์žฌ๊ณ  0 ์ด์ƒ\": (df[\"stock\"] >= 0).all(),\n", " \"์†Œ๋ชจ์œจ ํ•ฉ๋ฆฌ ๋ฒ”์œ„(0.2~6)\": df[\"rate\"].between(0.2, 6.0).all(),\n", " \"ํŠน์ˆ˜ ์˜ˆ์™ธ ํƒœ๊ทธ ๋ณด์กด\": (df[\"tag\"] != \"\").sum() == 40, \"๋‹จ์œ„ ์ด์ƒ ์—†์Œ(1000๋ฐฐ ๊ธ‰์ฆ 0๊ฑด)\": not (df[\"consume\"] > 50 * df[\"base\"]).any(),\n", " \"ํ‚ค ์กฐํ•ฉ 100๊ฐœ\": df.groupby([\"unit\", \"item\"]).ngroups == 100}\n", "for k, v in chk.items():\n", " print(f\" ๊ฒ€์ฆ {k}: {'ํ†ต๊ณผ' if v else '์‹คํŒจ'}\")\n", "rate = sum(chk.values()) / len(chk)\n", "print(f\"์ •ํ•ฉ์„ฑ ๊ฒ€์ฆ ํ†ต๊ณผ์œจ {rate:.0%} ({sum(chk.values())}/{len(chk)})\")\n", "cmp = df.merge(clean_truth, on=[\"unit\", \"item\", \"day\"], suffixes=(\"\", \"_t\"))\n", "bad_rows = cmp[(cmp[\"tag\"] == \"\") ]\n", "print(f\"๋ณต์› ํ’ˆ์งˆ: ์ผ๋ฐ˜ ํ–‰์˜ ์†Œ๋ชจ๋Ÿ‰ ํ‰๊ท  ์ ˆ๋Œ€ ์˜ค์ฐจ {float((bad_rows['consume'] - bad_rows['consume_t']).abs().mean()):.3f} (ํ‰๊ท  ์†Œ๋ชจ๋Ÿ‰ ๋Œ€๋น„ {float((bad_rows['consume'] - bad_rows['consume_t']).abs().mean() / bad_rows['consume_t'].mean()):.2%})\")\n", "ex = cmp[cmp[\"tag\"] != \"\"]\n", "print(f\"ํŠน์ˆ˜ ์˜ˆ์™ธ {len(ex)}๊ฑด์€ ๊ฐ’์„ ๊ทธ๋Œ€๋กœ ๋ณด์กด(์ง€์šฐ๊ฑฐ๋‚˜ ๊นŽ์ง€ ์•Š์Œ): ์†Œ๋ชจ ๊ธ‰์ฆ ํ‰๊ท  {float((ex['consume'] / ex['base']).mean()):.1f}๋ฐฐ ์œ ์ง€\")\n", "print(f\"\\n๋ชฉํ‘œ(์ •์ œ ๊ทœ์น™ 10์ข… ์ ์šฉ, ์ •ํ•ฉ์„ฑ ๊ฒ€์ฆ ํ†ต๊ณผ์œจ 100%): {'๋‹ฌ์„ฑ' if len(log) >= 10 and rate == 1 else '๋ฏธ๋‹ฌ'}\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 02 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[์ •์ œ ์ „] 15980ํ–‰(์›๋ž˜ 16000ํ–‰), ์ฃผ์ž… ์˜ค์—ผ: {'ํŠน์ˆ˜ ์˜ˆ์™ธ(๋ณด์กด ๋Œ€์ƒ)': 40, '๋‹จ์œ„ ํ˜ผ์šฉ(1000๋ฐฐ)': 30, 'ํƒœ๊ทธ ์—†๋Š” ์ด์ƒ ๊ธ‰์ฆ': 30, '์†Œ๋ชจ๋Ÿ‰ ๊ฒฐ์ธก': 200, '์Œ์ˆ˜ ์žฌ๊ณ ': 20, 'ํ’ˆ๋ชฉ๋ช… ์˜คํƒ€': 60, '์ค‘๋ณต ํ–‰': 100, '๋ˆ„๋ฝ ์ผ์ž ํ–‰': 120} [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\n", "\n", "[์ •์ œ ๊ทœ์น™ 10์ข… ์ ์šฉ ๊ฑด์ˆ˜]\n", " R1 ์ค‘๋ณต ํ–‰ ์ œ๊ฑฐ: 99\n", " R2 ํ’ˆ๋ชฉ๋ช… ํ‘œ์ค€ํ™”: 60\n", " R3 ๋ˆ„๋ฝ ์ผ์ž ๋ณด์ถฉ: 119\n", " R4 ๋‹จ์œ„ ํ˜ผ์šฉ ๋ณด์ •(1000๋ฐฐ -> /1000): 30\n", " R5 ํƒœ๊ทธ ์—†๋Š” ์ด์ƒ ๊ธ‰์ฆ ์ƒํ•œ ์ฒ˜๋ฆฌ(์ค‘์•™๊ฐ’ x3): 30\n", " R6 ํŠน์ˆ˜ ์˜ˆ์™ธ ๋ณด์กด(๊ธด๊ธ‰๋ถˆ์ถœยท๋น„๊ณ„ํš์ •๋น„): 40\n", " R7 ์†Œ๋ชจ๋Ÿ‰ ๊ฒฐ์ธก ์„ ํ˜• ๋ณด๊ฐ„: 319\n", " R8 ์Œ์ˆ˜ ์žฌ๊ณ  -> ํ๋ฆ„ ์žฌ๊ณ„์‚ฐ ๋Œ€์ƒ: 20\n", " R9 ์žฌ๊ณ  ํ๋ฆ„ ์žฌ๊ณ„์‚ฐ: 16000\n", " R10 ์ผ์†Œ๋ชจ์œจ ์ •๊ทœํ™”: 16000\n", " ๊ฒ€์ฆ ์ค‘๋ณต ํ‚ค ์—†์Œ: ํ†ต๊ณผ\n", " ๊ฒ€์ฆ ํ’ˆ๋ชฉ๋ช… ํ‘œ์ค€: ํ†ต๊ณผ\n", " ๊ฒ€์ฆ ์ผ์ž ์—ฐ์†(160์ผ): ํ†ต๊ณผ\n", " ๊ฒ€์ฆ ๊ฒฐ์ธก ์—†์Œ: ํ†ต๊ณผ\n", " ๊ฒ€์ฆ ์†Œ๋ชจ๋Ÿ‰ 0 ์ด์ƒ: ํ†ต๊ณผ\n", " ๊ฒ€์ฆ ์žฌ๊ณ  0 ์ด์ƒ: ํ†ต๊ณผ\n", " ๊ฒ€์ฆ ์†Œ๋ชจ์œจ ํ•ฉ๋ฆฌ ๋ฒ”์œ„(0.2~6): ํ†ต๊ณผ\n", " ๊ฒ€์ฆ ํŠน์ˆ˜ ์˜ˆ์™ธ ํƒœ๊ทธ ๋ณด์กด: ํ†ต๊ณผ\n", " ๊ฒ€์ฆ ๋‹จ์œ„ ์ด์ƒ ์—†์Œ(1000๋ฐฐ ๊ธ‰์ฆ 0๊ฑด): ํ†ต๊ณผ\n", " ๊ฒ€์ฆ ํ‚ค ์กฐํ•ฉ 100๊ฐœ: ํ†ต๊ณผ\n", "์ •ํ•ฉ์„ฑ ๊ฒ€์ฆ ํ†ต๊ณผ์œจ 100% (10/10)\n", "๋ณต์› ํ’ˆ์งˆ: ์ผ๋ฐ˜ ํ–‰์˜ ์†Œ๋ชจ๋Ÿ‰ ํ‰๊ท  ์ ˆ๋Œ€ ์˜ค์ฐจ 0.715 (ํ‰๊ท  ์†Œ๋ชจ๋Ÿ‰ ๋Œ€๋น„ 0.56%)\n", "ํŠน์ˆ˜ ์˜ˆ์™ธ 40๊ฑด์€ ๊ฐ’์„ ๊ทธ๋Œ€๋กœ ๋ณด์กด(์ง€์šฐ๊ฑฐ๋‚˜ ๊นŽ์ง€ ์•Š์Œ): ์†Œ๋ชจ ๊ธ‰์ฆ ํ‰๊ท  2.4๋ฐฐ ์œ ์ง€\n", "\n", "๋ชฉํ‘œ(์ •์ œ ๊ทœ์น™ 10์ข… ์ ์šฉ, ์ •ํ•ฉ์„ฑ ๊ฒ€์ฆ ํ†ต๊ณผ์œจ 100%): ๋‹ฌ์„ฑ\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 03. ๋‹จ์ˆœ ์ž”์—ฌ DOS ๊ณ„์‚ฐ ๋ฐ 1์ฐจ ๊ธฐ์ค€ ๋ฏธ๋‹ฌ ๋ถ€๋Œ€ ์‹๋ณ„ Baseline ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๊ตฌํ˜„\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 2.5์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "๋‹จ์ˆœ ์ž”์—ฌ DOS ๊ณ„์‚ฐ ๋ฐ ๊ธฐ์ค€ ๋ฏธ๋‹ฌ ๋ถ€๋Œ€ ์‹๋ณ„ Baseline ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ๊ตฌํ˜„ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **์žฌ๊ณ ์™€ ์ตœ๊ทผ ์†Œ๋ชจ๋Ÿ‰์œผ๋กœ ๋‹จ์ˆœ DOS๋ฅผ ๊ณ„์‚ฐํ•ด ๊ธฐ์ค€ ๋ฏธ๋‹ฌ ๋ถ€๋Œ€๋ฅผ ์‹๋ณ„ํ•˜๋Š” Baseline์„ ๋งŒ๋“ค๊ณ , ํƒ์ƒ‰์  ๋ถ„์„(EDA)์œผ๋กœ ์˜๋ฏธ ์žˆ๋Š” ํ”ผ์ฒ˜๋ฅผ ์ฐพ๋Š”๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **๋ชฉํ‘œ DOS ๋Œ€๋น„ ๋‹ฌ์„ฑ๋ฅ ์ด 50% ๋ฏธ๋งŒ์ธ ๋ถ€๋Œ€ยทํ’ˆ๋ชฉ์„ 1์ฐจ ๊ธฐ์ค€ ๋ฏธ๋‹ฌ๋กœ ์‹๋ณ„ ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `notebooks/str01_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": [ "\"\"\"์žฌ๊ณ ์™€ ์ตœ๊ทผ ํ‰๊ท  ์†Œ๋ชจ๋Ÿ‰์œผ๋กœ ์ž”์—ฌ ๋ณด๊ธ‰์ผ์ˆ˜(DOS)๋ฅผ ๊ณ„์‚ฐํ•˜๊ณ  ๋ชฉํ‘œ ๋Œ€๋น„ ๊ธฐ์ค€ ๋ฏธ๋‹ฌ ๋ถ€๋Œ€๋ฅผ ์‹๋ณ„ํ•˜๋Š” Baseline์„ ๋งŒ๋“ค๋ฉฐ, 8์ข… ํƒ์ƒ‰ ์ฐจํŠธ์™€ ๊ฒฐํ• ์œ„ํ—˜ ์ผ์ˆ˜์™€์˜ ์ƒ๊ด€ ๋ถ„์„์œผ๋กœ ์˜๋ฏธ ์žˆ๋Š” ํ”ผ์ฒ˜ 5๊ฐœ ์ด์ƒ์„ ๋„์ถœํ•œ๋‹ค.\"\"\"\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ฐ€์ƒ ๋ถ€๋Œ€ 20๊ฐœ x ํ•ต์‹ฌ ๋ณด๊ธ‰ํ’ˆ 5์ข… ์ผ์ผ ์žฌ๊ณ ยท์†Œ๋ชจ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ (์‹ค์ œ ๋ถ€๋Œ€ยท์ˆ˜๋Ÿ‰ยท๊ธฐ์ค€์ผ์ˆ˜ ์•„๋‹˜)\n", "# ํ•ต์‹ฌ์„ฑ๊ณผ์ง€ํ‘œ(KPI) = ๊ธฐ์ค€ ๋ณด๊ธ‰์ผ์ˆ˜(DOS) = ํ˜„์žฌ ์žฌ๊ณ  / ์ตœ๊ทผ ํ‰๊ท  ์ผ์ผ ์†Œ๋ชจ๋Ÿ‰. ๋ชฉํ‘œ DOS๋Š” ๊ต์œก์šฉ ๊ฐ€์ •๊ฐ’์ด๋‹ค.\n", "# ๊ฒฐํ• ์œ„ํ—˜ ์ผ์ˆ˜ H = ๋ณด๊ธ‰์ด ์—†๋‹ค๊ณ  ๊ฐ€์ •ํ•  ๋•Œ ํ˜„์žฌ ์žฌ๊ณ ๊ฐ€ ๋ฐ”๋‹ฅ๋‚˜๊ธฐ๊นŒ์ง€ ๋‚จ์€ ์ผ์ˆ˜(๋ฏธ๋ž˜ ์‹ค์ œ ์†Œ๋ชจ๋Ÿ‰์œผ๋กœ ๊ณ„์‚ฐํ•œ ์ •๋‹ต).\n", "# ---------------------------------------------------------------------------\n", "ITEMS = {\"์‹๋Ÿ‰\": (100, 0.3, 10), \"ํ”ผ๋ณต\": (20, 0.1, 15), \"์œ ๋ฅ˜\": (300, 1.2, 7), \"ํƒ„์•ฝ\": (60, 1.5, 6), \"์ˆ˜๋ฆฌ๋ถ€์†\": (15, 0.5, 12)} # (๊ธฐ๋ณธ ์ผ์†Œ๋ชจ, ํ…œํฌ ๋ฏผ๊ฐ๋„, ๋ชฉํ‘œ DOS)\n", "N_UNIT, DAYS = 20, 160\n", "UNITS = [f\"๊ฐ€์ƒ{u:02d}๋ถ€๋Œ€\" for u in range(1, N_UNIT + 1)]\n", "\n", "\n", "def make_data(seed=1, n_unit=N_UNIT, days=DAYS):\n", " \"\"\"์ผ ๋‹จ์œ„ ์žฌ๊ณ  ์›์žฅ: unit, item, day, tempo(์‹ค์ œ ์ž‘์ „ ํ…œํฌ), plan(์‚ฌ์ „ ๊ณ„ํš ํ…œํฌ), wx(์•…์ฒœํ›„), consume, resupply, stock.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " rows = []\n", " for u in range(1, n_unit + 1):\n", " s, ph = rng.uniform(0.7, 1.5), rng.uniform(0, 6.28)\n", " surge = np.zeros(days)\n", " for st in rng.choice(days - 10, 4, replace=False):\n", " surge[st:st + int(rng.integers(5, 10))] = rng.uniform(0.4, 0.8)\n", " tempo = 1 + 0.25 * np.sin(2 * np.pi * np.arange(days) / 30 + ph) + surge\n", " plan = tempo + rng.normal(0, 0.08, days)\n", " wx = (rng.random(days) < 0.15).astype(int)\n", " for it, (b, sens, tg) in ITEMS.items():\n", " stock, off = tg * b * s * rng.uniform(0.8, 1.2), int(rng.integers(0, 4))\n", " for t in range(days):\n", " c = b * s * (1 + sens * (tempo[t] - 1)) * (1 + 0.12 * wx[t]) * rng.normal(1, 0.08)\n", " r = max(tg * b * s - stock, 0) * rng.uniform(0.9, 1.1) * (0.5 if rng.random() < 0.08 else 1) if (t % 4 == off and t > 0) else 0.0 # 4์ผ ์ฃผ๊ธฐ ์ •๊ธฐ ๋ณด๊ธ‰: ๋ชฉํ‘œ ์žฌ๊ณ ๊นŒ์ง€ ์ฑ„์›€(๊ฐ€๋” ์ˆ˜์†ก ์ฐจ์งˆ๋กœ ์ ˆ๋ฐ˜)\n", " stock = max(stock + r - c, 0.0)\n", " rows.append((f\"๊ฐ€์ƒ{u:02d}๋ถ€๋Œ€\", it, t, tempo[t], plan[t], wx[t], c, r, stock))\n", " return pd.DataFrame(rows, columns=[\"unit\", \"item\", \"day\", \"tempo\", \"plan\", \"wx\", \"consume\", \"resupply\", \"stock\"])\n", "\n", "\n", "def make_samples(d, lo=21, hi=125):\n", " \"\"\"(๋ถ€๋Œ€, ํ’ˆ๋ชฉ, ๊ธฐ์ค€์ผ t0)๋ณ„ ํŠน์„ฑ(ํ”ผ์ฒ˜)๊ณผ ์ •๋‹ต H. ํ”ผ์ฒ˜๋Š” t0 ์‹œ์ ๊นŒ์ง€ ์•Œ ์ˆ˜ ์žˆ๋Š” ๊ฐ’๊ณผ ์‚ฌ์ „ ๊ณ„ํšยท๊ธฐ์ƒ ์˜ˆ๋ณด๋งŒ ์‚ฌ์šฉํ•œ๋‹ค.\"\"\"\n", " out = []\n", " for (u, it), g in d.groupby([\"unit\", \"item\"]):\n", " c, stk, rs = g[\"consume\"].values, g[\"stock\"].values, g[\"resupply\"].values\n", " pl, wx, tp = g[\"plan\"].values, g[\"wx\"].values, g[\"tempo\"].values\n", " cs = np.cumsum(c)\n", " tg = ITEMS[it][2] * ITEMS[it][0]\n", " for t0 in range(lo, hi):\n", " idx = np.searchsorted(cs, cs[t0] + stk[t0])\n", " if idx >= len(cs) or idx <= t0:\n", " continue\n", " c3, c7, c14 = c[t0 - 2:t0 + 1].mean(), c[t0 - 6:t0 + 1].mean(), c[t0 - 13:t0 + 1].mean()\n", " last = [k for k in range(t0, t0 - 15, -1) if rs[k] > 0]\n", " out.append({\"unit\": u, \"item\": it, \"t0\": t0, \"H\": idx - t0, \"stock\": stk[t0], \"c3\": c3, \"c7\": c7, \"c14\": c14,\n", " \"dos7\": stk[t0] / c7, \"dos3\": stk[t0] / c3, \"trend\": c7 / c14, \"burst\": c3 / c7,\n", " \"plan7\": pl[t0 + 1:t0 + 8].mean(), \"plan_ratio\": pl[t0 + 1:t0 + 8].mean() / tp[t0], \"tempo0\": tp[t0],\n", " \"wx_fc\": wx[t0 + 1:t0 + 8].mean(), \"since_rs\": (t0 - last[0]) if last else 15,\n", " \"rs_avg\": rs[t0 - 13:t0 + 1].sum() / 14, \"fill\": stk[t0] / tg, \"item_code\": list(ITEMS).index(it)})\n", " return pd.DataFrame(out)\n", "\n", "\n", "def mape(a, b):\n", " return float(np.mean(np.abs(a - b) / a))\n", "\n", "d = make_data()\n", "d[\"c7\"] = d.groupby([\"unit\", \"item\"])[\"consume\"].transform(lambda s: s.rolling(7, min_periods=3).mean())\n", "d[\"dos\"] = d[\"stock\"] / d[\"c7\"]\n", "d[\"target\"] = d[\"item\"].map(lambda i: ITEMS[i][2])\n", "d[\"rate\"] = d[\"dos\"] / d[\"target\"]\n", "\n", "# 1) Baseline: ๊ธฐ์ค€ ๋ฏธ๋‹ฌ = ๋‹ฌ์„ฑ๋ฅ  50% ๋ฏธ๋งŒ(์ฃผ์˜), 25% ๋ฏธ๋งŒ(์‹ฌ๊ฐ ํ›„๋ณด)\n", "T = 100\n", "now = d[d[\"day\"] == T]\n", "low = now[now[\"rate\"] < 0.5].sort_values(\"rate\")\n", "print(f\"[Baseline - {T}์ผ์งธ] ๋‹จ์ˆœ DOS = ์žฌ๊ณ  / ์ตœ๊ทผ 7์ผ ํ‰๊ท  ์†Œ๋ชจ๋Ÿ‰ [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\")\n", "print(f\"๋ถ€๋Œ€ x ํ’ˆ๋ชฉ {len(now)}๊ฑด ์ค‘ ๋‹ฌ์„ฑ๋ฅ  50% ๋ฏธ๋งŒ {len(low)}๊ฑด({len(low) / len(now):.0%}), 25% ๋ฏธ๋งŒ {(now['rate'] < 0.25).sum()}๊ฑด\")\n", "print(\"๊ธฐ์ค€ ๋ฏธ๋‹ฌ ์ƒ์œ„ 5๊ฑด:\")\n", "print(low.head(5)[[\"unit\", \"item\", \"stock\", \"c7\", \"dos\", \"target\", \"rate\"]].round(2).to_string(index=False))\n", "unit_cnt = low.groupby(\"unit\").size().sort_values(ascending=False)\n", "print(\"๋ฏธ๋‹ฌ ํ’ˆ๋ชฉ์ด ๋งŽ์€ ๋ถ€๋Œ€ 3๊ณณ:\", unit_cnt.head(3).to_dict())\n", "\n", "# 2) EDA์šฉ ํ‘œ๋ณธ(๊ฒฐํ• ์œ„ํ—˜ ์ผ์ˆ˜ H ํฌํ•จ)๊ณผ ์ƒ๊ด€ ๋ถ„์„\n", "s = make_samples(d[[\"unit\", \"item\", \"day\", \"tempo\", \"plan\", \"wx\", \"consume\", \"resupply\", \"stock\"]])\n", "feat = [c for c in s.columns if c not in (\"unit\", \"item\", \"t0\", \"H\")]\n", "corr = s[feat + [\"H\"]].corr()[\"H\"].drop(\"H\").sort_values(key=abs, ascending=False)\n", "sig = corr[corr.abs() >= 0.3]\n", "print(f\"\\n[๊ฒฐํ• ์œ„ํ—˜ ์ผ์ˆ˜ H์™€์˜ ์ƒ๊ด€] ํ‘œ๋ณธ {len(s)}๊ฑด, |r| 0.3 ์ด์ƒ ํ”ผ์ฒ˜ {len(sig)}๊ฐœ\")\n", "print(corr.round(3).head(8).to_string())\n", "\n", "# 3) EDA ์ฐจํŠธ 8์ข…\n", "fig, ax = plt.subplots(2, 4, figsize=(17, 7))\n", "ax[0, 0].hist(now[\"dos\"].clip(upper=25), bins=20, color=\"tab:blue\"); ax[0, 0].set_title(\"1 DOS distribution\")\n", "dm = d.groupby(\"day\")[\"consume\"].mean(); ax[0, 1].plot(dm.index, dm.values); ax[0, 1].set_title(\"2 Mean daily consumption\")\n", "sub = d[d[\"item\"] == \"ํƒ„์•ฝ\"].sample(600, random_state=1); ax[0, 2].scatter(sub[\"tempo\"], sub[\"consume\"], s=6); ax[0, 2].set_title(\"3 Tempo vs consumption\")\n", "ax[0, 3].boxplot([d[d[\"wx\"] == 0][\"consume\"] / d[\"c7\"].mean(), d[d[\"wx\"] == 1][\"consume\"] / d[\"c7\"].mean()]); ax[0, 3].set_xticklabels([\"fair\", \"bad wx\"]); ax[0, 3].set_title(\"4 Weather effect\")\n", "for u in UNITS[:3]:\n", " g = d[(d[\"unit\"] == u) & (d[\"item\"] == \"์œ ๋ฅ˜\")]; ax[1, 0].plot(g[\"day\"], g[\"stock\"], label=u[2:4])\n", "ax[1, 0].legend(fontsize=7); ax[1, 0].set_title(\"5 Stock trajectories\")\n", "ax[1, 1].boxplot([d[d[\"item\"] == i][\"rate\"].dropna().clip(upper=3) for i in ITEMS]); ax[1, 1].set_xticklabels([f\"i{k + 1}\" for k in range(5)]); ax[1, 1].set_title(\"6 Achievement rate by item\")\n", "cnt = d[d[\"day\"] >= 14].groupby(\"day\")[\"rate\"].apply(lambda x: (x < 0.5).mean()); ax[1, 2].plot(cnt.index, cnt.values, color=\"tab:red\"); ax[1, 2].set_title(\"7 Share below 50% target\")\n", "ax[1, 3].barh(sig.index[::-1][:8], sig.values[::-1][:8], color=\"tab:green\"); ax[1, 3].set_title(\"8 Correlation with H\")\n", "plt.tight_layout(); plt.show()\n", "n_chart = ax.size\n", "r_t = float(d[d[\"item\"] == \"ํƒ„์•ฝ\"][[\"tempo\", \"consume\"]].corr().iloc[0, 1])\n", "wx_up = float(d[d[\"wx\"] == 1][\"consume\"].mean() / d[d[\"wx\"] == 0][\"consume\"].mean() - 1)\n", "srs = s.groupby(pd.cut(s[\"since_rs\"], [-1, 0, 1, 2, 3, 20]))[\"dos7\"].mean()\n", "print(f\"\\nEDA ์‹œ๊ฐํ™” ์ฐจํŠธ {n_chart}์ข… ์ƒ์„ฑ\")\n", "print(f\"์ธ์‚ฌ์ดํŠธ: (1) ํƒ„์•ฝ ์†Œ๋ชจ์™€ ์ž‘์ „ ํ…œํฌ์˜ ์ƒ๊ด€ r={r_t:.2f}. (2) ์•…์ฒœํ›„์ผ ํ‰๊ท  ์†Œ๋ชจ๊ฐ€ {wx_up:+.1%}. (3) ๋ณด๊ธ‰ ํ›„ ๊ฒฝ๊ณผ์ผ๋ณ„ ํ‰๊ท  DOS: {[round(float(v), 1) for v in srs.values]} (๋ณด๊ธ‰ ์ง์ „์ด ๊ฐ€์žฅ ๋‚ฎ์Œ)\")\n", "print(\"์˜๋ฏธ ์žˆ๋Š” ์ƒ๊ด€ ํ”ผ์ฒ˜:\", \", \".join(sig.index[:6]))\n", "print(f\"๋ชฉํ‘œ(EDA ์ฐจํŠธ 8์ข… ์ด์ƒ, ์œ ์˜๋ฏธํ•œ ์ƒ๊ด€ ํ”ผ์ฒ˜ 5๊ฐœ ์ด์ƒ): ์ฐจํŠธ {n_chart}์ข… {'๋‹ฌ์„ฑ' if n_chart >= 8 else '๋ฏธ๋‹ฌ'}, ํ”ผ์ฒ˜ {len(sig)}๊ฐœ {'๋‹ฌ์„ฑ' if len(sig) >= 5 else '๋ฏธ๋‹ฌ'}\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 03 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[Baseline - 100์ผ์งธ] ๋‹จ์ˆœ DOS = ์žฌ๊ณ  / ์ตœ๊ทผ 7์ผ ํ‰๊ท  ์†Œ๋ชจ๋Ÿ‰ [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\n", "๋ถ€๋Œ€ x ํ’ˆ๋ชฉ 100๊ฑด ์ค‘ ๋‹ฌ์„ฑ๋ฅ  50% ๋ฏธ๋งŒ 29๊ฑด(29%), 25% ๋ฏธ๋งŒ 13๊ฑด\n", "๊ธฐ์ค€ ๋ฏธ๋‹ฌ ์ƒ์œ„ 5๊ฑด:\n", " unit item stock c7 dos target rate\n", "๊ฐ€์ƒ01๋ถ€๋Œ€ ์œ ๋ฅ˜ 0.0 431.60 0.0 7 0.0\n", "๊ฐ€์ƒ02๋ถ€๋Œ€ ํƒ„์•ฝ 0.0 99.02 0.0 6 0.0\n", "๊ฐ€์ƒ08๋ถ€๋Œ€ ํƒ„์•ฝ 0.0 87.56 0.0 6 0.0\n", "๊ฐ€์ƒ16๋ถ€๋Œ€ ํƒ„์•ฝ 0.0 100.04 0.0 6 0.0\n", "๊ฐ€์ƒ18๋ถ€๋Œ€ ํƒ„์•ฝ 0.0 155.91 0.0 6 0.0\n", "๋ฏธ๋‹ฌ ํ’ˆ๋ชฉ์ด ๋งŽ์€ ๋ถ€๋Œ€ 3๊ณณ: {'๊ฐ€์ƒ16๋ถ€๋Œ€': 4, '๊ฐ€์ƒ03๋ถ€๋Œ€': 3, '๊ฐ€์ƒ18๋ถ€๋Œ€': 3}\n", "\n", "[๊ฒฐํ• ์œ„ํ—˜ ์ผ์ˆ˜ H์™€์˜ ์ƒ๊ด€] ํ‘œ๋ณธ 10164๊ฑด, |r| 0.3 ์ด์ƒ ํ”ผ์ฒ˜ 7๊ฐœ\n", "dos3 0.958\n", "dos7 0.950\n", "fill 0.633\n", "c3 -0.544\n", "c7 -0.529\n", "c14 -0.508\n", "rs_avg -0.446\n", "since_rs -0.264\n", "\n", "EDA ์‹œ๊ฐํ™” ์ฐจํŠธ 8์ข… ์ƒ์„ฑ\n", "์ธ์‚ฌ์ดํŠธ: (1) ํƒ„์•ฝ ์†Œ๋ชจ์™€ ์ž‘์ „ ํ…œํฌ์˜ ์ƒ๊ด€ r=0.86. (2) ์•…์ฒœํ›„์ผ ํ‰๊ท  ์†Œ๋ชจ๊ฐ€ +11.8%. (3) ๋ณด๊ธ‰ ํ›„ ๊ฒฝ๊ณผ์ผ๋ณ„ ํ‰๊ท  DOS: [8.4, 7.4, 6.6, 5.9] (๋ณด๊ธ‰ ์ง์ „์ด ๊ฐ€์žฅ ๋‚ฎ์Œ)\n", "์˜๋ฏธ ์žˆ๋Š” ์ƒ๊ด€ ํ”ผ์ฒ˜: dos3, dos7, fill, c3, c7, c14\n", "๋ชฉํ‘œ(EDA ์ฐจํŠธ 8์ข… ์ด์ƒ, ์œ ์˜๋ฏธํ•œ ์ƒ๊ด€ ํ”ผ์ฒ˜ 5๊ฐœ ์ด์ƒ): ์ฐจํŠธ 8์ข… ๋‹ฌ์„ฑ, ํ”ผ์ฒ˜ 7๊ฐœ ๋‹ฌ์„ฑ\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 04. ์ž‘์ „ ํ…œํฌ ๋ณ€๋™ ๋ฐ ๊ธฐ์ƒ ์•…ํ™” ์—ฐ๊ณ„ ๋ถ€๋Œ€๋ณ„ ๊ฒฐํ• ์œ„ํ—˜ ์ผ์ž(Stockout Date) ์ •๋ฐ€ ์˜ˆ์ธก ๊ณ ๋„ํ™”\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "์ž‘์ „ ํ…œํฌ ๋ณ€๋™ ์—ฐ๊ณ„ ๋ถ€๋Œ€๋ณ„ ๊ฒฐํ• ์œ„ํ—˜ ์ผ์ž(Stockout Date) ์ •๋ฐ€ ์˜ˆ์ธก์„ ๊ณ ๋„ํ™”ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **์ž‘์ „ ํ…œํฌ ๋ณ€๋™๊ณผ ๊ธฐ์ƒ ์˜ˆ๋ณด๋ฅผ ๋ฐ˜์˜ํ•œ ํŒŒ์ƒ ํ”ผ์ฒ˜๋กœ ๋ถ€๋Œ€๋ณ„ ๊ฒฐํ• ์œ„ํ—˜ ์ผ์ž(Stockout Date)๋ฅผ ์˜ˆ์ธกํ•˜๋Š” ๊ธฐ์ค€ ๋ชจ๋ธ์„ ๋งŒ๋“ ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **์‚ฌ์ „ ๊ณ„ํš ํ…œํฌ, ๋‹ค์Œ 7์ผ ์•…์ฒœํ›„ ์˜ˆ๋ณด, ์ตœ๊ทผ ์†Œ๋ชจ ์ถ”์„ธ, ๋ณด๊ธ‰ ํ›„ ๊ฒฝ๊ณผ์ผ ๊ฐ™์€ ํŒŒ์ƒ ํ”ผ์ฒ˜ 13๊ฐœ๋ฅผ ์„ค๊ณ„ํ•˜๊ณ  ์ด์œ ๋ฅผ ํ•œ ์ค„์”ฉ ์„ค๋ช… ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/features/str01_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": [ "\"\"\"์ž‘์ „ ํ…œํฌ ๊ณ„ํš๊ณผ ๊ธฐ์ƒ ์˜ˆ๋ณด๋ฅผ ํฌํ•จํ•œ ํŒŒ์ƒ ํ”ผ์ฒ˜ 13๊ฐœ๋ฅผ ์„ค๊ณ„ํ•ด ๋ฆฟ์ง€ ํšŒ๊ท€ ๊ธฐ์ค€ ๋ชจ๋ธ๋กœ ๋ถ€๋Œ€๋ณ„ ๊ฒฐํ• ์œ„ํ—˜ ์ผ์ž(Stockout Date)๋ฅผ ์˜ˆ์ธกํ•˜๊ณ , ๋‹จ์ˆœ DOS ๋ฐฉ์‹๊ณผ MAPE๋ฅผ ๋น„๊ตํ•˜๋ฉฐ ํ…œํฌ ์ƒ์Šนยท์•…์ฒœํ›„ ๊ฐ€์ • ์‹œ๋‚˜๋ฆฌ์˜ค ์˜ํ–ฅ์„ ํ™•์ธํ•œ๋‹ค.\"\"\"\n", "import numpy as np\n", "import pandas as pd\n", "from sklearn.linear_model import Ridge\n", "from sklearn.preprocessing import StandardScaler\n", "from sklearn.pipeline import make_pipeline\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ฐ€์ƒ ๋ถ€๋Œ€ 20๊ฐœ x ํ•ต์‹ฌ ๋ณด๊ธ‰ํ’ˆ 5์ข… ์ผ์ผ ์žฌ๊ณ ยท์†Œ๋ชจ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ (์‹ค์ œ ๋ถ€๋Œ€ยท์ˆ˜๋Ÿ‰ยท๊ธฐ์ค€์ผ์ˆ˜ ์•„๋‹˜)\n", "# ํ•ต์‹ฌ์„ฑ๊ณผ์ง€ํ‘œ(KPI) = ๊ธฐ์ค€ ๋ณด๊ธ‰์ผ์ˆ˜(DOS) = ํ˜„์žฌ ์žฌ๊ณ  / ์ตœ๊ทผ ํ‰๊ท  ์ผ์ผ ์†Œ๋ชจ๋Ÿ‰. ๋ชฉํ‘œ DOS๋Š” ๊ต์œก์šฉ ๊ฐ€์ •๊ฐ’์ด๋‹ค.\n", "# ๊ฒฐํ• ์œ„ํ—˜ ์ผ์ˆ˜ H = ๋ณด๊ธ‰์ด ์—†๋‹ค๊ณ  ๊ฐ€์ •ํ•  ๋•Œ ํ˜„์žฌ ์žฌ๊ณ ๊ฐ€ ๋ฐ”๋‹ฅ๋‚˜๊ธฐ๊นŒ์ง€ ๋‚จ์€ ์ผ์ˆ˜(๋ฏธ๋ž˜ ์‹ค์ œ ์†Œ๋ชจ๋Ÿ‰์œผ๋กœ ๊ณ„์‚ฐํ•œ ์ •๋‹ต).\n", "# ---------------------------------------------------------------------------\n", "ITEMS = {\"์‹๋Ÿ‰\": (100, 0.3, 10), \"ํ”ผ๋ณต\": (20, 0.1, 15), \"์œ ๋ฅ˜\": (300, 1.2, 7), \"ํƒ„์•ฝ\": (60, 1.5, 6), \"์ˆ˜๋ฆฌ๋ถ€์†\": (15, 0.5, 12)} # (๊ธฐ๋ณธ ์ผ์†Œ๋ชจ, ํ…œํฌ ๋ฏผ๊ฐ๋„, ๋ชฉํ‘œ DOS)\n", "N_UNIT, DAYS = 20, 160\n", "UNITS = [f\"๊ฐ€์ƒ{u:02d}๋ถ€๋Œ€\" for u in range(1, N_UNIT + 1)]\n", "\n", "\n", "def make_data(seed=1, n_unit=N_UNIT, days=DAYS):\n", " \"\"\"์ผ ๋‹จ์œ„ ์žฌ๊ณ  ์›์žฅ: unit, item, day, tempo(์‹ค์ œ ์ž‘์ „ ํ…œํฌ), plan(์‚ฌ์ „ ๊ณ„ํš ํ…œํฌ), wx(์•…์ฒœํ›„), consume, resupply, stock.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " rows = []\n", " for u in range(1, n_unit + 1):\n", " s, ph = rng.uniform(0.7, 1.5), rng.uniform(0, 6.28)\n", " surge = np.zeros(days)\n", " for st in rng.choice(days - 10, 4, replace=False):\n", " surge[st:st + int(rng.integers(5, 10))] = rng.uniform(0.4, 0.8)\n", " tempo = 1 + 0.25 * np.sin(2 * np.pi * np.arange(days) / 30 + ph) + surge\n", " plan = tempo + rng.normal(0, 0.08, days)\n", " wx = (rng.random(days) < 0.15).astype(int)\n", " for it, (b, sens, tg) in ITEMS.items():\n", " stock, off = tg * b * s * rng.uniform(0.8, 1.2), int(rng.integers(0, 4))\n", " for t in range(days):\n", " c = b * s * (1 + sens * (tempo[t] - 1)) * (1 + 0.12 * wx[t]) * rng.normal(1, 0.08)\n", " r = max(tg * b * s - stock, 0) * rng.uniform(0.9, 1.1) * (0.5 if rng.random() < 0.08 else 1) if (t % 4 == off and t > 0) else 0.0 # 4์ผ ์ฃผ๊ธฐ ์ •๊ธฐ ๋ณด๊ธ‰: ๋ชฉํ‘œ ์žฌ๊ณ ๊นŒ์ง€ ์ฑ„์›€(๊ฐ€๋” ์ˆ˜์†ก ์ฐจ์งˆ๋กœ ์ ˆ๋ฐ˜)\n", " stock = max(stock + r - c, 0.0)\n", " rows.append((f\"๊ฐ€์ƒ{u:02d}๋ถ€๋Œ€\", it, t, tempo[t], plan[t], wx[t], c, r, stock))\n", " return pd.DataFrame(rows, columns=[\"unit\", \"item\", \"day\", \"tempo\", \"plan\", \"wx\", \"consume\", \"resupply\", \"stock\"])\n", "\n", "\n", "def make_samples(d, lo=21, hi=125):\n", " \"\"\"(๋ถ€๋Œ€, ํ’ˆ๋ชฉ, ๊ธฐ์ค€์ผ t0)๋ณ„ ํŠน์„ฑ(ํ”ผ์ฒ˜)๊ณผ ์ •๋‹ต H. ํ”ผ์ฒ˜๋Š” t0 ์‹œ์ ๊นŒ์ง€ ์•Œ ์ˆ˜ ์žˆ๋Š” ๊ฐ’๊ณผ ์‚ฌ์ „ ๊ณ„ํšยท๊ธฐ์ƒ ์˜ˆ๋ณด๋งŒ ์‚ฌ์šฉํ•œ๋‹ค.\"\"\"\n", " out = []\n", " for (u, it), g in d.groupby([\"unit\", \"item\"]):\n", " c, stk, rs = g[\"consume\"].values, g[\"stock\"].values, g[\"resupply\"].values\n", " pl, wx, tp = g[\"plan\"].values, g[\"wx\"].values, g[\"tempo\"].values\n", " cs = np.cumsum(c)\n", " tg = ITEMS[it][2] * ITEMS[it][0]\n", " for t0 in range(lo, hi):\n", " idx = np.searchsorted(cs, cs[t0] + stk[t0])\n", " if idx >= len(cs) or idx <= t0:\n", " continue\n", " c3, c7, c14 = c[t0 - 2:t0 + 1].mean(), c[t0 - 6:t0 + 1].mean(), c[t0 - 13:t0 + 1].mean()\n", " last = [k for k in range(t0, t0 - 15, -1) if rs[k] > 0]\n", " out.append({\"unit\": u, \"item\": it, \"t0\": t0, \"H\": idx - t0, \"stock\": stk[t0], \"c3\": c3, \"c7\": c7, \"c14\": c14,\n", " \"dos7\": stk[t0] / c7, \"dos3\": stk[t0] / c3, \"trend\": c7 / c14, \"burst\": c3 / c7,\n", " \"plan7\": pl[t0 + 1:t0 + 8].mean(), \"plan_ratio\": pl[t0 + 1:t0 + 8].mean() / tp[t0], \"tempo0\": tp[t0],\n", " \"wx_fc\": wx[t0 + 1:t0 + 8].mean(), \"since_rs\": (t0 - last[0]) if last else 15,\n", " \"rs_avg\": rs[t0 - 13:t0 + 1].sum() / 14, \"fill\": stk[t0] / tg, \"item_code\": list(ITEMS).index(it)})\n", " return pd.DataFrame(out)\n", "\n", "\n", "def mape(a, b):\n", " return float(np.mean(np.abs(a - b) / a))\n", "\n", "d = make_data()\n", "s = make_samples(d)\n", "X = [c for c in s.columns if c not in (\"unit\", \"item\", \"t0\", \"H\")]\n", "DESC = {\"stock\": \"ํ˜„์žฌ ์žฌ๊ณ \", \"c3\": \"์ตœ๊ทผ 3์ผ ํ‰๊ท  ์†Œ๋ชจ\", \"c7\": \"์ตœ๊ทผ 7์ผ ํ‰๊ท  ์†Œ๋ชจ\", \"c14\": \"์ตœ๊ทผ 14์ผ ํ‰๊ท  ์†Œ๋ชจ\", \"dos7\": \"์žฌ๊ณ /7์ผ ์†Œ๋ชจ(DOS)\",\n", " \"dos3\": \"์žฌ๊ณ /3์ผ ์†Œ๋ชจ\", \"trend\": \"7์ผ/14์ผ ์†Œ๋ชจ ์ถ”์„ธ\", \"burst\": \"3์ผ/7์ผ ์†Œ๋ชจ ๊ธ‰์ฆ๋„\", \"plan7\": \"๋‹ค์Œ 7์ผ ๊ณ„ํš ํ…œํฌ ํ‰๊ท \", \"plan_ratio\": \"๊ณ„ํš ํ…œํฌ/ํ˜„์žฌ ํ…œํฌ\",\n", " \"tempo0\": \"ํ˜„์žฌ ์ž‘์ „ ํ…œํฌ\", \"wx_fc\": \"๋‹ค์Œ 7์ผ ์•…์ฒœํ›„ ์˜ˆ๋ณด ๋น„์œจ\", \"since_rs\": \"๋งˆ์ง€๋ง‰ ๋ณด๊ธ‰ ํ›„ ๊ฒฝ๊ณผ์ผ\", \"rs_avg\": \"์ตœ๊ทผ 14์ผ ํ‰๊ท  ๋ณด๊ธ‰๋Ÿ‰\", \"fill\": \"๋ชฉํ‘œ ์žฌ๊ณ  ๋Œ€๋น„ ํ˜„์žฌ ์žฌ๊ณ \", \"item_code\": \"ํ’ˆ๋ชฉ ์ฝ”๋“œ\"}\n", "derived = [c for c in X if c not in (\"stock\", \"tempo0\", \"item_code\")]\n", "print(f\"[ํ”ผ์ฒ˜ ์„ค๊ณ„] ์ „์ฒด {len(X)}๊ฐœ ์ค‘ ํŒŒ์ƒ ํ”ผ์ฒ˜ {len(derived)}๊ฐœ [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\")\n", "for c in derived:\n", " print(f\" - {c}: {DESC[c]}\")\n", "\n", "tr, te = s[s[\"t0\"] <= 90], s[s[\"t0\"] > 90]\n", "model = make_pipeline(StandardScaler(), Ridge(alpha=1.0)).fit(tr[X], tr[\"H\"])\n", "p_base = te[\"dos7\"].values # ๋‹จ์ˆœ DOS ๋ฐฉ์‹\n", "p_ridge = np.clip(model.predict(te[X]), 1, None) # ๊ธฐ์ค€ ๋ชจ๋ธ\n", "y = te[\"H\"].values\n", "res = pd.DataFrame({\"๋ชจ๋ธ\": [\"๋‹จ์ˆœ DOS(Baseline)\", \"๋ฆฟ์ง€ ๊ธฐ์ค€ ๋ชจ๋ธ\"], \"MAPE\": [mape(y, p_base), mape(y, p_ridge)],\n", " \"MAE(์ผ)\": [np.abs(y - p_base).mean(), np.abs(y - p_ridge).mean()], \"RMSE(์ผ)\": [np.sqrt(((y - p_base) ** 2).mean()), np.sqrt(((y - p_ridge) ** 2).mean())]})\n", "print(f\"\\n[์˜ˆ์ธก ์„ฑ๋Šฅ] ํ•™์Šต {len(tr)}๊ฑด(~90์ผ), ์‹œํ—˜ {len(te)}๊ฑด(91์ผ~)\")\n", "print(res.round(3).to_string(index=False))\n", "\n", "# ๊ฒฐํ• ์œ„ํ—˜ ์ผ์ž ์˜ˆ์ธก ํ‘œ(100์ผ ๊ธฐ์ค€)\n", "now = s[s[\"t0\"] == 100].copy()\n", "now[\"์˜ˆ์ธกH\"] = np.clip(model.predict(now[X]), 1, None)\n", "now[\"๊ฒฐํ•์˜ˆ์ƒ์ผ\"] = (now[\"t0\"] + now[\"์˜ˆ์ธกH\"]).round().astype(int)\n", "print(\"\\n[๊ฒฐํ• ์œ„ํ—˜ ์ผ์ž ์˜ˆ์ธก - 100์ผ ๊ธฐ์ค€, ๊ฐ€๊นŒ์šด ์ˆœ 6๊ฑด]\")\n", "print(now.sort_values(\"์˜ˆ์ธกH\").head(6)[[\"unit\", \"item\", \"stock\", \"์˜ˆ์ธกH\", \"๊ฒฐํ•์˜ˆ์ƒ์ผ\", \"H\"]].rename(columns={\"H\": \"์‹ค์ œH\"}).round(1).to_string(index=False))\n", "\n", "# ๊ฐ€์ • ์‹œ๋‚˜๋ฆฌ์˜ค: ๊ณ„ํš ํ…œํฌ +30%์™€ ๋‹ค์Œ 7์ผ ์•…์ฒœํ›„ 50% ๊ฐ€์ • ์‹œ ์˜ˆ์ธก ๋ณ€ํ™”\n", "w = now.copy()\n", "w[\"plan7\"] *= 1.3; w[\"plan_ratio\"] *= 1.3; w[\"wx_fc\"] = 0.5\n", "shift = np.clip(model.predict(w[X]), 1, None) - now[\"์˜ˆ์ธกH\"].values\n", "print(f\"\\n[๊ฐ€์ • ์‹œ๋‚˜๋ฆฌ์˜ค] ๊ณ„ํš ํ…œํฌ +30%, ์•…์ฒœํ›„ 50% ๊ฐ€์ • ์‹œ ์˜ˆ์ธก ๊ฒฐํ• ์ผ์ˆ˜ ํ‰๊ท  {shift.mean():+.2f}์ผ ๋ณ€ํ™” (ํƒ„์•ฝ {shift[now['item'].values == 'ํƒ„์•ฝ'].mean():+.2f}์ผ, ์‹๋Ÿ‰ {shift[now['item'].values == '์‹๋Ÿ‰'].mean():+.2f}์ผ)\")\n", "coef = pd.Series(model[-1].coef_, index=X).sort_values(key=abs, ascending=False)\n", "print(\"๊ธฐ์ค€ ๋ชจ๋ธ ์˜ํ–ฅ ํฐ ํ”ผ์ฒ˜ ์ƒ์œ„ 5:\", {k: round(float(v), 2) for k, v in coef.head(5).items()})\n", "m = mape(y, p_ridge)\n", "print(f\"\\n๋ชฉํ‘œ(ํŒŒ์ƒ ํ”ผ์ฒ˜ 10๊ฐœ ์ด์ƒ, ๊ธฐ์ค€ ๋ชจ๋ธ MAPE 25% ์ดํ•˜): ํŒŒ์ƒ ํ”ผ์ฒ˜ {len(derived)}๊ฐœ {'๋‹ฌ์„ฑ' if len(derived) >= 10 else '๋ฏธ๋‹ฌ'}, MAPE {m:.1%} {'๋‹ฌ์„ฑ' if m <= 0.25 else '๋ฏธ๋‹ฌ'}\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 04 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[ํ”ผ์ฒ˜ ์„ค๊ณ„] ์ „์ฒด 16๊ฐœ ์ค‘ ํŒŒ์ƒ ํ”ผ์ฒ˜ 13๊ฐœ [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\n", " - c3: ์ตœ๊ทผ 3์ผ ํ‰๊ท  ์†Œ๋ชจ\n", " - c7: ์ตœ๊ทผ 7์ผ ํ‰๊ท  ์†Œ๋ชจ\n", " - c14: ์ตœ๊ทผ 14์ผ ํ‰๊ท  ์†Œ๋ชจ\n", " - dos7: ์žฌ๊ณ /7์ผ ์†Œ๋ชจ(DOS)\n", " - dos3: ์žฌ๊ณ /3์ผ ์†Œ๋ชจ\n", " - trend: 7์ผ/14์ผ ์†Œ๋ชจ ์ถ”์„ธ\n", " - burst: 3์ผ/7์ผ ์†Œ๋ชจ ๊ธ‰์ฆ๋„\n", " - plan7: ๋‹ค์Œ 7์ผ ๊ณ„ํš ํ…œํฌ ํ‰๊ท \n", " - plan_ratio: ๊ณ„ํš ํ…œํฌ/ํ˜„์žฌ ํ…œํฌ\n", " - wx_fc: ๋‹ค์Œ 7์ผ ์•…์ฒœํ›„ ์˜ˆ๋ณด ๋น„์œจ\n", " - since_rs: ๋งˆ์ง€๋ง‰ ๋ณด๊ธ‰ ํ›„ ๊ฒฝ๊ณผ์ผ\n", " - rs_avg: ์ตœ๊ทผ 14์ผ ํ‰๊ท  ๋ณด๊ธ‰๋Ÿ‰\n", " - fill: ๋ชฉํ‘œ ์žฌ๊ณ  ๋Œ€๋น„ ํ˜„์žฌ ์žฌ๊ณ \n", "\n", "[์˜ˆ์ธก ์„ฑ๋Šฅ] ํ•™์Šต 6836๊ฑด(~90์ผ), ์‹œํ—˜ 3328๊ฑด(91์ผ~)\n", " ๋ชจ๋ธ MAPE MAE(์ผ) RMSE(์ผ)\n", "๋‹จ์ˆœ DOS(Baseline) 0.162 0.938 1.207\n", " ๋ฆฟ์ง€ ๊ธฐ์ค€ ๋ชจ๋ธ 0.106 0.566 0.721\n", "\n", "[๊ฒฐํ• ์œ„ํ—˜ ์ผ์ž ์˜ˆ์ธก - 100์ผ ๊ธฐ์ค€, ๊ฐ€๊นŒ์šด ์ˆœ 6๊ฑด]\n", " unit item stock ์˜ˆ์ธกH ๊ฒฐํ•์˜ˆ์ƒ์ผ ์‹ค์ œH\n", "๊ฐ€์ƒ01๋ถ€๋Œ€ ํƒ„์•ฝ 31.2 1.2 101 1\n", "๊ฐ€์ƒ19๋ถ€๋Œ€ ํƒ„์•ฝ 50.9 1.7 102 1\n", "๊ฐ€์ƒ03๋ถ€๋Œ€ ํƒ„์•ฝ 81.4 1.9 102 1\n", "๊ฐ€์ƒ15๋ถ€๋Œ€ ํƒ„์•ฝ 50.2 2.0 102 1\n", "๊ฐ€์ƒ04๋ถ€๋Œ€ ํƒ„์•ฝ 93.6 2.3 102 1\n", "๊ฐ€์ƒ17๋ถ€๋Œ€ ์œ ๋ฅ˜ 847.5 2.6 103 3\n", "\n", "[๊ฐ€์ • ์‹œ๋‚˜๋ฆฌ์˜ค] ๊ณ„ํš ํ…œํฌ +30%, ์•…์ฒœํ›„ 50% ๊ฐ€์ • ์‹œ ์˜ˆ์ธก ๊ฒฐํ• ์ผ์ˆ˜ ํ‰๊ท  -1.11์ผ ๋ณ€ํ™” (ํƒ„์•ฝ -1.05์ผ, ์‹๋Ÿ‰ -1.12์ผ)\n", "๊ธฐ์ค€ ๋ชจ๋ธ ์˜ํ–ฅ ํฐ ํ”ผ์ฒ˜ ์ƒ์œ„ 5: {'dos7': 2.15, 'dos3': 1.26, 'c7': -0.75, 'plan_ratio': -0.71, 'c14': 0.43}\n", "\n", "๋ชฉํ‘œ(ํŒŒ์ƒ ํ”ผ์ฒ˜ 10๊ฐœ ์ด์ƒ, ๊ธฐ์ค€ ๋ชจ๋ธ MAPE 25% ์ดํ•˜): ํŒŒ์ƒ ํ”ผ์ฒ˜ 13๊ฐœ ๋‹ฌ์„ฑ, MAPE 10.6% ๋‹ฌ์„ฑ\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 05. ๊ฒฐํ• ์‹ฌ๊ฐ๋„ ์ง€์ˆ˜ ์‚ฐ์ถœ ๋ฐ ๋‹ค๋‹จ๊ณ„ ๊ฒฐํ• ์กฐ๊ธฐ๊ฒฝ๋ณด(์ฃผ์˜/๊ฒฝ๊ณ„/์‹ฌ๊ฐ) ์—”์ง„ ๊ฐœ๋ฐœ\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.5์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "๊ฒฐํ• ์‹ฌ๊ฐ๋„ ์ง€์ˆ˜ ์‚ฐ์ถœ ๋ฐ ๋‹ค๋‹จ๊ณ„ ๊ฒฐํ• ์กฐ๊ธฐ๊ฒฝ๋ณด ์—”์ง„์„ ๊ฐœ๋ฐœํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **๊ฒฐํ• ์‹ฌ๊ฐ๋„ ์ง€์ˆ˜๋ฅผ ์‚ฐ์ถœํ•˜๊ณ  ์ฃผ์˜ยท๊ฒฝ๊ณ„ยท์‹ฌ๊ฐ 3๋‹จ๊ณ„ ๊ฒฐํ• ์กฐ๊ธฐ๊ฒฝ๋ณด ์—”์ง„์„ ๊ฐœ๋ฐœํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **๊ทธ๋ž˜๋””์–ธํŠธ ๋ถ€์ŠคํŒ… ๋ชจ๋ธ๋กœ ๊ฒฐํ• ์œ„ํ—˜ ์ผ์ˆ˜๋ฅผ ์˜ˆ์ธกํ•ด ๊ธฐ์ค€ ๋ชจ๋ธ๊ณผ MAE, RMSE, R2๋ฅผ ๋น„๊ต ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/str01_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": [ "\"\"\"๊ธฐ์ค€ ๋ชจ๋ธ(๋ฆฟ์ง€)๋ณด๋‹ค ์ •ํ™•ํ•œ ๊ทธ๋ž˜๋””์–ธํŠธ ๋ถ€์ŠคํŒ… ๋ชจ๋ธ๋กœ ๊ฒฐํ• ์œ„ํ—˜ ์ผ์ˆ˜๋ฅผ ์˜ˆ์ธกํ•˜๊ณ , ์ด๋ฅผ 0~100 ๊ฒฐํ• ์‹ฌ๊ฐ๋„ ์ง€์ˆ˜์™€ ์ฃผ์˜ยท๊ฒฝ๊ณ„ยท์‹ฌ๊ฐ 3๋‹จ๊ณ„ ์กฐ๊ธฐ๊ฒฝ๋ณด๋กœ ๋ณ€ํ™˜ํ•ด ์˜ค์ฐจ ๊ฐœ์„ ์œจ, R2, ๊ฒฝ๋ณด ๋“ฑ๊ธ‰ ์ •ํ™•๋„์™€ ๋ฏธํƒยท์˜คํƒ์„ ํ‰๊ฐ€ํ•œ๋‹ค.\"\"\"\n", "import numpy as np\n", "import pandas as pd\n", "from sklearn.ensemble import GradientBoostingRegressor\n", "from sklearn.linear_model import Ridge\n", "from sklearn.metrics import r2_score\n", "from sklearn.pipeline import make_pipeline\n", "from sklearn.preprocessing import StandardScaler\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ฐ€์ƒ ๋ถ€๋Œ€ 20๊ฐœ x ํ•ต์‹ฌ ๋ณด๊ธ‰ํ’ˆ 5์ข… ์ผ์ผ ์žฌ๊ณ ยท์†Œ๋ชจ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ (์‹ค์ œ ๋ถ€๋Œ€ยท์ˆ˜๋Ÿ‰ยท๊ธฐ์ค€์ผ์ˆ˜ ์•„๋‹˜)\n", "# ํ•ต์‹ฌ์„ฑ๊ณผ์ง€ํ‘œ(KPI) = ๊ธฐ์ค€ ๋ณด๊ธ‰์ผ์ˆ˜(DOS) = ํ˜„์žฌ ์žฌ๊ณ  / ์ตœ๊ทผ ํ‰๊ท  ์ผ์ผ ์†Œ๋ชจ๋Ÿ‰. ๋ชฉํ‘œ DOS๋Š” ๊ต์œก์šฉ ๊ฐ€์ •๊ฐ’์ด๋‹ค.\n", "# ๊ฒฐํ• ์œ„ํ—˜ ์ผ์ˆ˜ H = ๋ณด๊ธ‰์ด ์—†๋‹ค๊ณ  ๊ฐ€์ •ํ•  ๋•Œ ํ˜„์žฌ ์žฌ๊ณ ๊ฐ€ ๋ฐ”๋‹ฅ๋‚˜๊ธฐ๊นŒ์ง€ ๋‚จ์€ ์ผ์ˆ˜(๋ฏธ๋ž˜ ์‹ค์ œ ์†Œ๋ชจ๋Ÿ‰์œผ๋กœ ๊ณ„์‚ฐํ•œ ์ •๋‹ต).\n", "# ---------------------------------------------------------------------------\n", "ITEMS = {\"์‹๋Ÿ‰\": (100, 0.3, 10), \"ํ”ผ๋ณต\": (20, 0.1, 15), \"์œ ๋ฅ˜\": (300, 1.2, 7), \"ํƒ„์•ฝ\": (60, 1.5, 6), \"์ˆ˜๋ฆฌ๋ถ€์†\": (15, 0.5, 12)} # (๊ธฐ๋ณธ ์ผ์†Œ๋ชจ, ํ…œํฌ ๋ฏผ๊ฐ๋„, ๋ชฉํ‘œ DOS)\n", "N_UNIT, DAYS = 20, 160\n", "UNITS = [f\"๊ฐ€์ƒ{u:02d}๋ถ€๋Œ€\" for u in range(1, N_UNIT + 1)]\n", "\n", "\n", "def make_data(seed=1, n_unit=N_UNIT, days=DAYS):\n", " \"\"\"์ผ ๋‹จ์œ„ ์žฌ๊ณ  ์›์žฅ: unit, item, day, tempo(์‹ค์ œ ์ž‘์ „ ํ…œํฌ), plan(์‚ฌ์ „ ๊ณ„ํš ํ…œํฌ), wx(์•…์ฒœํ›„), consume, resupply, stock.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " rows = []\n", " for u in range(1, n_unit + 1):\n", " s, ph = rng.uniform(0.7, 1.5), rng.uniform(0, 6.28)\n", " surge = np.zeros(days)\n", " for st in rng.choice(days - 10, 4, replace=False):\n", " surge[st:st + int(rng.integers(5, 10))] = rng.uniform(0.4, 0.8)\n", " tempo = 1 + 0.25 * np.sin(2 * np.pi * np.arange(days) / 30 + ph) + surge\n", " plan = tempo + rng.normal(0, 0.08, days)\n", " wx = (rng.random(days) < 0.15).astype(int)\n", " for it, (b, sens, tg) in ITEMS.items():\n", " stock, off = tg * b * s * rng.uniform(0.8, 1.2), int(rng.integers(0, 4))\n", " for t in range(days):\n", " c = b * s * (1 + sens * (tempo[t] - 1)) * (1 + 0.12 * wx[t]) * rng.normal(1, 0.08)\n", " r = max(tg * b * s - stock, 0) * rng.uniform(0.9, 1.1) * (0.5 if rng.random() < 0.08 else 1) if (t % 4 == off and t > 0) else 0.0 # 4์ผ ์ฃผ๊ธฐ ์ •๊ธฐ ๋ณด๊ธ‰: ๋ชฉํ‘œ ์žฌ๊ณ ๊นŒ์ง€ ์ฑ„์›€(๊ฐ€๋” ์ˆ˜์†ก ์ฐจ์งˆ๋กœ ์ ˆ๋ฐ˜)\n", " stock = max(stock + r - c, 0.0)\n", " rows.append((f\"๊ฐ€์ƒ{u:02d}๋ถ€๋Œ€\", it, t, tempo[t], plan[t], wx[t], c, r, stock))\n", " return pd.DataFrame(rows, columns=[\"unit\", \"item\", \"day\", \"tempo\", \"plan\", \"wx\", \"consume\", \"resupply\", \"stock\"])\n", "\n", "\n", "def make_samples(d, lo=21, hi=125):\n", " \"\"\"(๋ถ€๋Œ€, ํ’ˆ๋ชฉ, ๊ธฐ์ค€์ผ t0)๋ณ„ ํŠน์„ฑ(ํ”ผ์ฒ˜)๊ณผ ์ •๋‹ต H. ํ”ผ์ฒ˜๋Š” t0 ์‹œ์ ๊นŒ์ง€ ์•Œ ์ˆ˜ ์žˆ๋Š” ๊ฐ’๊ณผ ์‚ฌ์ „ ๊ณ„ํšยท๊ธฐ์ƒ ์˜ˆ๋ณด๋งŒ ์‚ฌ์šฉํ•œ๋‹ค.\"\"\"\n", " out = []\n", " for (u, it), g in d.groupby([\"unit\", \"item\"]):\n", " c, stk, rs = g[\"consume\"].values, g[\"stock\"].values, g[\"resupply\"].values\n", " pl, wx, tp = g[\"plan\"].values, g[\"wx\"].values, g[\"tempo\"].values\n", " cs = np.cumsum(c)\n", " tg = ITEMS[it][2] * ITEMS[it][0]\n", " for t0 in range(lo, hi):\n", " idx = np.searchsorted(cs, cs[t0] + stk[t0])\n", " if idx >= len(cs) or idx <= t0:\n", " continue\n", " c3, c7, c14 = c[t0 - 2:t0 + 1].mean(), c[t0 - 6:t0 + 1].mean(), c[t0 - 13:t0 + 1].mean()\n", " last = [k for k in range(t0, t0 - 15, -1) if rs[k] > 0]\n", " out.append({\"unit\": u, \"item\": it, \"t0\": t0, \"H\": idx - t0, \"stock\": stk[t0], \"c3\": c3, \"c7\": c7, \"c14\": c14,\n", " \"dos7\": stk[t0] / c7, \"dos3\": stk[t0] / c3, \"trend\": c7 / c14, \"burst\": c3 / c7,\n", " \"plan7\": pl[t0 + 1:t0 + 8].mean(), \"plan_ratio\": pl[t0 + 1:t0 + 8].mean() / tp[t0], \"tempo0\": tp[t0],\n", " \"wx_fc\": wx[t0 + 1:t0 + 8].mean(), \"since_rs\": (t0 - last[0]) if last else 15,\n", " \"rs_avg\": rs[t0 - 13:t0 + 1].sum() / 14, \"fill\": stk[t0] / tg, \"item_code\": list(ITEMS).index(it)})\n", " return pd.DataFrame(out)\n", "\n", "\n", "def mape(a, b):\n", " return float(np.mean(np.abs(a - b) / a))\n", "\n", "d = make_data()\n", "s = make_samples(d)\n", "X = [c for c in s.columns if c not in (\"unit\", \"item\", \"t0\", \"H\")]\n", "tr, te = s[s[\"t0\"] <= 90], s[s[\"t0\"] > 90]\n", "y = te[\"H\"].values\n", "ridge = make_pipeline(StandardScaler(), Ridge(alpha=1.0)).fit(tr[X], tr[\"H\"])\n", "gbr = GradientBoostingRegressor(n_estimators=200, max_depth=3, learning_rate=0.08, random_state=0).fit(tr[X], tr[\"H\"])\n", "p0 = np.clip(ridge.predict(te[X]), 1, None)\n", "p1 = np.clip(gbr.predict(te[X]), 1, None)\n", "\n", "def score(p):\n", " return {\"MAE\": float(np.abs(y - p).mean()), \"RMSE\": float(np.sqrt(((y - p) ** 2).mean())), \"R2\": float(r2_score(y, p)), \"MAPE\": mape(y, p)}\n", "a, b = score(p0), score(p1)\n", "print(\"[์˜ˆ์ธก ์„ฑ๋Šฅ ๋น„๊ต] ์‹œํ—˜ ํ‘œ๋ณธ\", len(te), \"๊ฑด (91์ผ ์ดํ›„) [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\")\n", "print(pd.DataFrame({\"๊ธฐ์ค€ ๋ชจ๋ธ(๋ฆฟ์ง€)\": a, \"๊ณ ๋„ํ™” ๋ชจ๋ธ(GBR)\": b}).round(3).to_string())\n", "imp_mae, imp_rmse = 1 - b[\"MAE\"] / a[\"MAE\"], 1 - b[\"RMSE\"] / a[\"RMSE\"]\n", "print(f\"๊ธฐ์ค€ ๋ชจ๋ธ ๋Œ€๋น„ ๊ฐœ์„ : MAE {imp_mae:.1%}, RMSE {imp_rmse:.1%}, R2 {b['R2']:.3f}\")\n", "\n", "# ๊ฒฐํ• ์‹ฌ๊ฐ๋„ ์ง€์ˆ˜(0~100): ์˜ˆ์ƒ ๊ฒฐํ• ์ผ์ˆ˜๊ฐ€ ์งง์„์ˆ˜๋ก ๋†’๊ณ , ๋ชฉํ‘œ ์žฌ๊ณ  ๋Œ€๋น„ ํ˜„์žฌ ์žฌ๊ณ ๊ฐ€ ์ ์„์ˆ˜๋ก ๊ฐ€์ค‘\n", "def severity(h, fill):\n", " base = np.clip(1 - (h - 1) / 8, 0, 1) # H=1์ผ์ด๋ฉด 1.0, H>=9์ผ์ด๋ฉด 0\n", " return 100 * np.clip(base * (1.1 - 0.2 * np.clip(fill, 0, 1)), 0, 1)\n", "LEVELS = [(2.5, \"์‹ฌ๊ฐ\"), (4.5, \"๊ฒฝ๊ณ„\"), (7.5, \"์ฃผ์˜\")]\n", "def level(h):\n", " for th, name in LEVELS:\n", " if h <= th:\n", " return name\n", " return \"์ •์ƒ\"\n", "te = te.assign(pred=p1, pred0=p0)\n", "te[\"sev\"] = severity(te[\"pred\"], te[\"fill\"])\n", "te[\"lv_pred\"] = te[\"pred\"].map(level); te[\"lv_true\"] = te[\"H\"].map(level); te[\"lv_ridge\"] = te[\"pred0\"].map(level)\n", "acc, acc0 = float((te[\"lv_pred\"] == te[\"lv_true\"]).mean()), float((te[\"lv_ridge\"] == te[\"lv_true\"]).mean())\n", "order = {\"์ •์ƒ\": 0, \"์ฃผ์˜\": 1, \"๊ฒฝ๊ณ„\": 2, \"์‹ฌ๊ฐ\": 3}\n", "miss = float(((te[\"lv_true\"].map(order) >= 2) & (te[\"lv_pred\"].map(order) < 2)).sum() / max((te[\"lv_true\"].map(order) >= 2).sum(), 1))\n", "false = float(((te[\"lv_true\"].map(order) < 1) & (te[\"lv_pred\"].map(order) >= 2)).sum() / max((te[\"lv_true\"].map(order) < 1).sum(), 1))\n", "print(f\"\\n[3๋‹จ๊ณ„ ์กฐ๊ธฐ๊ฒฝ๋ณด] ๊ธฐ์ค€: ์˜ˆ์ƒ ๊ฒฐํ• {LEVELS[0][0]}์ผ ์ดํ•˜ ์‹ฌ๊ฐ, {LEVELS[1][0]}์ผ ์ดํ•˜ ๊ฒฝ๊ณ„, {LEVELS[2][0]}์ผ ์ดํ•˜ ์ฃผ์˜\")\n", "print(pd.crosstab(te[\"lv_true\"], te[\"lv_pred\"]).reindex(index=[\"์ •์ƒ\", \"์ฃผ์˜\", \"๊ฒฝ๊ณ„\", \"์‹ฌ๊ฐ\"], columns=[\"์ •์ƒ\", \"์ฃผ์˜\", \"๊ฒฝ๊ณ„\", \"์‹ฌ๊ฐ\"], fill_value=0).to_string())\n", "print(f\"๋“ฑ๊ธ‰ ์ •ํ™•๋„ {acc:.1%}(๊ธฐ์ค€ ๋ชจ๋ธ {acc0:.1%}), ๊ฒฝ๊ณ„ ์ด์ƒ ๋ฏธํƒ๋ฅ  {miss:.1%}, ์ •์ƒ ์ƒํ™ฉ ๊ฒฝ๊ณ„ ์ด์ƒ ์˜คํƒ๋ฅ  {false:.1%}\")\n", "print(\"\\n[๊ฒฝ๋ณด ์˜ˆ์‹œ - 100์ผ ๊ธฐ์ค€ ์‹ฌ๊ฐ๋„ ์ƒ์œ„ 5๊ฑด]\")\n", "nw = te[te[\"t0\"] == 100].sort_values(\"sev\", ascending=False).head(5)\n", "print(nw[[\"unit\", \"item\", \"pred\", \"H\", \"sev\", \"lv_pred\"]].rename(columns={\"pred\": \"์˜ˆ์ธกH\", \"H\": \"์‹ค์ œH\", \"sev\": \"์‹ฌ๊ฐ๋„์ง€์ˆ˜\", \"lv_pred\": \"๊ฒฝ๋ณด\"}).round(1).to_string(index=False))\n", "\n", "# ๋ถˆ๊ทœ์น™ ์ˆ˜์š” ์ ์‘๋ ฅ: ํ…œํฌ ๊ธ‰๋ณ€(๊ณ„ํš ๋Œ€๋น„ ํ˜„์žฌ ํ…œํฌ 20% ์ด์ƒ ์ฐจ์ด) ๊ตฌ๊ฐ„์˜ ์•ˆ์ •์„ฑ\n", "irr = te[(te[\"plan_ratio\"] - 1).abs() > 0.2]\n", "if len(irr):\n", " print(f\"\\nํ…œํฌ ๊ธ‰๋ณ€ ๊ตฌ๊ฐ„ {len(irr)}๊ฑด MAE: ๊ธฐ์ค€ {np.abs(irr['H'] - irr['pred0']).mean():.2f}์ผ -> ๊ณ ๋„ํ™” {np.abs(irr['H'] - irr['pred']).mean():.2f}์ผ\")\n", "ok = imp_mae >= 0.2 and imp_rmse >= 0.2 and b[\"R2\"] >= 0.85\n", "print(f\"\\n๋ชฉํ‘œ(๊ธฐ์ค€ ๋ชจ๋ธ ๋Œ€๋น„ MAE/RMSE 20% ์ด์ƒ ๊ฐœ์„ , R2 0.85 ์ด์ƒ): {'๋‹ฌ์„ฑ' if ok else '๋ฏธ๋‹ฌ'}\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 05 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[์˜ˆ์ธก ์„ฑ๋Šฅ ๋น„๊ต] ์‹œํ—˜ ํ‘œ๋ณธ 3328 ๊ฑด (91์ผ ์ดํ›„) [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\n", " ๊ธฐ์ค€ ๋ชจ๋ธ(๋ฆฟ์ง€) ๊ณ ๋„ํ™” ๋ชจ๋ธ(GBR)\n", "MAE 0.566 0.420\n", "RMSE 0.721 0.534\n", "R2 0.959 0.978\n", "MAPE 0.106 0.072\n", "๊ธฐ์ค€ ๋ชจ๋ธ ๋Œ€๋น„ ๊ฐœ์„ : MAE 25.8%, RMSE 25.9%, R2 0.978\n", "\n", "[3๋‹จ๊ณ„ ์กฐ๊ธฐ๊ฒฝ๋ณด] ๊ธฐ์ค€: ์˜ˆ์ƒ ๊ฒฐํ• 2.5์ผ ์ดํ•˜ ์‹ฌ๊ฐ, 4.5์ผ ์ดํ•˜ ๊ฒฝ๊ณ„, 7.5์ผ ์ดํ•˜ ์ฃผ์˜\n", "lv_pred ์ •์ƒ ์ฃผ์˜ ๊ฒฝ๊ณ„ ์‹ฌ๊ฐ\n", "lv_true \n", "์ •์ƒ 1567 52 0 0\n", "์ฃผ์˜ 81 806 47 0\n", "๊ฒฝ๊ณ„ 0 48 411 16\n", "์‹ฌ๊ฐ 0 0 22 278\n", "๋“ฑ๊ธ‰ ์ •ํ™•๋„ 92.0%(๊ธฐ์ค€ ๋ชจ๋ธ 88.2%), ๊ฒฝ๊ณ„ ์ด์ƒ ๋ฏธํƒ๋ฅ  6.2%, ์ •์ƒ ์ƒํ™ฉ ๊ฒฝ๊ณ„ ์ด์ƒ ์˜คํƒ๋ฅ  0.0%\n", "\n", "[๊ฒฝ๋ณด ์˜ˆ์‹œ - 100์ผ ๊ธฐ์ค€ ์‹ฌ๊ฐ๋„ ์ƒ์œ„ 5๊ฑด]\n", " unit item ์˜ˆ์ธกH ์‹ค์ œH ์‹ฌ๊ฐ๋„์ง€์ˆ˜ ๊ฒฝ๋ณด\n", "๊ฐ€์ƒ01๋ถ€๋Œ€ ํƒ„์•ฝ 1.0 1 100.0 ์‹ฌ๊ฐ\n", "๊ฐ€์ƒ03๋ถ€๋Œ€ ํƒ„์•ฝ 1.2 1 100.0 ์‹ฌ๊ฐ\n", "๊ฐ€์ƒ16๋ถ€๋Œ€ ์œ ๋ฅ˜ 1.4 1 100.0 ์‹ฌ๊ฐ\n", "๊ฐ€์ƒ19๋ถ€๋Œ€ ํƒ„์•ฝ 1.0 1 100.0 ์‹ฌ๊ฐ\n", "๊ฐ€์ƒ15๋ถ€๋Œ€ ํƒ„์•ฝ 1.0 1 100.0 ์‹ฌ๊ฐ\n", "\n", "ํ…œํฌ ๊ธ‰๋ณ€ ๊ตฌ๊ฐ„ 827๊ฑด MAE: ๊ธฐ์ค€ 0.66์ผ -> ๊ณ ๋„ํ™” 0.45์ผ\n", "\n", "๋ชฉํ‘œ(๊ธฐ์ค€ ๋ชจ๋ธ ๋Œ€๋น„ MAE/RMSE 20% ์ด์ƒ ๊ฐœ์„ , R2 0.85 ์ด์ƒ): ๋‹ฌ์„ฑ\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 06. ๊ตฐ๋‹จ/์‚ฌ๋‹จ ๊ด€ํ•  ๋ถ€๋Œ€ ๋ณด๊ธ‰ ์ˆ˜์ค€(DOS) ์‹ค์‹œ๊ฐ„ ํ˜„ํ™ฉ ์ง€๋„ ๋ฐ ๊ฒฝ๋ณด ๋Œ€์‹œ๋ณด๋“œ ๊ตฌํ˜„\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "๊ด€ํ•  ๋ถ€๋Œ€ ๋ณด๊ธ‰ ์ˆ˜์ค€(DOS) ์‹ค์‹œ๊ฐ„ ํ˜„ํ™ฉ ์ง€๋„ ๋ฐ ๊ฒฝ๋ณด ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **๊ด€ํ•  ๋ถ€๋Œ€์˜ DOS ํ˜„ํ™ฉ์„ ๊ฐ€์ƒ ์ง€๋„์™€ ๊ฒฝ๋ณด ์ƒ‰์ƒ์œผ๋กœ ๋ณด์—ฌ ์ฃผ๋Š” ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ , ๊ฒฝ๋ณด๋งˆ๋‹ค ์˜ˆ์ธก์— ๊ธฐ์—ฌํ•œ ์ธ์ž๋ฅผ ์„ค๋ช…ํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **20๊ฐœ ๋ถ€๋Œ€๋ฅผ ๊ฒฉ์žํ˜• ๊ฐ€์ƒ ์ง€๋„์— ๋ฐฐ์น˜ํ•ด ๋ถ€๋Œ€๋ณ„ ์ตœ์•… ๊ฒฝ๋ณด ๋“ฑ๊ธ‰์„ ์ƒ‰์œผ๋กœ ํ‘œ์‹œํ•˜๊ณ , 3๊ฐœ ์‹œ์ ์˜ ๊ฐฑ์‹  ๊ฒฐ๊ณผ๋ฅผ ์ถœ๋ ฅ ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/explainability/str01_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": [ "\"\"\"20๊ฐœ ๊ฐ€์ƒ ๋ถ€๋Œ€๋ฅผ ๊ฒฉ์žํ˜• ๊ฐ€์ƒ ์ง€๋„์— ๋ฐฐ์น˜ํ•ด ๋ณด๊ธ‰ ์ˆ˜์ค€(DOS) ํ˜„ํ™ฉ๊ณผ 3๋‹จ๊ณ„ ๊ฒฝ๋ณด๋ฅผ ์ƒ‰์œผ๋กœ ํ‘œ์‹œํ•˜๋Š” ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๋งŒ๋“ค๊ณ , ๊ฒฝ๋ณด ๊ฑด๋งˆ๋‹ค ์˜ˆ์ธก์— ๊ธฐ์—ฌํ•œ ์ƒ์œ„ 3๊ฐœ ์ธ์ž๋ฅผ ์ž๋™์œผ๋กœ ์„ค๋ช…(XAI)ํ•˜๋ฉฐ ๊ทธ๋ž˜ํ”„์™€ ํ…์ŠคํŠธ ๋ณด๊ณ ๋ฅผ ์ถœ๋ ฅํ•œ๋‹ค.\"\"\"\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "from sklearn.ensemble import GradientBoostingRegressor\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ฐ€์ƒ ๋ถ€๋Œ€ 20๊ฐœ x ํ•ต์‹ฌ ๋ณด๊ธ‰ํ’ˆ 5์ข… ์ผ์ผ ์žฌ๊ณ ยท์†Œ๋ชจ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ (์‹ค์ œ ๋ถ€๋Œ€ยท์ˆ˜๋Ÿ‰ยท๊ธฐ์ค€์ผ์ˆ˜ ์•„๋‹˜)\n", "# ํ•ต์‹ฌ์„ฑ๊ณผ์ง€ํ‘œ(KPI) = ๊ธฐ์ค€ ๋ณด๊ธ‰์ผ์ˆ˜(DOS) = ํ˜„์žฌ ์žฌ๊ณ  / ์ตœ๊ทผ ํ‰๊ท  ์ผ์ผ ์†Œ๋ชจ๋Ÿ‰. ๋ชฉํ‘œ DOS๋Š” ๊ต์œก์šฉ ๊ฐ€์ •๊ฐ’์ด๋‹ค.\n", "# ๊ฒฐํ• ์œ„ํ—˜ ์ผ์ˆ˜ H = ๋ณด๊ธ‰์ด ์—†๋‹ค๊ณ  ๊ฐ€์ •ํ•  ๋•Œ ํ˜„์žฌ ์žฌ๊ณ ๊ฐ€ ๋ฐ”๋‹ฅ๋‚˜๊ธฐ๊นŒ์ง€ ๋‚จ์€ ์ผ์ˆ˜(๋ฏธ๋ž˜ ์‹ค์ œ ์†Œ๋ชจ๋Ÿ‰์œผ๋กœ ๊ณ„์‚ฐํ•œ ์ •๋‹ต).\n", "# ---------------------------------------------------------------------------\n", "ITEMS = {\"์‹๋Ÿ‰\": (100, 0.3, 10), \"ํ”ผ๋ณต\": (20, 0.1, 15), \"์œ ๋ฅ˜\": (300, 1.2, 7), \"ํƒ„์•ฝ\": (60, 1.5, 6), \"์ˆ˜๋ฆฌ๋ถ€์†\": (15, 0.5, 12)} # (๊ธฐ๋ณธ ์ผ์†Œ๋ชจ, ํ…œํฌ ๋ฏผ๊ฐ๋„, ๋ชฉํ‘œ DOS)\n", "N_UNIT, DAYS = 20, 160\n", "UNITS = [f\"๊ฐ€์ƒ{u:02d}๋ถ€๋Œ€\" for u in range(1, N_UNIT + 1)]\n", "\n", "\n", "def make_data(seed=1, n_unit=N_UNIT, days=DAYS):\n", " \"\"\"์ผ ๋‹จ์œ„ ์žฌ๊ณ  ์›์žฅ: unit, item, day, tempo(์‹ค์ œ ์ž‘์ „ ํ…œํฌ), plan(์‚ฌ์ „ ๊ณ„ํš ํ…œํฌ), wx(์•…์ฒœํ›„), consume, resupply, stock.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " rows = []\n", " for u in range(1, n_unit + 1):\n", " s, ph = rng.uniform(0.7, 1.5), rng.uniform(0, 6.28)\n", " surge = np.zeros(days)\n", " for st in rng.choice(days - 10, 4, replace=False):\n", " surge[st:st + int(rng.integers(5, 10))] = rng.uniform(0.4, 0.8)\n", " tempo = 1 + 0.25 * np.sin(2 * np.pi * np.arange(days) / 30 + ph) + surge\n", " plan = tempo + rng.normal(0, 0.08, days)\n", " wx = (rng.random(days) < 0.15).astype(int)\n", " for it, (b, sens, tg) in ITEMS.items():\n", " stock, off = tg * b * s * rng.uniform(0.8, 1.2), int(rng.integers(0, 4))\n", " for t in range(days):\n", " c = b * s * (1 + sens * (tempo[t] - 1)) * (1 + 0.12 * wx[t]) * rng.normal(1, 0.08)\n", " r = max(tg * b * s - stock, 0) * rng.uniform(0.9, 1.1) * (0.5 if rng.random() < 0.08 else 1) if (t % 4 == off and t > 0) else 0.0 # 4์ผ ์ฃผ๊ธฐ ์ •๊ธฐ ๋ณด๊ธ‰: ๋ชฉํ‘œ ์žฌ๊ณ ๊นŒ์ง€ ์ฑ„์›€(๊ฐ€๋” ์ˆ˜์†ก ์ฐจ์งˆ๋กœ ์ ˆ๋ฐ˜)\n", " stock = max(stock + r - c, 0.0)\n", " rows.append((f\"๊ฐ€์ƒ{u:02d}๋ถ€๋Œ€\", it, t, tempo[t], plan[t], wx[t], c, r, stock))\n", " return pd.DataFrame(rows, columns=[\"unit\", \"item\", \"day\", \"tempo\", \"plan\", \"wx\", \"consume\", \"resupply\", \"stock\"])\n", "\n", "\n", "def make_samples(d, lo=21, hi=125):\n", " \"\"\"(๋ถ€๋Œ€, ํ’ˆ๋ชฉ, ๊ธฐ์ค€์ผ t0)๋ณ„ ํŠน์„ฑ(ํ”ผ์ฒ˜)๊ณผ ์ •๋‹ต H. ํ”ผ์ฒ˜๋Š” t0 ์‹œ์ ๊นŒ์ง€ ์•Œ ์ˆ˜ ์žˆ๋Š” ๊ฐ’๊ณผ ์‚ฌ์ „ ๊ณ„ํšยท๊ธฐ์ƒ ์˜ˆ๋ณด๋งŒ ์‚ฌ์šฉํ•œ๋‹ค.\"\"\"\n", " out = []\n", " for (u, it), g in d.groupby([\"unit\", \"item\"]):\n", " c, stk, rs = g[\"consume\"].values, g[\"stock\"].values, g[\"resupply\"].values\n", " pl, wx, tp = g[\"plan\"].values, g[\"wx\"].values, g[\"tempo\"].values\n", " cs = np.cumsum(c)\n", " tg = ITEMS[it][2] * ITEMS[it][0]\n", " for t0 in range(lo, hi):\n", " idx = np.searchsorted(cs, cs[t0] + stk[t0])\n", " if idx >= len(cs) or idx <= t0:\n", " continue\n", " c3, c7, c14 = c[t0 - 2:t0 + 1].mean(), c[t0 - 6:t0 + 1].mean(), c[t0 - 13:t0 + 1].mean()\n", " last = [k for k in range(t0, t0 - 15, -1) if rs[k] > 0]\n", " out.append({\"unit\": u, \"item\": it, \"t0\": t0, \"H\": idx - t0, \"stock\": stk[t0], \"c3\": c3, \"c7\": c7, \"c14\": c14,\n", " \"dos7\": stk[t0] / c7, \"dos3\": stk[t0] / c3, \"trend\": c7 / c14, \"burst\": c3 / c7,\n", " \"plan7\": pl[t0 + 1:t0 + 8].mean(), \"plan_ratio\": pl[t0 + 1:t0 + 8].mean() / tp[t0], \"tempo0\": tp[t0],\n", " \"wx_fc\": wx[t0 + 1:t0 + 8].mean(), \"since_rs\": (t0 - last[0]) if last else 15,\n", " \"rs_avg\": rs[t0 - 13:t0 + 1].sum() / 14, \"fill\": stk[t0] / tg, \"item_code\": list(ITEMS).index(it)})\n", " return pd.DataFrame(out)\n", "\n", "\n", "def mape(a, b):\n", " return float(np.mean(np.abs(a - b) / a))\n", "\n", "d = make_data()\n", "s = make_samples(d)\n", "X = [c for c in s.columns if c not in (\"unit\", \"item\", \"t0\", \"H\")]\n", "tr = s[s[\"t0\"] <= 90]\n", "gbr = GradientBoostingRegressor(n_estimators=200, max_depth=3, learning_rate=0.08, random_state=0).fit(tr[X], tr[\"H\"])\n", "mean_x = tr[X].mean()\n", "LABEL = {\"stock\": \"ํ˜„์žฌ ์žฌ๊ณ \", \"c3\": \"์ตœ๊ทผ 3์ผ ์†Œ๋ชจ\", \"c7\": \"์ตœ๊ทผ 7์ผ ์†Œ๋ชจ\", \"c14\": \"์ตœ๊ทผ 14์ผ ์†Œ๋ชจ\", \"dos7\": \"7์ผ ๊ธฐ์ค€ DOS\", \"dos3\": \"3์ผ ๊ธฐ์ค€ DOS\", \"trend\": \"์†Œ๋ชจ ์ถ”์„ธ\",\n", " \"burst\": \"์†Œ๋ชจ ๊ธ‰์ฆ๋„\", \"plan7\": \"๋‹ค์Œ 7์ผ ๊ณ„ํš ํ…œํฌ\", \"plan_ratio\": \"๊ณ„ํš/ํ˜„์žฌ ํ…œํฌ ๋น„\", \"tempo0\": \"ํ˜„์žฌ ์ž‘์ „ ํ…œํฌ\", \"wx_fc\": \"์•…์ฒœํ›„ ์˜ˆ๋ณด\", \"since_rs\": \"๋ณด๊ธ‰ ํ›„ ๊ฒฝ๊ณผ์ผ\",\n", " \"rs_avg\": \"์ตœ๊ทผ ๋ณด๊ธ‰๋Ÿ‰\", \"fill\": \"๋ชฉํ‘œ ๋Œ€๋น„ ์žฌ๊ณ \", \"item_code\": \"ํ’ˆ๋ชฉ\"}\n", "def level(h):\n", " return \"์‹ฌ๊ฐ\" if h <= 2.5 else \"๊ฒฝ๊ณ„\" if h <= 4.5 else \"์ฃผ์˜\" if h <= 7.5 else \"์ •์ƒ\"\n", "ORDER = {\"์ •์ƒ\": 0, \"์ฃผ์˜\": 1, \"๊ฒฝ๊ณ„\": 2, \"์‹ฌ๊ฐ\": 3}\n", "COL = {\"์ •์ƒ\": \"tab:green\", \"์ฃผ์˜\": \"gold\", \"๊ฒฝ๊ณ„\": \"orange\", \"์‹ฌ๊ฐ\": \"red\"}\n", "\n", "def explain(row):\n", " \"\"\"XAI: ๊ฐ ํ”ผ์ฒ˜๋ฅผ ํ•™์Šต ํ‰๊ท ๊ฐ’์œผ๋กœ ๋ฐ”๊ฟจ์„ ๋•Œ ์˜ˆ์ธก์ด ๋ณ€ํ•˜๋Š” ์ •๋„(๊ธฐ์—ฌ๋„)๋กœ ์ƒ์œ„ 3๊ฐœ ์ธ์ž๋ฅผ ๋ฝ‘๋Š”๋‹ค. ์Œ์ˆ˜๋ฉด ๊ทธ ์ธ์ž ๋•Œ๋ฌธ์— ๊ฒฐํ• ์ผ์ˆ˜๊ฐ€ ์งง์•„์กŒ๋‹ค๋Š” ๋œป.\"\"\"\n", " x = row[X].astype(float)\n", " alts = pd.DataFrame([x] * (len(X) + 1)).reset_index(drop=True)\n", " for i, f in enumerate(X):\n", " alts.loc[i + 1, f] = mean_x[f]\n", " p = gbr.predict(alts)\n", " contrib = {f: float(p[0] - p[i + 1]) for i, f in enumerate(X)}\n", " return float(p[0]), sorted(contrib.items(), key=lambda kv: -abs(kv[1]))[:3]\n", "\n", "# ๊ฐ€์ƒ ์ง€๋„: 4ํ–‰ x 5์—ด ๊ฒฉ์ž(์‹ค์ œ ์œ„์น˜ ์•„๋‹˜)\n", "POS = {u: (i % 5, i // 5) for i, u in enumerate(UNITS)}\n", "print(\"[์ง€๋„ ๋Œ€์‹œ๋ณด๋“œ] ๊ฐ€์ƒ ๊ฒฉ์ž ์ง€๋„(์œ„์น˜๋Š” ์‹ค์ œ์™€ ๋ฌด๊ด€) [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\")\n", "snaps = {}\n", "for day in (100, 101, 102):\n", " now = s[s[\"t0\"] == day].copy()\n", " now[\"pred\"] = np.clip(gbr.predict(now[X]), 1, None)\n", " now[\"lv\"] = now[\"pred\"].map(level)\n", " worst = now.loc[now.groupby(\"unit\")[\"pred\"].idxmin()].set_index(\"unit\")\n", " snaps[day] = (now, worst)\n", " cnt = now[\"lv\"].value_counts().reindex([\"์‹ฌ๊ฐ\", \"๊ฒฝ๊ณ„\", \"์ฃผ์˜\", \"์ •์ƒ\"], fill_value=0).to_dict()\n", " print(f\" {day}์ผ์ฐจ: ๊ฒฝ๋ณด ๋ถ„ํฌ {cnt}, ๊ฐ€์žฅ ์œ„ํ—˜ํ•œ ๋ถ€๋Œ€ {worst['pred'].idxmin()}({worst['item'][worst['pred'].idxmin()]}, ์˜ˆ์ƒ ๊ฒฐํ• {worst['pred'].min():.1f}์ผ)\")\n", "\n", "now, worst = snaps[102]\n", "alerts = now[now[\"lv\"] != \"์ •์ƒ\"].sort_values(\"pred\")\n", "print(f\"\\n[๊ฒฝ๋ณด ์„ค๋ช…(XAI)] 102์ผ์ฐจ ๊ฒฝ๋ณด {len(alerts)}๊ฑด ์ค‘ ์ƒ์œ„ 6๊ฑด (๊ธฐ์—ฌ๋„ ์ƒ์œ„ 3๊ฐœ ์ธ์ž)\")\n", "ok = 0\n", "rows = []\n", "for _, r in alerts.iterrows():\n", " base, top = explain(r)\n", " ok += (len(top) == 3 and all(np.isfinite(v) for _, v in top))\n", " rows.append((r[\"unit\"], r[\"item\"], r[\"lv\"], base, top))\n", "for u, it, lv, base, top in rows[:6]:\n", " txt = \", \".join(f\"{LABEL[f]}({v:+.1f}์ผ)\" for f, v in top)\n", " print(f\" - {u} {it} [{lv}] ์˜ˆ์ƒ ๊ฒฐํ• {base:.1f}์ผ <- {txt}\")\n", "print(f\"์„ค๋ช… ์ž๋™ ์‚ฐ์ถœ๋ฅ  {ok}/{len(alerts)} = {ok / max(len(alerts), 1):.0%}\")\n", "fid = []\n", "for u, it, lv, base, top in rows[:20]:\n", " r = alerts[(alerts[\"unit\"] == u) & (alerts[\"item\"] == it)].iloc[0]\n", " k = [f for f in X if f not in [t for t, _ in top]][0]\n", " alt = pd.DataFrame([r[X].astype(float)] * 2).reset_index(drop=True)\n", " alt.loc[0, top[0][0]] = mean_x[top[0][0]]; alt.loc[1, k] = mean_x[k]\n", " p = gbr.predict(alt)\n", " fid.append(abs(base - p[0]) >= abs(base - p[1]))\n", "print(f\"์„ค๋ช… ํƒ€๋‹น์„ฑ ์ ๊ฒ€: 1์ˆœ์œ„ ์ธ์ž๋ฅผ ์ œ๊ฑฐํ•  ๋•Œ์˜ ์˜ˆ์ธก ๋ณ€ํ™”๊ฐ€ ๋ฌด๊ด€ํ•œ ์ธ์ž ์ œ๊ฑฐ๋ณด๋‹ค ํฐ ๋น„์œจ {np.mean(fid):.0%}\")\n", "\n", "fig, ax = plt.subplots(1, 2, figsize=(13, 4.8))\n", "for u, (x, y) in POS.items():\n", " lv = worst.loc[u, \"lv\"]\n", " ax[0].scatter(x, -y, s=1500, color=COL[lv], edgecolor=\"black\"); ax[0].text(x, -y, u[2:4], ha=\"center\", va=\"center\", fontsize=9)\n", "ax[0].set_title(\"Virtual map: worst alert level per unit (102nd day)\"); ax[0].axis(\"off\")\n", "if rows:\n", " u, it, lv, base, top = rows[0]\n", " ax[1].barh([f for f, _ in top][::-1], [v for _, v in top][::-1], color=\"tab:blue\")\n", " ax[1].set_title(\"Top-3 contributions (days) for the most urgent alert\"); ax[1].set_xlabel(\"change in predicted days to stockout\")\n", "plt.tight_layout(); plt.show()\n", "print(f\"\\n๋ชฉํ‘œ(์ฃผ์š” ์˜ˆ์ธก ๊ฑด๋‹น ๊ธฐ์—ฌ๋„ ์ƒ์œ„ 3๊ฐœ ์ธ์ž ์ž๋™ ์‚ฐ์ถœ๋ฅ  100%): {'๋‹ฌ์„ฑ' if ok == len(alerts) else '๋ฏธ๋‹ฌ'}\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 06 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[์ง€๋„ ๋Œ€์‹œ๋ณด๋“œ] ๊ฐ€์ƒ ๊ฒฉ์ž ์ง€๋„(์œ„์น˜๋Š” ์‹ค์ œ์™€ ๋ฌด๊ด€) [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\n", " 100์ผ์ฐจ: ๊ฒฝ๋ณด ๋ถ„ํฌ {'์‹ฌ๊ฐ': 8, '๊ฒฝ๊ณ„': 11, '์ฃผ์˜': 29, '์ •์ƒ': 47}, ๊ฐ€์žฅ ์œ„ํ—˜ํ•œ ๋ถ€๋Œ€ ๊ฐ€์ƒ15๋ถ€๋Œ€(ํƒ„์•ฝ, ์˜ˆ์ƒ ๊ฒฐํ• 1.0์ผ)\n", " 101์ผ์ฐจ: ๊ฒฝ๋ณด ๋ถ„ํฌ {'์‹ฌ๊ฐ': 8, '๊ฒฝ๊ณ„': 14, '์ฃผ์˜': 24, '์ •์ƒ': 51}, ๊ฐ€์žฅ ์œ„ํ—˜ํ•œ ๋ถ€๋Œ€ ๊ฐ€์ƒ20๋ถ€๋Œ€(ํƒ„์•ฝ, ์˜ˆ์ƒ ๊ฒฐํ• 1.0์ผ)\n", " 102์ผ์ฐจ: ๊ฒฝ๋ณด ๋ถ„ํฌ {'์‹ฌ๊ฐ': 9, '๊ฒฝ๊ณ„': 12, '์ฃผ์˜': 24, '์ •์ƒ': 54}, ๊ฐ€์žฅ ์œ„ํ—˜ํ•œ ๋ถ€๋Œ€ ๊ฐ€์ƒ17๋ถ€๋Œ€(ํƒ„์•ฝ, ์˜ˆ์ƒ ๊ฒฐํ• 1.0์ผ)\n", "\n", "[๊ฒฝ๋ณด ์„ค๋ช…(XAI)] 102์ผ์ฐจ ๊ฒฝ๋ณด 45๊ฑด ์ค‘ ์ƒ์œ„ 6๊ฑด (๊ธฐ์—ฌ๋„ ์ƒ์œ„ 3๊ฐœ ์ธ์ž)\n", " - ๊ฐ€์ƒ17๋ถ€๋Œ€ ํƒ„์•ฝ [์‹ฌ๊ฐ] ์˜ˆ์ƒ ๊ฒฐํ• 0.9์ผ <- 3์ผ ๊ธฐ์ค€ DOS(-5.1์ผ), ๋ชฉํ‘œ ๋Œ€๋น„ ์žฌ๊ณ (-0.3์ผ), 7์ผ ๊ธฐ์ค€ DOS(-0.2์ผ)\n", " - ๊ฐ€์ƒ07๋ถ€๋Œ€ ํƒ„์•ฝ [์‹ฌ๊ฐ] ์˜ˆ์ƒ ๊ฒฐํ• 1.0์ผ <- 3์ผ ๊ธฐ์ค€ DOS(-5.2์ผ), 7์ผ ๊ธฐ์ค€ DOS(-0.4์ผ), ๋ชฉํ‘œ ๋Œ€๋น„ ์žฌ๊ณ (-0.4์ผ)\n", " - ๊ฐ€์ƒ03๋ถ€๋Œ€ ์œ ๋ฅ˜ [์‹ฌ๊ฐ] ์˜ˆ์ƒ ๊ฒฐํ• 1.0์ผ <- 3์ผ ๊ธฐ์ค€ DOS(-5.4์ผ), ๋ชฉํ‘œ ๋Œ€๋น„ ์žฌ๊ณ (-0.4์ผ), 7์ผ ๊ธฐ์ค€ DOS(-0.3์ผ)\n", " - ๊ฐ€์ƒ18๋ถ€๋Œ€ ํƒ„์•ฝ [์‹ฌ๊ฐ] ์˜ˆ์ƒ ๊ฒฐํ• 1.1์ผ <- 3์ผ ๊ธฐ์ค€ DOS(-5.4์ผ), 7์ผ ๊ธฐ์ค€ DOS(-0.3์ผ), ๋ชฉํ‘œ ๋Œ€๋น„ ์žฌ๊ณ (-0.3์ผ)\n", " - ๊ฐ€์ƒ08๋ถ€๋Œ€ ์œ ๋ฅ˜ [์‹ฌ๊ฐ] ์˜ˆ์ƒ ๊ฒฐํ• 1.2์ผ <- 3์ผ ๊ธฐ์ค€ DOS(-5.1์ผ), 7์ผ ๊ธฐ์ค€ DOS(-0.4์ผ), ๋ชฉํ‘œ ๋Œ€๋น„ ์žฌ๊ณ (-0.3์ผ)\n", " - ๊ฐ€์ƒ18๋ถ€๋Œ€ ์œ ๋ฅ˜ [์‹ฌ๊ฐ] ์˜ˆ์ƒ ๊ฒฐํ• 1.3์ผ <- 3์ผ ๊ธฐ์ค€ DOS(-5.6์ผ), ๋ชฉํ‘œ ๋Œ€๋น„ ์žฌ๊ณ (-0.3์ผ), 7์ผ ๊ธฐ์ค€ DOS(-0.3์ผ)\n", "์„ค๋ช… ์ž๋™ ์‚ฐ์ถœ๋ฅ  45/45 = 100%\n", "์„ค๋ช… ํƒ€๋‹น์„ฑ ์ ๊ฒ€: 1์ˆœ์œ„ ์ธ์ž๋ฅผ ์ œ๊ฑฐํ•  ๋•Œ์˜ ์˜ˆ์ธก ๋ณ€ํ™”๊ฐ€ ๋ฌด๊ด€ํ•œ ์ธ์ž ์ œ๊ฑฐ๋ณด๋‹ค ํฐ ๋น„์œจ 100%\n", "\n", "๋ชฉํ‘œ(์ฃผ์š” ์˜ˆ์ธก ๊ฑด๋‹น ๊ธฐ์—ฌ๋„ ์ƒ์œ„ 3๊ฐœ ์ธ์ž ์ž๋™ ์‚ฐ์ถœ๋ฅ  100%): ๋‹ฌ์„ฑ\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 07. ๊ฒฐํ• ๋ถ€๋Œ€ ๊ธด๊ธ‰ ๋ฌผ์ž ์ „ํ™˜ ๊ฒฐ์ •์„ ์œ„ํ•œ ๊ตฐ์ˆ˜์ฒ˜์žฅ HITL ์Šน์ธ ์›Œํฌํ”Œ๋กœ์šฐ ์„ค๊ณ„\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.5์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "๊ฒฐํ• ๋ถ€๋Œ€ ๊ธด๊ธ‰ ๋ฌผ์ž ์ „ํ™˜ ๊ฒฐ์ •์„ ์œ„ํ•œ ๊ตฐ์ˆ˜์ฒ˜์žฅ HITL ์Šน์ธ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ์„ค๊ณ„ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **์˜ˆ์ธก ๊ธฐ๋ฐ˜ ๋ณด๊ธ‰ ์ •์ฑ…์˜ ํšจ๊ณผ๋ฅผ ์‹œ๋ฎฌ๋ ˆ์ด์…˜์œผ๋กœ ๋น„๊ตํ•˜๊ณ , ๊ฒฐํ• ๋ถ€๋Œ€์— ๋Œ€ํ•œ ๊ธด๊ธ‰ ๋ฌผ์ž ์ „ํ™˜์€ ๊ตฐ์ˆ˜์ฒ˜์žฅ ์Šน์ธ์œผ๋กœ๋งŒ ์ง‘ํ–‰ํ•˜๋Š” HITL ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ์„ค๊ณ„ํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **๊ณ ์ • ๋ชฉํ‘œ ์žฌ๊ณ  ์ •์ฑ…๊ณผ ์˜ˆ์ธก ๊ธฐ๋ฐ˜ ๋™์  ๋ชฉํ‘œ ์ •์ฑ…์„ ๊ฐ™์€ 50์ผ ์ž๋ฃŒ๋กœ ์žฌํ˜„ํ•ด ๊ฒฐํ•๋Ÿ‰, ์žฌ๊ณ  ํšŒ์ „, ๋น„์šฉ์„ ๋น„๊ต ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/optimization/str01_simulator.py` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ํ˜„์‹ค์  ๊ตฐ์ˆ˜ ์ œ์•ฝ์กฐ๊ฑด ๋ฐ˜์˜๋„ ๋ฐ ์‹œ๋‚˜๋ฆฌ์˜ค๋ณ„ ๋Œ€์•ˆ ์ œ์‹œ ์ ์ ˆ์„ฑ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ์ตœ์ ํ™” ์ ์šฉ ์‹œ ๋น„์šฉ 15% ์ ˆ๊ฐ ๋˜๋Š” ์žฌ๊ณ  ํšŒ์ „์œจ 20% ํ–ฅ์ƒ\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ์„ ํ˜•/์ •์ˆ˜ ๊ณ„ํš๋ฒ•(LP/MIP) ์ตœ์ ํ™” ํ•ด ๋„์ถœ ๋ฐ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์™„๋ฃŒ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ๊ณผ๊ฑฐ ์‹ค์  ๋ฐ์ดํ„ฐ ๋Œ€๋น„ ์ตœ์ ํ™” ๊ฒฐ๊ณผ ๋น„๊ต ๋ฐฑํ…Œ์ŠคํŒ…\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"์˜ˆ์ธก ๊ธฐ๋ฐ˜ ๋™์  ๋ชฉํ‘œ ์žฌ๊ณ  ์ •์ฑ…์„ ๊ณ ์ • ๋ชฉํ‘œ ์ •์ฑ…๊ณผ ์‹œ๋ฎฌ๋ ˆ์ด์…˜์œผ๋กœ ๋น„๊ตํ•ด ๋น„์šฉยท์žฌ๊ณ  ํšŒ์ „์œจ ํšจ๊ณผ๋ฅผ ๊ตฌํ•˜๊ณ , ๊ฒฐํ• ์˜ˆ์ƒ ๋ถ€๋Œ€์— ๋Œ€ํ•œ ๊ธด๊ธ‰ ๋ฌผ์ž ์ „ํ™˜ ์ œ์•ˆ์„ ๋งŒ๋“ค๋ฉฐ ์ตœ์ข… ์ง‘ํ–‰์€ ๊ตฐ์ˆ˜์ฒ˜์žฅ ์Šน์ธ์œผ๋กœ๋งŒ ์ด๋ฃจ์–ด์ง€๋Š” HITL ์Šน์ธ ์›Œํฌํ”Œ๋กœ์šฐ์™€ ๊ฐ์‚ฌ ์ด๋ ฅยท์šฐํšŒ ์ฐจ๋‹จ์„ ๊ตฌํ˜„ํ•œ๋‹ค.\"\"\"\n", "import hashlib\n", "import json\n", "import numpy as np\n", "import pandas as pd\n", "from sklearn.ensemble import GradientBoostingRegressor\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ฐ€์ƒ ๋ถ€๋Œ€ 20๊ฐœ x ํ•ต์‹ฌ ๋ณด๊ธ‰ํ’ˆ 5์ข… ์ผ์ผ ์žฌ๊ณ ยท์†Œ๋ชจ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ (์‹ค์ œ ๋ถ€๋Œ€ยท์ˆ˜๋Ÿ‰ยท๊ธฐ์ค€์ผ์ˆ˜ ์•„๋‹˜)\n", "# ํ•ต์‹ฌ์„ฑ๊ณผ์ง€ํ‘œ(KPI) = ๊ธฐ์ค€ ๋ณด๊ธ‰์ผ์ˆ˜(DOS) = ํ˜„์žฌ ์žฌ๊ณ  / ์ตœ๊ทผ ํ‰๊ท  ์ผ์ผ ์†Œ๋ชจ๋Ÿ‰. ๋ชฉํ‘œ DOS๋Š” ๊ต์œก์šฉ ๊ฐ€์ •๊ฐ’์ด๋‹ค.\n", "# ๊ฒฐํ• ์œ„ํ—˜ ์ผ์ˆ˜ H = ๋ณด๊ธ‰์ด ์—†๋‹ค๊ณ  ๊ฐ€์ •ํ•  ๋•Œ ํ˜„์žฌ ์žฌ๊ณ ๊ฐ€ ๋ฐ”๋‹ฅ๋‚˜๊ธฐ๊นŒ์ง€ ๋‚จ์€ ์ผ์ˆ˜(๋ฏธ๋ž˜ ์‹ค์ œ ์†Œ๋ชจ๋Ÿ‰์œผ๋กœ ๊ณ„์‚ฐํ•œ ์ •๋‹ต).\n", "# ---------------------------------------------------------------------------\n", "ITEMS = {\"์‹๋Ÿ‰\": (100, 0.3, 10), \"ํ”ผ๋ณต\": (20, 0.1, 15), \"์œ ๋ฅ˜\": (300, 1.2, 7), \"ํƒ„์•ฝ\": (60, 1.5, 6), \"์ˆ˜๋ฆฌ๋ถ€์†\": (15, 0.5, 12)} # (๊ธฐ๋ณธ ์ผ์†Œ๋ชจ, ํ…œํฌ ๋ฏผ๊ฐ๋„, ๋ชฉํ‘œ DOS)\n", "N_UNIT, DAYS = 20, 160\n", "UNITS = [f\"๊ฐ€์ƒ{u:02d}๋ถ€๋Œ€\" for u in range(1, N_UNIT + 1)]\n", "\n", "\n", "def make_data(seed=1, n_unit=N_UNIT, days=DAYS):\n", " \"\"\"์ผ ๋‹จ์œ„ ์žฌ๊ณ  ์›์žฅ: unit, item, day, tempo(์‹ค์ œ ์ž‘์ „ ํ…œํฌ), plan(์‚ฌ์ „ ๊ณ„ํš ํ…œํฌ), wx(์•…์ฒœํ›„), consume, resupply, stock.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " rows = []\n", " for u in range(1, n_unit + 1):\n", " s, ph = rng.uniform(0.7, 1.5), rng.uniform(0, 6.28)\n", " surge = np.zeros(days)\n", " for st in rng.choice(days - 10, 4, replace=False):\n", " surge[st:st + int(rng.integers(5, 10))] = rng.uniform(0.4, 0.8)\n", " tempo = 1 + 0.25 * np.sin(2 * np.pi * np.arange(days) / 30 + ph) + surge\n", " plan = tempo + rng.normal(0, 0.08, days)\n", " wx = (rng.random(days) < 0.15).astype(int)\n", " for it, (b, sens, tg) in ITEMS.items():\n", " stock, off = tg * b * s * rng.uniform(0.8, 1.2), int(rng.integers(0, 4))\n", " for t in range(days):\n", " c = b * s * (1 + sens * (tempo[t] - 1)) * (1 + 0.12 * wx[t]) * rng.normal(1, 0.08)\n", " r = max(tg * b * s - stock, 0) * rng.uniform(0.9, 1.1) * (0.5 if rng.random() < 0.08 else 1) if (t % 4 == off and t > 0) else 0.0 # 4์ผ ์ฃผ๊ธฐ ์ •๊ธฐ ๋ณด๊ธ‰: ๋ชฉํ‘œ ์žฌ๊ณ ๊นŒ์ง€ ์ฑ„์›€(๊ฐ€๋” ์ˆ˜์†ก ์ฐจ์งˆ๋กœ ์ ˆ๋ฐ˜)\n", " stock = max(stock + r - c, 0.0)\n", " rows.append((f\"๊ฐ€์ƒ{u:02d}๋ถ€๋Œ€\", it, t, tempo[t], plan[t], wx[t], c, r, stock))\n", " return pd.DataFrame(rows, columns=[\"unit\", \"item\", \"day\", \"tempo\", \"plan\", \"wx\", \"consume\", \"resupply\", \"stock\"])\n", "\n", "\n", "def make_samples(d, lo=21, hi=125):\n", " \"\"\"(๋ถ€๋Œ€, ํ’ˆ๋ชฉ, ๊ธฐ์ค€์ผ t0)๋ณ„ ํŠน์„ฑ(ํ”ผ์ฒ˜)๊ณผ ์ •๋‹ต H. ํ”ผ์ฒ˜๋Š” t0 ์‹œ์ ๊นŒ์ง€ ์•Œ ์ˆ˜ ์žˆ๋Š” ๊ฐ’๊ณผ ์‚ฌ์ „ ๊ณ„ํšยท๊ธฐ์ƒ ์˜ˆ๋ณด๋งŒ ์‚ฌ์šฉํ•œ๋‹ค.\"\"\"\n", " out = []\n", " for (u, it), g in d.groupby([\"unit\", \"item\"]):\n", " c, stk, rs = g[\"consume\"].values, g[\"stock\"].values, g[\"resupply\"].values\n", " pl, wx, tp = g[\"plan\"].values, g[\"wx\"].values, g[\"tempo\"].values\n", " cs = np.cumsum(c)\n", " tg = ITEMS[it][2] * ITEMS[it][0]\n", " for t0 in range(lo, hi):\n", " idx = np.searchsorted(cs, cs[t0] + stk[t0])\n", " if idx >= len(cs) or idx <= t0:\n", " continue\n", " c3, c7, c14 = c[t0 - 2:t0 + 1].mean(), c[t0 - 6:t0 + 1].mean(), c[t0 - 13:t0 + 1].mean()\n", " last = [k for k in range(t0, t0 - 15, -1) if rs[k] > 0]\n", " out.append({\"unit\": u, \"item\": it, \"t0\": t0, \"H\": idx - t0, \"stock\": stk[t0], \"c3\": c3, \"c7\": c7, \"c14\": c14,\n", " \"dos7\": stk[t0] / c7, \"dos3\": stk[t0] / c3, \"trend\": c7 / c14, \"burst\": c3 / c7,\n", " \"plan7\": pl[t0 + 1:t0 + 8].mean(), \"plan_ratio\": pl[t0 + 1:t0 + 8].mean() / tp[t0], \"tempo0\": tp[t0],\n", " \"wx_fc\": wx[t0 + 1:t0 + 8].mean(), \"since_rs\": (t0 - last[0]) if last else 15,\n", " \"rs_avg\": rs[t0 - 13:t0 + 1].sum() / 14, \"fill\": stk[t0] / tg, \"item_code\": list(ITEMS).index(it)})\n", " return pd.DataFrame(out)\n", "\n", "\n", "def mape(a, b):\n", " return float(np.mean(np.abs(a - b) / a))\n", "\n", "d = make_data()\n", "s = make_samples(d)\n", "X = [c for c in s.columns if c not in (\"unit\", \"item\", \"t0\", \"H\")]\n", "tr = s[s[\"t0\"] <= 90]\n", "gbr = GradientBoostingRegressor(n_estimators=120, max_depth=3, learning_rate=0.1, random_state=0).fit(tr[X], tr[\"H\"])\n", "now = s[s[\"t0\"] == 100].copy()\n", "now[\"pred\"] = np.clip(gbr.predict(now[X]), 1, None)\n", "POS = {u: np.array([i % 5, i // 5]) * 20.0 for i, u in enumerate(UNITS)} # ๊ฐ€์ƒ ์ขŒํ‘œ(์‹ค์ œ ์œ„์น˜ ์•„๋‹˜)\n", "T0, T1 = 100, 150\n", "\n", "# 1) ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ: 100~149์ผ์„ ์‹ค์ œ ์†Œ๋ชจยท๊ณ„ํšยท๊ธฐ์ƒ ์ž๋ฃŒ๋กœ ์žฌํ˜„ํ•ด ๋‘ ๊ฐ€์ง€ ๋ณด๊ธ‰ ์ •์ฑ…์„ ๋น„๊ต(4์ผ ์ฃผ๊ธฐ ์ •๊ธฐ ๋ณด๊ธ‰)\n", "def simulate(policy, d_b=6.5):\n", " \"\"\"์ •์ฑ… A: ๊ณ ์ • ๋ชฉํ‘œ ์žฌ๊ณ (ํ˜„ํ–‰). ์ •์ฑ… B: ์˜ˆ์ธก ๊ธฐ๋ฐ˜ ๋™์  ๋ชฉํ‘œ(๋‹ค์Œ 4์ผ ๊ณ„ํš ํ…œํฌยท๊ธฐ์ƒ ์˜ˆ๋ณด๋ฅผ ๋ฐ˜์˜ํ•œ ์†Œ๋ชจ ์˜ˆ์ƒ x d_b์ผ์น˜).\"\"\"\n", " tot = {\"short\": 0.0, \"stock\": 0.0, \"cons\": 0.0, \"ship\": 0.0, \"n\": 0}\n", " for (u, it), g in d.groupby([\"unit\", \"item\"]):\n", " c, pl, wx, rs = g[\"consume\"].values, g[\"plan\"].values, g[\"wx\"].values, g[\"resupply\"].values\n", " b, sens, tg = ITEMS[it]\n", " stock = g[\"stock\"].values[T0 - 1]\n", " off = [t for t in range(1, 30) if rs[t] > 0][0] % 4\n", " base_tg = tg * float(np.mean(c[:21]))\n", " for t in range(T0, T1):\n", " if t % 4 == off:\n", " if policy == \"A\":\n", " target = base_tg\n", " else:\n", " pr = pl[t:t + 4].mean() / max(pl[t - 7:t].mean(), 0.3)\n", " f = c[t - 7:t].mean() * (1 + sens * (pr - 1)) * (1 + 0.12 * wx[t:t + 4].mean()) / (1 + 0.12 * wx[t - 7:t].mean())\n", " target = d_b * f\n", " r = max(target - stock, 0); stock += r; tot[\"ship\"] += r\n", " use = min(stock, c[t]); tot[\"short\"] += c[t] - use; stock -= use; tot[\"stock\"] += stock; tot[\"cons\"] += use; tot[\"n\"] += 1\n", " avg = tot[\"stock\"] / tot[\"n\"]\n", " tot[\"turn\"] = tot[\"cons\"] / avg / (T1 - T0) # ์žฌ๊ณ  ํšŒ์ „์œจ(์—ฐ๊ฐ„ ํ™˜์‚ฐ ์•„๋‹˜: ์ผํ‰๊ท  ์†Œ๋ชจ/ํ‰๊ท  ์žฌ๊ณ ์˜ ํ•ฉ๊ณ„ ๊ธฐ์ค€ ์ƒ๋Œ€ ์ง€ํ‘œ)\n", " tot[\"short_rate\"] = tot[\"short\"] / (tot[\"cons\"] + tot[\"short\"])\n", " tot[\"cost\"] = 0.02 * tot[\"stock\"] + 8.0 * tot[\"short\"] # ๊ฐ€์ •: ๋ณด์œ ๋น„ 0.02/์žฌ๊ณ ์ผ, ๊ฒฐํ• ์†์‹ค 8.0/๋‹จ์œ„\n", " return tot\n", "A, B = simulate(\"A\"), simulate(\"B\")\n", "print(f\"[๋ณด๊ธ‰ ์ •์ฑ… ์‹œ๋ฎฌ๋ ˆ์ด์…˜ {T0}~{T1 - 1}์ผ] ๋ถ€๋Œ€ 20 x ํ’ˆ๋ชฉ 5 [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\")\n", "print(f\"{'':<16}{'A ๊ณ ์ • ๋ชฉํ‘œ ์žฌ๊ณ ':>18}{'B ์˜ˆ์ธก ๊ธฐ๋ฐ˜ ๋™์  ๋ชฉํ‘œ':>22}\")\n", "for k, nm, fmt in ((\"short\", \"๊ฒฐํ•๋Ÿ‰ ํ•ฉ๊ณ„\", \"{:,.0f}\"), (\"short_rate\", \"๊ฒฐํ•๋ฅ \", \"{:.2%}\"), (\"stock\", \"์žฌ๊ณ ์ผ ํ•ฉ๊ณ„\", \"{:,.0f}\"), (\"turn\", \"์žฌ๊ณ  ํšŒ์ „ ์ง€ํ‘œ\", \"{:.2f}\"), (\"cost\", \"๋น„์šฉ(๊ฐ€์ • ๋‹จ๊ฐ€)\", \"{:,.0f}\")):\n", " print(f\"{nm:<14}{fmt.format(A[k]):>20}{fmt.format(B[k]):>22}\")\n", "sav, turn_gain = 1 - B[\"cost\"] / A[\"cost\"], B[\"turn\"] / A[\"turn\"] - 1\n", "print(f\"์žฌ๊ณ  ํšŒ์ „์œจ ํ–ฅ์ƒ {turn_gain:.1%}, ๋น„์šฉ ์ ˆ๊ฐ {sav:.1%}(๋ณด์œ ๋น„+๊ฒฐํ• ์†์‹ค, ๊ฐ€์ • ๋‹จ๊ฐ€), ๊ฒฐํ•๋Ÿ‰ ๊ฐ์†Œ {1 - B['short'] / A['short']:.1%}\")\n", "print(f\"๋ชฉํ‘œ(๋น„์šฉ 15% ์ ˆ๊ฐ ๋˜๋Š” ํšŒ์ „์œจ 20% ํ–ฅ์ƒ): {'๋‹ฌ์„ฑ' if sav >= 0.15 or turn_gain >= 0.2 else '๋ฏธ๋‹ฌ'}\")\n", "print(\"์ œ์•ฝ: 4์ผ ์ฃผ๊ธฐ ์ •๊ธฐ ๋ณด๊ธ‰, ๋ณด๊ธ‰๋Ÿ‰์€ ๋ชฉํ‘œ ์žฌ๊ณ ๊นŒ์ง€, ๊ฒฐํ•์€ ์ด์›”ํ•˜์ง€ ์•Š์Œ(๊ฐ€์ •). ์‹œ๋‚˜๋ฆฌ์˜ค A์™€ B ์ค‘ B๊ฐ€ ๊ฒฐํ•์„ ์ค„์ด๋ฉด์„œ ์žฌ๊ณ ๋ฅผ ์ค„์ด๋Š”์ง€ ํ•จ๊ป˜ ํ™•์ธํ•œ๋‹ค.\")\n", "\n", "# 2) ๊ธด๊ธ‰ ์ „ํ™˜ ์ œ์•ˆ ์ƒ์„ฑ: ์˜ˆ์ธก ๊ฒฐํ•(๊ฒฝ๊ณ„ ์ดํ•˜) ๋ถ€๋Œ€์— ๋‹ค์Œ ๋ณด๊ธ‰์ผ๊นŒ์ง€ ๋ฒ„ํ‹ธ ๋ฌผ๋Ÿ‰์„ ์—ฌ์œ  ๋ถ€๋Œ€์—์„œ ์˜ฎ๊ธฐ๊ฑฐ๋‚˜ ๋ณด๊ธ‰์†Œ ์˜ˆ๋น„๋กœ ์ถฉ๋‹น\n", "now[\"nxt\"] = (3 - now[\"since_rs\"]).clip(lower=0) + 1\n", "now[\"keep\"] = 1.3 * now[\"c7\"] * (now[\"nxt\"] + 1)\n", "donors = now[(now[\"pred\"] > 6) & (now[\"stock\"] > now[\"keep\"])].copy(); donors[\"avail\"] = donors[\"stock\"] - donors[\"keep\"]\n", "plan = []\n", "for r in now[now[\"pred\"] <= 4.5].sort_values(\"pred\").itertuples():\n", " q = r.keep - r.stock\n", " if q <= 0:\n", " continue\n", " cand = donors[(donors[\"item\"] == r.item) & (donors[\"avail\"] >= q)]\n", " if len(cand):\n", " j = cand.assign(dist=[np.linalg.norm(POS[u] - POS[r.unit]) for u in cand[\"unit\"]]).sort_values(\"dist\").iloc[0]\n", " donors.loc[(donors[\"unit\"] == j[\"unit\"]) & (donors[\"item\"] == j[\"item\"]), \"avail\"] -= q\n", " plan.append((j[\"unit\"], r.unit, r.item, float(q)))\n", " else:\n", " plan.append((\"๋ณด๊ธ‰์†Œ ์˜ˆ๋น„\", r.unit, r.item, float(q)))\n", "n_lat = sum(m[0] != \"๋ณด๊ธ‰์†Œ ์˜ˆ๋น„\" for m in plan)\n", "print(f\"\\n[๊ธด๊ธ‰ ์ „ํ™˜ ์ œ์•ˆ - {T0}์ผ] ๊ฒฐํ• ์˜ˆ์ƒ(๊ฒฝ๊ณ„ ์ดํ•˜) {int((now['pred'] <= 4.5).sum())}๊ฑด ์ค‘ ์ œ์•ˆ {len(plan)}๊ฑด(๋ถ€๋Œ€ ๊ฐ„ ์ „ํ™˜ {n_lat}๊ฑด, ๋ณด๊ธ‰์†Œ ์˜ˆ๋น„ {len(plan) - n_lat}๊ฑด)\")\n", "for m in plan[:4]:\n", " print(f\" {m[0]} -> {m[1]} {m[2]} {m[3]:.0f}\")\n", "\n", "# 2) HITL: ๊ตฐ์ˆ˜์ฒ˜์žฅ ์Šน์ธ ์›Œํฌํ”Œ๋กœ์šฐ(์ž๋™ ์ง‘ํ–‰ ๊ธˆ์ง€)\n", "USERS = {\"bot\": \"์‹œ์Šคํ…œ\", \"kim\": \"๊ตฐ์ˆ˜๋‹ด๋‹น\", \"lee\": \"๊ตฐ์ˆ˜๋‹ด๋‹น\", \"jang\": \"๊ตฐ์ˆ˜์ฒ˜์žฅ\", \"han\": \"์—ด๋žŒ์ž\"}\n", "AUDIT, PROP = [], {}\n", "def h(prev, rec):\n", " return hashlib.sha256((prev + json.dumps(rec, ensure_ascii=False, sort_keys=True)).encode()).hexdigest()[:16]\n", "def log(pid, user, act, res, note=\"\"):\n", " rec = {\"seq\": len(AUDIT) + 1, \"pid\": pid, \"user\": user, \"role\": USERS.get(user, \"๋ฏธ๋“ฑ๋ก\"), \"act\": act, \"res\": res, \"note\": note}\n", " rec[\"hash\"] = h(AUDIT[-1][\"hash\"] if AUDIT else \"G\", {k: v for k, v in rec.items() if k != \"hash\"}); AUDIT.append(rec)\n", "def blocked(pid, user, act, why):\n", " log(pid, user, act, \"์ฐจ๋‹จ\", why); return False\n", "def propose(pid, user, move):\n", " if USERS.get(user) not in (\"์‹œ์Šคํ…œ\", \"๊ตฐ์ˆ˜๋‹ด๋‹น\"):\n", " return blocked(pid, user, \"์ œ์•ˆ\", \"์ œ์•ˆ ๊ถŒํ•œ ์—†์Œ\")\n", " PROP[pid] = {\"move\": move, \"by\": user, \"status\": \"๋Œ€๊ธฐ\"}; log(pid, user, \"์ œ์•ˆ\", \"์ ‘์ˆ˜\", f\"{move[0]}->{move[1]} {move[2]} {move[3]:.0f}\"); return True\n", "def decide(pid, user, act, note):\n", " p = PROP.get(pid)\n", " if p is None:\n", " return blocked(pid, user, act, \"์ œ์•ˆ ์—†์Œ\")\n", " if USERS.get(user) != \"๊ตฐ์ˆ˜์ฒ˜์žฅ\":\n", " return blocked(pid, user, act, f\"๊ถŒํ•œ ์—†์Œ: ์—ญํ•  '{USERS.get(user, '๋ฏธ๋“ฑ๋ก')}'์€(๋Š”) ๊ฒฐ์ • ๋ถˆ๊ฐ€(์ž๋™ ์ง‘ํ–‰ ๊ธˆ์ง€)\")\n", " if p[\"status\"] != \"๋Œ€๊ธฐ\":\n", " return blocked(pid, user, act, f\"์ด๋ฏธ {p['status']}\")\n", " if len(note.strip()) < 5:\n", " return blocked(pid, user, act, \"๊ฒฐ์ • ๊ทผ๊ฑฐ 5์ž ์ด์ƒ ํ•„์š”\")\n", " p[\"status\"] = \"์Šน์ธ\" if act == \"์Šน์ธ\" else \"๋ฐ˜๋ ค\"; log(pid, user, act, \"์™„๋ฃŒ\", note); return True\n", "def execute(pid, user):\n", " p = PROP.get(pid)\n", " if p is None or p[\"status\"] != \"์Šน์ธ\":\n", " return blocked(pid, user, \"์ง‘ํ–‰\", \"์Šน์ธ๋˜์ง€ ์•Š์€ ๊ฑด ์ง‘ํ–‰ ๋ถˆ๊ฐ€\")\n", " p[\"status\"] = \"์ง‘ํ–‰\"; log(pid, user, \"์ง‘ํ–‰\", \"์™„๋ฃŒ\", \"์Šน์ธ ๊ฑด ์ง‘ํ–‰(๋ชจ์˜)\"); return True\n", "\n", "big = plan[:8]\n", "for i, mv in enumerate(big):\n", " propose(f\"P{i + 1}\", \"bot\" if i % 2 else \"kim\", mv)\n", "if len(plan) > 8:\n", " propose(\"P9\", \"kim\", plan[8]) # ๊ฒฐ์ • ์ „ ๋Œ€๊ธฐ ๊ฑด(์šฐํšŒ ์‹œํ—˜์šฉ)\n", "for i, mv in enumerate(big):\n", " pid = f\"P{i + 1}\"\n", " if i == 5:\n", " decide(pid, \"jang\", \"๋ฐ˜๋ ค\", \"์—ฌ์œ  ๋ถ€๋Œ€์˜ ๋‹ค์Œ ํ›ˆ๋ จ ์ผ์ •๊ณผ ์ถฉ๋Œ ์šฐ๋ ค\")\n", " else:\n", " decide(pid, \"jang\", \"์Šน์ธ\", \"์˜ˆ์ธก ๊ทผ๊ฑฐ์™€ ์—ฌ์œ  ๋ถ€๋Œ€ 5์ผ์น˜ ์œ ์ง€ ํ™•์ธ\")\n", " execute(pid, \"kim\")\n", "att = [(\"์‹œ์Šคํ…œ์ด ์ž๋™ ์Šน์ธ\", lambda: decide(\"P1\", \"bot\", \"์Šน์ธ\", \"์ž๋™ ์Šน์ธ ์ฒ˜๋ฆฌ\")), (\"๊ตฐ์ˆ˜๋‹ด๋‹น์ด ์ง์ ‘ ์Šน์ธ\", lambda: decide(\"P2\", \"lee\", \"์Šน์ธ\", \"๋‹ด๋‹น์ด ์ง์ ‘ ์Šน์ธํ•จ\")),\n", " (\"์—ด๋žŒ์ž๊ฐ€ ์Šน์ธ\", lambda: decide(\"P3\", \"han\", \"์Šน์ธ\", \"์—ด๋žŒ์ž๊ฐ€ ์Šน์ธ ์‹œ๋„ํ•จ\")), (\"๋ฏธ๋“ฑ๋ก ์‚ฌ์šฉ์ž ์Šน์ธ\", lambda: decide(\"P4\", \"x\", \"์Šน์ธ\", \"์™ธ๋ถ€ ๊ณ„์ • ์Šน์ธ ์‹œ๋„\")),\n", " (\"์Šน์ธ ๊ทผ๊ฑฐ ์—†์ด ๊ฒฐ์ •\", lambda: decide(\"P9\", \"jang\", \"์Šน์ธ\", \"\")), (\"๋ฐ˜๋ ค ๊ฑด ์ง‘ํ–‰\", lambda: execute(\"P6\", \"kim\")),\n", " (\"๋ฏธ์Šน์ธ ๊ฑด ์ง‘ํ–‰(์—†๋Š” ์ œ์•ˆ)\", lambda: execute(\"P99\", \"kim\")), (\"์ด๋ฏธ ์ง‘ํ–‰๋œ ๊ฑด ์žฌ์Šน์ธ\", lambda: decide(\"P1\", \"jang\", \"์Šน์ธ\", \"์ค‘๋ณต ์Šน์ธ ์‹œ๋„ ํ™•์ธ\"))]\n", "print(\"\\n[HITL ์Šน์ธ ๊ฒฐ๊ณผ]\", {k: sum(p[\"status\"] == k for p in PROP.values()) for k in (\"์ง‘ํ–‰\", \"๋ฐ˜๋ ค\", \"๋Œ€๊ธฐ\")})\n", "nb = 0\n", "for name, fn in att:\n", " n = len(AUDIT); fn(); r = AUDIT[-1]; ok = len(AUDIT) == n + 1 and r[\"res\"] == \"์ฐจ๋‹จ\"; nb += ok\n", " print(f\" - {name}: {r['res']} ({r['note']})\")\n", "prev, okc = \"G\", 0\n", "for r in AUDIT:\n", " okc += r[\"hash\"] == h(prev, {k: v for k, v in r.items() if k != \"hash\"}); prev = r[\"hash\"]\n", "print(f\"์šฐํšŒ ์‹œ๋„ {len(att)}๊ฑด ์ค‘ ์ฐจ๋‹จ {nb}๊ฑด, ๊ฐ์‚ฌ ์ด๋ ฅ {len(AUDIT)}๊ฑด ํ•ด์‹œ ๊ฒ€์ฆ {okc}/{len(AUDIT)}\")\n", "print(\"๊ตฐ์ˆ˜์ฒ˜์žฅ ์Šน์ธ ์—†์ด ์ง‘ํ–‰๋œ ๊ฑด 0๊ฑด. ์‹ค์ œ ๋ฌผ์ž ์ „ํ™˜ ๊ฒฐ์ •์€ ์ง€ํœ˜๊ณ„ํ†ต๊ณผ ๊ตฐ์ˆ˜ ๊ทœ์ •์— ๋”ฐ๋ฅด๋ฉฐ ๋ณธ ์ฝ”๋“œ๋Š” ํ•™์Šต์šฉ ๋ชจ์˜์ด๋‹ค.\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 07 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[๋ณด๊ธ‰ ์ •์ฑ… ์‹œ๋ฎฌ๋ ˆ์ด์…˜ 100~149์ผ] ๋ถ€๋Œ€ 20 x ํ’ˆ๋ชฉ 5 [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\n", " A ๊ณ ์ • ๋ชฉํ‘œ ์žฌ๊ณ  B ์˜ˆ์ธก ๊ธฐ๋ฐ˜ ๋™์  ๋ชฉํ‘œ\n", "๊ฒฐํ•๋Ÿ‰ ํ•ฉ๊ณ„ 5,546 894\n", "๊ฒฐํ•๋ฅ  0.89% 0.14%\n", "์žฌ๊ณ ์ผ ํ•ฉ๊ณ„ 3,317,341 2,505,060\n", "์žฌ๊ณ  ํšŒ์ „ ์ง€ํ‘œ 18.54 24.74\n", "๋น„์šฉ(๊ฐ€์ • ๋‹จ๊ฐ€) 110,716 57,250\n", "์žฌ๊ณ  ํšŒ์ „์œจ ํ–ฅ์ƒ 33.4%, ๋น„์šฉ ์ ˆ๊ฐ 48.3%(๋ณด์œ ๋น„+๊ฒฐํ• ์†์‹ค, ๊ฐ€์ • ๋‹จ๊ฐ€), ๊ฒฐํ•๋Ÿ‰ ๊ฐ์†Œ 83.9%\n", "๋ชฉํ‘œ(๋น„์šฉ 15% ์ ˆ๊ฐ ๋˜๋Š” ํšŒ์ „์œจ 20% ํ–ฅ์ƒ): ๋‹ฌ์„ฑ\n", "์ œ์•ฝ: 4์ผ ์ฃผ๊ธฐ ์ •๊ธฐ ๋ณด๊ธ‰, ๋ณด๊ธ‰๋Ÿ‰์€ ๋ชฉํ‘œ ์žฌ๊ณ ๊นŒ์ง€, ๊ฒฐํ•์€ ์ด์›”ํ•˜์ง€ ์•Š์Œ(๊ฐ€์ •). ์‹œ๋‚˜๋ฆฌ์˜ค A์™€ B ์ค‘ B๊ฐ€ ๊ฒฐํ•์„ ์ค„์ด๋ฉด์„œ ์žฌ๊ณ ๋ฅผ ์ค„์ด๋Š”์ง€ ํ•จ๊ป˜ ํ™•์ธํ•œ๋‹ค.\n", "\n", "[๊ธด๊ธ‰ ์ „ํ™˜ ์ œ์•ˆ - 100์ผ] ๊ฒฐํ• ์˜ˆ์ƒ(๊ฒฝ๊ณ„ ์ดํ•˜) 19๊ฑด ์ค‘ ์ œ์•ˆ 17๊ฑด(๋ถ€๋Œ€ ๊ฐ„ ์ „ํ™˜ 3๊ฑด, ๋ณด๊ธ‰์†Œ ์˜ˆ๋น„ 14๊ฑด)\n", " ๋ณด๊ธ‰์†Œ ์˜ˆ๋น„ -> ๊ฐ€์ƒ01๋ถ€๋Œ€ ํƒ„์•ฝ 195\n", " ๋ณด๊ธ‰์†Œ ์˜ˆ๋น„ -> ๊ฐ€์ƒ19๋ถ€๋Œ€ ํƒ„์•ฝ 211\n", " ๋ณด๊ธ‰์†Œ ์˜ˆ๋น„ -> ๊ฐ€์ƒ15๋ถ€๋Œ€ ํƒ„์•ฝ 238\n", " ๋ณด๊ธ‰์†Œ ์˜ˆ๋น„ -> ๊ฐ€์ƒ03๋ถ€๋Œ€ ํƒ„์•ฝ 582\n", "\n", "[HITL ์Šน์ธ ๊ฒฐ๊ณผ] {'์ง‘ํ–‰': 7, '๋ฐ˜๋ ค': 1, '๋Œ€๊ธฐ': 1}\n", " - ์‹œ์Šคํ…œ์ด ์ž๋™ ์Šน์ธ: ์ฐจ๋‹จ (๊ถŒํ•œ ์—†์Œ: ์—ญํ•  '์‹œ์Šคํ…œ'์€(๋Š”) ๊ฒฐ์ • ๋ถˆ๊ฐ€(์ž๋™ ์ง‘ํ–‰ ๊ธˆ์ง€))\n", " - ๊ตฐ์ˆ˜๋‹ด๋‹น์ด ์ง์ ‘ ์Šน์ธ: ์ฐจ๋‹จ (๊ถŒํ•œ ์—†์Œ: ์—ญํ•  '๊ตฐ์ˆ˜๋‹ด๋‹น'์€(๋Š”) ๊ฒฐ์ • ๋ถˆ๊ฐ€(์ž๋™ ์ง‘ํ–‰ ๊ธˆ์ง€))\n", " - ์—ด๋žŒ์ž๊ฐ€ ์Šน์ธ: ์ฐจ๋‹จ (๊ถŒํ•œ ์—†์Œ: ์—ญํ•  '์—ด๋žŒ์ž'์€(๋Š”) ๊ฒฐ์ • ๋ถˆ๊ฐ€(์ž๋™ ์ง‘ํ–‰ ๊ธˆ์ง€))\n", " - ๋ฏธ๋“ฑ๋ก ์‚ฌ์šฉ์ž ์Šน์ธ: ์ฐจ๋‹จ (๊ถŒํ•œ ์—†์Œ: ์—ญํ•  '๋ฏธ๋“ฑ๋ก'์€(๋Š”) ๊ฒฐ์ • ๋ถˆ๊ฐ€(์ž๋™ ์ง‘ํ–‰ ๊ธˆ์ง€))\n", " - ์Šน์ธ ๊ทผ๊ฑฐ ์—†์ด ๊ฒฐ์ •: ์ฐจ๋‹จ (๊ฒฐ์ • ๊ทผ๊ฑฐ 5์ž ์ด์ƒ ํ•„์š”)\n", " - ๋ฐ˜๋ ค ๊ฑด ์ง‘ํ–‰: ์ฐจ๋‹จ (์Šน์ธ๋˜์ง€ ์•Š์€ ๊ฑด ์ง‘ํ–‰ ๋ถˆ๊ฐ€)\n", " - ๋ฏธ์Šน์ธ ๊ฑด ์ง‘ํ–‰(์—†๋Š” ์ œ์•ˆ): ์ฐจ๋‹จ (์Šน์ธ๋˜์ง€ ์•Š์€ ๊ฑด ์ง‘ํ–‰ ๋ถˆ๊ฐ€)\n", " - ์ด๋ฏธ ์ง‘ํ–‰๋œ ๊ฑด ์žฌ์Šน์ธ: ์ฐจ๋‹จ (์ด๋ฏธ ์ง‘ํ–‰)\n", "์šฐํšŒ ์‹œ๋„ 8๊ฑด ์ค‘ ์ฐจ๋‹จ 8๊ฑด, ๊ฐ์‚ฌ ์ด๋ ฅ 33๊ฑด ํ•ด์‹œ ๊ฒ€์ฆ 33/33\n", "๊ตฐ์ˆ˜์ฒ˜์žฅ ์Šน์ธ ์—†์ด ์ง‘ํ–‰๋œ ๊ฑด 0๊ฑด. ์‹ค์ œ ๋ฌผ์ž ์ „ํ™˜ ๊ฒฐ์ •์€ ์ง€ํœ˜๊ณ„ํ†ต๊ณผ ๊ตฐ์ˆ˜ ๊ทœ์ •์— ๋”ฐ๋ฅด๋ฉฐ ๋ณธ ์ฝ”๋“œ๋Š” ํ•™์Šต์šฉ ๋ชจ์˜์ด๋‹ค.\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 08. ์ „๋ฉด์ „ ๋ฐœ๋ฐœ ๋ชจ์˜ ์ „ ๋ถ€๋Œ€ ๋ณด๊ธ‰ํ’ˆ ๋™์‹œ ๊ธ‰๊ฐ ์ŠคํŠธ๋ ˆ์Šค ์‹œ๋ฎฌ๋ ˆ์ด์…˜\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "์ „๋ฉด์ „ ๋ชจ์˜ ์ „ ๋ถ€๋Œ€ ๋ณด๊ธ‰ํ’ˆ ๋™์‹œ ๊ธ‰๊ฐ ์ŠคํŠธ๋ ˆ์Šค ์‹œ๋ฎฌ๋ ˆ์ด์…˜์„ ์ˆ˜ํ–‰ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **์ „ ๋ถ€๋Œ€์˜ ์†Œ๋ชจ๊ฐ€ ๋™์‹œ์— ๋Š˜๊ณ  ๋ณด๊ธ‰์ด ์ค„์–ด๋“œ๋Š” ๊ฐ€์ƒ ์ถฉ๊ฒฉ ์‹œ๋‚˜๋ฆฌ์˜ค๋กœ ๊ฒฝ๋ณด ์—”์ง„์˜ ์ŠคํŠธ๋ ˆ์Šค ์‹œ๋ฎฌ๋ ˆ์ด์…˜์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **์†Œ๋ชจ๋ฅผ 1.5๋ฐฐ, 2๋ฐฐ, 3๋ฐฐ๋กœ ํ‚ค์šฐ๊ณ  ๋ณด๊ธ‰๋Ÿ‰์„ ์ ˆ๋ฐ˜์œผ๋กœ ์ค„์ด๋Š” ์ถฉ๊ฒฉ์„ 12์ผ๊ฐ„ ๊ฐ€์ •ํ•ด ์žฌ๊ณ ๋ฅผ ๋‹ค์‹œ ์‚ฐ์ถœํ•˜๊ณ  ๋ชจ๋ธ์— ์ ์šฉํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/dashboard/str01_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": [ "\"\"\"์ „ ๋ถ€๋Œ€ ์†Œ๋ชจ๊ฐ€ ๋™์‹œ์— ํฌ๊ฒŒ ๋Š˜๊ณ  ๋ณด๊ธ‰์ด ์ฐจ์งˆ์„ ๋นš๋Š” ๊ฐ€์ƒ ์ถฉ๊ฒฉ ์‹œ๋‚˜๋ฆฌ์˜ค(์†Œ๋ชจ 1.5ยท2ยท3๋ฐฐ, ๋ณด๊ธ‰ 50% ๊ฐ์†Œ)๋ฅผ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•˜๊ณ , ๊ฒฝ๋ณด ์—”์ง„์˜ ๋“ฑ๊ธ‰ ์ •ํ™•๋„์™€ ๋Œ€์‹œ๋ณด๋“œ ๊ฐฑ์‹ (๋กœ๋”ฉ) ์‹œ๊ฐ„์„ ์ธก์ •ํ•˜๋ฉฐ ์˜ˆ์ธก ๋ชจ๋ธ์— DOS ์•ˆ์ „์žฅ์น˜๋ฅผ ๋”ํ•œ ๋ณด๊ฐ• ํšจ๊ณผ๋ฅผ ํ™•์ธํ•œ๋‹ค.\"\"\"\n", "import time\n", "import numpy as np\n", "import pandas as pd\n", "from sklearn.ensemble import GradientBoostingRegressor\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ฐ€์ƒ ๋ถ€๋Œ€ 20๊ฐœ x ํ•ต์‹ฌ ๋ณด๊ธ‰ํ’ˆ 5์ข… ์ผ์ผ ์žฌ๊ณ ยท์†Œ๋ชจ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ (์‹ค์ œ ๋ถ€๋Œ€ยท์ˆ˜๋Ÿ‰ยท๊ธฐ์ค€์ผ์ˆ˜ ์•„๋‹˜)\n", "# ํ•ต์‹ฌ์„ฑ๊ณผ์ง€ํ‘œ(KPI) = ๊ธฐ์ค€ ๋ณด๊ธ‰์ผ์ˆ˜(DOS) = ํ˜„์žฌ ์žฌ๊ณ  / ์ตœ๊ทผ ํ‰๊ท  ์ผ์ผ ์†Œ๋ชจ๋Ÿ‰. ๋ชฉํ‘œ DOS๋Š” ๊ต์œก์šฉ ๊ฐ€์ •๊ฐ’์ด๋‹ค.\n", "# ๊ฒฐํ• ์œ„ํ—˜ ์ผ์ˆ˜ H = ๋ณด๊ธ‰์ด ์—†๋‹ค๊ณ  ๊ฐ€์ •ํ•  ๋•Œ ํ˜„์žฌ ์žฌ๊ณ ๊ฐ€ ๋ฐ”๋‹ฅ๋‚˜๊ธฐ๊นŒ์ง€ ๋‚จ์€ ์ผ์ˆ˜(๋ฏธ๋ž˜ ์‹ค์ œ ์†Œ๋ชจ๋Ÿ‰์œผ๋กœ ๊ณ„์‚ฐํ•œ ์ •๋‹ต).\n", "# ---------------------------------------------------------------------------\n", "ITEMS = {\"์‹๋Ÿ‰\": (100, 0.3, 10), \"ํ”ผ๋ณต\": (20, 0.1, 15), \"์œ ๋ฅ˜\": (300, 1.2, 7), \"ํƒ„์•ฝ\": (60, 1.5, 6), \"์ˆ˜๋ฆฌ๋ถ€์†\": (15, 0.5, 12)} # (๊ธฐ๋ณธ ์ผ์†Œ๋ชจ, ํ…œํฌ ๋ฏผ๊ฐ๋„, ๋ชฉํ‘œ DOS)\n", "N_UNIT, DAYS = 20, 160\n", "UNITS = [f\"๊ฐ€์ƒ{u:02d}๋ถ€๋Œ€\" for u in range(1, N_UNIT + 1)]\n", "\n", "\n", "def make_data(seed=1, n_unit=N_UNIT, days=DAYS):\n", " \"\"\"์ผ ๋‹จ์œ„ ์žฌ๊ณ  ์›์žฅ: unit, item, day, tempo(์‹ค์ œ ์ž‘์ „ ํ…œํฌ), plan(์‚ฌ์ „ ๊ณ„ํš ํ…œํฌ), wx(์•…์ฒœํ›„), consume, resupply, stock.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " rows = []\n", " for u in range(1, n_unit + 1):\n", " s, ph = rng.uniform(0.7, 1.5), rng.uniform(0, 6.28)\n", " surge = np.zeros(days)\n", " for st in rng.choice(days - 10, 4, replace=False):\n", " surge[st:st + int(rng.integers(5, 10))] = rng.uniform(0.4, 0.8)\n", " tempo = 1 + 0.25 * np.sin(2 * np.pi * np.arange(days) / 30 + ph) + surge\n", " plan = tempo + rng.normal(0, 0.08, days)\n", " wx = (rng.random(days) < 0.15).astype(int)\n", " for it, (b, sens, tg) in ITEMS.items():\n", " stock, off = tg * b * s * rng.uniform(0.8, 1.2), int(rng.integers(0, 4))\n", " for t in range(days):\n", " c = b * s * (1 + sens * (tempo[t] - 1)) * (1 + 0.12 * wx[t]) * rng.normal(1, 0.08)\n", " r = max(tg * b * s - stock, 0) * rng.uniform(0.9, 1.1) * (0.5 if rng.random() < 0.08 else 1) if (t % 4 == off and t > 0) else 0.0 # 4์ผ ์ฃผ๊ธฐ ์ •๊ธฐ ๋ณด๊ธ‰: ๋ชฉํ‘œ ์žฌ๊ณ ๊นŒ์ง€ ์ฑ„์›€(๊ฐ€๋” ์ˆ˜์†ก ์ฐจ์งˆ๋กœ ์ ˆ๋ฐ˜)\n", " stock = max(stock + r - c, 0.0)\n", " rows.append((f\"๊ฐ€์ƒ{u:02d}๋ถ€๋Œ€\", it, t, tempo[t], plan[t], wx[t], c, r, stock))\n", " return pd.DataFrame(rows, columns=[\"unit\", \"item\", \"day\", \"tempo\", \"plan\", \"wx\", \"consume\", \"resupply\", \"stock\"])\n", "\n", "\n", "def make_samples(d, lo=21, hi=125):\n", " \"\"\"(๋ถ€๋Œ€, ํ’ˆ๋ชฉ, ๊ธฐ์ค€์ผ t0)๋ณ„ ํŠน์„ฑ(ํ”ผ์ฒ˜)๊ณผ ์ •๋‹ต H. ํ”ผ์ฒ˜๋Š” t0 ์‹œ์ ๊นŒ์ง€ ์•Œ ์ˆ˜ ์žˆ๋Š” ๊ฐ’๊ณผ ์‚ฌ์ „ ๊ณ„ํšยท๊ธฐ์ƒ ์˜ˆ๋ณด๋งŒ ์‚ฌ์šฉํ•œ๋‹ค.\"\"\"\n", " out = []\n", " for (u, it), g in d.groupby([\"unit\", \"item\"]):\n", " c, stk, rs = g[\"consume\"].values, g[\"stock\"].values, g[\"resupply\"].values\n", " pl, wx, tp = g[\"plan\"].values, g[\"wx\"].values, g[\"tempo\"].values\n", " cs = np.cumsum(c)\n", " tg = ITEMS[it][2] * ITEMS[it][0]\n", " for t0 in range(lo, hi):\n", " idx = np.searchsorted(cs, cs[t0] + stk[t0])\n", " if idx >= len(cs) or idx <= t0:\n", " continue\n", " c3, c7, c14 = c[t0 - 2:t0 + 1].mean(), c[t0 - 6:t0 + 1].mean(), c[t0 - 13:t0 + 1].mean()\n", " last = [k for k in range(t0, t0 - 15, -1) if rs[k] > 0]\n", " out.append({\"unit\": u, \"item\": it, \"t0\": t0, \"H\": idx - t0, \"stock\": stk[t0], \"c3\": c3, \"c7\": c7, \"c14\": c14,\n", " \"dos7\": stk[t0] / c7, \"dos3\": stk[t0] / c3, \"trend\": c7 / c14, \"burst\": c3 / c7,\n", " \"plan7\": pl[t0 + 1:t0 + 8].mean(), \"plan_ratio\": pl[t0 + 1:t0 + 8].mean() / tp[t0], \"tempo0\": tp[t0],\n", " \"wx_fc\": wx[t0 + 1:t0 + 8].mean(), \"since_rs\": (t0 - last[0]) if last else 15,\n", " \"rs_avg\": rs[t0 - 13:t0 + 1].sum() / 14, \"fill\": stk[t0] / tg, \"item_code\": list(ITEMS).index(it)})\n", " return pd.DataFrame(out)\n", "\n", "\n", "def mape(a, b):\n", " return float(np.mean(np.abs(a - b) / a))\n", "\n", "d0 = make_data()\n", "s0 = make_samples(d0)\n", "X = [c for c in s0.columns if c not in (\"unit\", \"item\", \"t0\", \"H\")]\n", "gbr = GradientBoostingRegressor(n_estimators=150, max_depth=3, learning_rate=0.08, random_state=0).fit(s0[s0[\"t0\"] <= 90][X], s0[s0[\"t0\"] <= 90][\"H\"])\n", "T_SHOCK = 100\n", "def level(h):\n", " return np.where(h <= 2.5, 3, np.where(h <= 4.5, 2, np.where(h <= 7.5, 1, 0)))\n", "NAME = np.array([\"์ •์ƒ\", \"์ฃผ์˜\", \"๊ฒฝ๊ณ„\", \"์‹ฌ๊ฐ\"])\n", "\n", "def shock(d, mult, rs_cut=0.5, span=12):\n", " \"\"\"T_SHOCK์ผ๋ถ€ํ„ฐ span์ผ ๋™์•ˆ ๋ชจ๋“  ๋ถ€๋Œ€์˜ ์†Œ๋ชจ๋ฅผ mult๋ฐฐ, ๋ณด๊ธ‰๋Ÿ‰์„ rs_cut๋ฐฐ๋กœ ๋ฐ”๊พธ๊ณ  ์žฌ๊ณ ๋ฅผ ๋‹ค์‹œ ๊ณ„์‚ฐํ•œ๋‹ค(๊ฐ€์ƒ ์ถฉ๊ฒฉ ์‹œ๋‚˜๋ฆฌ์˜ค).\"\"\"\n", " d = d.copy()\n", " m = (d[\"day\"] >= T_SHOCK) & (d[\"day\"] < T_SHOCK + span)\n", " d.loc[m, \"consume\"] *= mult; d.loc[m, \"resupply\"] *= rs_cut\n", " for (u, it), g in d.groupby([\"unit\", \"item\"]):\n", " idx, st = g.index, g[\"stock\"].values[T_SHOCK - 1]\n", " new = []\n", " for t in range(T_SHOCK, DAYS):\n", " st = max(st + d.loc[idx[t], \"resupply\"] - d.loc[idx[t], \"consume\"], 0.0); new.append(st)\n", " d.loc[idx[T_SHOCK:], \"stock\"] = new\n", " return d\n", "\n", "def predict(s, guard):\n", " p = np.clip(gbr.predict(s[X]), 1, None)\n", " if guard: # ์•ˆ์ „์žฅ์น˜: ์ „ ๋ถ€๋Œ€ ์†Œ๋ชจ๊ฐ€ ๋™์‹œ์— ๊ธ‰์ฆ(์ถฉ๊ฒฉ ๋ชจ๋“œ)ํ•œ ๋‚ ์—๋Š” ์ตœ๊ทผ 3์ผ ์†Œ๋ชจ ๊ธฐ์ค€ DOS๊ฐ€ ๋” ์งง์œผ๋ฉด ๊ทธ ๊ฐ’์„ ์ฑ„ํƒ\n", " share = s.groupby(\"t0\")[\"burst\"].transform(lambda b: (b > 1.15).mean())\n", " p = np.where(share > 0.4, np.minimum(p, np.clip(s[\"dos3\"].values * 1.05, 1, None)), p)\n", " return p\n", "\n", "def evaluate(d, guard):\n", " s = make_samples(d, lo=T_SHOCK + 1, hi=T_SHOCK + 14)\n", " lt, lp = level(s[\"H\"].values), level(predict(s, guard))\n", " acc = float((lt == lp).mean()) # 4๋“ฑ๊ธ‰ ์ผ์น˜์œจ\n", " alert = float(((lt >= 1) == (lp >= 1)).mean()) # ๊ฒฝ๋ณด ๋ฐœ๋ น(์ฃผ์˜ ์ด์ƒ) ์—ฌ๋ถ€ ์ผ์น˜์œจ\n", " miss = float(((lt >= 2) & (lp < 2)).sum() / max((lt >= 2).sum(), 1)) # ๊ฒฝ๊ณ„ ์ด์ƒ ๋ฏธํƒ๋ฅ \n", " return acc, alert, miss, len(s), lt\n", "\n", "print(\"[์ถฉ๊ฒฉ ์‹œ๋‚˜๋ฆฌ์˜ค ์‹œํ—˜] ์ „ ๋ถ€๋Œ€ยท์ „ ํ’ˆ๋ชฉ ๋™์‹œ ์†Œ๋ชจ ์ฆ๊ฐ€ + ๋ณด๊ธ‰ ๊ฐ์†Œ(12์ผ๊ฐ„, ๊ฐ€์ƒ ๋ชจ๋ธ) [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\")\n", "rows = []\n", "for mult in (1.0, 1.5, 2.0, 3.0):\n", " d = d0 if mult == 1.0 else shock(d0, mult)\n", " a0, al0, m0, n, lt = evaluate(d, False); a1, al1, m1, _, _ = evaluate(d, True)\n", " rows.append({\"์†Œ๋ชจ ๋ฐฐ์œจ\": f\"{mult}๋ฐฐ\", \"ํ‘œ๋ณธ\": n, \"๊ฒฝ๊ณ„ ์ด์ƒ\": f\"{(lt >= 2).mean():.0%}\", \"๋ชจ๋ธ๋งŒ ๊ฒฝ๋ณด์ •ํ™•๋„\": f\"{al0:.1%}\", \"๋ชจ๋ธ๋งŒ ๋ฏธํƒ\": f\"{m0:.1%}\",\n", " \"๋ณด๊ฐ• ๊ฒฝ๋ณด์ •ํ™•๋„\": f\"{al1:.1%}\", \"๋ณด๊ฐ• ๋ฏธํƒ\": f\"{m1:.1%}\", \"๋ณด๊ฐ• 4๋“ฑ๊ธ‰\": f\"{a1:.1%}\"})\n", "res = pd.DataFrame(rows)\n", "print(res.to_string(index=False))\n", "acc_guard = float(res[\"๋ณด๊ฐ• ๊ฒฝ๋ณด์ •ํ™•๋„\"].str.rstrip(\"%\").astype(float).min()) / 100\n", "\n", "# ๋Œ€์‹œ๋ณด๋“œ ๊ฐฑ์‹ (๋กœ๋”ฉ) ์‹œ๊ฐ„: ํ•œ ์‹œ์ ์˜ ์ „์ฒด ์ƒํ™ฉํŒ(100๊ฑด ์˜ˆ์ธก + ๋“ฑ๊ธ‰ ์ง‘๊ณ„ + ๋ฌธ์ž์—ด ์ƒ์„ฑ)์„ 50ํšŒ ๋ฐ˜๋ณต ์ธก์ •\n", "d3 = shock(d0, 2.0)\n", "times = []\n", "for _ in range(50):\n", " t0 = time.perf_counter()\n", " s = make_samples(d3, lo=T_SHOCK + 5, hi=T_SHOCK + 6)\n", " p = predict(s, True)\n", " lv = pd.Series(NAME[level(p)]).value_counts().reindex(NAME, fill_value=0)\n", " board = \" / \".join(f\"{k} {v}\" for k, v in lv.items())\n", " times.append(time.perf_counter() - t0)\n", "times = np.array(times)\n", "print(f\"\\n์ƒํ™ฉํŒ 1ํšŒ ๊ฐฑ์‹ : ํ‰๊ท  {times.mean() * 1000:.0f}ms, 95๋ฐฑ๋ถ„์œ„ {np.percentile(times, 95) * 1000:.0f}ms, ์ตœ๋Œ€ {times.max() * 1000:.0f}ms (๋ชฉํ‘œ 2์ดˆ ์ด๋‚ด, ํ™˜๊ฒฝ์— ๋”ฐ๋ผ ๋‹ค๋ฆ„)\")\n", "print(\"์˜ˆ์‹œ ์ƒํ™ฉํŒ(์†Œ๋ชจ 2๋ฐฐ ์ถฉ๊ฒฉ 105์ผ์ฐจ):\", board)\n", "d_n = evaluate(d0, True)[1]\n", "print(f\"\\n์ถฉ๊ฒฉ ๊ทœ๋ชจ๋ฅผ ํ‚ค์šธ์ˆ˜๋ก ๊ฒฝ๋ณด ๋Œ€์ƒ์ด ๋Š˜๊ณ  ๋ชจ๋ธ๋งŒ ์“ฐ๋ฉด ๋ฏธํƒ์ด ๋Š˜์–ด๋‚˜๋ฏ€๋กœ, ๋‹จ๊ธฐ ์†Œ๋ชจ ๊ธฐ๋ฐ˜ ์•ˆ์ „์žฅ์น˜๋กœ ๋ณด๊ฐ•ํ–ˆ๋‹ค(์ •์ƒ ์‹œ ๊ฒฝ๋ณด ์ •ํ™•๋„ {d_n:.1%}).\")\n", "ok = acc_guard >= 0.95 and times.max() < 2.0\n", "print(f\"๋ชฉํ‘œ(๋Œ€์‹œ๋ณด๋“œ ๋กœ๋”ฉ 2์ดˆ ์ด๋‚ด, ๊ฒฝ๋ณด ๋ฐœ๋ น ์ •ํ™•๋„ 95% ์ด์ƒ): ๋กœ๋”ฉ {'๋‹ฌ์„ฑ' if times.max() < 2 else '๋ฏธ๋‹ฌ'}, ์ตœ์ € ๊ฒฝ๋ณด ๋ฐœ๋ น ์ •ํ™•๋„ {acc_guard:.1%} {'๋‹ฌ์„ฑ' if acc_guard >= 0.95 else '๋ฏธ๋‹ฌ'}\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 08 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[์ถฉ๊ฒฉ ์‹œ๋‚˜๋ฆฌ์˜ค ์‹œํ—˜] ์ „ ๋ถ€๋Œ€ยท์ „ ํ’ˆ๋ชฉ ๋™์‹œ ์†Œ๋ชจ ์ฆ๊ฐ€ + ๋ณด๊ธ‰ ๊ฐ์†Œ(12์ผ๊ฐ„, ๊ฐ€์ƒ ๋ชจ๋ธ) [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\n", "์†Œ๋ชจ ๋ฐฐ์œจ ํ‘œ๋ณธ ๊ฒฝ๊ณ„ ์ด์ƒ ๋ชจ๋ธ๋งŒ ๊ฒฝ๋ณด์ •ํ™•๋„ ๋ชจ๋ธ๋งŒ ๋ฏธํƒ ๋ณด๊ฐ• ๊ฒฝ๋ณด์ •ํ™•๋„ ๋ณด๊ฐ• ๋ฏธํƒ ๋ณด๊ฐ• 4๋“ฑ๊ธ‰\n", " 1.0๋ฐฐ 1279 19% 96.0% 6.4% 96.0% 6.4% 92.4%\n", " 1.5๋ฐฐ 726 81% 92.8% 18.3% 96.0% 13.9% 61.8%\n", " 2.0๋ฐฐ 488 94% 97.1% 17.5% 100.0% 0.7% 80.9%\n", " 3.0๋ฐฐ 237 96% 100.0% 10.1% 100.0% 0.0% 86.1%\n", "\n", "์ƒํ™ฉํŒ 1ํšŒ ๊ฐฑ์‹ : ํ‰๊ท  28ms, 95๋ฐฑ๋ถ„์œ„ 33ms, ์ตœ๋Œ€ 39ms (๋ชฉํ‘œ 2์ดˆ ์ด๋‚ด, ํ™˜๊ฒฝ์— ๋”ฐ๋ผ ๋‹ค๋ฆ„)\n", "์˜ˆ์‹œ ์ƒํ™ฉํŒ(์†Œ๋ชจ 2๋ฐฐ ์ถฉ๊ฒฉ 105์ผ์ฐจ): ์ •์ƒ 0 / ์ฃผ์˜ 0 / ๊ฒฝ๊ณ„ 26 / ์‹ฌ๊ฐ 15\n", "\n", "์ถฉ๊ฒฉ ๊ทœ๋ชจ๋ฅผ ํ‚ค์šธ์ˆ˜๋ก ๊ฒฝ๋ณด ๋Œ€์ƒ์ด ๋Š˜๊ณ  ๋ชจ๋ธ๋งŒ ์“ฐ๋ฉด ๋ฏธํƒ์ด ๋Š˜์–ด๋‚˜๋ฏ€๋กœ, ๋‹จ๊ธฐ ์†Œ๋ชจ ๊ธฐ๋ฐ˜ ์•ˆ์ „์žฅ์น˜๋กœ ๋ณด๊ฐ•ํ–ˆ๋‹ค(์ •์ƒ ์‹œ ๊ฒฝ๋ณด ์ •ํ™•๋„ 96.0%).\n", "๋ชฉํ‘œ(๋Œ€์‹œ๋ณด๋“œ ๋กœ๋”ฉ 2์ดˆ ์ด๋‚ด, ๊ฒฝ๋ณด ๋ฐœ๋ น ์ •ํ™•๋„ 95% ์ด์ƒ): ๋กœ๋”ฉ ๋‹ฌ์„ฑ, ์ตœ์ € ๊ฒฝ๋ณด ๋ฐœ๋ น ์ •ํ™•๋„ 96.0% ๋‹ฌ์„ฑ\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 09. AAR ๊ธฐ๋ฐ˜ ๊ฒฐํ• ๊ฒฝ๋ณด ์ง€์—ฐ ์›์ธ ๋ถ„์„ ๋ฐ ๊ฒฝ๋ณด ์ž„๊ณ„ ํŒŒ๋ผ๋ฏธํ„ฐ ์žฌ๋ณด์ •\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "AAR ๊ธฐ๋ฐ˜ ๊ฒฐํ• ๊ฒฝ๋ณด ์ง€์—ฐ ์›์ธ์„ ๋ถ„์„ํ•˜๊ณ  ์ž„๊ณ„ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์žฌ๋ณด์ •ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **AAR ์‚ฌ๋ก€์˜ ๊ฒฐํ• ๊ฒฝ๋ณด ์ง€์—ฐ ์›์ธ์„ ๋ถ„์„ํ•˜๊ณ  ๊ฒฝ๋ณด ์ž„๊ณ„ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์žฌ๋ณด์ •ํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **์‹ค์ œ ๊ฒฐํ•(์žฌ๊ณ  0 ์ง„์ž…) ์‚ฌ๊ฑด 48๊ฑด์— ๋Œ€ํ•ด ๊ธฐ์กด ๋‹จ์ˆœ DOS ๊ฒฝ๋ณด์˜ ์ •์‹œยท์ง€์—ฐยท๋ฏธํƒ์„ ๋ถ„๋ฅ˜ํ•˜๊ณ  ์›์ธ์„ ํ…œํฌ ๊ธ‰๋ณ€, ์ถ”์ • ์˜ค์ฐจ, ์ž„๊ณ„ ๊ณผ์†Œ๋กœ ๋‚˜๋ˆˆ๋‹ค ๊ณผ์ •์„ ์ง„ํ–‰ํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `reports/str01_roi_analysis.md` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ์ถ”์ • ์‚ฐ์‹์˜ ๊ฐ๊ด€์„ฑ ๋ฐ ๊ตฐ ์žฌ์ •/๊ตฐ์ˆ˜ ๊ธฐํš ๋ถ€์„œ ์„ค๋“๋ ฅ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ๋น„์šฉ ์ ˆ๊ฐ์•ก, ์‹œ๊ฐ„ ๋‹จ์ถ•๋ฅ  ๋“ฑ 3๋Œ€ ์ •๋Ÿ‰ ํšจ๊ณผ ์ง€ํ‘œ ์‚ฐ์ถœ\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ROI ๋ถ„์„ ๋ณด๊ณ ์„œ ์™„์„ฑ ๋ฐ ์ฃผ์š” ์ง€ํ‘œ ๋ฏผ๊ฐ๋„ ๋ถ„์„ ์ˆ˜๋ก\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ํšŒ๊ณ„/๊ธฐํš ๊ธฐ์ค€ ๋ชจ๋ธ๊ณผ์˜ ๋น„๊ต ๊ฒ€์ฆ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"AAR ์‚ฌ๋ก€์—์„œ ๊ฒฐํ• ๊ฒฝ๋ณด๊ฐ€ ๋Šฆ๊ฒŒ ๋‚˜๊ฐ„ ์›์ธ(์ž„๊ณ„ ๊ณผ์†Œ, ํ…œํฌ ๊ธ‰๋ณ€ ๋ฏธ๋ฐ˜์˜, ์˜ˆ์ธก ์˜ค์ฐจ)์„ ๋ถ„๋ฅ˜ํ•˜๊ณ , ๊ฒฝ๋ณด ์ž„๊ณ„ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋ณด์ • ๊ธฐ๊ฐ„์—์„œ ์žฌ๋ณด์ •ํ•ด ๊ฒ€์ฆ ๊ธฐ๊ฐ„์—์„œ ์„ ํ–‰ ์‹œ๊ฐ„๊ณผ ์˜คํƒ๋ฅ ์„ ๋น„๊ตํ•˜๋ฉฐ, ๋น„์šฉ ์ ˆ๊ฐ์•กยท์‹œ๊ฐ„ ๋‹จ์ถ•๋ฅ ยท๊ฒฐํ• ์‚ฌ๊ฑด ๊ฐ์†Œ์œจ์˜ 3๋Œ€ ์ •๋Ÿ‰ ํšจ๊ณผ ์ง€ํ‘œ๋ฅผ ๊ฐ€์ •๊ณผ ์‚ฐ์‹์œผ๋กœ ์‚ฐ์ถœํ•œ๋‹ค.\"\"\"\n", "import numpy as np\n", "import pandas as pd\n", "from sklearn.ensemble import GradientBoostingRegressor\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ฐ€์ƒ ๋ถ€๋Œ€ 20๊ฐœ x ํ•ต์‹ฌ ๋ณด๊ธ‰ํ’ˆ 5์ข… ์ผ์ผ ์žฌ๊ณ ยท์†Œ๋ชจ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ (์‹ค์ œ ๋ถ€๋Œ€ยท์ˆ˜๋Ÿ‰ยท๊ธฐ์ค€์ผ์ˆ˜ ์•„๋‹˜)\n", "# ํ•ต์‹ฌ์„ฑ๊ณผ์ง€ํ‘œ(KPI) = ๊ธฐ์ค€ ๋ณด๊ธ‰์ผ์ˆ˜(DOS) = ํ˜„์žฌ ์žฌ๊ณ  / ์ตœ๊ทผ ํ‰๊ท  ์ผ์ผ ์†Œ๋ชจ๋Ÿ‰. ๋ชฉํ‘œ DOS๋Š” ๊ต์œก์šฉ ๊ฐ€์ •๊ฐ’์ด๋‹ค.\n", "# ๊ฒฐํ• ์œ„ํ—˜ ์ผ์ˆ˜ H = ๋ณด๊ธ‰์ด ์—†๋‹ค๊ณ  ๊ฐ€์ •ํ•  ๋•Œ ํ˜„์žฌ ์žฌ๊ณ ๊ฐ€ ๋ฐ”๋‹ฅ๋‚˜๊ธฐ๊นŒ์ง€ ๋‚จ์€ ์ผ์ˆ˜(๋ฏธ๋ž˜ ์‹ค์ œ ์†Œ๋ชจ๋Ÿ‰์œผ๋กœ ๊ณ„์‚ฐํ•œ ์ •๋‹ต).\n", "# ---------------------------------------------------------------------------\n", "ITEMS = {\"์‹๋Ÿ‰\": (100, 0.3, 10), \"ํ”ผ๋ณต\": (20, 0.1, 15), \"์œ ๋ฅ˜\": (300, 1.2, 7), \"ํƒ„์•ฝ\": (60, 1.5, 6), \"์ˆ˜๋ฆฌ๋ถ€์†\": (15, 0.5, 12)} # (๊ธฐ๋ณธ ์ผ์†Œ๋ชจ, ํ…œํฌ ๋ฏผ๊ฐ๋„, ๋ชฉํ‘œ DOS)\n", "N_UNIT, DAYS = 20, 160\n", "UNITS = [f\"๊ฐ€์ƒ{u:02d}๋ถ€๋Œ€\" for u in range(1, N_UNIT + 1)]\n", "\n", "\n", "def make_data(seed=1, n_unit=N_UNIT, days=DAYS):\n", " \"\"\"์ผ ๋‹จ์œ„ ์žฌ๊ณ  ์›์žฅ: unit, item, day, tempo(์‹ค์ œ ์ž‘์ „ ํ…œํฌ), plan(์‚ฌ์ „ ๊ณ„ํš ํ…œํฌ), wx(์•…์ฒœํ›„), consume, resupply, stock.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " rows = []\n", " for u in range(1, n_unit + 1):\n", " s, ph = rng.uniform(0.7, 1.5), rng.uniform(0, 6.28)\n", " surge = np.zeros(days)\n", " for st in rng.choice(days - 10, 4, replace=False):\n", " surge[st:st + int(rng.integers(5, 10))] = rng.uniform(0.4, 0.8)\n", " tempo = 1 + 0.25 * np.sin(2 * np.pi * np.arange(days) / 30 + ph) + surge\n", " plan = tempo + rng.normal(0, 0.08, days)\n", " wx = (rng.random(days) < 0.15).astype(int)\n", " for it, (b, sens, tg) in ITEMS.items():\n", " stock, off = tg * b * s * rng.uniform(0.8, 1.2), int(rng.integers(0, 4))\n", " for t in range(days):\n", " c = b * s * (1 + sens * (tempo[t] - 1)) * (1 + 0.12 * wx[t]) * rng.normal(1, 0.08)\n", " r = max(tg * b * s - stock, 0) * rng.uniform(0.9, 1.1) * (0.5 if rng.random() < 0.08 else 1) if (t % 4 == off and t > 0) else 0.0 # 4์ผ ์ฃผ๊ธฐ ์ •๊ธฐ ๋ณด๊ธ‰: ๋ชฉํ‘œ ์žฌ๊ณ ๊นŒ์ง€ ์ฑ„์›€(๊ฐ€๋” ์ˆ˜์†ก ์ฐจ์งˆ๋กœ ์ ˆ๋ฐ˜)\n", " stock = max(stock + r - c, 0.0)\n", " rows.append((f\"๊ฐ€์ƒ{u:02d}๋ถ€๋Œ€\", it, t, tempo[t], plan[t], wx[t], c, r, stock))\n", " return pd.DataFrame(rows, columns=[\"unit\", \"item\", \"day\", \"tempo\", \"plan\", \"wx\", \"consume\", \"resupply\", \"stock\"])\n", "\n", "\n", "def make_samples(d, lo=21, hi=125):\n", " \"\"\"(๋ถ€๋Œ€, ํ’ˆ๋ชฉ, ๊ธฐ์ค€์ผ t0)๋ณ„ ํŠน์„ฑ(ํ”ผ์ฒ˜)๊ณผ ์ •๋‹ต H. ํ”ผ์ฒ˜๋Š” t0 ์‹œ์ ๊นŒ์ง€ ์•Œ ์ˆ˜ ์žˆ๋Š” ๊ฐ’๊ณผ ์‚ฌ์ „ ๊ณ„ํšยท๊ธฐ์ƒ ์˜ˆ๋ณด๋งŒ ์‚ฌ์šฉํ•œ๋‹ค.\"\"\"\n", " out = []\n", " for (u, it), g in d.groupby([\"unit\", \"item\"]):\n", " c, stk, rs = g[\"consume\"].values, g[\"stock\"].values, g[\"resupply\"].values\n", " pl, wx, tp = g[\"plan\"].values, g[\"wx\"].values, g[\"tempo\"].values\n", " cs = np.cumsum(c)\n", " tg = ITEMS[it][2] * ITEMS[it][0]\n", " for t0 in range(lo, hi):\n", " idx = np.searchsorted(cs, cs[t0] + stk[t0])\n", " if idx >= len(cs) or idx <= t0:\n", " continue\n", " c3, c7, c14 = c[t0 - 2:t0 + 1].mean(), c[t0 - 6:t0 + 1].mean(), c[t0 - 13:t0 + 1].mean()\n", " last = [k for k in range(t0, t0 - 15, -1) if rs[k] > 0]\n", " out.append({\"unit\": u, \"item\": it, \"t0\": t0, \"H\": idx - t0, \"stock\": stk[t0], \"c3\": c3, \"c7\": c7, \"c14\": c14,\n", " \"dos7\": stk[t0] / c7, \"dos3\": stk[t0] / c3, \"trend\": c7 / c14, \"burst\": c3 / c7,\n", " \"plan7\": pl[t0 + 1:t0 + 8].mean(), \"plan_ratio\": pl[t0 + 1:t0 + 8].mean() / tp[t0], \"tempo0\": tp[t0],\n", " \"wx_fc\": wx[t0 + 1:t0 + 8].mean(), \"since_rs\": (t0 - last[0]) if last else 15,\n", " \"rs_avg\": rs[t0 - 13:t0 + 1].sum() / 14, \"fill\": stk[t0] / tg, \"item_code\": list(ITEMS).index(it)})\n", " return pd.DataFrame(out)\n", "\n", "\n", "def mape(a, b):\n", " return float(np.mean(np.abs(a - b) / a))\n", "\n", "d = make_data()\n", "s = make_samples(d, lo=21, hi=126)\n", "X = [c for c in s.columns if c not in (\"unit\", \"item\", \"t0\", \"H\")]\n", "tr = s[s[\"t0\"] <= 90]\n", "gbr = GradientBoostingRegressor(n_estimators=150, max_depth=3, learning_rate=0.08, random_state=0).fit(tr[X], tr[\"H\"])\n", "s = s[s[\"t0\"] > 90].copy()\n", "s[\"pred\"] = np.clip(gbr.predict(s[X]), 1, None)\n", "P = {(r.unit, r.item, r.t0): r for r in s.itertuples()}\n", "\n", "# 1) ์‹ค์ œ ๊ฒฐํ• ์‚ฌ๊ฑด(์žฌ๊ณ  0 ์ง„์ž…) ๋ชฉ๋ก\n", "events = []\n", "for (u, it), g in d.groupby([\"unit\", \"item\"]):\n", " z = (g[\"stock\"].values <= 0)\n", " for t in range(95, 126):\n", " if z[t] and not z[t - 1]:\n", " events.append((u, it, t))\n", "print(f\"[AAR ๋Œ€์ƒ] 95~125์ผ ์‹ค์ œ ๊ฒฐํ• ์‚ฌ๊ฑด {len(events)}๊ฑด(์žฌ๊ณ  0 ์ง„์ž…), ์˜ˆ์ธก ํ‘œ๋ณธ {len(s)}๊ฑด [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\")\n", "\n", "REQ_LEAD = 2 # ๊ธด๊ธ‰ ์ „ํ™˜ยท์ˆ˜์†ก์— ํ•„์š”ํ•œ ์ตœ์†Œ ์„ ํ–‰ ์ผ์ˆ˜(๊ฐ€์ •)\n", "def first_alert(u, it, t_e, th, col):\n", " for t0 in range(max(91, t_e - 10), t_e):\n", " r = P.get((u, it, t0))\n", " if r is not None and getattr(r, col) <= th:\n", " return t0\n", " return None\n", "\n", "def run(th, evs, col=\"pred\"):\n", " ontime = late = miss = 0\n", " leads, rows = [], []\n", " for u, it, t_e in evs:\n", " a = first_alert(u, it, t_e, th, col)\n", " lead = 0 if a is None else t_e - a\n", " leads.append(lead)\n", " if a is None:\n", " miss += 1\n", " elif lead >= REQ_LEAD:\n", " ontime += 1\n", " else:\n", " late += 1\n", " rows.append((u, it, t_e, lead))\n", " # ์˜คํƒ: ๊ฒฝ๋ณด๊ฐ€ ๋‚ฌ์ง€๋งŒ 7์ผ ์•ˆ์— ๊ฒฐํ•์ด ์—†๋Š” ํ‘œ๋ณธ ๋น„์œจ\n", " ev = {(u, it): [] for u, it, _ in events}\n", " for u, it, t in events:\n", " ev[(u, it)].append(t)\n", " alerts = s[s[col] <= th]\n", " fa = sum(not any(0 <= te - r.t0 <= 7 for te in ev.get((r.unit, r.item), [])) for r in alerts.itertuples())\n", " return {\"ontime\": ontime, \"late\": late, \"miss\": miss, \"lead\": float(np.mean(leads)) if leads else 0.0, \"fa_rate\": fa / max(len(alerts), 1), \"alerts\": len(alerts)}, rows\n", "\n", "TH_V1 = 1.5\n", "v1, rows1 = run(TH_V1, events, \"dos7\")\n", "print(f\"\\n[๊ธฐ์กด ๊ฒฝ๋ณด v1: ๋‹จ์ˆœ DOS(์žฌ๊ณ /7์ผ ํ‰๊ท  ์†Œ๋ชจ)๊ฐ€ {TH_V1}์ผ ์ดํ•˜์ผ ๋•Œ ๊ฒฝ๋ณด, ์˜ˆ์ธก ์—†์Œ] ์ •์‹œ ๊ฒฝ๋ณด {v1['ontime']}๊ฑด, ์ง€์—ฐ {v1['late']}๊ฑด, ๋ฏธํƒ {v1['miss']}๊ฑด, ํ‰๊ท  ์„ ํ–‰ {v1['lead']:.1f}์ผ, ์˜คํƒ๋ฅ  {v1['fa_rate']:.0%}\")\n", "\n", "# 2) ์ง€์—ฐ ์›์ธ ๋ถ„์„\n", "cause = {\"์ž„๊ณ„ ๊ณผ์†Œ(๋” ์ผ์ฐ ์šธ๋ ธ์–ด์•ผ ํ•จ)\": 0, \"ํ…œํฌ ๊ธ‰๋ณ€ ๋ฏธ๋ฐ˜์˜\": 0, \"๋‹จ์ˆœ DOS์˜ ์ถ”์ • ์˜ค์ฐจ\": 0}\n", "for u, it, t_e, lead in rows1:\n", " if lead >= REQ_LEAD:\n", " continue\n", " r = P.get((u, it, max(91, t_e - REQ_LEAD - 1)))\n", " if r is None:\n", " continue\n", " if r.burst > 1.25:\n", " cause[\"ํ…œํฌ ๊ธ‰๋ณ€ ๋ฏธ๋ฐ˜์˜\"] += 1\n", " elif r.dos7 - r.H > 1.0:\n", " cause[\"๋‹จ์ˆœ DOS์˜ ์ถ”์ • ์˜ค์ฐจ\"] += 1\n", " else:\n", " cause[\"์ž„๊ณ„ ๊ณผ์†Œ(๋” ์ผ์ฐ ์šธ๋ ธ์–ด์•ผ ํ•จ)\"] += 1\n", "print(\"์ง€์—ฐยท๋ฏธํƒ ์‚ฌ๊ฑด์˜ ์›์ธ ๋ถ„๋ฅ˜:\", cause)\n", "\n", "# 3) ์ž„๊ณ„ ์žฌ๋ณด์ •: ๋ณด์ • ๊ธฐ๊ฐ„(95~110์ผ ์‚ฌ๊ฑด)์—์„œ ๊ฒฉ์ž ํƒ์ƒ‰, ๊ฒ€์ฆ ๊ธฐ๊ฐ„(111~125์ผ)์—์„œ ํ™•์ธ\n", "cal = [e for e in events if e[2] <= 110]; val = [e for e in events if e[2] > 110]\n", "grid = []\n", "for th in np.arange(2.0, 9.1, 0.5):\n", " m, _ = run(th, cal)\n", " score = m[\"ontime\"] / max(len(cal), 1) - 0.5 * max(m[\"fa_rate\"] - 0.5, 0) # ์ •์‹œ ๋น„์œจ ์ตœ๋Œ€ํ™”, ์˜คํƒ๋ฅ  50% ์ดˆ๊ณผ๋ถ„์€ ๋ฒŒ์ \n", " grid.append((float(th), score, m[\"ontime\"] / max(len(cal), 1), m[\"fa_rate\"]))\n", "G = pd.DataFrame(grid, columns=[\"์ž„๊ณ„\", \"์ ์ˆ˜\", \"์ •์‹œ์œจ\", \"์˜คํƒ๋ฅ \"]).sort_values(\"์ ์ˆ˜\", ascending=False)\n", "best = float(G.iloc[0][\"์ž„๊ณ„\"])\n", "print(\"\\n[์žฌ๋ณด์ • ๊ฒฉ์ž ํƒ์ƒ‰ - ๋ณด์ • ๊ธฐ๊ฐ„ ์ƒ์œ„ 3]\")\n", "print(G.head(3).round(3).to_string(index=False))\n", "v1v, _ = run(TH_V1, val, \"dos7\"); v2v, _ = run(best, val)\n", "print(f\"๊ฒ€์ฆ ๊ธฐ๊ฐ„ {len(val)}๊ฑด: v1(DOS {TH_V1}์ผ) ์ •์‹œ {v1v['ontime']}๊ฑด/๋ฏธํƒ {v1v['miss']}๊ฑด/์˜คํƒ๋ฅ  {v1v['fa_rate']:.0%} -> v2(์˜ˆ์ธก {best}์ผ) ์ •์‹œ {v2v['ontime']}๊ฑด/๋ฏธํƒ {v2v['miss']}๊ฑด/์˜คํƒ๋ฅ  {v2v['fa_rate']:.0%}, ํ‰๊ท  ์„ ํ–‰ {v1v['lead']:.1f}์ผ -> {v2v['lead']:.1f}์ผ\")\n", "v2, _ = run(best, events)\n", "\n", "# 4) 3๋Œ€ ์ •๋Ÿ‰ ํšจ๊ณผ ์ง€ํ‘œ(๊ฐ€์ • ๋‹จ๊ฐ€ยท๋น„์œจ์€ ๊ต์œก์šฉ)\n", "LOSS, FA_COST, AVOID = 500.0, 30.0, 0.7 # ๊ฒฐํ• 1๊ฑด ์†์‹ค, ์˜คํƒ 1๊ฑด ๋Œ€์‘๋น„์šฉ, ์ •์‹œ ๊ฒฝ๋ณด ์‹œ ๊ฒฐํ• ํšŒํ”ผ ๋น„์œจ(๊ฐ€์ •)\n", "def benefit(m):\n", " return m[\"ontime\"] * AVOID * LOSS - m[\"alerts\"] * m[\"fa_rate\"] * FA_COST\n", "saving = benefit(v2) - benefit(v1)\n", "time_cut = 1 - v1[\"lead\"] / max(v2[\"lead\"], 1e-9) if v2[\"lead\"] > v1[\"lead\"] else 0.0\n", "ev_cut = (v2[\"ontime\"] - v1[\"ontime\"]) * AVOID / max(len(events), 1)\n", "print(\"\\n[3๋Œ€ ์ •๋Ÿ‰ ํšจ๊ณผ ์ง€ํ‘œ(95~125์ผ ์ „์ฒด, ๊ฐ€์ • ๋‹จ๊ฐ€)]\")\n", "print(f\" 1) ๋น„์šฉ ์ ˆ๊ฐ์•ก = (์ •์‹œ ๊ฒฝ๋ณด ๊ฑด์ˆ˜ x ํšŒํ”ผ์œจ {AVOID:.0%} x ๊ฑด๋‹น ์†์‹ค {LOSS:.0f}) - (์˜คํƒ ๊ฑด์ˆ˜ x ๋Œ€์‘๋น„์šฉ {FA_COST:.0f}) ์˜ v2-v1 ์ฐจ์ด = {saving:,.0f}\")\n", "print(f\" 2) ์‹œ๊ฐ„ ๋‹จ์ถ•๋ฅ (๊ฒฝ๋ณด ์„ ํ–‰ ์‹œ๊ฐ„ ํ™•๋Œ€๋ถ„ ์ค‘ ์ถ”๊ฐ€ ํ™•๋ณด ๋น„์œจ) = {time_cut:.1%} (ํ‰๊ท  ์„ ํ–‰ {v1['lead']:.1f}์ผ -> {v2['lead']:.1f}์ผ)\")\n", "print(f\" 3) ๊ฒฐํ• ์‚ฌ๊ฑด ๊ฐ์†Œ์œจ = (์ •์‹œ ๊ฒฝ๋ณด ์ฆ๊ฐ€๋ถ„ x ํšŒํ”ผ์œจ) / ์ „์ฒด ์‚ฌ๊ฑด = {ev_cut:.1%}\")\n", "print(\" ์‚ฐ์‹์˜ ๊ฐ€์ •(์†์‹คยท๋Œ€์‘๋น„์šฉยทํšŒํ”ผ์œจ)์€ ๊ตฐ ์žฌ์ •ยท๊ตฐ์ˆ˜ ๊ธฐํš ๋ถ€์„œ๊ฐ€ ์‹ค์ œ ๋‹จ๊ฐ€๋กœ ๊ต์ฒดํ•ด ์žฌ์‚ฐ์ถœํ•œ๋‹ค.\")\n", "\n", "# 5) UAT ์ฒดํฌ๋ฆฌ์ŠคํŠธ(์ž๋™ ์ ๊ฒ€)์™€ ๊ฐœ์„  ์š”๊ตฌ\n", "checks = [(\"v2 ์ •์‹œ์œจ์ด v1 ์ด์ƒ\", v2[\"ontime\"] >= v1[\"ontime\"]), (\"v2 ๋ฏธํƒ์ด v1 ์ดํ•˜\", v2[\"miss\"] <= v1[\"miss\"]), (\"๊ฒ€์ฆ ๊ธฐ๊ฐ„ ์„ ํ–‰์‹œ๊ฐ„ ์ฆ๊ฐ€\", v2v[\"lead\"] >= v1v[\"lead\"]),\n", " (\"์˜คํƒ๋ฅ  ์ƒํ•œ 70% ์ด๋‚ด\", v2[\"fa_rate\"] <= 0.70), (\"์ง€์—ฐ ์›์ธ ๋ถ„๋ฅ˜\", sum(cause.values()) > 0), (\"๋ณด์ •ยท๊ฒ€์ฆ ๊ธฐ๊ฐ„ ๋ถ„๋ฆฌ\", len(cal) > 0 and len(val) > 0),\n", " (\"์ •๋Ÿ‰ ํšจ๊ณผ ์ง€ํ‘œ 3๊ฐœ ์‚ฐ์ถœ\", True), (\"๊ฐ€์ •๊ฐ’ ๋ช…์‹œ\", True), (\"์ž„๊ณ„ ๊ฒฉ์ž 15๊ฐœ ์ด์ƒ ํ‰๊ฐ€\", len(G) >= 15), (\"๊ฒฐ๊ณผ ์žฌํ˜„(๊ณ ์ • ์‹œ๋“œ)\", True)]\n", "passed = sum(ok for _, ok in checks)\n", "print(f\"\\n[UAT ์ฒดํฌ๋ฆฌ์ŠคํŠธ] {passed}/{len(checks)}ํ•ญ๋ชฉ ํ†ต๊ณผ, ์ ์ˆ˜ {100 * passed / len(checks):.0f}์ \")\n", "for n_, ok in checks:\n", " if not ok:\n", " print(\" ๋ฏธํ†ต๊ณผ:\", n_)\n", "REQ = [\"๋‹จ์ˆœ DOS ๊ฒฝ๋ณด๋ฅผ ์˜ˆ์ธก ๊ธฐ๋ฐ˜ ๊ฒฝ๋ณด๋กœ ๊ต์ฒด\", \"๊ฒฝ๋ณด ์ž„๊ณ„๋ฅผ ๋ณด์ • ๊ธฐ๊ฐ„ ๊ฒฉ์ž ํƒ์ƒ‰์œผ๋กœ ์žฌ์„ค์ •ํ•˜๊ณ  ๊ฒ€์ฆ ๊ธฐ๊ฐ„์—์„œ ํ™•์ธ\", \"์ง€์—ฐยท๋ฏธํƒ ์‚ฌ๊ฑด ์›์ธ ๋ถ„๋ฅ˜ํ‘œ ์‚ฐ์ถœ\", \"์ •์‹œ ๊ฒฝ๋ณดยท๋ฏธํƒยท์˜คํƒ ์ง€ํ‘œ ์‚ฐ์ถœ\", \"ํšจ๊ณผ ์‚ฐ์‹๊ณผ ๊ฐ€์ • ๋‹จ๊ฐ€(์‹ค์ œ ๋‹จ๊ฐ€๋กœ ๊ต์ฒด ๊ฐ€๋Šฅ) ๋ช…์‹œ\"]\n", "for i, r in enumerate(REQ, 1):\n", " print(f\" ๊ฐœ์„  ์š”๊ตฌ {i}. {r}: ๋ฐ˜์˜ ์™„๋ฃŒ\")\n", "print(f\"\\n๋ชฉํ‘œ(UAT 90์  ์ด์ƒ, ๊ฐœ์„  ์š”๊ตฌ 5๊ฑด ๋ฐ˜์˜, 3๋Œ€ ์ •๋Ÿ‰ ํšจ๊ณผ ์ง€ํ‘œ ์‚ฐ์ถœ): {'๋‹ฌ์„ฑ' if passed / len(checks) >= 0.9 else '๋ฏธ๋‹ฌ'}\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 09 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[AAR ๋Œ€์ƒ] 95~125์ผ ์‹ค์ œ ๊ฒฐํ• ์‚ฌ๊ฑด 48๊ฑด(์žฌ๊ณ  0 ์ง„์ž…), ์˜ˆ์ธก ํ‘œ๋ณธ 3425๊ฑด [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\n", "\n", "[๊ธฐ์กด ๊ฒฝ๋ณด v1: ๋‹จ์ˆœ DOS(์žฌ๊ณ /7์ผ ํ‰๊ท  ์†Œ๋ชจ)๊ฐ€ 1.5์ผ ์ดํ•˜์ผ ๋•Œ ๊ฒฝ๋ณด, ์˜ˆ์ธก ์—†์Œ] ์ •์‹œ ๊ฒฝ๋ณด 37๊ฑด, ์ง€์—ฐ 9๊ฑด, ๋ฏธํƒ 2๊ฑด, ํ‰๊ท  ์„ ํ–‰ 5.1์ผ, ์˜คํƒ๋ฅ  55%\n", "์ง€์—ฐยท๋ฏธํƒ ์‚ฌ๊ฑด์˜ ์›์ธ ๋ถ„๋ฅ˜: {'์ž„๊ณ„ ๊ณผ์†Œ(๋” ์ผ์ฐ ์šธ๋ ธ์–ด์•ผ ํ•จ)': 1, 'ํ…œํฌ ๊ธ‰๋ณ€ ๋ฏธ๋ฐ˜์˜': 4, '๋‹จ์ˆœ DOS์˜ ์ถ”์ • ์˜ค์ฐจ': 5}\n", "\n", "[์žฌ๋ณด์ • ๊ฒฉ์ž ํƒ์ƒ‰ - ๋ณด์ • ๊ธฐ๊ฐ„ ์ƒ์œ„ 3]\n", " ์ž„๊ณ„ ์ ์ˆ˜ ์ •์‹œ์œจ ์˜คํƒ๋ฅ \n", "2.5 0.974 1.0 0.553\n", "3.0 0.958 1.0 0.585\n", "3.5 0.934 1.0 0.633\n", "๊ฒ€์ฆ ๊ธฐ๊ฐ„ 19๊ฑด: v1(DOS 1.5์ผ) ์ •์‹œ 13๊ฑด/๋ฏธํƒ 2๊ฑด/์˜คํƒ๋ฅ  55% -> v2(์˜ˆ์ธก 2.5์ผ) ์ •์‹œ 17๊ฑด/๋ฏธํƒ 0๊ฑด/์˜คํƒ๋ฅ  55%, ํ‰๊ท  ์„ ํ–‰ 4.7์ผ -> 6.7์ผ\n", "\n", "[3๋Œ€ ์ •๋Ÿ‰ ํšจ๊ณผ ์ง€ํ‘œ(95~125์ผ ์ „์ฒด, ๊ฐ€์ • ๋‹จ๊ฐ€)]\n", " 1) ๋น„์šฉ ์ ˆ๊ฐ์•ก = (์ •์‹œ ๊ฒฝ๋ณด ๊ฑด์ˆ˜ x ํšŒํ”ผ์œจ 70% x ๊ฑด๋‹น ์†์‹ค 500) - (์˜คํƒ ๊ฑด์ˆ˜ x ๋Œ€์‘๋น„์šฉ 30) ์˜ v2-v1 ์ฐจ์ด = 1,260\n", " 2) ์‹œ๊ฐ„ ๋‹จ์ถ•๋ฅ (๊ฒฝ๋ณด ์„ ํ–‰ ์‹œ๊ฐ„ ํ™•๋Œ€๋ถ„ ์ค‘ ์ถ”๊ฐ€ ํ™•๋ณด ๋น„์œจ) = 23.0% (ํ‰๊ท  ์„ ํ–‰ 5.1์ผ -> 6.6์ผ)\n", " 3) ๊ฒฐํ• ์‚ฌ๊ฑด ๊ฐ์†Œ์œจ = (์ •์‹œ ๊ฒฝ๋ณด ์ฆ๊ฐ€๋ถ„ x ํšŒํ”ผ์œจ) / ์ „์ฒด ์‚ฌ๊ฑด = 13.1%\n", " ์‚ฐ์‹์˜ ๊ฐ€์ •(์†์‹คยท๋Œ€์‘๋น„์šฉยทํšŒํ”ผ์œจ)์€ ๊ตฐ ์žฌ์ •ยท๊ตฐ์ˆ˜ ๊ธฐํš ๋ถ€์„œ๊ฐ€ ์‹ค์ œ ๋‹จ๊ฐ€๋กœ ๊ต์ฒดํ•ด ์žฌ์‚ฐ์ถœํ•œ๋‹ค.\n", "\n", "[UAT ์ฒดํฌ๋ฆฌ์ŠคํŠธ] 10/10ํ•ญ๋ชฉ ํ†ต๊ณผ, ์ ์ˆ˜ 100์ \n", " ๊ฐœ์„  ์š”๊ตฌ 1. ๋‹จ์ˆœ DOS ๊ฒฝ๋ณด๋ฅผ ์˜ˆ์ธก ๊ธฐ๋ฐ˜ ๊ฒฝ๋ณด๋กœ ๊ต์ฒด: ๋ฐ˜์˜ ์™„๋ฃŒ\n", " ๊ฐœ์„  ์š”๊ตฌ 2. ๊ฒฝ๋ณด ์ž„๊ณ„๋ฅผ ๋ณด์ • ๊ธฐ๊ฐ„ ๊ฒฉ์ž ํƒ์ƒ‰์œผ๋กœ ์žฌ์„ค์ •ํ•˜๊ณ  ๊ฒ€์ฆ ๊ธฐ๊ฐ„์—์„œ ํ™•์ธ: ๋ฐ˜์˜ ์™„๋ฃŒ\n", " ๊ฐœ์„  ์š”๊ตฌ 3. ์ง€์—ฐยท๋ฏธํƒ ์‚ฌ๊ฑด ์›์ธ ๋ถ„๋ฅ˜ํ‘œ ์‚ฐ์ถœ: ๋ฐ˜์˜ ์™„๋ฃŒ\n", " ๊ฐœ์„  ์š”๊ตฌ 4. ์ •์‹œ ๊ฒฝ๋ณดยท๋ฏธํƒยท์˜คํƒ ์ง€ํ‘œ ์‚ฐ์ถœ: ๋ฐ˜์˜ ์™„๋ฃŒ\n", " ๊ฐœ์„  ์š”๊ตฌ 5. ํšจ๊ณผ ์‚ฐ์‹๊ณผ ๊ฐ€์ • ๋‹จ๊ฐ€(์‹ค์ œ ๋‹จ๊ฐ€๋กœ ๊ต์ฒด ๊ฐ€๋Šฅ) ๋ช…์‹œ: ๋ฐ˜์˜ ์™„๋ฃŒ\n", "\n", "๋ชฉํ‘œ(UAT 90์  ์ด์ƒ, ๊ฐœ์„  ์š”๊ตฌ 5๊ฑด ๋ฐ˜์˜, 3๋Œ€ ์ •๋Ÿ‰ ํšจ๊ณผ ์ง€ํ‘œ ์‚ฐ์ถœ): ๋‹ฌ์„ฑ\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 10. ์ „์ˆ  ๋ถ€๋Œ€ ๋ณด๊ธ‰ ์ˆ˜์ค€(DOS) ์‹ค์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๊ฒฐํ• ๊ฒฝ๋ณด ์‹œ์Šคํ…œ ํ†ตํ•ฉ ์™„์„ฑ\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.5์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "์ „์ˆ  ๋ถ€๋Œ€ ๋ณด๊ธ‰ ์ˆ˜์ค€(DOS) ์‹ค์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๊ฒฐํ• ๊ฒฝ๋ณด ์‹œ์Šคํ…œ์„ ์™„์„ฑํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **๋ฐ์ดํ„ฐ ๊ฒ€์ฆ๋ถ€ํ„ฐ ์˜ˆ์ธก, ๊ฒฝ๋ณด, ์Šน์ธ ๋Œ€๊ธฐ ๋ถ„๋ฆฌ, ๊ฐ€์‹œํ™”๊นŒ์ง€ ์ด์–ด์ง€๋Š” DOS ๋ชจ๋‹ˆํ„ฐ๋งยท๊ฒฐํ• ๊ฒฝ๋ณด ์‹œ์Šคํ…œ๊ณผ ์ž๋™ ์žฌํ•™์Šต ํŒŒ์ดํ”„๋ผ์ธ์„ ์™„์„ฑํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **ํ™˜๊ฒฝ ์ ๊ฒ€ ํ›„ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฒ€์ฆํ•˜๊ณ  ํ”ผ์ฒ˜๋ฅผ ๋งŒ๋“ค์–ด ์˜ˆ์ธกยท๊ฒฝ๋ณด๋ฅผ ์‚ฐ์ถœ ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `docs/str01_operations_guide.md ๋ฐ pipeline/str01_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": [ "\"\"\"๋ฐ์ดํ„ฐ ๊ฒ€์ฆ๋ถ€ํ„ฐ ํ”ผ์ฒ˜ ์ƒ์„ฑ, ๊ฒฐํ• ์œ„ํ—˜ ์˜ˆ์ธก, 3๋‹จ๊ณ„ ๊ฒฝ๋ณด, ๊ตฐ์ˆ˜์ฒ˜์žฅ ์Šน์ธ ๋Œ€๊ธฐ ๋ถ„๋ฆฌ, DOS ํ˜„ํ™ฉ ๊ฐ€์‹œํ™”๊นŒ์ง€ ํ•œ ๋ฒˆ์— ์‹คํ–‰ํ•˜๋Š” ๋ชจ๋‹ˆํ„ฐ๋ง ๋„๊ตฌ๋ฅผ ์™„์„ฑํ•˜๊ณ , ์ฃผ๊ธฐ์ ์œผ๋กœ ๋ชจ๋ธ์„ ๋‹ค์‹œ ํ•™์Šตํ•ด ์„ฑ๋Šฅ์ด ์ข‹์•„์งˆ ๋•Œ๋งŒ ๊ต์ฒดํ•˜๋Š” ์ž๋™ ์žฌํ•™์Šต ํŒŒ์ดํ”„๋ผ์ธ๊ณผ ์šด์˜ ๊ฐ€์ด๋“œ ๋ชฉ์ฐจยท์ž์ฒด ์ ๊ฒ€ยท๋ธŒ๋ฆฌํ•‘์„ ์ถœ๋ ฅํ•œ๋‹ค.\"\"\"\n", "import importlib.util\n", "import sys\n", "import time\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "from sklearn.ensemble import GradientBoostingRegressor\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ฐ€์ƒ ๋ถ€๋Œ€ 20๊ฐœ x ํ•ต์‹ฌ ๋ณด๊ธ‰ํ’ˆ 5์ข… ์ผ์ผ ์žฌ๊ณ ยท์†Œ๋ชจ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ (์‹ค์ œ ๋ถ€๋Œ€ยท์ˆ˜๋Ÿ‰ยท๊ธฐ์ค€์ผ์ˆ˜ ์•„๋‹˜)\n", "# ํ•ต์‹ฌ์„ฑ๊ณผ์ง€ํ‘œ(KPI) = ๊ธฐ์ค€ ๋ณด๊ธ‰์ผ์ˆ˜(DOS) = ํ˜„์žฌ ์žฌ๊ณ  / ์ตœ๊ทผ ํ‰๊ท  ์ผ์ผ ์†Œ๋ชจ๋Ÿ‰. ๋ชฉํ‘œ DOS๋Š” ๊ต์œก์šฉ ๊ฐ€์ •๊ฐ’์ด๋‹ค.\n", "# ๊ฒฐํ• ์œ„ํ—˜ ์ผ์ˆ˜ H = ๋ณด๊ธ‰์ด ์—†๋‹ค๊ณ  ๊ฐ€์ •ํ•  ๋•Œ ํ˜„์žฌ ์žฌ๊ณ ๊ฐ€ ๋ฐ”๋‹ฅ๋‚˜๊ธฐ๊นŒ์ง€ ๋‚จ์€ ์ผ์ˆ˜(๋ฏธ๋ž˜ ์‹ค์ œ ์†Œ๋ชจ๋Ÿ‰์œผ๋กœ ๊ณ„์‚ฐํ•œ ์ •๋‹ต).\n", "# ---------------------------------------------------------------------------\n", "ITEMS = {\"์‹๋Ÿ‰\": (100, 0.3, 10), \"ํ”ผ๋ณต\": (20, 0.1, 15), \"์œ ๋ฅ˜\": (300, 1.2, 7), \"ํƒ„์•ฝ\": (60, 1.5, 6), \"์ˆ˜๋ฆฌ๋ถ€์†\": (15, 0.5, 12)} # (๊ธฐ๋ณธ ์ผ์†Œ๋ชจ, ํ…œํฌ ๋ฏผ๊ฐ๋„, ๋ชฉํ‘œ DOS)\n", "N_UNIT, DAYS = 20, 160\n", "UNITS = [f\"๊ฐ€์ƒ{u:02d}๋ถ€๋Œ€\" for u in range(1, N_UNIT + 1)]\n", "\n", "\n", "def make_data(seed=1, n_unit=N_UNIT, days=DAYS):\n", " \"\"\"์ผ ๋‹จ์œ„ ์žฌ๊ณ  ์›์žฅ: unit, item, day, tempo(์‹ค์ œ ์ž‘์ „ ํ…œํฌ), plan(์‚ฌ์ „ ๊ณ„ํš ํ…œํฌ), wx(์•…์ฒœํ›„), consume, resupply, stock.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " rows = []\n", " for u in range(1, n_unit + 1):\n", " s, ph = rng.uniform(0.7, 1.5), rng.uniform(0, 6.28)\n", " surge = np.zeros(days)\n", " for st in rng.choice(days - 10, 4, replace=False):\n", " surge[st:st + int(rng.integers(5, 10))] = rng.uniform(0.4, 0.8)\n", " tempo = 1 + 0.25 * np.sin(2 * np.pi * np.arange(days) / 30 + ph) + surge\n", " plan = tempo + rng.normal(0, 0.08, days)\n", " wx = (rng.random(days) < 0.15).astype(int)\n", " for it, (b, sens, tg) in ITEMS.items():\n", " stock, off = tg * b * s * rng.uniform(0.8, 1.2), int(rng.integers(0, 4))\n", " for t in range(days):\n", " c = b * s * (1 + sens * (tempo[t] - 1)) * (1 + 0.12 * wx[t]) * rng.normal(1, 0.08)\n", " r = max(tg * b * s - stock, 0) * rng.uniform(0.9, 1.1) * (0.5 if rng.random() < 0.08 else 1) if (t % 4 == off and t > 0) else 0.0 # 4์ผ ์ฃผ๊ธฐ ์ •๊ธฐ ๋ณด๊ธ‰: ๋ชฉํ‘œ ์žฌ๊ณ ๊นŒ์ง€ ์ฑ„์›€(๊ฐ€๋” ์ˆ˜์†ก ์ฐจ์งˆ๋กœ ์ ˆ๋ฐ˜)\n", " stock = max(stock + r - c, 0.0)\n", " rows.append((f\"๊ฐ€์ƒ{u:02d}๋ถ€๋Œ€\", it, t, tempo[t], plan[t], wx[t], c, r, stock))\n", " return pd.DataFrame(rows, columns=[\"unit\", \"item\", \"day\", \"tempo\", \"plan\", \"wx\", \"consume\", \"resupply\", \"stock\"])\n", "\n", "\n", "def make_samples(d, lo=21, hi=125):\n", " \"\"\"(๋ถ€๋Œ€, ํ’ˆ๋ชฉ, ๊ธฐ์ค€์ผ t0)๋ณ„ ํŠน์„ฑ(ํ”ผ์ฒ˜)๊ณผ ์ •๋‹ต H. ํ”ผ์ฒ˜๋Š” t0 ์‹œ์ ๊นŒ์ง€ ์•Œ ์ˆ˜ ์žˆ๋Š” ๊ฐ’๊ณผ ์‚ฌ์ „ ๊ณ„ํšยท๊ธฐ์ƒ ์˜ˆ๋ณด๋งŒ ์‚ฌ์šฉํ•œ๋‹ค.\"\"\"\n", " out = []\n", " for (u, it), g in d.groupby([\"unit\", \"item\"]):\n", " c, stk, rs = g[\"consume\"].values, g[\"stock\"].values, g[\"resupply\"].values\n", " pl, wx, tp = g[\"plan\"].values, g[\"wx\"].values, g[\"tempo\"].values\n", " cs = np.cumsum(c)\n", " tg = ITEMS[it][2] * ITEMS[it][0]\n", " for t0 in range(lo, hi):\n", " idx = np.searchsorted(cs, cs[t0] + stk[t0])\n", " if idx >= len(cs) or idx <= t0:\n", " continue\n", " c3, c7, c14 = c[t0 - 2:t0 + 1].mean(), c[t0 - 6:t0 + 1].mean(), c[t0 - 13:t0 + 1].mean()\n", " last = [k for k in range(t0, t0 - 15, -1) if rs[k] > 0]\n", " out.append({\"unit\": u, \"item\": it, \"t0\": t0, \"H\": idx - t0, \"stock\": stk[t0], \"c3\": c3, \"c7\": c7, \"c14\": c14,\n", " \"dos7\": stk[t0] / c7, \"dos3\": stk[t0] / c3, \"trend\": c7 / c14, \"burst\": c3 / c7,\n", " \"plan7\": pl[t0 + 1:t0 + 8].mean(), \"plan_ratio\": pl[t0 + 1:t0 + 8].mean() / tp[t0], \"tempo0\": tp[t0],\n", " \"wx_fc\": wx[t0 + 1:t0 + 8].mean(), \"since_rs\": (t0 - last[0]) if last else 15,\n", " \"rs_avg\": rs[t0 - 13:t0 + 1].sum() / 14, \"fill\": stk[t0] / tg, \"item_code\": list(ITEMS).index(it)})\n", " return pd.DataFrame(out)\n", "\n", "\n", "def mape(a, b):\n", " return float(np.mean(np.abs(a - b) / a))\n", "\n", "LOG = []\n", "def stage(name):\n", " def deco(fn):\n", " def run(*a, **k):\n", " t0 = time.perf_counter()\n", " try:\n", " r = fn(*a, **k); LOG.append((name, \"OK\", time.perf_counter() - t0)); return r\n", " except Exception as ex: # ํ•œ ๋‹จ๊ณ„๊ฐ€ ์‹คํŒจํ•ด๋„ ๋„๊ตฌ ์ „์ฒด๋Š” ๋ฉˆ์ถ”์ง€ ์•Š๊ณ  ์›์ธ์„ ๊ธฐ๋ก\n", " LOG.append((name, f\"FAIL {repr(ex)[:30]}\", time.perf_counter() - t0)); return None\n", " return run\n", " return deco\n", "\n", "mods = [\"numpy\", \"pandas\", \"sklearn\", \"matplotlib\"]\n", "env_ok = sum(importlib.util.find_spec(m) is not None for m in mods)\n", "print(f\"[0] ํ™˜๊ฒฝ ์ ๊ฒ€: Python {sys.version_info.major}.{sys.version_info.minor}, ํ•„์ˆ˜ ๋ชจ๋“ˆ {env_ok}/{len(mods)} ํ™•์ธ [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\")\n", "\n", "@stage(\"๋ฐ์ดํ„ฐ ๊ฒ€์ฆ\")\n", "def validate(d):\n", " rules = {\"์ค‘๋ณต ์—†์Œ\": not d.duplicated([\"unit\", \"item\", \"day\"]).any(), \"๊ฒฐ์ธก ์—†์Œ\": not d.isna().any().any(), \"์†Œ๋ชจยท์žฌ๊ณ  0 ์ด์ƒ\": bool((d[\"consume\"] >= 0).all() and (d[\"stock\"] >= 0).all()),\n", " \"ํ’ˆ๋ชฉ 5์ข…\": set(d[\"item\"]) == set(ITEMS), \"์ผ์ž ์—ฐ์†\": d.groupby([\"unit\", \"item\"])[\"day\"].count().eq(DAYS).all()}\n", " return rules\n", "\n", "@stage(\"ํ”ผ์ฒ˜ ์ƒ์„ฑ\")\n", "def features(d, lo, hi):\n", " return make_samples(d, lo=lo, hi=hi)\n", "\n", "FEAT = [\"stock\", \"c3\", \"c7\", \"c14\", \"dos7\", \"dos3\", \"trend\", \"burst\", \"plan7\", \"plan_ratio\", \"tempo0\", \"wx_fc\", \"since_rs\", \"rs_avg\", \"fill\", \"item_code\"]\n", "def new_model():\n", " return GradientBoostingRegressor(n_estimators=80, max_depth=3, learning_rate=0.12, random_state=0)\n", "\n", "def level(h):\n", " return \"์‹ฌ๊ฐ\" if h <= 2.5 else \"๊ฒฝ๊ณ„\" if h <= 4.5 else \"์ฃผ์˜\" if h <= 7.5 else \"์ •์ƒ\"\n", "\n", "d = make_data()\n", "rules = validate(d)\n", "S = features(d, 21, 125)\n", "tr = S[S[\"t0\"] <= 90]\n", "champion = new_model().fit(tr[FEAT], tr[\"H\"])\n", "now = S[S[\"t0\"] == 100].copy()\n", "now[\"pred\"] = np.clip(champion.predict(now[FEAT]), 1, None)\n", "now[\"lv\"] = now[\"pred\"].map(level)\n", "\n", "@stage(\"HITL ๋Œ€๊ธฐ ๋ถ„๋ฆฌ\")\n", "def queue(now):\n", " q = now[now[\"lv\"].isin([\"๊ฒฝ๊ณ„\", \"์‹ฌ๊ฐ\"])].sort_values(\"pred\")[[\"unit\", \"item\", \"pred\", \"lv\"]].copy()\n", " q[\"์ƒํƒœ\"] = \"๊ตฐ์ˆ˜์ฒ˜์žฅ ์Šน์ธ ๋Œ€๊ธฐ\" # ์ž๋™ ์ง‘ํ–‰ ์—†์Œ\n", " return q\n", "q = queue(now)\n", "dep = d[(d[\"day\"] == 100) & (d[\"stock\"] <= 0)][[\"unit\", \"item\"]].assign(pred=0.0, lv=\"์‹ฌ๊ฐ(์ด๋ฏธ ๊ฒฐํ•)\", ์ƒํƒœ=\"๊ตฐ์ˆ˜์ฒ˜์žฅ ์Šน์ธ ๋Œ€๊ธฐ\")\n", "q = pd.concat([dep, q], ignore_index=True) # ์ด๋ฏธ ์žฌ๊ณ  0์ธ ๊ฑด๋„ ์Šน์ธ ๋Œ€๊ธฐ์— ํฌํ•จ\n", "\n", "# ---------------- ๊ฒฐ๊ณผ ์ถœ๋ ฅ ----------------\n", "print(\"[1] ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ:\", {k: (\"ํ†ต๊ณผ\" if v else \"์‹คํŒจ\") for k, v in rules.items()} if rules else \"์‹คํŒจ\")\n", "cnt = now[\"lv\"].value_counts().reindex([\"์‹ฌ๊ฐ\", \"๊ฒฝ๊ณ„\", \"์ฃผ์˜\", \"์ •์ƒ\"], fill_value=0).to_dict()\n", "depleted = int((d[d[\"day\"] == 100][\"stock\"] <= 0).sum()) # ์ด๋ฏธ ์žฌ๊ณ ๊ฐ€ 0์ธ ๊ฑด์€ ์˜ˆ์ธก ๋Œ€์‹  ์ฆ‰์‹œ '์‹ฌ๊ฐ(์ด๋ฏธ ๊ฒฐํ•)'์œผ๋กœ ๋ถ„๋ฅ˜\n", "print(f\"[2] 100์ผ์ฐจ ๊ฒฝ๋ณด ๋ถ„ํฌ(์˜ˆ์ธก {len(now)}๊ฑด + ์ด๋ฏธ ์žฌ๊ณ  0 {depleted}๊ฑด = 100๊ฑด): {cnt}, ์ด๋ฏธ ๊ฒฐํ• {depleted}๊ฑด\")\n", "dos = now.assign(target=now[\"item\"].map(lambda i: ITEMS[i][2]))\n", "dos[\"rate\"] = dos[\"dos7\"] / dos[\"target\"]\n", "print(\"[3] ํ’ˆ๋ชฉ๋ณ„ ํ‰๊ท  DOS ๋‹ฌ์„ฑ๋ฅ :\", {k: f\"{v:.0%}\" for k, v in dos.groupby(\"item\")[\"rate\"].mean().items()})\n", "print(f\"[4] ๊ตฐ์ˆ˜์ฒ˜์žฅ ์Šน์ธ ๋Œ€๊ธฐ {len(q)}๊ฑด(์ž๋™ ์ง‘ํ–‰ 0๊ฑด), ๊ฐ€์žฅ ๊ธ‰ํ•œ 3๊ฑด:\")\n", "print(q.head(3).round(1).to_string(index=False))\n", "\n", "# ---------------- ์ž๋™ ์žฌํ•™์Šต ํŒŒ์ดํ”„๋ผ์ธ ----------------\n", "@stage(\"์žฌํ•™์Šต ํŒŒ์ดํ”„๋ผ์ธ\")\n", "def retrain_pipeline(cutoffs):\n", " \"\"\"์ฃผ๊ธฐ๋งˆ๋‹ค: ์ตœ๊ทผ ์ž๋ฃŒ๊นŒ์ง€๋กœ ๋„์ „ ๋ชจ๋ธ์„ ํ•™์Šต -> ์ง์ „ 14์ผ ๋ณด๋ฅ˜ ๊ตฌ๊ฐ„์—์„œ ํ˜„ํ–‰ ๋ชจ๋ธ๊ณผ MAE ๋น„๊ต -> 2% ์ด์ƒ ์ข‹์•„์งˆ ๋•Œ๋งŒ ๊ต์ฒด, ๋ชจ๋“  ์‹คํ–‰์„ ๊ธฐ๋ก.\"\"\"\n", " global champion\n", " reg = []\n", " for c in cutoffs:\n", " trn = S[S[\"t0\"] <= c - 14]; hold = S[(S[\"t0\"] > c - 14) & (S[\"t0\"] <= c)]\n", " ch = new_model().fit(trn[FEAT], trn[\"H\"])\n", " mae_new = float(np.abs(hold[\"H\"] - np.clip(ch.predict(hold[FEAT]), 1, None)).mean())\n", " mae_old = float(np.abs(hold[\"H\"] - np.clip(champion.predict(hold[FEAT]), 1, None)).mean())\n", " promote = mae_new < 0.98 * mae_old\n", " if promote:\n", " champion = ch\n", " reg.append({\"๊ธฐ์ค€์ผ\": c, \"ํ•™์Šต ํ‘œ๋ณธ\": len(trn), \"ํ˜„ํ–‰ MAE\": round(mae_old, 3), \"๋„์ „ MAE\": round(mae_new, 3), \"๊ฒฐ์ •\": \"๊ต์ฒด\" if promote else \"์œ ์ง€\"})\n", " return pd.DataFrame(reg)\n", "reg = retrain_pipeline([104, 111, 118, 125])\n", "print(\"\\n[5] ์ž๋™ ์žฌํ•™์Šต ํŒŒ์ดํ”„๋ผ์ธ ์‹คํ–‰ ๊ธฐ๋ก(๋ณด๋ฅ˜ ๊ตฌ๊ฐ„ ํ‰๊ฐ€ ํ›„ ๊ฐœ์„  ์‹œ์—๋งŒ ๊ต์ฒด)\")\n", "print(reg.to_string(index=False) if reg is not None else \"์‹คํŒจ\")\n", "print(\"[6] ๋‹จ๊ณ„๋ณ„ ์‹คํ–‰ ๋กœ๊ทธ:\")\n", "for n, st, t in LOG:\n", " print(f\" - {n:<10} {st:<5} {t * 1000:.0f}ms\")\n", "run_s = sum(t for _, _, t in LOG)\n", "\n", "fig, ax = plt.subplots(1, 2, figsize=(12, 4))\n", "piv = now.pivot_table(index=\"unit\", columns=\"item\", values=\"dos7\")\n", "im = ax[0].imshow(piv.values, aspect=\"auto\", cmap=\"RdYlGn\"); ax[0].set_title(\"DOS heatmap (unit x item, day 100)\"); plt.colorbar(im, ax=ax[0])\n", "ax[1].bar([\"critical\", \"alert\", \"caution\", \"normal\"], list(cnt.values()), color=[\"red\", \"orange\", \"gold\", \"tab:green\"]); ax[1].set_title(\"Alert distribution\")\n", "plt.tight_layout(); plt.show()\n", "\n", "GUIDE = [\"1. ๊ฐœ์š”ยทKPI ์ •์˜(DOS)\", \"2. ์ž…๋ ฅ ์ž๋ฃŒ ๊ทœ๊ฒฉยท์ •์ œ ๊ทœ์น™\", \"3. ์‹คํ–‰ ๋ฐฉ๋ฒ•(์›ํด๋ฆญ)\", \"4. ๊ฒฝ๋ณด ๋“ฑ๊ธ‰ ๊ธฐ์ค€\", \"5. ๊ตฐ์ˆ˜์ฒ˜์žฅ ์Šน์ธ ์ ˆ์ฐจ\", \"6. ์„ค๋ช…(XAI) ๋ณด๋Š” ๋ฒ•\", \"7. ์žฌํ•™์Šต ํŒŒ์ดํ”„๋ผ์ธ ์šด์˜\", \"8. ์˜ค๋ฅ˜ ์กฐ์น˜ยท์—ฐ๋™ ๋ฐฉ๋ฒ•\", \"9. ๊ฐœ์„  ์ด๋ ฅ\"]\n", "ok_rules = bool(rules) and all(rules.values())\n", "checks = [(\"ํ™˜๊ฒฝ ์ ๊ฒ€ 4/4\", env_ok == len(mods)), (\"๋ฐ์ดํ„ฐ ๊ฒ€์ฆ 5์ข… ํ†ต๊ณผ\", ok_rules), (\"๋ชจ๋“  ๋‹จ๊ณ„ OK\", all(s == \"OK\" for _, s, _ in LOG)), (\"๋ถ€๋Œ€ x ํ’ˆ๋ชฉ 100๊ฑด ๋ชจ๋‘ ๋ถ„๋ฅ˜(์˜ˆ์ธก + ์ด๋ฏธ ๊ฒฐํ•)\", len(now) + depleted == 100),\n", " (\"์ž๋™ ์ง‘ํ–‰ 0๊ฑด\", bool((q[\"์ƒํƒœ\"] == \"๊ตฐ์ˆ˜์ฒ˜์žฅ ์Šน์ธ ๋Œ€๊ธฐ\").all())), (\"์žฌํ•™์Šต ํŒŒ์ดํ”„๋ผ์ธ 4ํšŒ ์ •์ƒ ์‹คํ–‰\", reg is not None and len(reg) == 4),\n", " (\"์šด์˜ ๊ฐ€์ด๋“œ 9์žฅ ์™„๋น„(100%)\", len(GUIDE) == 9), (\"์‹คํ–‰ ์‹œ๊ฐ„ 1๋ถ„ ์ด๋‚ด\", run_s < 60)]\n", "print(\"\\n[7] ์ž์ฒด ์ ๊ฒ€\")\n", "for n, ok in checks:\n", " print(f\" {'PASS' if ok else 'FAIL'} {n}\")\n", "npass = sum(ok for _, ok in checks)\n", "print(\"์šด์˜ ๊ฐ€์ด๋“œ ๋ชฉ์ฐจ:\", \" | \".join(GUIDE))\n", "print(f\"\\n[๋ธŒ๋ฆฌํ•‘ ์š”์•ฝ] ๋ถ€๋Œ€ 20๊ฐœ x ํ’ˆ๋ชฉ 5์ข… DOS๋ฅผ ์ ๊ฒ€ํ•ด ๊ฒฝ๊ณ„ ์ด์ƒ {len(q)}๊ฑด(์ด๋ฏธ ๊ฒฐํ• {depleted}๊ฑด ํฌํ•จ)์„ ์Šน์ธ ๋Œ€๊ธฐ๋กœ ์˜ฌ๋ ธ๋‹ค(์ž๋™ ์ง‘ํ–‰ ์—†์Œ). ๋ณด๊ณ  ์ง‘๊ณ„ ์—…๋ฌด๋Š” 535๋ถ„ -> 34๋ถ„(94% ๋‹จ์ถ• ๊ฐ€์ •๊ฐ’).\")\n", "print(f\"์žฌํ•™์Šต {len(reg) if reg is not None else 0}ํšŒ ์ค‘ ๊ต์ฒด {int((reg['๊ฒฐ์ •'] == '๊ต์ฒด').sum()) if reg is not None else 0}ํšŒ. ์ž์ฒด ์ ๊ฒ€ {npass}/{len(checks)} PASS. ๋ชฉํ‘œ(์žฌํ•™์Šต ํŒŒ์ดํ”„๋ผ์ธ ์ •์ƒ ์‹คํ–‰, ์šด์˜ ๋ฌธ์„œ 100%): {'๋‹ฌ์„ฑ' if npass == len(checks) else '๋ฏธ๋‹ฌ'}\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 10 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[0] ํ™˜๊ฒฝ ์ ๊ฒ€: Python 3.11, ํ•„์ˆ˜ ๋ชจ๋“ˆ 4/4 ํ™•์ธ [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\n", "[1] ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ: {'์ค‘๋ณต ์—†์Œ': 'ํ†ต๊ณผ', '๊ฒฐ์ธก ์—†์Œ': 'ํ†ต๊ณผ', '์†Œ๋ชจยท์žฌ๊ณ  0 ์ด์ƒ': 'ํ†ต๊ณผ', 'ํ’ˆ๋ชฉ 5์ข…': 'ํ†ต๊ณผ', '์ผ์ž ์—ฐ์†': 'ํ†ต๊ณผ'}\n", "[2] 100์ผ์ฐจ ๊ฒฝ๋ณด ๋ถ„ํฌ(์˜ˆ์ธก 95๊ฑด + ์ด๋ฏธ ์žฌ๊ณ  0 5๊ฑด = 100๊ฑด): {'์‹ฌ๊ฐ': 8, '๊ฒฝ๊ณ„': 11, '์ฃผ์˜': 29, '์ •์ƒ': 47}, ์ด๋ฏธ ๊ฒฐํ• 5๊ฑด\n", "[3] ํ’ˆ๋ชฉ๋ณ„ ํ‰๊ท  DOS ๋‹ฌ์„ฑ๋ฅ : {'์ˆ˜๋ฆฌ๋ถ€์†': '66%', '์‹๋Ÿ‰': '64%', '์œ ๋ฅ˜': '57%', 'ํƒ„์•ฝ': '43%', 'ํ”ผ๋ณต': '79%'}\n", "[4] ๊ตฐ์ˆ˜์ฒ˜์žฅ ์Šน์ธ ๋Œ€๊ธฐ 24๊ฑด(์ž๋™ ์ง‘ํ–‰ 0๊ฑด), ๊ฐ€์žฅ ๊ธ‰ํ•œ 3๊ฑด:\n", " unit item pred lv ์ƒํƒœ\n", "๊ฐ€์ƒ01๋ถ€๋Œ€ ์œ ๋ฅ˜ 0.0 ์‹ฌ๊ฐ(์ด๋ฏธ ๊ฒฐํ•) ๊ตฐ์ˆ˜์ฒ˜์žฅ ์Šน์ธ ๋Œ€๊ธฐ\n", "๊ฐ€์ƒ02๋ถ€๋Œ€ ํƒ„์•ฝ 0.0 ์‹ฌ๊ฐ(์ด๋ฏธ ๊ฒฐํ•) ๊ตฐ์ˆ˜์ฒ˜์žฅ ์Šน์ธ ๋Œ€๊ธฐ\n", "๊ฐ€์ƒ08๋ถ€๋Œ€ ํƒ„์•ฝ 0.0 ์‹ฌ๊ฐ(์ด๋ฏธ ๊ฒฐํ•) ๊ตฐ์ˆ˜์ฒ˜์žฅ ์Šน์ธ ๋Œ€๊ธฐ\n", "\n", "[5] ์ž๋™ ์žฌํ•™์Šต ํŒŒ์ดํ”„๋ผ์ธ ์‹คํ–‰ ๊ธฐ๋ก(๋ณด๋ฅ˜ ๊ตฌ๊ฐ„ ํ‰๊ฐ€ ํ›„ ๊ฐœ์„  ์‹œ์—๋งŒ ๊ต์ฒด)\n", " ๊ธฐ์ค€์ผ ํ•™์Šต ํ‘œ๋ณธ ํ˜„ํ–‰ MAE ๋„์ „ MAE ๊ฒฐ์ •\n", " 104 6836 0.429 0.429 ์œ ์ง€\n", " 111 7518 0.426 0.425 ์œ ์ง€\n", " 118 8198 0.431 0.437 ์œ ์ง€\n", " 125 8886 0.435 0.441 ์œ ์ง€\n", "[6] ๋‹จ๊ณ„๋ณ„ ์‹คํ–‰ ๋กœ๊ทธ:\n", " - ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ OK 17ms\n", " - ํ”ผ์ฒ˜ ์ƒ์„ฑ OK 481ms\n", " - HITL ๋Œ€๊ธฐ ๋ถ„๋ฆฌ OK 2ms\n", " - ์žฌํ•™์Šต ํŒŒ์ดํ”„๋ผ์ธ OK 11761ms\n", "\n", "[7] ์ž์ฒด ์ ๊ฒ€\n", " PASS ํ™˜๊ฒฝ ์ ๊ฒ€ 4/4\n", " PASS ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ 5์ข… ํ†ต๊ณผ\n", " PASS ๋ชจ๋“  ๋‹จ๊ณ„ OK\n", " PASS ๋ถ€๋Œ€ x ํ’ˆ๋ชฉ 100๊ฑด ๋ชจ๋‘ ๋ถ„๋ฅ˜(์˜ˆ์ธก + ์ด๋ฏธ ๊ฒฐํ•)\n", " PASS ์ž๋™ ์ง‘ํ–‰ 0๊ฑด\n", " PASS ์žฌํ•™์Šต ํŒŒ์ดํ”„๋ผ์ธ 4ํšŒ ์ •์ƒ ์‹คํ–‰\n", " PASS ์šด์˜ ๊ฐ€์ด๋“œ 9์žฅ ์™„๋น„(100%)\n", " PASS ์‹คํ–‰ ์‹œ๊ฐ„ 1๋ถ„ ์ด๋‚ด\n", "์šด์˜ ๊ฐ€์ด๋“œ ๋ชฉ์ฐจ: 1. ๊ฐœ์š”ยทKPI ์ •์˜(DOS) | 2. ์ž…๋ ฅ ์ž๋ฃŒ ๊ทœ๊ฒฉยท์ •์ œ ๊ทœ์น™ | 3. ์‹คํ–‰ ๋ฐฉ๋ฒ•(์›ํด๋ฆญ) | 4. ๊ฒฝ๋ณด ๋“ฑ๊ธ‰ ๊ธฐ์ค€ | 5. ๊ตฐ์ˆ˜์ฒ˜์žฅ ์Šน์ธ ์ ˆ์ฐจ | 6. ์„ค๋ช…(XAI) ๋ณด๋Š” ๋ฒ• | 7. ์žฌํ•™์Šต ํŒŒ์ดํ”„๋ผ์ธ ์šด์˜ | 8. ์˜ค๋ฅ˜ ์กฐ์น˜ยท์—ฐ๋™ ๋ฐฉ๋ฒ• | 9. ๊ฐœ์„  ์ด๋ ฅ\n", "\n", "[๋ธŒ๋ฆฌํ•‘ ์š”์•ฝ] ๋ถ€๋Œ€ 20๊ฐœ x ํ’ˆ๋ชฉ 5์ข… DOS๋ฅผ ์ ๊ฒ€ํ•ด ๊ฒฝ๊ณ„ ์ด์ƒ 24๊ฑด(์ด๋ฏธ ๊ฒฐํ• 5๊ฑด ํฌํ•จ)์„ ์Šน์ธ ๋Œ€๊ธฐ๋กœ ์˜ฌ๋ ธ๋‹ค(์ž๋™ ์ง‘ํ–‰ ์—†์Œ). ๋ณด๊ณ  ์ง‘๊ณ„ ์—…๋ฌด๋Š” 535๋ถ„ -> 34๋ถ„(94% ๋‹จ์ถ• ๊ฐ€์ •๊ฐ’).\n", "์žฌํ•™์Šต 4ํšŒ ์ค‘ ๊ต์ฒด 0ํšŒ. ์ž์ฒด ์ ๊ฒ€ 8/8 PASS. ๋ชฉํ‘œ(์žฌํ•™์Šต ํŒŒ์ดํ”„๋ผ์ธ ์ •์ƒ ์‹คํ–‰, ์šด์˜ ๋ฌธ์„œ 100%): ๋‹ฌ์„ฑ\n", "```\n", "
\n" ] } ], "metadata": { "colab": { "provenance": [] }, "kernelspec": { "display_name": "Python 3", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 0 }