{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# ๐ŸŽฏ [QLT04] Data ๊ธฐ๋ฐ˜ ์™ธ์ฃผ์ •๋น„ ํ’ˆ์งˆ๊ฒ€์‚ฌ ํ†ตํ•ฉ ์†”๋ฃจ์…˜\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/QLT04/QLT04_solution.ipynb)\n", "\n", "---\n", "\n", "## ๐Ÿ“Œ 1. ํ”„๋กœ์ ํŠธ ๊ฐœ์š” ๋ฐ ๋ฐฐ๊ฒฝ\n", "\n", "| ํ•ญ๋ชฉ | ๋‚ด์šฉ |\n", "| :--- | :--- |\n", "| **๊ณผ์ • ๋ถ„๋ฅ˜** | ํ’ˆ์งˆยท๊ทœ๊ฒฉยท๊ฒ€์‚ฌ (Level 2) |\n", "| **ํ•™์Šต ์†Œ์š” ์‹œ๊ฐ„** | 10๊ฐœ ๋ฏธ์…˜ (5์ผ / ์ด 27.0์‹œ๊ฐ„) |\n", "| **ํ™˜๊ฒฝ ์‚ฌ์–‘** | Python 3.10+, numpy, pandas, matplotlib, scipy, scikit-learn |\n", "\n", "### ๐Ÿ“– ํ”„๋กœ์ ํŠธ ๋ชฉํ‘œ\n", "์™ธ์ฃผ์ •๋น„๋Š” ์ •๋น„ ๋Œ€๊ธฐ, ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋Œ€๊ธฐ, ์žฌ์ž‘์—…, ๊ทœ๊ฒฉยท์ œ์› ๋ถˆ์ผ์น˜๊ฐ€ ๊ฒน์น˜๋ฉด์„œ ์žฅ๋น„ ๊ฐ€๋™๋ฅ ์„ ๋–จ์–ด๋œจ๋ฆฌ์ง€๋งŒ ์š”์ธ๋ณ„ ์˜ํ–ฅ์ด ํ•œ๋ˆˆ์— ๋ณด์ด์ง€ ์•Š์•„ ์–ด๋””์— ์ •๋น„ ์ž์›์„ ๋จผ์ € ํˆฌ์ž…ํ• ์ง€ ์ •ํ•˜๊ธฐ ์–ด๋ ต๋‹ค. ๋ณธ ํ”„๋กœ์ ํŠธ๋Š” ๋ฏธ ๊ตญ๋ฐฉ๋ถ€ ์™ธ์ฃผ์ •๋น„ ํ’ˆ์งˆ๋ณด์ฆ ์‚ฌ๋ก€์˜ ๋ฌธ์ œ์˜์‹(๊ทœ๊ฒฉ ์ •ํ•ฉ์„ฑยท๊ฒ€์‚ฌ ๋ณ‘๋ชฉยทํ•˜์ž ์—ฐ๊ฒฐ)์„ ๊ต์œก์šฉ ๊ฐ€์ƒ ์ž๋ฃŒ๋กœ ์žฌ๊ตฌ์„ฑํ•ด, ์ •๋น„ ๋Œ€๊ธฐ ์š”์ธ ๋ถ„๋ฅ˜, ์ €๊ฐ€๋™ ์š”์ธ ๋ถ„์„, ์žฌ๋ฐฐ์น˜ ์‹œ๋ฎฌ๋ ˆ์ด์…˜, ์ถ”์„ธ ๋Œ€์‹œ๋ณด๋“œ, ์‚ฌ๋žŒ ๊ฒ€ํ†  ์›Œํฌํ”Œ๋กœ์šฐ๊นŒ์ง€ ํ”„๋กœํ† ํƒ€์ž…์œผ๋กœ ๊ตฌํ˜„ํ•˜๋Š” ๊ณผ์ œ์ด๋‹ค. ๊ฐ€์ƒ ์žฅ๋น„๋Š” ์œ ํ˜• AยทBยทC์™€ ๊ฐ€์ƒ ๋ถ€๋Œ€๋กœ๋งŒ ๊ตฌ๋ถ„ํ•˜๋ฉฐ ์‹ค์ œ ์žฅ๋น„ยท๋ถ€๋Œ€ ์ž๋ฃŒ๋Š” ์“ฐ์ง€ ์•Š๋Š”๋‹ค. ์ตœ์ข… ๋ชฉํ‘œ๋Š” ๊ฐ€๋™๋ฅ  ์ €ํ•˜ ์š”์ธ๊ณผ ์™ธ์ฃผ ํ’ˆ์งˆ๊ฒ€์‚ฌ ๋ณ‘๋ชฉ์„ ๊ทผ๊ฑฐ์™€ ํ•จ๊ป˜ ๋ณด์—ฌ ์ฃผ๋˜, ์ •๋น„ ์šฐ์„ ์ˆœ์œ„์™€ ์ž์› ํˆฌ์ž… ๊ฒฐ์ •์€ ์ •๋น„๊ด€ยทํ’ˆ์งˆ๊ฒ€์‚ฌ๊ด€ยท๊ตฐ์ˆ˜์ฐธ๋ชจ๊ฐ€ ํ•˜๋„๋ก ์ง€์›ํ•˜๋Š” ๋„๊ตฌ๋ฅผ ์™„์„ฑํ•˜๋Š” ๊ฒƒ์ด๋‹ค.\n", "\n", "> **โš ๏ธ [๊ต์œก์šฉ ๊ฐ€์ƒ ๋ฐ์ดํ„ฐ์…‹ ์›์น™]**\n", "> - ๋ณธ ๋…ธํŠธ๋ถ์˜ ๋ชจ๋“  ๋ฐ์ดํ„ฐ(์ œ์›, ๋กœ๊ทธ, ์‹œ๋‚˜๋ฆฌ์˜ค ๋“ฑ)๋Š” ๋ณด์•ˆ ๊ทœ์ •์„ ์ค€์ˆ˜ํ•˜์—ฌ **์ฝ”๋“œ ๋‚ด ๋‚œ์ˆ˜ ๊ธฐ๋ฐ˜ ๊ฐ€์ƒ ๋ฐ์ดํ„ฐ๋กœ ์ƒ์„ฑ**๋ฉ๋‹ˆ๋‹ค.\n", "> - ์‹ค์ œ ๋ถ€๋Œ€ ์œ„์น˜, ์ž‘์ „ ์ธ๊ฐ€, ์‹ค์ œ ์ „์žฅ ์ •๋ณด, ๊ตฐ์‚ฌ ๊ธฐ๋ฐ€ ๋ฐ์ดํ„ฐ๋Š” ์ผ์ฒด ํฌํ•จ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.\n", "\n", "---\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# ============================================================================\n", "# [Google Colab ํ™˜๊ฒฝ ์„ค์ •] ํ•„์ˆ˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์น˜ ๋ฐ ์ž„ํฌํŠธ\n", "# ============================================================================\n", "!pip install -q numpy pandas matplotlib scipy scikit-learn\n", "\n", "import os\n", "import sys\n", "import time\n", "import math\n", "import json\n", "import re\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "import warnings\n", "warnings.filterwarnings('ignore')\n", "\n", "# ๊ทธ๋ž˜ํ”„ ์˜๋ฌธ ํฐํŠธ ๊ธฐ๋ณธ ์„ค์ • (ํ™˜๊ฒฝ ๋…๋ฆฝ์„ฑ ๋ณด์žฅ)\n", "plt.rcParams['font.sans-serif'] = 'DejaVu Sans'\n", "plt.rcParams['axes.unicode_minus'] = False\n", "\n", "print(f\"์‹คํ–‰ ํ™˜๊ฒฝ ์ค€๋น„ ์™„๋ฃŒ (Python {sys.version.split()[0]})\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 01. ์‚ฌ๋‹จ/์—ฌ๋‹จ๋ณ„ ์ฃผ์š” ์ „์ˆ  ์žฅ๋น„(์ „์ฐจ, ์ž์ฃผํฌ, ์žฅ๊ฐ‘์ฐจ) ์ผ์ผ ๊ฐ€๋™๋ฅ  ๋ฐ ์ •๋น„ ๋Œ€๊ธฐ ๋ฐ์ดํ„ฐ์…‹ ์ •์˜\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 1.5์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "์ „์ˆ  ์žฅ๋น„(์ „์ฐจ, ์ž์ฃผํฌ) ์ผ์ผ ๊ฐ€๋™๋ฅ  ๋ฐ ์ •๋น„ ๋Œ€๊ธฐ ๋ฐ์ดํ„ฐ์…‹์„ ์ •์˜ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **๋ถ€๋Œ€๋ณ„ ์ „์ˆ  ์žฅ๋น„(์œ ํ˜• AยทBยทC๋กœ ์ถ”์ƒํ™”ํ•œ ๊ฐ€์ƒ ์žฅ๋น„)์˜ ์ผ์ผ ๊ฐ€๋™๋ฅ ๊ณผ ์ •๋น„ ๋Œ€๊ธฐ ์ž๋ฃŒ์˜ ๊ตฌ์กฐ์™€ ์ž…์ถœ๋ ฅ ๋ช…์„ธ๋ฅผ ์ •์˜ํ•˜๊ณ , ์™ธ์ฃผ์ •๋น„ ์ž…๊ณ ๋ถ€ํ„ฐ ํ’ˆ์งˆ๊ฒ€์‚ฌยท๊ฐ€๋™ ๋ณต๊ท€๊นŒ์ง€์˜ ํ˜„ํ–‰(AS-IS) ์—…๋ฌด๋ฅผ ๋‹จ๊ณ„๋กœ ๋‚˜๋ˆ„์–ด ๋ณ‘๋ชฉ์„ ์ฐพ๋Š”๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **์žฅ๋น„ ์‹๋ณ„, ์ •๋น„ ์š”์ฒญ, ๋Œ€๊ธฐ ์š”์ธ๋ณ„ ์ผ์ˆ˜(๋ถ€ํ’ˆยท์ •๋น„๋ณ‘ยท์™ธ์ฃผ ์ •๋น„ยท์™ธ์ฃผ ๊ฒ€์‚ฌ), ํ’ˆ์งˆ ์ง€ํ‘œ(์žฌ์ž‘์—…ยท๊ทœ๊ฒฉ ๋ถˆ์ผ์น˜ยทํ•˜์ž), ์ž์› ์ˆ˜์ค€, ๊ฐ€๋™๋ฅ ์„ ๋ฌถ์–ด ํ•„๋“œ 16๊ฐœ์˜ ์Šคํ‚ค๋งˆ์™€ ๊ฒ€์ฆ ๊ทœ์น™ ์ž‘์„ฑํ•˜์—ฌ ์›์ธ์„ ๋ถ„์„ํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `docs/qlt04_as_is_to_be_spec.md` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ํ˜„ํ–‰ ์—…๋ฌด(AS-IS)์™€ ๊ฐœ์„  ์—…๋ฌด(TO-BE) ๋น„๊ต์˜ ๋ช…ํ™•์„ฑ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ์—…๋ฌด ๋‹จ๊ณ„ ๋ถ„ํ•ด 10๋‹จ๊ณ„ ์ด์ƒ, ๋ณ‘๋ชฉ ๊ตฌ๊ฐ„ 3๊ฐœ์†Œ ์ด์ƒ ๋„์ถœ\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ์—…๋ฌด ์ •์˜์„œ ๋ฐ ์ž…์ถœ๋ ฅ ๋ฐ์ดํ„ฐ ๋ช…์„ธ ์ž‘์„ฑ ์™„๋ฃŒ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ์‹ค๋ฌด ํ”„๋กœ์„ธ์Šค์™€์˜ ์ผ์น˜์„ฑ ๊ฒ€์ฆ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"๊ฐ€์ƒ ์žฅ๋น„ ๊ฐ€๋™๋ฅ ยท์ •๋น„ ๋Œ€๊ธฐ ์ž๋ฃŒ(ํ•„๋“œ 16๊ฐœ)์˜ ์Šคํ‚ค๋งˆ์™€ ๊ฒ€์ฆ ๊ทœ์น™์„ ์ •์˜ํ•˜๊ณ  ์˜ค๋ฅ˜ ์ฃผ์ž… ์‹œํ—˜์œผ๋กœ ๊ฒ€์ถœ๋ ฅ์„ ํ™•์ธํ•œ ๋’ค, ์ •๋น„ ์ž…๊ณ ๋ถ€ํ„ฐ ์™ธ์ฃผ ์ •๋น„ ๊ฒ€์‚ฌยท๊ฐ€๋™ ๋ณต๊ท€๊นŒ์ง€์˜ ํ˜„ํ–‰(AS-IS) ์—…๋ฌด๋ฅผ 12๋‹จ๊ณ„๋กœ ๋ถ„ํ•ดํ•ด ๋ณ‘๋ชฉ ๊ตฌ๊ฐ„๊ณผ ๊ฐœ์„ (TO-BE) ํšจ๊ณผ๋ฅผ ๋น„๊ตํ•œ๋‹ค.\"\"\"\n", "import re\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ฐ€์ƒ ์žฅ๋น„ ๊ฐ€๋™๋ฅ ยท์ •๋น„ ๋Œ€๊ธฐ ์ž๋ฃŒ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ๋ถ€๋Œ€ยท์žฅ๋น„ยท์ œ์›ยท๊ฐ€๋™๋ฅ  ์ •๋ณด ์—†์Œ)\n", "# ์žฅ๋น„ ์œ ํ˜• A(๊ถค๋„ํ˜•)ยทB(ํ™”ํฌํ˜•)ยทC(์ฐจ๋ฅœํ˜•)์€ ์ถ”์ƒ ๊ตฌ๋ถ„์ด๋ฉฐ ์‹ค์ œ ์žฅ๋น„์™€ ๋ฌด๊ด€ํ•˜๋‹ค.\n", "# ์ตœ๊ทผ 90์ผ ๋™์•ˆ ์žฅ๋น„ 1๋Œ€์˜ ์ •๋น„ ๋Œ€๊ธฐ ์š”์ธ(๋ถ€ํ’ˆยท์ •๋น„๋ณ‘ยท์™ธ์ฃผ ์ •๋น„ยท์™ธ์ฃผ ๊ฒ€์‚ฌยท์žฌ์ž‘์—…)๊ณผ ๊ฐ€๋™๋ฅ ์„ ๋งŒ๋“ ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "UNITS = [\"๊ฐ€์ƒA๋ถ€๋Œ€\", \"๊ฐ€์ƒB๋ถ€๋Œ€\", \"๊ฐ€์ƒC๋ถ€๋Œ€\", \"๊ฐ€์ƒD๋ถ€๋Œ€\", \"๊ฐ€์ƒE๋ถ€๋Œ€\", \"๊ฐ€์ƒF๋ถ€๋Œ€\"]\n", "ETYPES = [\"์œ ํ˜•A\", \"์œ ํ˜•B\", \"์œ ํ˜•C\"]\n", "WINDOW = 90 # ๊ด€์ธก ๊ธฐ๊ฐ„(์ผ)\n", "CAUSES = [\"๋ถ€ํ’ˆ ๋Œ€๊ธฐ\", \"์ •๋น„๋ณ‘ ๋ถ€์กฑ\", \"์™ธ์ฃผ ์ •๋น„\", \"๊ฒ€์‚ฌยท์žฌ์ž‘์—…\"]\n", "CAUSE_COL = {\"๋ถ€ํ’ˆ ๋Œ€๊ธฐ\": \"parts_wait_days\", \"์ •๋น„๋ณ‘ ๋ถ€์กฑ\": \"labor_wait_days\", \"์™ธ์ฃผ ์ •๋น„\": \"outsource_days\", \"๊ฒ€์‚ฌยท์žฌ์ž‘์—…\": \"inspect_wait_days\"}\n", "NUM_FIELDS = [\"age_years\", \"op_hours\", \"maint_orders\", \"parts_wait_days\", \"labor_wait_days\", \"outsource_days\", \"inspect_wait_days\", \"rework_count\",\n", " \"spec_mismatch\", \"defect_reports\", \"parts_stock_pct\", \"mechanic_fill_pct\", \"avail_pct\"]\n", "FEAT = [\"age_years\", \"maint_orders\", \"parts_stock_pct\", \"mechanic_fill_pct\", \"outsource_days\", \"inspect_wait_days\", \"rework_count\", \"spec_mismatch\", \"defect_reports\", \"etype_B\", \"etype_C\"]\n", "FEAT_KO = {\"age_years\": \"์žฅ๋น„ ์—ฐ๋ น\", \"maint_orders\": \"์ •๋น„ ์š”์ฒญ ๊ฑด์ˆ˜\", \"parts_stock_pct\": \"๋ถ€ํ’ˆ ์žฌ๊ณ  ๊ฐ€์šฉ๋ฅ \", \"mechanic_fill_pct\": \"์ •๋น„๋ณ‘ ์ถฉ์›์œจ\", \"outsource_days\": \"์™ธ์ฃผ ์ •๋น„ ์ผ์ˆ˜\",\n", " \"inspect_wait_days\": \"์™ธ์ฃผ ๊ฒ€์‚ฌ ๋Œ€๊ธฐ ์ผ์ˆ˜\", \"rework_count\": \"์žฌ์ž‘์—… ๊ฑด์ˆ˜\", \"spec_mismatch\": \"๊ทœ๊ฒฉยท์ œ์› ๋ถˆ์ผ์น˜\", \"defect_reports\": \"ํ•˜์žยท๋ถˆ๋งŒ ์ ‘์ˆ˜\",\n", " \"etype_B\": \"์œ ํ˜•B ์—ฌ๋ถ€\", \"etype_C\": \"์œ ํ˜•C ์—ฌ๋ถ€\"}\n", "\n", "\n", "def unit_profile(seed=3):\n", " \"\"\"๋ถ€๋Œ€๋ณ„ ๊ณ ์ • ํŠน์„ฑ: ๋ถ€ํ’ˆ ์žฌ๊ณ  ์ˆ˜์ค€, ์ •๋น„๋ณ‘ ์ถฉ์›์œจ, ์™ธ์ฃผ ์ •๋น„์†Œ ๊ฑฐ๋ฆฌ(์ผ์ˆ˜ ๊ฐ€์ค‘), ์™ธ์ฃผ ๊ฒ€์‚ฌ ํ˜ผ์žก๋„.\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 1] ์‚ฌ๋‹จ/์—ฌ๋‹จ๋ณ„ ์ฃผ์š” ์ „์ˆ  ์žฅ๋น„(์ „์ฐจ, ์ž์ฃผํฌ, ์žฅ๊ฐ‘์ฐจ) ์ผ์ผ ๊ฐ€๋™๋ฅ  ๋ฐ ์ •๋น„ ๋Œ€๊ธฐ ๋ฐ์ดํ„ฐ์…‹ ์ •์˜ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def make_fleet(n=300, seed=7, shock=0.0):\n", " \"\"\"์žฅ๋น„ n๋Œ€์˜ 90์ผ ๊ด€์ธก ์ž๋ฃŒ(ํ•„๋“œ 16๊ฐœ)์™€ ์ •๋‹ต ์š”์ธ์„ ๋งŒ๋“ ๋‹ค. shock์€ ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋ณ‘๋ชฉ ์•…ํ™” ์ •๋„(AAR์šฉ ํ™˜๊ฒฝ ๋ณ€ํ™”).\"\"\"\n", " rng = np.random.default_rng(seed)\n", " pf = unit_profile()\n", " ui = rng.integers(0, len(UNITS), n)\n", " et = rng.choice(ETYPES, n, p=[.35, .3, .35])\n", " age = np.clip(rng.gamma(4, 2.2, n), 0.5, 30).round(1)\n", " eff = np.array([{\"์œ ํ˜•A\": 1.15, \"์œ ํ˜•B\": 1.0, \"์œ ํ˜•C\": 0.9}[t] for t in et])\n", " orders = rng.poisson(np.clip(1.2 + 0.12 * age, 0.5, 6) * eff)\n", " stock = np.clip(pf[\"stock\"].to_numpy()[ui] + rng.normal(0, 8, n), 20, 100).round(0)\n", " fill = np.clip(pf[\"fill\"].to_numpy()[ui] + rng.normal(0, 6, n), 30, 100).round(0)\n", " parts_w = (orders * rng.gamma(2, 0.55 * (1 + ((100 - stock) / 28) ** 2) / 2)).round(0)\n", " labor_w = (orders * rng.gamma(2, 0.7 * (1 + ((100 - fill) / 24) ** 2) / 2)).round(0)\n", " n_out = rng.binomial(orders, 0.3)\n", " out_d = (n_out * rng.gamma(3, 4.5 * pf[\"dist\"].to_numpy()[ui] / 3)).round(0)\n", " insp_w = (n_out * rng.gamma(2, (2.6 + shock) * pf[\"queue\"].to_numpy()[ui] / 2)).round(0)\n", " spec = rng.poisson(0.2 + 0.04 * age)\n", " rework = rng.binomial(n_out, np.clip(0.08 + 0.1 * spec, 0, .6))\n", " defect = rng.poisson(0.3 + 0.7 * rework)\n", " base_rep = orders * 1.8\n", " down = base_rep + parts_w + labor_w + out_d + insp_w + rework * 4 + rng.normal(0, 1.2, n)\n", " comp = np.column_stack([parts_w, labor_w, out_d, insp_w + rework * 4])\n", " df = pd.DataFrame({\"equip_id\": [f\"E-{i + 1:04d}\" for i in range(n)], \"unit\": np.array(UNITS)[ui], \"etype\": et, \"age_years\": age,\n", " \"op_hours\": np.clip(rng.normal(320 - 4 * age, 70, n), 40, 600).round(0), \"maint_orders\": orders,\n", " \"parts_wait_days\": parts_w, \"labor_wait_days\": labor_w, \"outsource_days\": out_d, \"inspect_wait_days\": insp_w,\n", " \"rework_count\": rework, \"spec_mismatch\": spec, \"defect_reports\": defect, \"parts_stock_pct\": stock, \"mechanic_fill_pct\": fill,\n", " \"avail_pct\": (100 * (1 - np.clip(down, 0, WINDOW) / WINDOW)).round(1)})\n", " df[\"main_cause\"] = np.where(comp.max(axis=1) < 3, \"์—†์Œ\", np.array(CAUSES)[comp.argmax(axis=1)])\n", " return df\n", "\n", "\n", "def feat_frame(df):\n", " \"\"\"ํšŒ๊ท€ยท๋ถ„์„์šฉ ์„ค๋ช… ๋ณ€์ˆ˜ ํ‘œ(์œ ํ˜•์€ ๋”๋ฏธ ๋ณ€์ˆ˜).\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 1] ์‚ฌ๋‹จ/์—ฌ๋‹จ๋ณ„ ์ฃผ์š” ์ „์ˆ  ์žฅ๋น„(์ „์ฐจ, ์ž์ฃผํฌ, ์žฅ๊ฐ‘์ฐจ) ์ผ์ผ ๊ฐ€๋™๋ฅ  ๋ฐ ์ •๋น„ ๋Œ€๊ธฐ ๋ฐ์ดํ„ฐ์…‹ ์ •์˜ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def make_trend(months=12, seed=5, shock_unit=None):\n", " \"\"\"[๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๋ถ€๋Œ€ x ์œ ํ˜• x ์›” ํ‰๊ท  ๊ฐ€๋™๋ฅ  ์ถ”์„ธ. shock_unit ๋ถ€๋Œ€๋Š” ํ•˜๋ฐ˜๊ธฐ์— ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋ณ‘๋ชฉ์œผ๋กœ ๊ฐ€๋™๋ฅ ์ด ๋–จ์–ด์ง„๋‹ค.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " pf = unit_profile()\n", " rows = []\n", " for _, u in pf.iterrows():\n", " base = 62 + 0.22 * u[\"stock\"] + 0.12 * u[\"fill\"] - 3.0 * u[\"queue\"]\n", " for t in ETYPES:\n", " off = {\"์œ ํ˜•A\": -3.0, \"์œ ํ˜•B\": 0.0, \"์œ ํ˜•C\": 2.0}[t]\n", " for m in range(months):\n", " v = base + off + 2.0 * np.sin(m / 12 * 2 * np.pi) + rng.normal(0, 1.0)\n", " if shock_unit == u[\"unit\"] and m >= months // 2:\n", " v -= 1.5 * (m - months // 2 + 1)\n", " rows.append((u[\"unit\"], t, m + 1, round(float(np.clip(v, 30, 99)), 1)))\n", " return pd.DataFrame(rows, columns=[\"unit\", \"etype\", \"month\", \"avail_pct\"])\n", "\n", "# 1) ์Šคํ‚ค๋งˆ ์ •์˜: ์žฅ๋น„ ์‹๋ณ„, ์ •๋น„ ์š”์ฒญ, ๋Œ€๊ธฐ ์š”์ธ๋ณ„ ์ผ์ˆ˜(๋ถ€ํ’ˆยท์ •๋น„๋ณ‘ยท์™ธ์ฃผ ์ •๋น„ยท์™ธ์ฃผ ๊ฒ€์‚ฌ), ํ’ˆ์งˆ ์ง€ํ‘œ(์žฌ์ž‘์—…ยท๊ทœ๊ฒฉ ๋ถˆ์ผ์น˜ยทํ•˜์ž), ์ž์› ์ˆ˜์ค€, ๊ฐ€๋™๋ฅ \n", "SCHEMA = {\n", " \"equip_id\": {\"type\": str, \"pattern\": r\"^E-\\d{4}$\", \"desc\": \"์žฅ๋น„ ๋ฒˆํ˜ธ\"},\n", " \"unit\": {\"type\": str, \"enum\": UNITS, \"desc\": \"์†Œ์† ๋ถ€๋Œ€(๊ฐ€์ƒ)\"},\n", " \"etype\": {\"type\": str, \"enum\": ETYPES, \"desc\": \"์žฅ๋น„ ์œ ํ˜•(์ถ”์ƒ ๊ตฌ๋ถ„)\"},\n", " \"age_years\": {\"type\": (int, float), \"min\": 0, \"max\": 40, \"desc\": \"์žฅ๋น„ ์—ฐ๋ น(๋…„)\"},\n", " \"op_hours\": {\"type\": (int, float), \"min\": 0, \"max\": 2160, \"desc\": \"์ตœ๊ทผ 90์ผ ๊ฐ€๋™ ์‹œ๊ฐ„\"},\n", " \"maint_orders\": {\"type\": int, \"min\": 0, \"max\": 30, \"desc\": \"์ •๋น„ ์š”์ฒญ ๊ฑด์ˆ˜\"},\n", " \"parts_wait_days\": {\"type\": (int, float), \"min\": 0, \"max\": 90, \"desc\": \"๋ถ€ํ’ˆ ๋Œ€๊ธฐ ์ผ์ˆ˜\"},\n", " \"labor_wait_days\": {\"type\": (int, float), \"min\": 0, \"max\": 90, \"desc\": \"์ •๋น„๋ณ‘ ๋ถ€์กฑ ๋Œ€๊ธฐ ์ผ์ˆ˜\"},\n", " \"outsource_days\": {\"type\": (int, float), \"min\": 0, \"max\": 90, \"desc\": \"์™ธ์ฃผ ์ •๋น„ ์ผ์ˆ˜\"},\n", " \"inspect_wait_days\": {\"type\": (int, float), \"min\": 0, \"max\": 90, \"desc\": \"์™ธ์ฃผ ๊ฒ€์‚ฌ ๋Œ€๊ธฐ ์ผ์ˆ˜\"},\n", " \"rework_count\": {\"type\": int, \"min\": 0, \"max\": 30, \"desc\": \"์žฌ์ž‘์—…(๊ฒ€์‚ฌ ๋ถˆํ•ฉ๊ฒฉ) ๊ฑด์ˆ˜\"},\n", " \"spec_mismatch\": {\"type\": int, \"min\": 0, \"max\": 20, \"desc\": \"๊ทœ๊ฒฉยท์ œ์› ๋ถˆ์ผ์น˜ ๊ฑด์ˆ˜\"},\n", " \"defect_reports\": {\"type\": int, \"min\": 0, \"max\": 50, \"desc\": \"ํ•˜์žยท๋ถˆ๋งŒ ์ ‘์ˆ˜ ๊ฑด์ˆ˜\"},\n", " \"parts_stock_pct\": {\"type\": (int, float), \"min\": 0, \"max\": 100, \"desc\": \"๋ถ€ํ’ˆ ์žฌ๊ณ  ๊ฐ€์šฉ๋ฅ (%)\"},\n", " \"mechanic_fill_pct\": {\"type\": (int, float), \"min\": 0, \"max\": 100, \"desc\": \"์ •๋น„๋ณ‘ ์ถฉ์›์œจ(%)\"},\n", " \"avail_pct\": {\"type\": (int, float), \"min\": 0, \"max\": 100, \"desc\": \"๊ฐ€๋™๋ฅ (%)\"},\n", "}\n", "\n", "\n", "def check(name, v, r):\n", " if v is None or (isinstance(v, float) and np.isnan(v)):\n", " return [f\"{name}: ํ•„์ˆ˜ ๊ฐ’ ๋ˆ„๋ฝ\"]\n", " if not isinstance(v, r[\"type\"]) or isinstance(v, bool):\n", " return [f\"{name}: ์ž๋ฃŒํ˜• ์˜ค๋ฅ˜({type(v).__name__})\"]\n", " errs = []\n", " if \"enum\" in r and v not in r[\"enum\"]:\n", " errs.append(f\"{name}: ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ ๊ฐ’ '{v}'\")\n", " if \"min\" in r and v < r[\"min\"]:\n", " errs.append(f\"{name}: ์ตœ์†Œ๊ฐ’ {r['min']} ๋ฏธ๋งŒ\")\n", " if \"max\" in r and v > r[\"max\"]:\n", " errs.append(f\"{name}: ์ตœ๋Œ€๊ฐ’ {r['max']} ์ดˆ๊ณผ\")\n", " if \"pattern\" in r and not re.match(r[\"pattern\"], v):\n", " errs.append(f\"{name}: ํ˜•์‹ ๋ถˆ์ผ์น˜ '{v}'\")\n", " return errs\n", "\n", "\n", "def validate(rec):\n", " errs = [f\"{k}: ํ•„๋“œ ๋ˆ„๋ฝ\" for k in SCHEMA if k not in rec]\n", " for k, r in SCHEMA.items():\n", " if k in rec:\n", " errs += check(k, rec[k], r)\n", " if not errs and rec[\"rework_count\"] > rec[\"maint_orders\"]: # ํ•„๋“œ ๊ฐ„ ๋…ผ๋ฆฌ ๊ทœ์น™\n", " errs.append(\"rework_count: ์ •๋น„ ์š”์ฒญ ๊ฑด์ˆ˜๋ณด๋‹ค ์žฌ์ž‘์—… ๊ฑด์ˆ˜๊ฐ€ ๋งŽ์Œ\")\n", " if not errs and rec[\"maint_orders\"] == 0 and (rec[\"parts_wait_days\"] + rec[\"labor_wait_days\"] + rec[\"outsource_days\"] + rec[\"inspect_wait_days\"]) > 0:\n", " errs.append(\"maint_orders: ์ •๋น„ ์š”์ฒญ์ด ์—†๋Š”๋ฐ ๋Œ€๊ธฐ ์ผ์ˆ˜๊ฐ€ ์žˆ์Œ\")\n", " if not errs and rec[\"parts_wait_days\"] + rec[\"labor_wait_days\"] + rec[\"outsource_days\"] + rec[\"inspect_wait_days\"] > 90 + 5:\n", " errs.append(\"wait_days: ๋Œ€๊ธฐ ์ผ์ˆ˜ ํ•ฉ๊ณ„๊ฐ€ ๊ด€์ธก ๊ธฐ๊ฐ„(90์ผ)์„ ์ดˆ๊ณผ\")\n", " return errs\n", "\n", "\n", "def to_record(row):\n", " return {k: (row[k].item() if hasattr(row[k], \"item\") else row[k]) for k in SCHEMA}\n", "\n", "\n", "fleet = make_fleet(100, seed=7)\n", "records = [to_record(r) for _, r in fleet.iterrows()]\n", "ok = sum(not validate(r) for r in records)\n", "print(f\"[์Šคํ‚ค๋งˆ ๊ฒ€์ฆ] ์žฅ๋น„ {len(records)}๋Œ€ ์ค‘ ํ†ต๊ณผ {ok}๋Œ€, ์˜ค๋ฅ˜ {len(records) - ok}๋Œ€ / ํ•„๋“œ {len(SCHEMA)}๊ฐœ\")\n", "print(f\"ํ‰๊ท  ๊ฐ€๋™๋ฅ  {fleet['avail_pct'].mean():.1f}%, ๊ฐ€๋™๋ฅ  70% ๋ฏธ๋งŒ {int((fleet['avail_pct'] < 70).sum())}๋Œ€, ์™ธ์ฃผ ์ •๋น„ ๊ฒฝํ—˜ ์žฅ๋น„ {int((fleet['outsource_days'] > 0).sum())}๋Œ€, ๊ทœ๊ฒฉยท์ œ์› ๋ถˆ์ผ์น˜ 1๊ฑด ์ด์ƒ {int((fleet['spec_mismatch'] > 0).sum())}๋Œ€\")\n", "\n", "# 2) ์˜ค๋ฅ˜ ์ฃผ์ž… ์‹œํ—˜: ํ˜•์‹ยทํ—ˆ์šฉ๊ฐ’ยท๋ฒ”์œ„ยท์ž๋ฃŒํ˜•ยท๋ˆ„๋ฝยท๋…ผ๋ฆฌ ๊ทœ์น™ ์œ„๋ฐ˜\n", "bad = dict(records[2], equip_id=\"E12\", unit=\"๋ฏธ์ƒ๋ถ€๋Œ€\", age_years=-3, op_hours=\"๋งŽ์Œ\")\n", "bad2 = dict(records[5], maint_orders=2, rework_count=5)\n", "bad3 = dict(records[0], maint_orders=0, parts_wait_days=5)\n", "bad4 = {k: v for k, v in records[7].items() if k != \"avail_pct\"}\n", "bad5 = dict(records[9], avail_pct=None)\n", "print(\"\\n[์˜ค๋ฅ˜ ์ฃผ์ž… ๊ฒฐ๊ณผ]\")\n", "for e in validate(bad) + validate(bad2) + validate(bad3) + validate(bad4) + validate(bad5):\n", " print(\" -\", e)\n", "\n", "# 3) AS-IS ์—…๋ฌด ๋‹จ๊ณ„ ๋ถ„ํ•ด: [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ์™ธ์ฃผ ์ •๋น„ 1๊ฑด์˜ ๋‹จ๊ณ„๋ณ„ ์†Œ์š”์ผ(์ž‘์—… + ๋Œ€๊ธฐ)\n", "rng = np.random.default_rng(3)\n", "STEPS = [(\"์ •๋น„ ์š”์ฒญ ์ ‘์ˆ˜ยท๊ณ ์žฅ ์ ‘์ˆ˜\", 0.5, 1), (\"๊ณ ์žฅ ์ง„๋‹จยท์ •๋น„ ๊ณ„ํš ์ˆ˜๋ฆฝ\", 1, 1), (\"๋ถ€ํ’ˆ ์ฒญ๊ตฌยท์กฐ๋‹ฌ\", 1, 6), (\"์ •๋น„๋ณ‘ ๋ฐฐ์ •\", 0.5, 3),\n", " (\"๋ถ€๋Œ€ ์ •๋น„ ์ˆ˜ํ–‰\", 2, 1), (\"์™ธ์ฃผ ์ •๋น„ ์˜๋ขฐยท์ž…๊ณ \", 1, 4), (\"์™ธ์ฃผ ์ •๋น„ ์ˆ˜ํ–‰\", 6, 2), (\"์™ธ์ฃผ ๊ฒ€์‚ฌ ๋Œ€๊ธฐ\", 0, 5),\n", " (\"์™ธ์ฃผ ์ •๋น„ ํ’ˆ์งˆ๊ฒ€์‚ฌ(๊ทœ๊ฒฉยท์ œ์› ๋Œ€์กฐ)\", 1, 1), (\"๋ถˆํ•ฉ๊ฒฉ ์‹œ ์žฌ์ž‘์—…ยทํ•˜์ž ์กฐ์น˜\", 3, 3), (\"์ถœ๊ณ  ์Šน์ธยท๋ถ€๋Œ€ ์ธ์ˆ˜\", 0.5, 2), (\"๊ฐ€๋™ ๋ณต๊ท€ ํ™•์ธยท์ด๋ ฅ ๊ธฐ๋ก\", 0.5, 1)]\n", "TOBE = {2: (\"๋ถ€ํ’ˆ ์†Œ์š” ์˜ˆ์ธกยท์‚ฌ์ „ ์ฒญ๊ตฌ(๋ฏธ์…˜ 4)\", 0.4), 3: (\"์ •๋น„๋ณ‘ ๋ถ€ํ•˜ ๊ธฐ๋ฐ˜ ๋ฐฐ์ • ์ถ”์ฒœ(๋ฏธ์…˜ 5)\", 0.5), 7: (\"๊ฒ€์‚ฌ ์ผ์ • ์ž๋™ ์กฐ์œจ(๋ฏธ์…˜ 5)\", 0.4),\n", " 8: (\"๊ทœ๊ฒฉยท์ œ์› ์ž๋™ ๋Œ€์กฐ ์‚ฌ์ „ ์ ๊ฒ€(๋ฏธ์…˜ 3)\", 0.5), 9: (\"์žฌ์ž‘์—… ์›์ธ ์ž๋™ ๋ถ„๋ฅ˜(๋ฏธ์…˜ 2ยท4)\", 0.6), 11: (\"์ด๋ ฅ ์ž๋™ ๊ธฐ๋ก(๋ฏธ์…˜ 2)\", 0.3)}\n", "rows = []\n", "for i, (name, work, wait) in enumerate(STEPS):\n", " w = rng.gamma(4, max(work, 0.01) / 4, 30); q = rng.gamma(2, wait / 2, 30)\n", " f = TOBE.get(i, (\"์œ ์ง€(์‚ฌ๋žŒ ํŒ๋‹จ)\", 1.0))\n", " rows.append({\"๋‹จ๊ณ„\": f\"{i + 1:02d}. {name}\", \"์ž‘์—…(์ผ)\": w.mean() if work else 0.0, \"๋Œ€๊ธฐ(์ผ)\": q.mean(), \"AS-IS(์ผ)\": (w.mean() if work else 0.0) + q.mean(),\n", " \"TO-BE ๋ฐฉ์‹\": f[0], \"TO-BE(์ผ)\": ((w.mean() if work else 0.0) + q.mean()) * f[1]})\n", "proc = pd.DataFrame(rows)\n", "proc[\"๋ณ‘๋ชฉ\"] = np.where(proc[\"๋Œ€๊ธฐ(์ผ)\"] >= proc[\"๋Œ€๊ธฐ(์ผ)\"].sort_values().iloc[-4], \"๋ณ‘๋ชฉ\", \"\") # ๋Œ€๊ธฐ์ผ ์ƒ์œ„ 4๊ฐœ ๋‹จ๊ณ„๋ฅผ ๋ณ‘๋ชฉ์œผ๋กœ ๋ณธ๋‹ค\n", "print(\"\\n[AS-IS / TO-BE ์—…๋ฌด ๋‹จ๊ณ„ ๋น„๊ต]\")\n", "print(proc.round(1).to_string(index=False))\n", "n_bn = int((proc[\"๋ณ‘๋ชฉ\"] == \"๋ณ‘๋ชฉ\").sum())\n", "asis, tobe = proc[\"AS-IS(์ผ)\"].sum(), proc[\"TO-BE(์ผ)\"].sum()\n", "print(f\"\\n์—…๋ฌด ๋‹จ๊ณ„ {len(proc)}๊ฐœ(๋ชฉํ‘œ 10๊ฐœ ์ด์ƒ: {'๋‹ฌ์„ฑ' if len(proc) >= 10 else '๋ฏธ๋‹ฌ'}), ๋ณ‘๋ชฉ {n_bn}๊ฐœ์†Œ(๋ชฉํ‘œ 3๊ฐœ์†Œ ์ด์ƒ: {'๋‹ฌ์„ฑ' if n_bn >= 3 else '๋ฏธ๋‹ฌ'})\")\n", "print(f\"์™ธ์ฃผ ์ •๋น„ 1๊ฑด ์†Œ์š”: AS-IS {asis:.1f}์ผ -> TO-BE {tobe:.1f}์ผ ({1 - tobe / asis:.0%} ๋‹จ์ถ• ์˜ˆ์ƒ). ํ’ˆ์งˆ๊ฒ€์‚ฌ ํŒ์ •, ์ •๋น„ ์šฐ์„ ์ˆœ์œ„ ๊ฒฐ์ •, ์ถœ๊ณ  ์Šน์ธ์€ ์‚ฌ๋žŒ์ด ์ˆ˜ํ–‰ํ•œ๋‹ค.\")\n", "\n", "# 4) ์ž…์ถœ๋ ฅ ๋ฐ์ดํ„ฐ ๋ช…์„ธ ์š”์•ฝ\n", "print(\"\\n[์ž…์ถœ๋ ฅ ๋ช…์„ธ] ์ž…๋ ฅ: ์žฅ๋น„ ๊ฐ€๋™ยท์ •๋น„ ๋Œ€๊ธฐ ๋ ˆ์ฝ”๋“œ(์œ„ 16๊ฐœ ํ•„๋“œ) / ์ถœ๋ ฅ: ์ €๊ฐ€๋™ ์š”์ธ ์ˆœ์œ„, ์ €๊ฐ€๋™ ์žฅ๋น„ ๋ชฉ๋ก, ์ •๋น„ ์ž์› ์žฌ๋ฐฐ์น˜ ์•ˆ, ๊ฒ€ํ† ยท๊ฒฐ์žฌ ์ด๋ ฅ\")\n", "print(\"[๊ถŒํ•œ๋ณ„ ์š”๊ตฌ] ์ •๋น„ ๋‹ด๋‹น: ์ž…๋ ฅยท์ƒ์‹  / ์ •๋น„๊ด€ยทํ’ˆ์งˆ๊ฒ€์‚ฌ๊ด€: ๊ฒ€ํ†  / ๊ตฐ์ˆ˜์ฐธ๋ชจ: ์ •์ฑ… ๊ฒฐ์ •(๋ฏธ์…˜ 7์—์„œ ๊ตฌํ˜„)\")\n", "\n", "plt.figure(figsize=(9, 5))\n", "y = np.arange(len(proc))\n", "plt.barh(y - 0.2, proc[\"AS-IS(์ผ)\"], 0.4, label=\"AS-IS\", color=\"tab:gray\")\n", "plt.barh(y + 0.2, proc[\"TO-BE(์ผ)\"], 0.4, label=\"TO-BE\", color=\"tab:blue\")\n", "plt.yticks(y, [f\"step {i + 1}{' *' if b else ''}\" for i, b in enumerate(proc[\"๋ณ‘๋ชฉ\"])])\n", "plt.gca().invert_yaxis(); plt.xlabel(\"days per case\"); plt.legend()\n", "plt.title(\"Outsourced maintenance flow: AS-IS vs TO-BE (* = bottleneck, synthetic)\")\n", "plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 01 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[์Šคํ‚ค๋งˆ ๊ฒ€์ฆ] ์žฅ๋น„ 100๋Œ€ ์ค‘ ํ†ต๊ณผ 100๋Œ€, ์˜ค๋ฅ˜ 0๋Œ€ / ํ•„๋“œ 16๊ฐœ\n", "ํ‰๊ท  ๊ฐ€๋™๋ฅ  79.3%, ๊ฐ€๋™๋ฅ  70% ๋ฏธ๋งŒ 25๋Œ€, ์™ธ์ฃผ ์ •๋น„ ๊ฒฝํ—˜ ์žฅ๋น„ 50๋Œ€, ๊ทœ๊ฒฉยท์ œ์› ๋ถˆ์ผ์น˜ 1๊ฑด ์ด์ƒ 49๋Œ€\n", "\n", "[์˜ค๋ฅ˜ ์ฃผ์ž… ๊ฒฐ๊ณผ]\n", " - equip_id: ํ˜•์‹ ๋ถˆ์ผ์น˜ 'E12'\n", " - unit: ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ ๊ฐ’ '๋ฏธ์ƒ๋ถ€๋Œ€'\n", " - age_years: ์ตœ์†Œ๊ฐ’ 0 ๋ฏธ๋งŒ\n", " - op_hours: ์ž๋ฃŒํ˜• ์˜ค๋ฅ˜(str)\n", " - rework_count: ์ •๋น„ ์š”์ฒญ ๊ฑด์ˆ˜๋ณด๋‹ค ์žฌ์ž‘์—… ๊ฑด์ˆ˜๊ฐ€ ๋งŽ์Œ\n", " - maint_orders: ์ •๋น„ ์š”์ฒญ์ด ์—†๋Š”๋ฐ ๋Œ€๊ธฐ ์ผ์ˆ˜๊ฐ€ ์žˆ์Œ\n", " - avail_pct: ํ•„๋“œ ๋ˆ„๋ฝ\n", " - avail_pct: ํ•„์ˆ˜ ๊ฐ’ ๋ˆ„๋ฝ\n", "\n", "[AS-IS / TO-BE ์—…๋ฌด ๋‹จ๊ณ„ ๋น„๊ต]\n", " ๋‹จ๊ณ„ ์ž‘์—…(์ผ) ๋Œ€๊ธฐ(์ผ) AS-IS(์ผ) TO-BE ๋ฐฉ์‹ TO-BE(์ผ) ๋ณ‘๋ชฉ\n", " 01. ์ •๋น„ ์š”์ฒญ ์ ‘์ˆ˜ยท๊ณ ์žฅ ์ ‘์ˆ˜ 0.5 1.0 1.5 ์œ ์ง€(์‚ฌ๋žŒ ํŒ๋‹จ) 1.5 \n", " 02. ๊ณ ์žฅ ์ง„๋‹จยท์ •๋น„ ๊ณ„ํš ์ˆ˜๋ฆฝ 1.3 0.9 2.2 ์œ ์ง€(์‚ฌ๋žŒ ํŒ๋‹จ) 2.2 \n", " 03. ๋ถ€ํ’ˆ ์ฒญ๊ตฌยท์กฐ๋‹ฌ 1.0 6.6 7.7 ๋ถ€ํ’ˆ ์†Œ์š” ์˜ˆ์ธกยท์‚ฌ์ „ ์ฒญ๊ตฌ(๋ฏธ์…˜ 4) 3.1 ๋ณ‘๋ชฉ\n", " 04. ์ •๋น„๋ณ‘ ๋ฐฐ์ • 0.6 3.0 3.6 ์ •๋น„๋ณ‘ ๋ถ€ํ•˜ ๊ธฐ๋ฐ˜ ๋ฐฐ์ • ์ถ”์ฒœ(๋ฏธ์…˜ 5) 1.8 \n", " 05. ๋ถ€๋Œ€ ์ •๋น„ ์ˆ˜ํ–‰ 2.4 1.1 3.5 ์œ ์ง€(์‚ฌ๋žŒ ํŒ๋‹จ) 3.5 \n", " 06. ์™ธ์ฃผ ์ •๋น„ ์˜๋ขฐยท์ž…๊ณ  1.0 4.1 5.1 ์œ ์ง€(์‚ฌ๋žŒ ํŒ๋‹จ) 5.1 ๋ณ‘๋ชฉ\n", " 07. ์™ธ์ฃผ ์ •๋น„ ์ˆ˜ํ–‰ 5.3 1.9 7.2 ์œ ์ง€(์‚ฌ๋žŒ ํŒ๋‹จ) 7.2 \n", " 08. ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋Œ€๊ธฐ 0.0 4.9 4.9 ๊ฒ€์‚ฌ ์ผ์ • ์ž๋™ ์กฐ์œจ(๋ฏธ์…˜ 5) 2.0 ๋ณ‘๋ชฉ\n", "09. ์™ธ์ฃผ ์ •๋น„ ํ’ˆ์งˆ๊ฒ€์‚ฌ(๊ทœ๊ฒฉยท์ œ์› ๋Œ€์กฐ) 1.1 1.0 2.1 ๊ทœ๊ฒฉยท์ œ์› ์ž๋™ ๋Œ€์กฐ ์‚ฌ์ „ ์ ๊ฒ€(๋ฏธ์…˜ 3) 1.0 \n", " 10. ๋ถˆํ•ฉ๊ฒฉ ์‹œ ์žฌ์ž‘์—…ยทํ•˜์ž ์กฐ์น˜ 3.1 3.6 6.7 ์žฌ์ž‘์—… ์›์ธ ์ž๋™ ๋ถ„๋ฅ˜(๋ฏธ์…˜ 2ยท4) 4.0 ๋ณ‘๋ชฉ\n", " 11. ์ถœ๊ณ  ์Šน์ธยท๋ถ€๋Œ€ ์ธ์ˆ˜ 0.5 2.3 2.7 ์œ ์ง€(์‚ฌ๋žŒ ํŒ๋‹จ) 2.7 \n", " 12. ๊ฐ€๋™ ๋ณต๊ท€ ํ™•์ธยท์ด๋ ฅ ๊ธฐ๋ก 0.5 1.0 1.6 ์ด๋ ฅ ์ž๋™ ๊ธฐ๋ก(๋ฏธ์…˜ 2) 0.5 \n", "\n", "์—…๋ฌด ๋‹จ๊ณ„ 12๊ฐœ(๋ชฉํ‘œ 10๊ฐœ ์ด์ƒ: ๋‹ฌ์„ฑ), ๋ณ‘๋ชฉ 4๊ฐœ์†Œ(๋ชฉํ‘œ 3๊ฐœ์†Œ ์ด์ƒ: ๋‹ฌ์„ฑ)\n", "์™ธ์ฃผ ์ •๋น„ 1๊ฑด ์†Œ์š”: AS-IS 48.9์ผ -> TO-BE 34.7์ผ (29% ๋‹จ์ถ• ์˜ˆ์ƒ). ํ’ˆ์งˆ๊ฒ€์‚ฌ ํŒ์ •, ์ •๋น„ ์šฐ์„ ์ˆœ์œ„ ๊ฒฐ์ •, ์ถœ๊ณ  ์Šน์ธ์€ ์‚ฌ๋žŒ์ด ์ˆ˜ํ–‰ํ•œ๋‹ค.\n", "\n", "[์ž…์ถœ๋ ฅ ๋ช…์„ธ] ์ž…๋ ฅ: ์žฅ๋น„ ๊ฐ€๋™ยท์ •๋น„ ๋Œ€๊ธฐ ๋ ˆ์ฝ”๋“œ(์œ„ 16๊ฐœ ํ•„๋“œ) / ์ถœ๋ ฅ: ์ €๊ฐ€๋™ ์š”์ธ ์ˆœ์œ„, ์ €๊ฐ€๋™ ์žฅ๋น„ ๋ชฉ๋ก, ์ •๋น„ ์ž์› ์žฌ๋ฐฐ์น˜ ์•ˆ, ๊ฒ€ํ† ยท๊ฒฐ์žฌ ์ด๋ ฅ\n", "[๊ถŒํ•œ๋ณ„ ์š”๊ตฌ] ์ •๋น„ ๋‹ด๋‹น: ์ž…๋ ฅยท์ƒ์‹  / ์ •๋น„๊ด€ยทํ’ˆ์งˆ๊ฒ€์‚ฌ๊ด€: ๊ฒ€ํ†  / ๊ตฐ์ˆ˜์ฐธ๋ชจ: ์ •์ฑ… ๊ฒฐ์ •(๋ฏธ์…˜ 7์—์„œ ๊ตฌํ˜„)\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 02. ์ •๋น„ ๋Œ€๊ธฐ ์‹œ๊ฐ„(๋ถ€ํ’ˆ ๋Œ€๊ธฐ, ์ •๋น„๋ณ‘ ๋ถ€์กฑ, ์™ธ์ฃผ ์ •๋น„) ์š”์ธ ๋ถ„๋ฅ˜ ์ „์ฒ˜๋ฆฌ ๋ชจ๋“ˆ ๊ตฌํ˜„\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 2.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "์ •๋น„ ๋Œ€๊ธฐ ์‹œ๊ฐ„(๋ถ€ํ’ˆ ๋Œ€๊ธฐ, ์ธ๋ ฅ ๋ถ€์กฑ) ์š”์ธ ๋ถ„๋ฅ˜ ์ „์ฒ˜๋ฆฌ ๋ชจ๋“ˆ์„ ๊ตฌํ˜„ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **์„œ์‹์ด ๋‹ค๋ฅธ ์ •๋น„ ๋Œ€๊ธฐ ๊ธฐ๋ก(CSV, ์—‘์…€ ๋‚ด๋ณด๋‚ด๊ธฐ, JSON, ์ •๋น„์ผ์ง€ ๋ฌธ์žฅ)์„ ํ•˜๋‚˜์˜ ํ‘œ์ค€ ์Šคํ‚ค๋งˆ๋กœ ๋ฐ”๊พธ๊ณ  ๋Œ€๊ธฐ ์‚ฌ์œ ๋ฅผ ๋ถ€ํ’ˆ ๋Œ€๊ธฐยท์ •๋น„๋ณ‘ ๋ถ€์กฑยท์™ธ์ฃผ ์ •๋น„ยท๊ฒ€์‚ฌยท์žฌ์ž‘์—…์œผ๋กœ ๋ถ„๋ฅ˜ํ•˜๋Š” ์ „์ฒ˜๋ฆฌ ๋ชจ๋“ˆ์„ ๊ตฌํ˜„ํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **์—ด ์ด๋ฆ„ ๋ณ„์นญ ์‚ฌ์ „์œผ๋กœ CSV, ํƒญ ๊ตฌ๋ถ„(์—‘์…€ ๋‚ด๋ณด๋‚ด๊ธฐ), JSON ์„ธ ํ˜•์‹์„ ํ‘œ์ค€ ์Šคํ‚ค๋งˆ๋กœ ๋ฐ”๊พธ๊ณ , ๋Œ€๊ธฐ ๊ธฐ๊ฐ„(์ผยท์ฃผยท๊ฐœ์›”ยท์‹œ๊ฐ„)๊ณผ ๋‚ ์งœยท๋ถ€๋Œ€๋ช… ํ‘œ๊ธฐ๋ฅผ ํ†ต์ผ ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/connectors/qlt04_data_loader.py ๋ฐ ์Šคํ‚ค๋งˆ ์ •์˜์„œ` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ๊ตญ๋ฐฉ ์„œ์‹/์–‘์‹ ๋ณ€๊ฒฝ์— ๋Œ€ํ•œ ์œ ์—ฐํ•œ ํ™•์žฅ์„ฑ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ์ง€์› ํŒŒ์ผ ํฌ๋งท(CSV, Excel, JSON ๋“ฑ) ํŒŒ์‹ฑ ์„ฑ๊ณต๋ฅ  100%\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ๋ฐ์ดํ„ฐ ๋กœ๋” ๋ฐ ์Šคํ‚ค๋งˆ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋ชจ๋“ˆ ์™„์„ฑ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ๋‹ค์–‘ํ•œ ํ˜•ํƒœ์˜ ์ƒ˜ํ”Œ ๊ตฐ์ˆ˜ ๋ฌธ์„œ ๋Œ€์ƒ ํŒŒ์‹ฑ ํ…Œ์ŠคํŠธ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"์„œ์‹์ด ์„œ๋กœ ๋‹ค๋ฅธ ๊ฐ€์ƒ ์ •๋น„ ๋Œ€๊ธฐ ๊ธฐ๋ก(CSV, ์—‘์…€ ๋‚ด๋ณด๋‚ด๊ธฐ TSV, JSON, ์ •๋น„์ผ์ง€ ๋ฌธ์žฅ)์„ ์—ด ์ด๋ฆ„ ๋ณ„์นญ ์‚ฌ์ „๊ณผ ์ •๊ทœ์‹์œผ๋กœ ํ‘œ์ค€ ์Šคํ‚ค๋งˆ๋กœ ๋ฐ”๊พธ๊ณ , ๋Œ€๊ธฐ ์ผ์ˆ˜ยท๋ถ€๋Œ€๋ช…ยท๋‚ ์งœ ํ‘œ๊ธฐ๋ฅผ ํ†ต์ผํ•˜๋ฉฐ ๋Œ€๊ธฐ ์‚ฌ์œ ๋ฅผ ๋ถ€ํ’ˆ ๋Œ€๊ธฐยท์ •๋น„๋ณ‘ ๋ถ€์กฑยท์™ธ์ฃผ ์ •๋น„ยท๊ฒ€์‚ฌยท์žฌ์ž‘์—…์œผ๋กœ ๋ถ„๋ฅ˜ํ•˜๋Š” ์ „์ฒ˜๋ฆฌ ๋ชจ๋“ˆ์„ ๊ตฌํ˜„ํ•œ๋‹ค.\"\"\"\n", "import csv\n", "import io\n", "import json\n", "import re\n", "from datetime import date\n", "import pandas as pd\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ์„œ๋กœ ๋‹ค๋ฅธ ํ˜•์‹์˜ ์›์‹œ ๊ธฐ๋ก (์‹ค์ œ ๋ถ€๋Œ€ยท์žฅ๋น„ ์•„๋‹˜). ํ‘œ๊ธฐ๊ฐ€ ์ผ๋ถ€๋Ÿฌ ์ œ๊ฐ๊ฐ์ด๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "RAW_CSV = \"\"\"์žฅ๋น„,๋ถ€๋Œ€,๋Œ€๊ธฐ์‚ฌ์œ ,๋Œ€๊ธฐ๊ธฐ๊ฐ„,์ ‘์ˆ˜์ผ\n", "E-0001,๊ฐ€์ƒA๋ถ€๋Œ€,๋ถ€ํ’ˆ ๋ฏธ์ž…๊ณ ,5์ผ,2026.03.02\n", "E-0002,๊ฐ€์ƒ B ๋ถ€๋Œ€,์ •๋น„๋ณ‘ ๋ถ€์กฑ(ํœด๊ฐ€ยท๊ต์œก),2์ฃผ,2026-3-15\n", "E-0003,C๋ถ€๋Œ€,์™ธ์ฃผ์—…์ฒด ์ž…๊ณ  ๋Œ€๊ธฐ,48์‹œ๊ฐ„,20260110\n", "E-0004,๊ฐ€์ƒD๋ถ€๋Œ€,์™ธ์ฃผ ๊ฒ€์‚ฌ ๋Œ€๊ธฐ,3์ผ,26/02/27\n", "E-0005,๊ฐ€์ƒE๋ถ€๋Œ€,๋ถ€ํ’ˆ ์ฒญ๊ตฌ ์ง€์—ฐ,1๊ฐœ์›”,2025.12.30\n", "E-0006,๊ฐ€์ƒF๋ถ€๋Œ€,์žฌ์ž‘์—…(๊ฒ€์‚ฌ ๋ถˆํ•ฉ๊ฒฉ),4์ผ,2026-01-05\n", "\"\"\"\n", "RAW_TSV = \"์žฅ๋น„๋ฒˆํ˜ธ\\t์†Œ์†\\t์‚ฌ์œ \\t์ง€์—ฐ์ผ์ˆ˜\\t์ ‘์ˆ˜\\n\" \\\n", " \"E-0007\\t๊ฐ€์ƒA๋ถ€๋Œ€\\t์ •๋น„์ธ๋ ฅ ๋ถ€์กฑ\\t6\\t2026.02.11\\n\" \\\n", " \"E-0008\\t๊ฐ€์ƒB๋ถ€๋Œ€\\t๋ถ€ํ’ˆ ์ž…๊ณ  ์ง€์—ฐ\\t10\\t2026.01.20\\n\" \\\n", " \"E-0009\\t๊ฐ€์ƒC๋ถ€๋Œ€\\t์™ธ์ฃผ ์ •๋น„ ์ˆ˜ํ–‰ ์ค‘\\t1์ฃผ\\t2026/03/05\\n\" \\\n", " \"E-0010\\tD๋ถ€๋Œ€\\t๊ทœ๊ฒฉ ๋ถˆ์ผ์น˜๋กœ ์žฌ๊ฒ€์‚ฌ\\t3\\t2025.11.03\\n\" \\\n", " \"E-0001\\t๊ฐ€์ƒA๋ถ€๋Œ€\\t๋ถ€ํ’ˆ ๋ฏธ์ž…๊ณ \\t5์ผ\\t2026.03.02\\n\"\n", "RAW_JSON = json.dumps({\"result\": \"OK\", \"items\": [\n", " {\"equip\": \"E-0011\", \"unit\": {\"name\": \"๊ฐ€์ƒE๋ถ€๋Œ€\"}, \"reason\": \"์˜ˆ๋น„ํ’ˆ ์ฒญ๊ตฌ ํ›„ ์ž…๊ณ  ๋Œ€๊ธฐ\", \"wait\": {\"value\": 2, \"unit\": \"week\"}, \"received\": \"2026-02-01\"},\n", " {\"equip\": \"E-0012\", \"unit\": {\"name\": \"๊ฐ€์ƒF๋ถ€๋Œ€\"}, \"reason\": \"์ •๋น„๋ณ‘ ๊ต์œก ํŒŒ๊ฒฌ\", \"wait\": {\"value\": 4, \"unit\": \"day\"}, \"received\": \"2026-01-09\"},\n", " {\"equip\": \"E-0013\", \"unit\": {\"name\": \"๊ฐ€์ƒA๋ถ€๋Œ€\"}, \"reason\": \"์™ธ์ฃผ ๊ฒ€์‚ฌ ์ผ์ • ๋Œ€๊ธฐ\", \"wait\": {\"value\": 72, \"unit\": \"hour\"}, \"received\": \"2026-03-20\"}]},\n", " ensure_ascii=False)\n", "LOGS = [\n", " \"[์ •๋น„์ผ์ง€] 2026-03-04 E-0021(๊ฐ€์ƒA๋ถ€๋Œ€) ๋ถ€ํ’ˆ ์ž…๊ณ  ์ง€์—ฐ์œผ๋กœ 6์ผ ๋Œ€๊ธฐ.\",\n", " \"๊ฐ€์ƒB๋ถ€๋Œ€ E-0022 : ์ •๋น„๋ณ‘ ์ธ๋ ฅ ๋ถ€์กฑ, 3์ผ๊ฐ„ ์ฐฉ์ˆ˜ ๋ชปํ•จ (2026.03.05)\",\n", " \"E-0023 / ์™ธ์ฃผ ์ •๋น„ ์ž…๊ณ  ๋Œ€๊ธฐ 2์ฃผ / ๊ฐ€์ƒC๋ถ€๋Œ€ / 26.03.06\",\n", " \"3์›” 7์ผ์ž ๊ฐ€์ƒD๋ถ€๋Œ€ E-0024 ๊ทœ๊ฒฉ ๋ถˆ์ผ์น˜๋กœ ์žฌ๊ฒ€์‚ฌ 5์ผ\",\n", " \"๊ฐ€์ƒE๋ถ€๋Œ€ E-0025 ์›์ธ ๋ถˆ๋ช… ๋Œ€๊ธฐ 4์ผ(2026-03-08)\",\n", "]\n", "# ์ •๋‹ต(๋ถ„๋ฅ˜ ์ •ํ™•๋„ ํ™•์ธ์šฉ): ๊ธฐ๋ก ์ˆœ์„œ๋Œ€๋กœ\n", "TRUTH = {\"E-0001\": \"๋ถ€ํ’ˆ ๋Œ€๊ธฐ\", \"E-0002\": \"์ •๋น„๋ณ‘ ๋ถ€์กฑ\", \"E-0003\": \"์™ธ์ฃผ ์ •๋น„\", \"E-0004\": \"๊ฒ€์‚ฌยท์žฌ์ž‘์—…\", \"E-0005\": \"๋ถ€ํ’ˆ ๋Œ€๊ธฐ\", \"E-0006\": \"๊ฒ€์‚ฌยท์žฌ์ž‘์—…\",\n", " \"E-0007\": \"์ •๋น„๋ณ‘ ๋ถ€์กฑ\", \"E-0008\": \"๋ถ€ํ’ˆ ๋Œ€๊ธฐ\", \"E-0009\": \"์™ธ์ฃผ ์ •๋น„\", \"E-0010\": \"๊ฒ€์‚ฌยท์žฌ์ž‘์—…\", \"E-0011\": \"๋ถ€ํ’ˆ ๋Œ€๊ธฐ\", \"E-0012\": \"์ •๋น„๋ณ‘ ๋ถ€์กฑ\",\n", " \"E-0013\": \"๊ฒ€์‚ฌยท์žฌ์ž‘์—…\", \"E-0021\": \"๋ถ€ํ’ˆ ๋Œ€๊ธฐ\", \"E-0022\": \"์ •๋น„๋ณ‘ ๋ถ€์กฑ\", \"E-0023\": \"์™ธ์ฃผ ์ •๋น„\", \"E-0024\": \"๊ฒ€์‚ฌยท์žฌ์ž‘์—…\", \"E-0025\": \"๊ธฐํƒ€\"}\n", "\n", "# ---- ์ •๊ทœํ™” ๊ทœ์น™(์„œ์‹์ด ๋ฐ”๋€Œ๋ฉด ์ด ์‚ฌ์ „๋งŒ ๊ณ ์นœ๋‹ค) ----\n", "COL_ALIAS = {\"equip\": [\"์žฅ๋น„\", \"์žฅ๋น„๋ฒˆํ˜ธ\", \"equip\"], \"unit\": [\"๋ถ€๋Œ€\", \"์†Œ์†\", \"unit\"], \"reason\": [\"๋Œ€๊ธฐ์‚ฌ์œ \", \"์‚ฌ์œ \", \"reason\"],\n", " \"wait\": [\"๋Œ€๊ธฐ๊ธฐ๊ฐ„\", \"์ง€์—ฐ์ผ์ˆ˜\", \"wait\"], \"received\": [\"์ ‘์ˆ˜์ผ\", \"์ ‘์ˆ˜\", \"received\"]}\n", "UNIT_DAYS = {\"์ผ\": 1, \"์ผ๊ฐ„\": 1, \"day\": 1, \"์ฃผ\": 7, \"week\": 7, \"๊ฐœ์›”\": 30, \"month\": 30, \"์‹œ๊ฐ„\": 1 / 24, \"hour\": 1 / 24}\n", "# ๋ถ„๋ฅ˜ ๊ทœ์น™: ์œ„์—์„œ๋ถ€ํ„ฐ ๋จผ์ € ๊ฑธ๋ฆฌ๋Š” ์‚ฌ์œ ๋กœ ๋ถ„๋ฅ˜(๊ฒ€์‚ฌยท์žฌ์ž‘์—… -> ์™ธ์ฃผ ์ •๋น„ -> ์ •๋น„๋ณ‘ -> ๋ถ€ํ’ˆ)\n", "CAUSE_KW = [(\"๊ฒ€์‚ฌยท์žฌ์ž‘์—…\", [\"๊ฒ€์‚ฌ\", \"์žฌ์ž‘์—…\", \"๋ถˆํ•ฉ๊ฒฉ\", \"๊ทœ๊ฒฉ ๋ถˆ์ผ์น˜\"]), (\"์™ธ์ฃผ ์ •๋น„\", [\"์™ธ์ฃผ\", \"์—…์ฒด ์ž…๊ณ \"]),\n", " (\"์ •๋น„๋ณ‘ ๋ถ€์กฑ\", [\"์ •๋น„๋ณ‘\", \"์ •๋น„์ธ๋ ฅ\", \"์ธ๋ ฅ ๋ถ€์กฑ\", \"ํœด๊ฐ€\", \"๊ต์œก\"]), (\"๋ถ€ํ’ˆ ๋Œ€๊ธฐ\", [\"๋ถ€ํ’ˆ\", \"๋ฏธ์ž…๊ณ \", \"์ฒญ๊ตฌ\", \"์ž…๊ณ  ์ง€์—ฐ\"])]\n", "DEFAULT_YEAR = 2026\n", "\n", "\n", "def norm_unit(s):\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 2] ์ •๋น„ ๋Œ€๊ธฐ ์‹œ๊ฐ„(๋ถ€ํ’ˆ ๋Œ€๊ธฐ, ์ •๋น„๋ณ‘ ๋ถ€์กฑ, ์™ธ์ฃผ ์ •๋น„) ์š”์ธ ๋ถ„๋ฅ˜ ์ „์ฒ˜๋ฆฌ ๋ชจ๋“ˆ ๊ตฌํ˜„ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def norm_wait(v):\n", " \"\"\"๋Œ€๊ธฐ ๊ธฐ๊ฐ„์„ ์ผ ๋‹จ์œ„ ์ˆซ์ž๋กœ ํ†ต์ผ(์ผยท์ฃผยท๊ฐœ์›”ยท์‹œ๊ฐ„).\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 2] ์ •๋น„ ๋Œ€๊ธฐ ์‹œ๊ฐ„(๋ถ€ํ’ˆ ๋Œ€๊ธฐ, ์ •๋น„๋ณ‘ ๋ถ€์กฑ, ์™ธ์ฃผ ์ •๋น„) ์š”์ธ ๋ถ„๋ฅ˜ ์ „์ฒ˜๋ฆฌ ๋ชจ๋“ˆ ๊ตฌํ˜„ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def norm_date(s):\n", " s = str(s).strip().rstrip(\".\")\n", " m = re.match(r\"^(\\d{2,4})[.\\-/๋…„\\s]*(\\d{1,2})[.\\-/์›”\\s]*(\\d{1,2})์ผ?$\", s) or re.match(r\"^(\\d{4})(\\d{2})(\\d{2})$\", s)\n", " if not m:\n", " m2 = re.match(r\"^(\\d{1,2})์›”\\s*(\\d{1,2})์ผ\", s)\n", " if m2:\n", " return date(DEFAULT_YEAR, int(m2.group(1)), int(m2.group(2))).isoformat()\n", " raise ValueError(f\"๋‚ ์งœ ํ•ด์„ ๋ถˆ๊ฐ€: {s}\")\n", " y, mo, d = (int(x) for x in m.groups())\n", " return date(y + 2000 if y < 100 else y, mo, d).isoformat()\n", "\n", "\n", "def classify(text):\n", " \"\"\"๋Œ€๊ธฐ ์‚ฌ์œ  ๋ฌธ์žฅ์„ 4๊ฐœ ์š”์ธ ์ค‘ ํ•˜๋‚˜๋กœ ๋ถ„๋ฅ˜ํ•œ๋‹ค. ์–ด๋А ํ‚ค์›Œ๋“œ์—๋„ ๊ฑธ๋ฆฌ์ง€ ์•Š์œผ๋ฉด '๊ธฐํƒ€'(์‚ฌ๋žŒ์ด ํ™•์ธ).\"\"\"\n", " for cause, kws in CAUSE_KW:\n", " if any(k in text for k in kws):\n", " return cause\n", " return \"๊ธฐํƒ€\"\n", "\n", "\n", "def pick(row, key):\n", " for k in COL_ALIAS[key]:\n", " if k in row:\n", " return row[k]\n", " raise KeyError(f\"ํ•„์ˆ˜ ์—ด ์—†์Œ: {key}\")\n", "\n", "\n", "def to_std(row, src):\n", " reason = str(pick(row, \"reason\"))\n", " return {\"equip_id\": str(pick(row, \"equip\")).strip(), \"unit\": norm_unit(pick(row, \"unit\")), \"wait_days\": norm_wait(pick(row, \"wait\")),\n", " \"date\": norm_date(pick(row, \"received\")), \"reason\": reason, \"cause\": classify(reason), \"source\": src}\n", "\n", "\n", "def load(text, fmt):\n", " \"\"\"ํ˜•์‹๋ณ„ ๋กœ๋”: csv / tsv(์—‘์…€ ๋‚ด๋ณด๋‚ด๊ธฐ) / json. ํ–‰๋งˆ๋‹ค ์„ฑ๊ณตยท์‹คํŒจ๋ฅผ ๋”ฐ๋กœ ์„ผ๋‹ค.\"\"\"\n", " if fmt == \"json\":\n", " rows = [{\"equip\": r[\"equip\"], \"unit\": r[\"unit\"][\"name\"], \"reason\": r[\"reason\"], \"wait\": r[\"wait\"], \"received\": r[\"received\"]} for r in json.loads(text)[\"items\"]]\n", " else:\n", " rows = list(csv.DictReader(io.StringIO(text), delimiter=\"\\t\" if fmt == \"tsv\" else \",\"))\n", " ok, fail = [], []\n", " for r in rows:\n", " try:\n", " ok.append(to_std(r, fmt))\n", " except (KeyError, ValueError) as e:\n", " fail.append((r, str(e)))\n", " return ok, fail\n", "\n", "\n", "def parse_log(line):\n", " \"\"\"์ •๋น„์ผ์ง€ ๋ฌธ์žฅ 1์ค„์—์„œ ์žฅ๋น„ยท๋ถ€๋Œ€ยท๋Œ€๊ธฐ ์ผ์ˆ˜ยท๋‚ ์งœยท์‚ฌ์œ ๋ฅผ ์ถ”์ถœํ•œ๋‹ค.\"\"\"\n", " eq = re.search(r\"E-\\d{4}\", line)\n", " unit = re.search(r\"(?:๊ฐ€์ƒ\\s*)?[A-F]\\s*๋ถ€๋Œ€\", line)\n", " wait = re.search(r\"(\\d+)\\s*(์ผ๊ฐ„|์ผ|์ฃผ|๊ฐœ์›”|์‹œ๊ฐ„)\", line)\n", " dt = re.search(r\"(\\d{2,4}[.\\-/]\\d{1,2}[.\\-/]\\d{1,2})|(\\d{1,2}์›”\\s*\\d{1,2}์ผ)\", line)\n", " if not (eq and unit and wait and dt):\n", " raise ValueError(\"ํ•„์ˆ˜ ํ•ญ๋ชฉ(์žฅ๋น„ยท๋ถ€๋Œ€ยท๋Œ€๊ธฐยท๋‚ ์งœ) ์ถ”์ถœ ์‹คํŒจ\")\n", " reason = re.sub(r\"E-\\d{4}|\\[์ •๋น„์ผ์ง€\\]|\\(.*?\\)|\\d{2,4}[.\\-/]\\d{1,2}[.\\-/]\\d{1,2}|\\d{1,2}์›”\\s*\\d{1,2}์ผ์ž?|๊ฐ€์ƒ\\s*[A-F]\\s*๋ถ€๋Œ€|[/:]\", \" \", line)\n", " reason = re.sub(r\"\\s+\", \" \", reason).strip()\n", " return {\"equip_id\": eq.group(0), \"unit\": norm_unit(unit.group(0)), \"wait_days\": norm_wait(wait.group(0)), \"date\": norm_date(dt.group(0)),\n", " \"reason\": reason, \"cause\": classify(reason), \"source\": \"log\"}\n", "\n", "\n", "# 1) ํ˜•์‹๋ณ„ ํŒŒ์‹ฑ\n", "results, total, success = [], 0, 0\n", "for fmt, text in [(\"csv\", RAW_CSV), (\"tsv\", RAW_TSV), (\"json\", RAW_JSON)]:\n", " ok, fail = load(text, fmt)\n", " results += ok; total += len(ok) + len(fail); success += len(ok)\n", " print(f\"[{fmt.upper():4s}] {len(ok) + len(fail)}ํ–‰ ์ค‘ {len(ok)}ํ–‰ ์„ฑ๊ณต\" + (f\", ์‹คํŒจ {[e for _, e in fail]}\" if fail else \"\"))\n", "logs, l_fail = [], 0\n", "for t in LOGS:\n", " try:\n", " logs.append(parse_log(t))\n", " except ValueError as e:\n", " l_fail += 1; print(\"์ผ์ง€ ํŒŒ์‹ฑ ์‹คํŒจ:\", e)\n", "results += logs; total += len(LOGS); success += len(logs)\n", "print(f\"[์ •๋น„์ผ์ง€ ๋ฌธ์žฅ] {len(LOGS)}๊ฑด ์ค‘ {len(logs)}๊ฑด ์„ฑ๊ณต\")\n", "rate = success / total\n", "print(f\"\\n์ „์ฒด ํŒŒ์‹ฑ ์„ฑ๊ณต๋ฅ  {rate:.0%} ({success}/{total}) - ๋ชฉํ‘œ 100%: {'๋‹ฌ์„ฑ' if rate == 1 else '๋ฏธ๋‹ฌ'}\")\n", "\n", "# 2) ์ •์ œ ๊ฒฐ๊ณผ(ํ‘œ๊ธฐ ํ†ต์ผ, ์ค‘๋ณต ๊ธฐ๋ก ์ •๋ฆฌ)\n", "std = pd.DataFrame(results)\n", "before = len(std)\n", "std = std.drop_duplicates([\"equip_id\", \"date\", \"cause\"]).sort_values(\"equip_id\")\n", "print(f\"\\n[์ •์ œ๋œ ์ •๋น„ ๋Œ€๊ธฐ ๊ธฐ๋ก] {before}ํ–‰ -> ์ค‘๋ณต ์ •๋ฆฌ ํ›„ {len(std)}ํ–‰, ๋ถ€๋Œ€ {std['unit'].nunique()}๊ณณ\")\n", "print(std[[\"equip_id\", \"unit\", \"date\", \"wait_days\", \"cause\", \"source\"]].to_string(index=False))\n", "\n", "# 3) ์š”์ธ ๋ถ„๋ฅ˜ ์ •ํ™•๋„์™€ ์‚ฌ๋žŒ ํ™•์ธ ๋Œ€์ƒ\n", "chk = std[std[\"equip_id\"].isin(TRUTH)]\n", "acc = (chk[\"cause\"] == chk[\"equip_id\"].map(TRUTH)).mean()\n", "print(f\"\\n[์š”์ธ ๋ถ„๋ฅ˜] ์ •๋‹ต์ด ์žˆ๋Š” ๊ธฐ๋ก {len(chk)}๊ฑด ์ค‘ ์ผ์น˜ {int((chk['cause'] == chk['equip_id'].map(TRUTH)).sum())}๊ฑด ({acc:.0%})\")\n", "print(std.groupby(\"cause\").agg(๊ฑด์ˆ˜=(\"equip_id\", \"size\"), ํ‰๊ท ๋Œ€๊ธฐ์ผ=(\"wait_days\", \"mean\")).round(1).to_string())\n", "print(\"์‚ฌ๋žŒ ํ™•์ธ ๋Œ€์ƒ('๊ธฐํƒ€'๋กœ ๋ถ„๋ฅ˜):\", \", \".join(f\"{r.equip_id}('{r.reason[:14]}')\" for r in std[std[\"cause\"] == \"๊ธฐํƒ€\"].itertuples()) or \"์—†์Œ\")\n", "\n", "# 4) ์„œ์‹ ๋ณ€๊ฒฝ ๋Œ€์‘ ์‹œํ—˜ โ‘ : ์—ด ์ด๋ฆ„์ด ๋ฐ”๋€ ์ƒˆ ์–‘์‹ -> ๋ณ„์นญ ์‚ฌ์ „ ์ถ”๊ฐ€\n", "new_form = \"์žฅ๋น„ ์ฝ”๋“œ,๋ฐฐ์†๋ถ€๋Œ€,์ง€์—ฐ ์›์ธ,์ง€์—ฐ ๊ธฐ๊ฐ„,์ ‘์ˆ˜ ์ผ์ž\\nE-0031,๊ฐ€์ƒB๋ถ€๋Œ€,๋ถ€ํ’ˆ ๋ฏธ์ž…๊ณ ,7์ผ,2026.04.01\\n\"\n", "ok, fail = load(new_form, \"csv\")\n", "print(f\"\\n์ƒˆ ์–‘์‹(์—ด ์ด๋ฆ„ ๋ณ€๊ฒฝ) 1์ฐจ ์‹œ๋„: ์„ฑ๊ณต {len(ok)} / ์‹คํŒจ {len(fail)} -> {fail[0][1] if fail else '-'}\")\n", "for k, alias in {\"equip\": \"์žฅ๋น„ ์ฝ”๋“œ\", \"unit\": \"๋ฐฐ์†๋ถ€๋Œ€\", \"reason\": \"์ง€์—ฐ ์›์ธ\", \"wait\": \"์ง€์—ฐ ๊ธฐ๊ฐ„\", \"received\": \"์ ‘์ˆ˜ ์ผ์ž\"}.items():\n", " COL_ALIAS[k].append(alias)\n", "ok, fail = load(new_form, \"csv\")\n", "print(f\"๋ณ„์นญ ์‚ฌ์ „ ๊ฐฑ์‹  ํ›„: ์„ฑ๊ณต {len(ok)} / ์‹คํŒจ {len(fail)} (์ฝ”๋“œ ์ˆ˜์ • ์—†์ด ์„ค์ •๋งŒ ๋ณ€๊ฒฝ)\")\n", "\n", "# 5) ์„œ์‹ ๋ณ€๊ฒฝ ๋Œ€์‘ ์‹œํ—˜ โ‘ก: ์ƒˆ ํ‘œํ˜„์˜ ์‚ฌ์œ  -> ๋ถ„๋ฅ˜ ํ‚ค์›Œ๋“œ ํ•œ ์ค„ ์ถ”๊ฐ€\n", "line = \"๊ฐ€์ƒC๋ถ€๋Œ€ E-0032 : ์˜ˆ๋น„ํ’ˆ ์†Œ์ง„์œผ๋กœ 8์ผ๊ฐ„ ์ •๋น„ ๋ณด๋ฅ˜ (2026.04.02)\"\n", "r1 = parse_log(line)\n", "CAUSE_KW[3][1].append(\"์˜ˆ๋น„ํ’ˆ\")\n", "r2 = parse_log(line)\n", "print(f\"์ƒˆ ์‚ฌ์œ  ํ‘œํ˜„('์˜ˆ๋น„ํ’ˆ ์†Œ์ง„') 1์ฐจ ๋ถ„๋ฅ˜: {r1['cause']} -> ํ‚ค์›Œ๋“œ 1๊ฐœ ์ถ”๊ฐ€ ํ›„: {r2['cause']} (์ฝ”๋“œ ์ˆ˜์ • ์—†์ด ์‚ฌ์ „๋งŒ ๋ณด์™„)\")\n", "print(\"โ€ป ์š”์ธ ๋ถ„๋ฅ˜๋Š” ํ‚ค์›Œ๋“œ ๊ธฐ๋ฐ˜ 1์ฐจ ๋ถ„๋ฅ˜์ด๋ฉฐ, '๊ธฐํƒ€'์™€ ๋ณตํ•ฉ ์‚ฌ์œ ๋Š” ์ •๋น„ ๋‹ด๋‹น์ž๊ฐ€ ํ™•์ธํ•ด ํ™•์ •ํ•œ๋‹ค.\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 02 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[CSV ] 6ํ–‰ ์ค‘ 6ํ–‰ ์„ฑ๊ณต\n", "[TSV ] 5ํ–‰ ์ค‘ 5ํ–‰ ์„ฑ๊ณต\n", "[JSON] 3ํ–‰ ์ค‘ 3ํ–‰ ์„ฑ๊ณต\n", "[์ •๋น„์ผ์ง€ ๋ฌธ์žฅ] 5๊ฑด ์ค‘ 5๊ฑด ์„ฑ๊ณต\n", "\n", "์ „์ฒด ํŒŒ์‹ฑ ์„ฑ๊ณต๋ฅ  100% (19/19) - ๋ชฉํ‘œ 100%: ๋‹ฌ์„ฑ\n", "\n", "[์ •์ œ๋œ ์ •๋น„ ๋Œ€๊ธฐ ๊ธฐ๋ก] 19ํ–‰ -> ์ค‘๋ณต ์ •๋ฆฌ ํ›„ 18ํ–‰, ๋ถ€๋Œ€ 6๊ณณ\n", "equip_id unit date wait_days cause source\n", " E-0001 ๊ฐ€์ƒA๋ถ€๋Œ€ 2026-03-02 5.0 ๋ถ€ํ’ˆ ๋Œ€๊ธฐ csv\n", " E-0002 ๊ฐ€์ƒB๋ถ€๋Œ€ 2026-03-15 14.0 ์ •๋น„๋ณ‘ ๋ถ€์กฑ csv\n", " E-0003 ๊ฐ€์ƒC๋ถ€๋Œ€ 2026-01-10 2.0 ์™ธ์ฃผ ์ •๋น„ csv\n", " E-0004 ๊ฐ€์ƒD๋ถ€๋Œ€ 2026-02-27 3.0 ๊ฒ€์‚ฌยท์žฌ์ž‘์—… csv\n", " E-0005 ๊ฐ€์ƒE๋ถ€๋Œ€ 2025-12-30 30.0 ๋ถ€ํ’ˆ ๋Œ€๊ธฐ csv\n", " E-0006 ๊ฐ€์ƒF๋ถ€๋Œ€ 2026-01-05 4.0 ๊ฒ€์‚ฌยท์žฌ์ž‘์—… csv\n", " E-0007 ๊ฐ€์ƒA๋ถ€๋Œ€ 2026-02-11 6.0 ์ •๋น„๋ณ‘ ๋ถ€์กฑ tsv\n", " E-0008 ๊ฐ€์ƒB๋ถ€๋Œ€ 2026-01-20 10.0 ๋ถ€ํ’ˆ ๋Œ€๊ธฐ tsv\n", " E-0009 ๊ฐ€์ƒC๋ถ€๋Œ€ 2026-03-05 7.0 ์™ธ์ฃผ ์ •๋น„ tsv\n", " E-0010 ๊ฐ€์ƒD๋ถ€๋Œ€ 2025-11-03 3.0 ๊ฒ€์‚ฌยท์žฌ์ž‘์—… tsv\n", " E-0011 ๊ฐ€์ƒE๋ถ€๋Œ€ 2026-02-01 14.0 ๋ถ€ํ’ˆ ๋Œ€๊ธฐ json\n", " E-0012 ๊ฐ€์ƒF๋ถ€๋Œ€ 2026-01-09 4.0 ์ •๋น„๋ณ‘ ๋ถ€์กฑ json\n", " E-0013 ๊ฐ€์ƒA๋ถ€๋Œ€ 2026-03-20 3.0 ๊ฒ€์‚ฌยท์žฌ์ž‘์—… json\n", " E-0021 ๊ฐ€์ƒA๋ถ€๋Œ€ 2026-03-04 6.0 ๋ถ€ํ’ˆ ๋Œ€๊ธฐ log\n", " E-0022 ๊ฐ€์ƒB๋ถ€๋Œ€ 2026-03-05 3.0 ์ •๋น„๋ณ‘ ๋ถ€์กฑ log\n", " E-0023 ๊ฐ€์ƒC๋ถ€๋Œ€ 2026-03-06 14.0 ์™ธ์ฃผ ์ •๋น„ log\n", " E-0024 ๊ฐ€์ƒD๋ถ€๋Œ€ 2026-03-07 7.0 ๊ฒ€์‚ฌยท์žฌ์ž‘์—… log\n", " E-0025 ๊ฐ€์ƒE๋ถ€๋Œ€ 2026-03-08 4.0 ๊ธฐํƒ€ log\n", "\n", "[์š”์ธ ๋ถ„๋ฅ˜] ์ •๋‹ต์ด ์žˆ๋Š” ๊ธฐ๋ก 18๊ฑด ์ค‘ ์ผ์น˜ 18๊ฑด (100%)\n", " ๊ฑด์ˆ˜ ํ‰๊ท ๋Œ€๊ธฐ์ผ\n", "cause \n", "๊ฒ€์‚ฌยท์žฌ์ž‘์—… 5 4.0\n", "๊ธฐํƒ€ 1 4.0\n", "๋ถ€ํ’ˆ ๋Œ€๊ธฐ 5 13.0\n", "์™ธ์ฃผ ์ •๋น„ 3 7.7\n", "์ •๋น„๋ณ‘ ๋ถ€์กฑ 4 6.8\n", "์‚ฌ๋žŒ ํ™•์ธ ๋Œ€์ƒ('๊ธฐํƒ€'๋กœ ๋ถ„๋ฅ˜): E-0025('์›์ธ ๋ถˆ๋ช… ๋Œ€๊ธฐ 4์ผ')\n", "\n", "์ƒˆ ์–‘์‹(์—ด ์ด๋ฆ„ ๋ณ€๊ฒฝ) 1์ฐจ ์‹œ๋„: ์„ฑ๊ณต 0 / ์‹คํŒจ 1 -> 'ํ•„์ˆ˜ ์—ด ์—†์Œ: reason'\n", "๋ณ„์นญ ์‚ฌ์ „ ๊ฐฑ์‹  ํ›„: ์„ฑ๊ณต 1 / ์‹คํŒจ 0 (์ฝ”๋“œ ์ˆ˜์ • ์—†์ด ์„ค์ •๋งŒ ๋ณ€๊ฒฝ)\n", "... (์ดํ•˜ ์ƒ๋žต)\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 03. ๊ฐ€๋™๋ฅ  ์ €ํ•˜ ์ƒ์œ„ 5๋Œ€ ์š”์ธ ๋„์ถœ 1์ฐจ ํ†ต๊ณ„ ๋ถ„์„ Baseline ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๊ตฌํ˜„\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 2.5์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "๊ฐ€๋™๋ฅ  ์ €ํ•˜ ์ƒ์œ„ ์š”์ธ ๋„์ถœ 1์ฐจ ํ†ต๊ณ„ ๋ถ„์„ Baseline ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ๊ตฌํ˜„ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **๊ฐ€๋™๋ฅ  ์ €ํ•˜์˜ ์ƒ์œ„ 5๋Œ€ ์š”์ธ์„ ์ƒ๊ด€ยทํšจ๊ณผํฌ๊ธฐยท์†์‹ค์ผ๋กœ ๋„์ถœํ•˜๋Š” 1์ฐจ ํ†ต๊ณ„ Baseline์„ ๋งŒ๋“ค๊ณ , ์ •๋น„ยท๊ฒ€์‚ฌ ๋ณ‘๋ชฉ ํŒ๋ณ„ ๋ฃฐ 15์ข… ์ด์ƒ์„ YAML ์„ค์ •์œผ๋กœ ๊ตฌํ˜„ํ•ด ์ €๊ฐ€๋™ ํ›„๋ณด๋ฅผ ๊ฐ€๋ ค๋‚ธ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **๊ฐ€์ƒ ์žฅ๋น„ 300๋Œ€์—์„œ ๊ฐ€๋™๋ฅ  70% ๋ฏธ๋งŒ ์žฅ๋น„์™€ ๋‚˜๋จธ์ง€์˜ ์š”์ธ๋ณ„ ํ‰๊ท  ์ฐจ์ด(ํšจ๊ณผํฌ๊ธฐ)์™€ ๊ฐ€๋™๋ฅ ๊ณผ์˜ ์ƒ๊ด€์„ ๊ตฌํ•ด ์ƒ์œ„ 5๊ฐœ ์š”์ธ์„ ์ • ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/engine/qlt04_rule_engine.py` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ๊ตฐ ๊ทœ์ • ๋ฐ ์ง€์นจ ์กฐํ•ญ๊ณผ์˜ ์ •ํ™•ํ•œ ์ผ์น˜์„ฑ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ์—…๋ฌด ๊ฒ€์ฆ ๋ฃฐ 15์ข… ์ด์ƒ ์ฝ”๋”ฉ, ๋ฃฐ ์ฒ˜๋ฆฌ ์ง€์—ฐ์‹œ๊ฐ„ 50ms ์ดํ•˜\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ๋ฃฐ ์—”์ง„ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ 100% ํ†ต๊ณผ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ๊ฒฝ๊ณ„ ์กฐ๊ฑด(Boundary condition)์— ๋Œ€ํ•œ ๋ฃฐ ํŒ์ • ๊ฒ€์ฆ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"๊ฐ€์ƒ ์žฅ๋น„ 300๋Œ€์˜ ๊ฐ€๋™๋ฅ  ์ €ํ•˜ ์š”์ธ์„ ์ƒ๊ด€ยทํšจ๊ณผํฌ๊ธฐยท์†์‹ค์ผ ๊ธฐ์ค€์œผ๋กœ ๋น„๊ตํ•˜๋Š” 1์ฐจ ํ†ต๊ณ„ Baseline์œผ๋กœ ์ƒ์œ„ 5๋Œ€ ์š”์ธ์„ ๋„์ถœํ•˜๊ณ , YAML ํ˜•์‹ ๋ฃฐ 16์ข…์œผ๋กœ ์ €๊ฐ€๋™ ํ›„๋ณด๋ฅผ ํŒ๋ณ„ํ•˜๋ฉฐ ๊ฒฝ๊ณ„ ์กฐ๊ฑด ๋‹จ์œ„ ์‹œํ—˜๊ณผ ๋ฃฐ ์ฒ˜๋ฆฌ ์ง€์—ฐ์‹œ๊ฐ„(99๋ฐฑ๋ถ„์œ„)์„ ํ™•์ธํ•œ๋‹ค.\"\"\"\n", "import time\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ฐ€์ƒ ์žฅ๋น„ ๊ฐ€๋™๋ฅ ยท์ •๋น„ ๋Œ€๊ธฐ ์ž๋ฃŒ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ๋ถ€๋Œ€ยท์žฅ๋น„ยท์ œ์›ยท๊ฐ€๋™๋ฅ  ์ •๋ณด ์—†์Œ)\n", "# ์žฅ๋น„ ์œ ํ˜• A(๊ถค๋„ํ˜•)ยทB(ํ™”ํฌํ˜•)ยทC(์ฐจ๋ฅœํ˜•)์€ ์ถ”์ƒ ๊ตฌ๋ถ„์ด๋ฉฐ ์‹ค์ œ ์žฅ๋น„์™€ ๋ฌด๊ด€ํ•˜๋‹ค.\n", "# ์ตœ๊ทผ 90์ผ ๋™์•ˆ ์žฅ๋น„ 1๋Œ€์˜ ์ •๋น„ ๋Œ€๊ธฐ ์š”์ธ(๋ถ€ํ’ˆยท์ •๋น„๋ณ‘ยท์™ธ์ฃผ ์ •๋น„ยท์™ธ์ฃผ ๊ฒ€์‚ฌยท์žฌ์ž‘์—…)๊ณผ ๊ฐ€๋™๋ฅ ์„ ๋งŒ๋“ ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "UNITS = [\"๊ฐ€์ƒA๋ถ€๋Œ€\", \"๊ฐ€์ƒB๋ถ€๋Œ€\", \"๊ฐ€์ƒC๋ถ€๋Œ€\", \"๊ฐ€์ƒD๋ถ€๋Œ€\", \"๊ฐ€์ƒE๋ถ€๋Œ€\", \"๊ฐ€์ƒF๋ถ€๋Œ€\"]\n", "ETYPES = [\"์œ ํ˜•A\", \"์œ ํ˜•B\", \"์œ ํ˜•C\"]\n", "WINDOW = 90 # ๊ด€์ธก ๊ธฐ๊ฐ„(์ผ)\n", "CAUSES = [\"๋ถ€ํ’ˆ ๋Œ€๊ธฐ\", \"์ •๋น„๋ณ‘ ๋ถ€์กฑ\", \"์™ธ์ฃผ ์ •๋น„\", \"๊ฒ€์‚ฌยท์žฌ์ž‘์—…\"]\n", "CAUSE_COL = {\"๋ถ€ํ’ˆ ๋Œ€๊ธฐ\": \"parts_wait_days\", \"์ •๋น„๋ณ‘ ๋ถ€์กฑ\": \"labor_wait_days\", \"์™ธ์ฃผ ์ •๋น„\": \"outsource_days\", \"๊ฒ€์‚ฌยท์žฌ์ž‘์—…\": \"inspect_wait_days\"}\n", "NUM_FIELDS = [\"age_years\", \"op_hours\", \"maint_orders\", \"parts_wait_days\", \"labor_wait_days\", \"outsource_days\", \"inspect_wait_days\", \"rework_count\",\n", " \"spec_mismatch\", \"defect_reports\", \"parts_stock_pct\", \"mechanic_fill_pct\", \"avail_pct\"]\n", "FEAT = [\"age_years\", \"maint_orders\", \"parts_stock_pct\", \"mechanic_fill_pct\", \"outsource_days\", \"inspect_wait_days\", \"rework_count\", \"spec_mismatch\", \"defect_reports\", \"etype_B\", \"etype_C\"]\n", "FEAT_KO = {\"age_years\": \"์žฅ๋น„ ์—ฐ๋ น\", \"maint_orders\": \"์ •๋น„ ์š”์ฒญ ๊ฑด์ˆ˜\", \"parts_stock_pct\": \"๋ถ€ํ’ˆ ์žฌ๊ณ  ๊ฐ€์šฉ๋ฅ \", \"mechanic_fill_pct\": \"์ •๋น„๋ณ‘ ์ถฉ์›์œจ\", \"outsource_days\": \"์™ธ์ฃผ ์ •๋น„ ์ผ์ˆ˜\",\n", " \"inspect_wait_days\": \"์™ธ์ฃผ ๊ฒ€์‚ฌ ๋Œ€๊ธฐ ์ผ์ˆ˜\", \"rework_count\": \"์žฌ์ž‘์—… ๊ฑด์ˆ˜\", \"spec_mismatch\": \"๊ทœ๊ฒฉยท์ œ์› ๋ถˆ์ผ์น˜\", \"defect_reports\": \"ํ•˜์žยท๋ถˆ๋งŒ ์ ‘์ˆ˜\",\n", " \"etype_B\": \"์œ ํ˜•B ์—ฌ๋ถ€\", \"etype_C\": \"์œ ํ˜•C ์—ฌ๋ถ€\"}\n", "\n", "\n", "def unit_profile(seed=3):\n", " \"\"\"๋ถ€๋Œ€๋ณ„ ๊ณ ์ • ํŠน์„ฑ: ๋ถ€ํ’ˆ ์žฌ๊ณ  ์ˆ˜์ค€, ์ •๋น„๋ณ‘ ์ถฉ์›์œจ, ์™ธ์ฃผ ์ •๋น„์†Œ ๊ฑฐ๋ฆฌ(์ผ์ˆ˜ ๊ฐ€์ค‘), ์™ธ์ฃผ ๊ฒ€์‚ฌ ํ˜ผ์žก๋„.\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 3] ๊ฐ€๋™๋ฅ  ์ €ํ•˜ ์ƒ์œ„ 5๋Œ€ ์š”์ธ ๋„์ถœ 1์ฐจ ํ†ต๊ณ„ ๋ถ„์„ Baseline ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๊ตฌํ˜„ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def make_fleet(n=300, seed=7, shock=0.0):\n", " \"\"\"์žฅ๋น„ n๋Œ€์˜ 90์ผ ๊ด€์ธก ์ž๋ฃŒ(ํ•„๋“œ 16๊ฐœ)์™€ ์ •๋‹ต ์š”์ธ์„ ๋งŒ๋“ ๋‹ค. shock์€ ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋ณ‘๋ชฉ ์•…ํ™” ์ •๋„(AAR์šฉ ํ™˜๊ฒฝ ๋ณ€ํ™”).\"\"\"\n", " rng = np.random.default_rng(seed)\n", " pf = unit_profile()\n", " ui = rng.integers(0, len(UNITS), n)\n", " et = rng.choice(ETYPES, n, p=[.35, .3, .35])\n", " age = np.clip(rng.gamma(4, 2.2, n), 0.5, 30).round(1)\n", " eff = np.array([{\"์œ ํ˜•A\": 1.15, \"์œ ํ˜•B\": 1.0, \"์œ ํ˜•C\": 0.9}[t] for t in et])\n", " orders = rng.poisson(np.clip(1.2 + 0.12 * age, 0.5, 6) * eff)\n", " stock = np.clip(pf[\"stock\"].to_numpy()[ui] + rng.normal(0, 8, n), 20, 100).round(0)\n", " fill = np.clip(pf[\"fill\"].to_numpy()[ui] + rng.normal(0, 6, n), 30, 100).round(0)\n", " parts_w = (orders * rng.gamma(2, 0.55 * (1 + ((100 - stock) / 28) ** 2) / 2)).round(0)\n", " labor_w = (orders * rng.gamma(2, 0.7 * (1 + ((100 - fill) / 24) ** 2) / 2)).round(0)\n", " n_out = rng.binomial(orders, 0.3)\n", " out_d = (n_out * rng.gamma(3, 4.5 * pf[\"dist\"].to_numpy()[ui] / 3)).round(0)\n", " insp_w = (n_out * rng.gamma(2, (2.6 + shock) * pf[\"queue\"].to_numpy()[ui] / 2)).round(0)\n", " spec = rng.poisson(0.2 + 0.04 * age)\n", " rework = rng.binomial(n_out, np.clip(0.08 + 0.1 * spec, 0, .6))\n", " defect = rng.poisson(0.3 + 0.7 * rework)\n", " base_rep = orders * 1.8\n", " down = base_rep + parts_w + labor_w + out_d + insp_w + rework * 4 + rng.normal(0, 1.2, n)\n", " comp = np.column_stack([parts_w, labor_w, out_d, insp_w + rework * 4])\n", " df = pd.DataFrame({\"equip_id\": [f\"E-{i + 1:04d}\" for i in range(n)], \"unit\": np.array(UNITS)[ui], \"etype\": et, \"age_years\": age,\n", " \"op_hours\": np.clip(rng.normal(320 - 4 * age, 70, n), 40, 600).round(0), \"maint_orders\": orders,\n", " \"parts_wait_days\": parts_w, \"labor_wait_days\": labor_w, \"outsource_days\": out_d, \"inspect_wait_days\": insp_w,\n", " \"rework_count\": rework, \"spec_mismatch\": spec, \"defect_reports\": defect, \"parts_stock_pct\": stock, \"mechanic_fill_pct\": fill,\n", " \"avail_pct\": (100 * (1 - np.clip(down, 0, WINDOW) / WINDOW)).round(1)})\n", " df[\"main_cause\"] = np.where(comp.max(axis=1) < 3, \"์—†์Œ\", np.array(CAUSES)[comp.argmax(axis=1)])\n", " return df\n", "\n", "\n", "def feat_frame(df):\n", " \"\"\"ํšŒ๊ท€ยท๋ถ„์„์šฉ ์„ค๋ช… ๋ณ€์ˆ˜ ํ‘œ(์œ ํ˜•์€ ๋”๋ฏธ ๋ณ€์ˆ˜).\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 3] ๊ฐ€๋™๋ฅ  ์ €ํ•˜ ์ƒ์œ„ 5๋Œ€ ์š”์ธ ๋„์ถœ 1์ฐจ ํ†ต๊ณ„ ๋ถ„์„ Baseline ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๊ตฌํ˜„ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def make_trend(months=12, seed=5, shock_unit=None):\n", " \"\"\"[๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๋ถ€๋Œ€ x ์œ ํ˜• x ์›” ํ‰๊ท  ๊ฐ€๋™๋ฅ  ์ถ”์„ธ. shock_unit ๋ถ€๋Œ€๋Š” ํ•˜๋ฐ˜๊ธฐ์— ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋ณ‘๋ชฉ์œผ๋กœ ๊ฐ€๋™๋ฅ ์ด ๋–จ์–ด์ง„๋‹ค.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " pf = unit_profile()\n", " rows = []\n", " for _, u in pf.iterrows():\n", " base = 62 + 0.22 * u[\"stock\"] + 0.12 * u[\"fill\"] - 3.0 * u[\"queue\"]\n", " for t in ETYPES:\n", " off = {\"์œ ํ˜•A\": -3.0, \"์œ ํ˜•B\": 0.0, \"์œ ํ˜•C\": 2.0}[t]\n", " for m in range(months):\n", " v = base + off + 2.0 * np.sin(m / 12 * 2 * np.pi) + rng.normal(0, 1.0)\n", " if shock_unit == u[\"unit\"] and m >= months // 2:\n", " v -= 1.5 * (m - months // 2 + 1)\n", " rows.append((u[\"unit\"], t, m + 1, round(float(np.clip(v, 30, 99)), 1)))\n", " return pd.DataFrame(rows, columns=[\"unit\", \"etype\", \"month\", \"avail_pct\"])\n", "\n", "# ---- ์ •๋น„ยท๊ฒ€์‚ฌ ๋ณ‘๋ชฉ ํŒ๋ณ„ ๋ฃฐ ์„ค์ •(YAML ํ˜•์‹ ๋ฌธ์ž์—ด: ๊ธฐ์ค€์ด ๋ฐ”๋€Œ๋ฉด ์ฝ”๋“œ๊ฐ€ ์•„๋‹ˆ๋ผ ์ด ์„ค์ •๋งŒ ๊ณ ์นœ๋‹ค) ----\n", "# clause๋Š” ๋ฃฐ๊ณผ ๋Œ€์‘์‹œํ‚จ '๊ฐ€์ƒ ํ’ˆ์งˆ์ง€์นจ ์กฐํ•ญ' ์˜ˆ์‹œ์ด๋ฉฐ ์‹ค์ œ ๊ทœ์ • ์›๋ฌธ์ด ์•„๋‹ˆ๋‹ค.\n", "RULES_YAML = \"\"\"\n", "- {id: Q01, name: ๋ถ€ํ’ˆ ์žฌ๊ณ  ๊ฐ€์šฉ๋ฅ  50% ๋ฏธ๋งŒ, field: parts_stock_pct, op: \"<\", value: 50, weight: 2, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ3์กฐ}\n", "- {id: Q02, name: ์ •๋น„๋ณ‘ ์ถฉ์›์œจ 60% ๋ฏธ๋งŒ, field: mechanic_fill_pct, op: \"<\", value: 60, weight: 2, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ4์กฐ}\n", "- {id: Q03, name: ๋ถ€ํ’ˆ ๋Œ€๊ธฐ 8์ผ ์ดˆ๊ณผ, field: parts_wait_days, op: \">\", value: 8, weight: 2, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ5์กฐ}\n", "- {id: Q04, name: ์ •๋น„๋ณ‘ ๋ถ€์กฑ ๋Œ€๊ธฐ 8์ผ ์ดˆ๊ณผ, field: labor_wait_days, op: \">\", value: 8, weight: 2, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ6์กฐ}\n", "- {id: Q05, name: ์™ธ์ฃผ ์ •๋น„ 15์ผ ์ดˆ๊ณผ, field: outsource_days, op: \">\", value: 15, weight: 2, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ7์กฐ}\n", "- {id: Q06, name: ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋Œ€๊ธฐ 6์ผ ์ดˆ๊ณผ, field: inspect_wait_days, op: \">\", value: 6, weight: 3, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ8์กฐ}\n", "- {id: Q07, name: ์žฌ์ž‘์—… 1๊ฑด ์ด์ƒ, field: rework_count, op: \">=\", value: 1, weight: 3, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ9์กฐ}\n", "- {id: Q08, name: ์žฌ์ž‘์—…๋ฅ  25% ์ดˆ๊ณผ, field: rework_rate, op: \">\", value: 0.25, weight: 2, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ9์กฐ}\n", "- {id: Q09, name: ๊ทœ๊ฒฉยท์ œ์› ๋ถˆ์ผ์น˜ 3๊ฑด ์ด์ƒ, field: spec_mismatch, op: \">=\", value: 3, weight: 3, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ10์กฐ}\n", "- {id: Q10, name: ํ•˜์žยท๋ถˆ๋งŒ 3๊ฑด ์ด์ƒ, field: defect_reports, op: \">=\", value: 3, weight: 2, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ11์กฐ}\n", "- {id: Q11, name: ์žฅ๋น„ ์—ฐ๋ น 15๋…„ ์ดˆ๊ณผ, field: age_years, op: \">\", value: 15, weight: 1, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ12์กฐ}\n", "- {id: Q12, name: ์ •๋น„ ์š”์ฒญ 6๊ฑด ์ด์ƒ, field: maint_orders, op: \">=\", value: 6, weight: 1, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ13์กฐ}\n", "- {id: Q13, name: ๋Œ€๊ธฐ ์ผ์ˆ˜ ํ•ฉ๊ณ„ 30์ผ ์ดˆ๊ณผ, field: total_wait, op: \">\", value: 30, weight: 3, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ14์กฐ}\n", "- {id: Q14, name: ๊ฒ€์‚ฌ ๋Œ€๊ธฐ๊ฐ€ ์™ธ์ฃผ ์ •๋น„ ์ผ์ˆ˜์˜ 80% ์ดˆ๊ณผ, field: insp_ratio, op: \">\", value: 0.8, weight: 2, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ8์กฐ}\n", "- {id: Q15, name: ๊ทœ๊ฒฉ ๋ถˆ์ผ์น˜ 2๊ฑด ์ด์ƒ๊ณผ ํ•˜์ž ์ ‘์ˆ˜ ๋™๋ฐ˜, field: spec_defect, op: \"==\", value: true, weight: 3, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ10์กฐ}\n", "- {id: Q16, name: ์ตœ๊ทผ 90์ผ ๊ฐ€๋™ ์‹œ๊ฐ„ 150์‹œ๊ฐ„ ๋ฏธ๋งŒ, field: op_hours, op: \"<\", value: 150, weight: 1, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ15์กฐ}\n", "\"\"\"\n", "GRADE_CUT = [(10, \"๊ณ ์œ„ํ—˜\"), (6, \"์ฃผ์˜\"), (3, \"๊ด€์‹ฌ\")]\n", "OPS = {\"<\": lambda a, b: a < b, \">\": lambda a, b: a > b, \"<=\": lambda a, b: a <= b, \">=\": lambda a, b: a >= b, \"==\": lambda a, b: a == b}\n", "\n", "\n", "def parse_rules(text):\n", " \"\"\"YAML ํ•œ ์ค„ ๋งต({key: value, ...}) ํ˜•์‹ ๋ฃฐ ๋ฌธ์ž์—ด์„ ์ฝ๋Š”๋‹ค(์™ธ๋ถ€ yaml ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—†์ด).\"\"\"\n", " out = []\n", " for line in text.strip().splitlines():\n", " body = line.strip()[3:-1]\n", " r = {}\n", " for part in body.split(\", \"):\n", " k, v = part.split(\": \", 1)\n", " v = v.strip().strip('\"')\n", " r[k] = True if v == \"true\" else False if v == \"false\" else (float(v) if v.replace(\".\", \"\", 1).isdigit() else v)\n", " out.append(r)\n", " return out\n", "\n", "\n", "def derive_rec(rec):\n", " \"\"\"๋ฃฐ์ด ์“ฐ๋Š” ํŒŒ์ƒ ํ•„๋“œ(๋Œ€๊ธฐ ํ•ฉ๊ณ„, ์žฌ์ž‘์—…๋ฅ , ๊ฒ€์‚ฌ ๋Œ€๊ธฐ ๋น„์œจ(์™ธ์ฃผ 5์ผ ์ด์ƒ์ผ ๋•Œ), ๊ทœ๊ฒฉ ๋ถˆ์ผ์น˜ 2๊ฑด ์ด์ƒ+ํ•˜์ž ๋™๋ฐ˜)๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.\"\"\"\n", " r = dict(rec)\n", " r[\"total_wait\"] = r[\"parts_wait_days\"] + r[\"labor_wait_days\"] + r[\"outsource_days\"] + r[\"inspect_wait_days\"]\n", " r[\"rework_rate\"] = r[\"rework_count\"] / r[\"maint_orders\"] if r[\"maint_orders\"] else 0.0\n", " r[\"insp_ratio\"] = r[\"inspect_wait_days\"] / r[\"outsource_days\"] if r[\"outsource_days\"] >= 5 else 0.0\n", " r[\"spec_defect\"] = bool(r[\"spec_mismatch\"] >= 2 and r[\"defect_reports\"] >= 1)\n", " return r\n", "\n", "\n", "def apply_rules(rec, rules, derived=False):\n", " \"\"\"๋ฃฐ ์ ์šฉ: (์ ์ˆ˜, ์ ์ค‘ ๋ฃฐ id ๋ชฉ๋ก). derived=True๋ฉด ์ด๋ฏธ ํŒŒ์ƒ ํ•„๋“œ๊ฐ€ ์žˆ๋Š” ๋ ˆ์ฝ”๋“œ(๊ฒฝ๊ณ„ ์‹œํ—˜์šฉ)๋กœ ๋ณธ๋‹ค.\"\"\"\n", " r = rec if derived else derive_rec(rec)\n", " hits = [x[\"id\"] for x in rules if OPS[x[\"op\"]](r[x[\"field\"]], x[\"value\"])]\n", " w = {x[\"id\"]: x[\"weight\"] for x in rules}\n", " return sum(w[h] for h in hits), hits\n", "\n", "\n", "def grade_of(score):\n", " for cut, g in GRADE_CUT:\n", " if score >= cut:\n", " return g\n", " return \"์ •์ƒ\"\n", "\n", "fleet = make_fleet(300, seed=7)\n", "fleet[\"low\"] = fleet[\"avail_pct\"] < 70 # ์ €๊ฐ€๋™: ๊ฐ€๋™๋ฅ  70% ๋ฏธ๋งŒ(๊ฐ€์ƒ ๊ธฐ์ค€)\n", "FAC = [\"maint_orders\", \"parts_wait_days\", \"labor_wait_days\", \"outsource_days\", \"inspect_wait_days\", \"rework_count\", \"spec_mismatch\", \"defect_reports\",\n", " \"age_years\", \"op_hours\", \"parts_stock_pct\", \"mechanic_fill_pct\"]\n", "KO = {**FEAT_KO, \"parts_wait_days\": \"๋ถ€ํ’ˆ ๋Œ€๊ธฐ ์ผ์ˆ˜\", \"labor_wait_days\": \"์ •๋น„๋ณ‘ ๋ถ€์กฑ ๋Œ€๊ธฐ ์ผ์ˆ˜\", \"op_hours\": \"๊ฐ€๋™ ์‹œ๊ฐ„\"}\n", "\n", "# 1) 1์ฐจ ํ†ต๊ณ„ Baseline: ๊ฐ€๋™๋ฅ ๊ณผ์˜ ์ƒ๊ด€๊ณ„์ˆ˜, ์ €๊ฐ€๋™๊ตฐ-์ •์ƒ๊ตฐ ํšจ๊ณผํฌ๊ธฐ(Cohen d), ์ €๊ฐ€๋™๊ตฐ ํ‰๊ท  ๊ฐ’\n", "rows = []\n", "for f in FAC:\n", " a, b = fleet.loc[fleet[\"low\"], f], fleet.loc[~fleet[\"low\"], f]\n", " sp = np.sqrt((a.var() + b.var()) / 2) or 1.0\n", " rows.append({\"์š”์ธ\": KO[f], \"์ƒ๊ด€(r)\": fleet[f].corr(fleet[\"avail_pct\"]), \"ํšจ๊ณผํฌ๊ธฐ(d)\": (a.mean() - b.mean()) / sp, \"์ €๊ฐ€๋™๊ตฐ ํ‰๊ท \": a.mean(), \"์ •์ƒ๊ตฐ ํ‰๊ท \": b.mean(), \"key\": f})\n", "fac = pd.DataFrame(rows)\n", "fac[\"์ˆœ์œ„์ ์ˆ˜\"] = fac[\"์ƒ๊ด€(r)\"].abs() + fac[\"ํšจ๊ณผํฌ๊ธฐ(d)\"].abs() / 3 # ๋‘ ์ง€ํ‘œ๋ฅผ ํ•จ๊ป˜ ๋ณธ ์ˆœ์œ„ ์ ์ˆ˜(ํœด๋ฆฌ์Šคํ‹ฑ)\n", "top5 = fac.sort_values(\"์ˆœ์œ„์ ์ˆ˜\", ascending=False).head(5)\n", "print(f\"[Baseline] ์žฅ๋น„ {len(fleet)}๋Œ€, ์ €๊ฐ€๋™(70% ๋ฏธ๋งŒ) {int(fleet['low'].sum())}๋Œ€, ํ‰๊ท  ๊ฐ€๋™๋ฅ  {fleet['avail_pct'].mean():.1f}%\")\n", "print(\"[๊ฐ€๋™๋ฅ  ์ €ํ•˜ ์ƒ์œ„ 5๋Œ€ ์š”์ธ]\")\n", "print(top5.drop(columns=[\"key\", \"์ˆœ์œ„์ ์ˆ˜\"]).round(2).to_string(index=False))\n", "lost = fleet.loc[fleet[\"low\"], [\"parts_wait_days\", \"labor_wait_days\", \"outsource_days\", \"inspect_wait_days\"]].mean()\n", "print(\"์ €๊ฐ€๋™ ์žฅ๋น„์˜ ํ‰๊ท  ๋Œ€๊ธฐ ์†์‹ค์ผ:\", \", \".join(f\"{KO[k]} {v:.1f}์ผ\" for k, v in lost.items()))\n", "print(\"์ฃผ์š” ์ €๊ฐ€๋™ ์š”์ธ ๋ถ„ํฌ(์ •๋‹ต):\", fleet[\"main_cause\"].value_counts().to_dict())\n", "\n", "# 2) ๋ฃฐ ์—”์ง„: ์žฅ๋น„ ๋ ˆ์ฝ”๋“œ์— 16์ข… ๋ฃฐ์„ ์ ์šฉ\n", "rules = parse_rules(RULES_YAML)\n", "recs = fleet.drop(columns=[\"main_cause\", \"low\"]).to_dict(\"records\")\n", "lat, scores, hits = [], [], []\n", "for r in recs:\n", " t = time.perf_counter(); s, h = apply_rules(r, rules); lat.append((time.perf_counter() - t) * 1000)\n", " scores.append(s); hits.append(h)\n", "fleet[\"score\"], fleet[\"grade\"] = scores, [grade_of(s) for s in scores]\n", "print(f\"\\n[๋ฃฐ ์—”์ง„] ๋ฃฐ {len(rules)}์ข… ์ ์šฉ, ์ฒ˜๋ฆฌ ์ง€์—ฐ ํ‰๊ท  {np.mean(lat):.3f}ms / 99๋ฐฑ๋ถ„์œ„ {np.percentile(lat, 99):.3f}ms (๋ชฉํ‘œ 50ms ์ดํ•˜: {'๋‹ฌ์„ฑ' if np.percentile(lat, 99) <= 50 else '๋ฏธ๋‹ฌ'})\")\n", "tab = fleet.groupby(\"grade\").agg(์žฅ๋น„์ˆ˜=(\"equip_id\", \"size\"), ์‹ค์ œ์ €๊ฐ€๋™=(\"low\", \"sum\"), ์ €๊ฐ€๋™๋น„์œจ=(\"low\", \"mean\")).reindex([\"๊ณ ์œ„ํ—˜\", \"์ฃผ์˜\", \"๊ด€์‹ฌ\", \"์ •์ƒ\"])\n", "print(tab.round(2).to_string())\n", "flag = fleet[\"score\"] >= 6 # ์ฃผ์˜ ์ด์ƒ์„ ์ €๊ฐ€๋™ ํ›„๋ณด๋กœ ๋ณธ๋‹ค\n", "tp = int((flag & fleet[\"low\"]).sum()); prec, rec = tp / max(flag.sum(), 1), tp / fleet[\"low\"].sum()\n", "print(f\"์ €๊ฐ€๋™ ํ›„๋ณด(์ฃผ์˜ ์ด์ƒ) {int(flag.sum())}๋Œ€ ์ค‘ ์‹ค์ œ ์ €๊ฐ€๋™ {tp}๋Œ€: ์ •๋ฐ€๋„ {prec:.0%}, ์žฌํ˜„์œจ {rec:.0%}\")\n", "cnt = pd.Series([h for hs in hits for h in hs]).value_counts()\n", "print(\"๋ฃฐ๋ณ„ ์ ์ค‘ ์ƒ์œ„:\", \", \".join(f\"{k} {v}๊ฑด\" for k, v in cnt.head(5).items()))\n", "\n", "# 3) ๊ฒฝ๊ณ„ ์กฐ๊ฑด ๋‹จ์œ„ ์‹œํ—˜: ๊ธฐ์ค€๊ฐ’ ์œ„ยท์•„๋ž˜์—์„œ ๋ฃฐ ํŒ์ •์ด ๋งž๋Š”์ง€(๋ฃฐ๋งˆ๋‹ค 2~3๊ฑด)\n", "BASE = derive_rec({\"parts_stock_pct\": 90, \"mechanic_fill_pct\": 90, \"parts_wait_days\": 0, \"labor_wait_days\": 0, \"outsource_days\": 0, \"inspect_wait_days\": 0,\n", " \"rework_count\": 0, \"maint_orders\": 0, \"spec_mismatch\": 0, \"defect_reports\": 0, \"age_years\": 5, \"op_hours\": 300})\n", "tests = ok = 0\n", "fails = []\n", "for x in rules:\n", " v, d = x[\"value\"], (0.01 if isinstance(x[\"value\"], float) and not float(x[\"value\"]).is_integer() else 1)\n", " cases = {\"<\": [(v, False), (v - d, True), (v + d, False)], \">\": [(v, False), (v + d, True), (v - d, False)], \">=\": [(v - d, False), (v, True), (v + d, True)],\n", " \"<=\": [(v + d, False), (v, True), (v - d, True)], \"==\": [(True, True), (False, False)]}[x[\"op\"]]\n", " for val, expect in cases:\n", " r = dict(BASE); r[x[\"field\"]] = val\n", " got = x[\"id\"] in apply_rules(r, rules, derived=True)[1]\n", " tests += 1; ok += got == expect\n", " if got != expect:\n", " fails.append((x[\"id\"], val))\n", "print(f\"\\n[๊ฒฝ๊ณ„ ์กฐ๊ฑด ๋‹จ์œ„ ์‹œํ—˜] {ok}/{tests} ํ†ต๊ณผ ({ok / tests:.0%}), ์‹คํŒจ {fails or '์—†์Œ'} (๋ชฉํ‘œ 100%)\")\n", "\n", "# 4) ํŒ์ • ๊ทผ๊ฑฐ ์˜ˆ์‹œ(์ƒ์œ„ 3๋Œ€)์™€ ์‚ฌ๋žŒ์ด ํ™•์ธํ•  ๊ฒƒ\n", "print(\"\\n[๊ณ ์œ„ํ—˜ ์ƒ์œ„ 3๋Œ€ ํŒ์ • ๊ทผ๊ฑฐ]\")\n", "nm = {x[\"id\"]: x for x in rules}\n", "for _, r in fleet.sort_values(\"score\", ascending=False).head(3).iterrows():\n", " h = apply_rules(recs[int(r.name)], rules)[1]\n", " print(f\" {r['equip_id']} {r['unit']} ์ ์ˆ˜ {r['score']:.0f} ๊ฐ€๋™๋ฅ  {r['avail_pct']}%: \" + \"; \".join(f\"{i} {nm[i]['name']}({nm[i]['clause']})\" for i in h[:3]))\n", "print(f\"๋ฃฐ {len(rules)}์ข…(๋ชฉํ‘œ 15์ข… ์ด์ƒ: {'๋‹ฌ์„ฑ' if len(rules) >= 15 else '๋ฏธ๋‹ฌ'}). ๊ธฐ์ค€๊ฐ’์€ YAML๋งŒ ๋ฐ”๊ฟ” ๊ฐฑ์‹ ํ•˜๋ฉฐ, ์ •๋น„ ์šฐ์„ ์ˆœ์œ„์™€ ํ’ˆ์งˆ ํŒ์ •์€ ์ •๋น„๊ด€ยทํ’ˆ์งˆ๊ฒ€์‚ฌ๊ด€์ด ํ™•์ธํ•œ๋‹ค.\")\n", "\n", "fig, ax = plt.subplots(1, 2, figsize=(10, 4))\n", "t5 = top5.iloc[::-1]\n", "ax[0].barh([k for k in t5[\"key\"]], t5[\"์ƒ๊ด€(r)\"], color=\"tab:blue\"); ax[0].set_title(\"Top-5 factors: correlation with availability\")\n", "ax[1].bar(tab.index.map({\"๊ณ ์œ„ํ—˜\": \"high\", \"์ฃผ์˜\": \"caution\", \"๊ด€์‹ฌ\": \"watch\", \"์ •์ƒ\": \"normal\"}), tab[\"์ €๊ฐ€๋™๋น„์œจ\"], color=\"tab:orange\"); ax[1].set_title(\"Low-availability rate by rule grade\")\n", "plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 03 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[Baseline] ์žฅ๋น„ 300๋Œ€, ์ €๊ฐ€๋™(70% ๋ฏธ๋งŒ) 63๋Œ€, ํ‰๊ท  ๊ฐ€๋™๋ฅ  80.8%\n", "[๊ฐ€๋™๋ฅ  ์ €ํ•˜ ์ƒ์œ„ 5๋Œ€ ์š”์ธ]\n", " ์š”์ธ ์ƒ๊ด€(r) ํšจ๊ณผํฌ๊ธฐ(d) ์ €๊ฐ€๋™๊ตฐ ํ‰๊ท  ์ •์ƒ๊ตฐ ํ‰๊ท \n", " ์ •๋น„ ์š”์ฒญ ๊ฑด์ˆ˜ -0.75 1.56 3.94 1.95\n", " ์™ธ์ฃผ ์ •๋น„ ์ผ์ˆ˜ -0.73 1.26 8.46 1.78\n", " ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋Œ€๊ธฐ ์ผ์ˆ˜ -0.66 1.13 5.56 1.02\n", " ๋ถ€ํ’ˆ ๋Œ€๊ธฐ ์ผ์ˆ˜ -0.63 1.18 8.70 2.48\n", "์ •๋น„๋ณ‘ ๋ถ€์กฑ ๋Œ€๊ธฐ ์ผ์ˆ˜ -0.57 1.01 8.27 2.59\n", "์ €๊ฐ€๋™ ์žฅ๋น„์˜ ํ‰๊ท  ๋Œ€๊ธฐ ์†์‹ค์ผ: ๋ถ€ํ’ˆ ๋Œ€๊ธฐ ์ผ์ˆ˜ 8.7์ผ, ์ •๋น„๋ณ‘ ๋ถ€์กฑ ๋Œ€๊ธฐ ์ผ์ˆ˜ 8.3์ผ, ์™ธ์ฃผ ์ •๋น„ ์ผ์ˆ˜ 8.5์ผ, ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋Œ€๊ธฐ ์ผ์ˆ˜ 5.6์ผ\n", "์ฃผ์š” ์ €๊ฐ€๋™ ์š”์ธ ๋ถ„ํฌ(์ •๋‹ต): {'์—†์Œ': 77, '๋ถ€ํ’ˆ ๋Œ€๊ธฐ': 72, '์ •๋น„๋ณ‘ ๋ถ€์กฑ': 70, '์™ธ์ฃผ ์ •๋น„': 59, '๊ฒ€์‚ฌยท์žฌ์ž‘์—…': 22}\n", "\n", "[๋ฃฐ ์—”์ง„] ๋ฃฐ 16์ข… ์ ์šฉ, ์ฒ˜๋ฆฌ ์ง€์—ฐ ํ‰๊ท  0.009ms / 99๋ฐฑ๋ถ„์œ„ 0.056ms (๋ชฉํ‘œ 50ms ์ดํ•˜: ๋‹ฌ์„ฑ)\n", " ์žฅ๋น„์ˆ˜ ์‹ค์ œ์ €๊ฐ€๋™ ์ €๊ฐ€๋™๋น„์œจ\n", "grade \n", "๊ณ ์œ„ํ—˜ 16 13 0.81\n", "์ฃผ์˜ 24 18 0.75\n", "๊ด€์‹ฌ 45 19 0.42\n", "์ •์ƒ 215 13 0.06\n", "์ €๊ฐ€๋™ ํ›„๋ณด(์ฃผ์˜ ์ด์ƒ) 40๋Œ€ ์ค‘ ์‹ค์ œ ์ €๊ฐ€๋™ 31๋Œ€: ์ •๋ฐ€๋„ 78%, ์žฌํ˜„์œจ 49%\n", "๋ฃฐ๋ณ„ ์ ์ค‘ ์ƒ์œ„: Q01 53๊ฑด, Q02 39๊ฑด, Q03 38๊ฑด, Q04 28๊ฑด, Q06 26๊ฑด\n", "\n", "[๊ฒฝ๊ณ„ ์กฐ๊ฑด ๋‹จ์œ„ ์‹œํ—˜] 47/47 ํ†ต๊ณผ (100%), ์‹คํŒจ ์—†์Œ (๋ชฉํ‘œ 100%)\n", "\n", "[๊ณ ์œ„ํ—˜ ์ƒ์œ„ 3๋Œ€ ํŒ์ • ๊ทผ๊ฑฐ]\n", " E-0047 ๊ฐ€์ƒA๋ถ€๋Œ€ ์ ์ˆ˜ 14 ๊ฐ€๋™๋ฅ  35.7%: Q01 ๋ถ€ํ’ˆ ์žฌ๊ณ  ๊ฐ€์šฉ๋ฅ  50% ๋ฏธ๋งŒ(๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ3์กฐ); Q03 ๋ถ€ํ’ˆ ๋Œ€๊ธฐ 8์ผ ์ดˆ๊ณผ(๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ5์กฐ); Q05 ์™ธ์ฃผ ์ •๋น„ 15์ผ ์ดˆ๊ณผ(๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ7์กฐ)\n", " E-0231 ๊ฐ€์ƒD๋ถ€๋Œ€ ์ ์ˆ˜ 14 ๊ฐ€๋™๋ฅ  51.7%: Q02 ์ •๋น„๋ณ‘ ์ถฉ์›์œจ 60% ๋ฏธ๋งŒ(๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ4์กฐ); Q03 ๋ถ€ํ’ˆ ๋Œ€๊ธฐ 8์ผ ์ดˆ๊ณผ(๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ5์กฐ); Q04 ์ •๋น„๋ณ‘ ๋ถ€์กฑ ๋Œ€๊ธฐ 8์ผ ์ดˆ๊ณผ(๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ6์กฐ)\n", " E-0041 ๊ฐ€์ƒC๋ถ€๋Œ€ ์ ์ˆ˜ 13 ๊ฐ€๋™๋ฅ  38.8%: Q05 ์™ธ์ฃผ ์ •๋น„ 15์ผ ์ดˆ๊ณผ(๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ7์กฐ); Q06 ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋Œ€๊ธฐ 6์ผ ์ดˆ๊ณผ(๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ8์กฐ); Q07 ์žฌ์ž‘์—… 1๊ฑด ์ด์ƒ(๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ9์กฐ)\n", "๋ฃฐ 16์ข…(๋ชฉํ‘œ 15์ข… ์ด์ƒ: ๋‹ฌ์„ฑ). ๊ธฐ์ค€๊ฐ’์€ YAML๋งŒ ๋ฐ”๊ฟ” ๊ฐฑ์‹ ํ•˜๋ฉฐ, ์ •๋น„ ์šฐ์„ ์ˆœ์œ„์™€ ํ’ˆ์งˆ ํŒ์ •์€ ์ •๋น„๊ด€ยทํ’ˆ์งˆ๊ฒ€์‚ฌ๊ด€์ด ํ™•์ธํ•œ๋‹ค.\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 04. ํšŒ๊ท€ ๋ถ„์„ ๋ฐ ์›์ธ ๊ทœ๋ช… ๋ชจ๋ธ ๊ธฐ๋ฐ˜ ๊ฐ€๋™๋ฅ  ์ €ํ•˜ ํ•ต์‹ฌ ๋ณ‘๋ชฉ ์š”์ธ ์ •๋ฐ€ ๋ถ„์„ ์—”์ง„ ๊ณ ๋„ํ™”\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "ํšŒ๊ท€ ๋ถ„์„ ๊ธฐ๋ฐ˜ ๊ฐ€๋™๋ฅ  ์ €ํ•˜ ํ•ต์‹ฌ ๋ณ‘๋ชฉ ์š”์ธ ์ •๋ฐ€ ๋ถ„์„ ์—”์ง„์„ ๊ณ ๋„ํ™”ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **ํšŒ๊ท€ ๋ถ„์„์œผ๋กœ ๊ฐ€๋™๋ฅ  ์ €ํ•˜์˜ ํ•ต์‹ฌ ๋ณ‘๋ชฉ ์š”์ธ์„ ์ •๋ฐ€ ๋ถ„์„ํ•˜๊ณ , ์ €ํ•˜ ์š”์ธ์„ ๋ถ„๋ฅ˜ํ•ด Top-3 ๊ฐœ์„  ์˜ต์…˜๊ณผ ์œ ์‚ฌ ์‚ฌ๋ก€ ์œ ์‚ฌ๋„๋ฅผ ์ œ๊ณตํ•˜๋Š” ๋ชจ๋ธ์˜ ์ •ํ™•๋„๋ฅผ ๊ฒ€์ฆํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **๊ฐ€์ƒ ์žฅ๋น„ 1,500๋Œ€๋ฅผ ํ•™์Šต 70%์™€ ๊ฒ€์ฆ 30%๋กœ ๋‚˜๋ˆ„๊ณ  ๋ฆฟ์ง€ ํšŒ๊ท€๋กœ ์š”์ธ๋ณ„ ํ‘œ์ค€ํ™” ๊ณ„์ˆ˜๋ฅผ ๊ตฌํ•œ ๋’ค ๋ถ€ํŠธ์ŠคํŠธ๋žฉ 20ํšŒ๋กœ ๊ณ„์ˆ˜ ๋ถ€ํ˜ธ์˜ ์•ˆ์ •์„ฑ ํ™•์ธํ•˜๊ณ  ์œ ํšจ์„ฑ์„ ์ ๊ฒ€ํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/ai/qlt04_intelligence_module.py` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ์ถ”์ฒœ ์‚ฌ์œ ์˜ ํ•ฉ๋ฆฌ์„ฑ ๋ฐ ์‹ค๋ฌด ์ˆ˜์šฉ์„ฑ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ์ถ”์ฒœ/๋ถ„๋ฅ˜ ์ •ํ™•๋„ 85% ์ด์ƒ ๋‹ฌ์„ฑ\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: AI ์ถ”๋ก  ๋ชจ๋“ˆ ๊ตฌํ˜„ ๋ฐ ๋ฒค์น˜๋งˆํฌ ํ…Œ์ŠคํŠธ ์™„๋ฃŒ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ๊ณผ๊ฑฐ ์Šน์ธ/์ฒ˜๋ฆฌ ์™„๋ฃŒ ๋ฐ์ดํ„ฐ์™€์˜ ์ถ”์ฒœ ์ผ์น˜๋„ ๊ฒ€์ฆ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"๊ฐ€์ƒ ์žฅ๋น„ 1,500๋Œ€์˜ ๊ฐ€๋™๋ฅ ์„ ๋ฆฟ์ง€ ํšŒ๊ท€๋กœ ์„ค๋ช…ํ•ด ํ•ต์‹ฌ ๋ณ‘๋ชฉ ์š”์ธ์˜ ์˜ํ–ฅ ํฌ๊ธฐ์™€ ๋ถ€ํ˜ธ ์•ˆ์ •์„ฑ์„ ๋ถ„์„ํ•˜๊ณ , ์ €๊ฐ€๋™ ์žฅ๋น„์˜ ์ฃผ์š” ์š”์ธ(๋ถ€ํ’ˆ ๋Œ€๊ธฐยท์ •๋น„๋ณ‘ ๋ถ€์กฑยท์™ธ์ฃผ ์ •๋น„ยท๊ฒ€์‚ฌยท์žฌ์ž‘์—…)์„ ๋ถ„๋ฅ˜ํ•ด Top-3 ๊ฐœ์„  ์˜ต์…˜๊ณผ ์œ ์‚ฌ ๊ณผ๊ฑฐ ์‚ฌ๋ก€ ์œ ์‚ฌ๋„ ์ ์ˆ˜๋ฅผ ์ œ๊ณตํ•˜๋ฉฐ ์ •ํ™•๋„๋ฅผ ๊ฒ€์ฆ ์„ธํŠธ๋กœ ์ธก์ •ํ•œ๋‹ค.\"\"\"\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "from sklearn.metrics import r2_score, mean_absolute_error\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ฐ€์ƒ ์žฅ๋น„ ๊ฐ€๋™๋ฅ ยท์ •๋น„ ๋Œ€๊ธฐ ์ž๋ฃŒ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ๋ถ€๋Œ€ยท์žฅ๋น„ยท์ œ์›ยท๊ฐ€๋™๋ฅ  ์ •๋ณด ์—†์Œ)\n", "# ์žฅ๋น„ ์œ ํ˜• A(๊ถค๋„ํ˜•)ยทB(ํ™”ํฌํ˜•)ยทC(์ฐจ๋ฅœํ˜•)์€ ์ถ”์ƒ ๊ตฌ๋ถ„์ด๋ฉฐ ์‹ค์ œ ์žฅ๋น„์™€ ๋ฌด๊ด€ํ•˜๋‹ค.\n", "# ์ตœ๊ทผ 90์ผ ๋™์•ˆ ์žฅ๋น„ 1๋Œ€์˜ ์ •๋น„ ๋Œ€๊ธฐ ์š”์ธ(๋ถ€ํ’ˆยท์ •๋น„๋ณ‘ยท์™ธ์ฃผ ์ •๋น„ยท์™ธ์ฃผ ๊ฒ€์‚ฌยท์žฌ์ž‘์—…)๊ณผ ๊ฐ€๋™๋ฅ ์„ ๋งŒ๋“ ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "UNITS = [\"๊ฐ€์ƒA๋ถ€๋Œ€\", \"๊ฐ€์ƒB๋ถ€๋Œ€\", \"๊ฐ€์ƒC๋ถ€๋Œ€\", \"๊ฐ€์ƒD๋ถ€๋Œ€\", \"๊ฐ€์ƒE๋ถ€๋Œ€\", \"๊ฐ€์ƒF๋ถ€๋Œ€\"]\n", "ETYPES = [\"์œ ํ˜•A\", \"์œ ํ˜•B\", \"์œ ํ˜•C\"]\n", "WINDOW = 90 # ๊ด€์ธก ๊ธฐ๊ฐ„(์ผ)\n", "CAUSES = [\"๋ถ€ํ’ˆ ๋Œ€๊ธฐ\", \"์ •๋น„๋ณ‘ ๋ถ€์กฑ\", \"์™ธ์ฃผ ์ •๋น„\", \"๊ฒ€์‚ฌยท์žฌ์ž‘์—…\"]\n", "CAUSE_COL = {\"๋ถ€ํ’ˆ ๋Œ€๊ธฐ\": \"parts_wait_days\", \"์ •๋น„๋ณ‘ ๋ถ€์กฑ\": \"labor_wait_days\", \"์™ธ์ฃผ ์ •๋น„\": \"outsource_days\", \"๊ฒ€์‚ฌยท์žฌ์ž‘์—…\": \"inspect_wait_days\"}\n", "NUM_FIELDS = [\"age_years\", \"op_hours\", \"maint_orders\", \"parts_wait_days\", \"labor_wait_days\", \"outsource_days\", \"inspect_wait_days\", \"rework_count\",\n", " \"spec_mismatch\", \"defect_reports\", \"parts_stock_pct\", \"mechanic_fill_pct\", \"avail_pct\"]\n", "FEAT = [\"age_years\", \"maint_orders\", \"parts_stock_pct\", \"mechanic_fill_pct\", \"outsource_days\", \"inspect_wait_days\", \"rework_count\", \"spec_mismatch\", \"defect_reports\", \"etype_B\", \"etype_C\"]\n", "FEAT_KO = {\"age_years\": \"์žฅ๋น„ ์—ฐ๋ น\", \"maint_orders\": \"์ •๋น„ ์š”์ฒญ ๊ฑด์ˆ˜\", \"parts_stock_pct\": \"๋ถ€ํ’ˆ ์žฌ๊ณ  ๊ฐ€์šฉ๋ฅ \", \"mechanic_fill_pct\": \"์ •๋น„๋ณ‘ ์ถฉ์›์œจ\", \"outsource_days\": \"์™ธ์ฃผ ์ •๋น„ ์ผ์ˆ˜\",\n", " \"inspect_wait_days\": \"์™ธ์ฃผ ๊ฒ€์‚ฌ ๋Œ€๊ธฐ ์ผ์ˆ˜\", \"rework_count\": \"์žฌ์ž‘์—… ๊ฑด์ˆ˜\", \"spec_mismatch\": \"๊ทœ๊ฒฉยท์ œ์› ๋ถˆ์ผ์น˜\", \"defect_reports\": \"ํ•˜์žยท๋ถˆ๋งŒ ์ ‘์ˆ˜\",\n", " \"etype_B\": \"์œ ํ˜•B ์—ฌ๋ถ€\", \"etype_C\": \"์œ ํ˜•C ์—ฌ๋ถ€\"}\n", "\n", "\n", "def unit_profile(seed=3):\n", " \"\"\"๋ถ€๋Œ€๋ณ„ ๊ณ ์ • ํŠน์„ฑ: ๋ถ€ํ’ˆ ์žฌ๊ณ  ์ˆ˜์ค€, ์ •๋น„๋ณ‘ ์ถฉ์›์œจ, ์™ธ์ฃผ ์ •๋น„์†Œ ๊ฑฐ๋ฆฌ(์ผ์ˆ˜ ๊ฐ€์ค‘), ์™ธ์ฃผ ๊ฒ€์‚ฌ ํ˜ผ์žก๋„.\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 4] ํšŒ๊ท€ ๋ถ„์„ ๋ฐ ์›์ธ ๊ทœ๋ช… ๋ชจ๋ธ ๊ธฐ๋ฐ˜ ๊ฐ€๋™๋ฅ  ์ €ํ•˜ ํ•ต์‹ฌ ๋ณ‘๋ชฉ ์š”์ธ ์ •๋ฐ€ ๋ถ„์„ ์—”์ง„ ๊ณ ๋„ํ™” ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def make_fleet(n=300, seed=7, shock=0.0):\n", " \"\"\"์žฅ๋น„ n๋Œ€์˜ 90์ผ ๊ด€์ธก ์ž๋ฃŒ(ํ•„๋“œ 16๊ฐœ)์™€ ์ •๋‹ต ์š”์ธ์„ ๋งŒ๋“ ๋‹ค. shock์€ ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋ณ‘๋ชฉ ์•…ํ™” ์ •๋„(AAR์šฉ ํ™˜๊ฒฝ ๋ณ€ํ™”).\"\"\"\n", " rng = np.random.default_rng(seed)\n", " pf = unit_profile()\n", " ui = rng.integers(0, len(UNITS), n)\n", " et = rng.choice(ETYPES, n, p=[.35, .3, .35])\n", " age = np.clip(rng.gamma(4, 2.2, n), 0.5, 30).round(1)\n", " eff = np.array([{\"์œ ํ˜•A\": 1.15, \"์œ ํ˜•B\": 1.0, \"์œ ํ˜•C\": 0.9}[t] for t in et])\n", " orders = rng.poisson(np.clip(1.2 + 0.12 * age, 0.5, 6) * eff)\n", " stock = np.clip(pf[\"stock\"].to_numpy()[ui] + rng.normal(0, 8, n), 20, 100).round(0)\n", " fill = np.clip(pf[\"fill\"].to_numpy()[ui] + rng.normal(0, 6, n), 30, 100).round(0)\n", " parts_w = (orders * rng.gamma(2, 0.55 * (1 + ((100 - stock) / 28) ** 2) / 2)).round(0)\n", " labor_w = (orders * rng.gamma(2, 0.7 * (1 + ((100 - fill) / 24) ** 2) / 2)).round(0)\n", " n_out = rng.binomial(orders, 0.3)\n", " out_d = (n_out * rng.gamma(3, 4.5 * pf[\"dist\"].to_numpy()[ui] / 3)).round(0)\n", " insp_w = (n_out * rng.gamma(2, (2.6 + shock) * pf[\"queue\"].to_numpy()[ui] / 2)).round(0)\n", " spec = rng.poisson(0.2 + 0.04 * age)\n", " rework = rng.binomial(n_out, np.clip(0.08 + 0.1 * spec, 0, .6))\n", " defect = rng.poisson(0.3 + 0.7 * rework)\n", " base_rep = orders * 1.8\n", " down = base_rep + parts_w + labor_w + out_d + insp_w + rework * 4 + rng.normal(0, 1.2, n)\n", " comp = np.column_stack([parts_w, labor_w, out_d, insp_w + rework * 4])\n", " df = pd.DataFrame({\"equip_id\": [f\"E-{i + 1:04d}\" for i in range(n)], \"unit\": np.array(UNITS)[ui], \"etype\": et, \"age_years\": age,\n", " \"op_hours\": np.clip(rng.normal(320 - 4 * age, 70, n), 40, 600).round(0), \"maint_orders\": orders,\n", " \"parts_wait_days\": parts_w, \"labor_wait_days\": labor_w, \"outsource_days\": out_d, \"inspect_wait_days\": insp_w,\n", " \"rework_count\": rework, \"spec_mismatch\": spec, \"defect_reports\": defect, \"parts_stock_pct\": stock, \"mechanic_fill_pct\": fill,\n", " \"avail_pct\": (100 * (1 - np.clip(down, 0, WINDOW) / WINDOW)).round(1)})\n", " df[\"main_cause\"] = np.where(comp.max(axis=1) < 3, \"์—†์Œ\", np.array(CAUSES)[comp.argmax(axis=1)])\n", " return df\n", "\n", "\n", "def feat_frame(df):\n", " \"\"\"ํšŒ๊ท€ยท๋ถ„์„์šฉ ์„ค๋ช… ๋ณ€์ˆ˜ ํ‘œ(์œ ํ˜•์€ ๋”๋ฏธ ๋ณ€์ˆ˜).\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 4] ํšŒ๊ท€ ๋ถ„์„ ๋ฐ ์›์ธ ๊ทœ๋ช… ๋ชจ๋ธ ๊ธฐ๋ฐ˜ ๊ฐ€๋™๋ฅ  ์ €ํ•˜ ํ•ต์‹ฌ ๋ณ‘๋ชฉ ์š”์ธ ์ •๋ฐ€ ๋ถ„์„ ์—”์ง„ ๊ณ ๋„ํ™” ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def make_trend(months=12, seed=5, shock_unit=None):\n", " \"\"\"[๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๋ถ€๋Œ€ x ์œ ํ˜• x ์›” ํ‰๊ท  ๊ฐ€๋™๋ฅ  ์ถ”์„ธ. shock_unit ๋ถ€๋Œ€๋Š” ํ•˜๋ฐ˜๊ธฐ์— ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋ณ‘๋ชฉ์œผ๋กœ ๊ฐ€๋™๋ฅ ์ด ๋–จ์–ด์ง„๋‹ค.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " pf = unit_profile()\n", " rows = []\n", " for _, u in pf.iterrows():\n", " base = 62 + 0.22 * u[\"stock\"] + 0.12 * u[\"fill\"] - 3.0 * u[\"queue\"]\n", " for t in ETYPES:\n", " off = {\"์œ ํ˜•A\": -3.0, \"์œ ํ˜•B\": 0.0, \"์œ ํ˜•C\": 2.0}[t]\n", " for m in range(months):\n", " v = base + off + 2.0 * np.sin(m / 12 * 2 * np.pi) + rng.normal(0, 1.0)\n", " if shock_unit == u[\"unit\"] and m >= months // 2:\n", " v -= 1.5 * (m - months // 2 + 1)\n", " rows.append((u[\"unit\"], t, m + 1, round(float(np.clip(v, 30, 99)), 1)))\n", " return pd.DataFrame(rows, columns=[\"unit\", \"etype\", \"month\", \"avail_pct\"])\n", "\n", "# ---- ๊ฐ€๋™๋ฅ  ํšŒ๊ท€ ๋ชจ๋ธ๊ณผ ์ €๊ฐ€๋™ ์š”์ธ ๋ถ„๋ฅ˜ยท์ถ”์ฒœ ๋ชจ๋ธ(์™ธ๋ถ€ AI ์„œ๋น„์Šค ์—†์ด scikit-learn ์‚ฌ์šฉ) ----\n", "from sklearn.linear_model import Ridge, LogisticRegression\n", "from sklearn.preprocessing import StandardScaler\n", "from sklearn.pipeline import make_pipeline\n", "\n", "CAUSE_COLS = [\"age_years\", \"maint_orders\", \"parts_stock_pct\", \"mechanic_fill_pct\", \"outsource_days\", \"inspect_wait_days\", \"rework_count\", \"spec_mismatch\",\n", " \"defect_reports\", \"parts_wait_days\", \"labor_wait_days\"]\n", "ACTION = {\"๋ถ€ํ’ˆ ๋Œ€๊ธฐ\": \"๋ถ€ํ’ˆ ์‚ฌ์ „ ์ฒญ๊ตฌยท์žฌ๊ณ  ๋ณด๊ฐ•\", \"์ •๋น„๋ณ‘ ๋ถ€์กฑ\": \"์ •๋น„๋ณ‘ ์žฌ๋ฐฐ์น˜ยท๊ต์œก ์ผ์ • ์กฐ์ •\", \"์™ธ์ฃผ ์ •๋น„\": \"์™ธ์ฃผ ์ •๋น„ ์ผ์ • ์กฐ์œจยท๋‚ด๋ถ€ ์ •๋น„ ํ™•๋Œ€\", \"๊ฒ€์‚ฌยท์žฌ์ž‘์—…\": \"๊ฒ€์‚ฌ ์ผ์ • ์กฐ์œจยท๊ทœ๊ฒฉ ์‚ฌ์ „ ๋Œ€์กฐ\"}\n", "\n", "\n", "def cause_frame(df):\n", " x = df[CAUSE_COLS].copy()\n", " x[\"out_insp\"] = x[\"outsource_days\"] + x[\"inspect_wait_days\"] + 4 * x[\"rework_count\"]\n", " x[\"wait_pm\"] = (x[\"parts_wait_days\"] + x[\"labor_wait_days\"]) / (x[\"maint_orders\"] + 1)\n", " return x\n", "\n", "\n", "def fit_avail(df, alpha=1.0):\n", " \"\"\"๊ฐ€๋™๋ฅ (%) ํšŒ๊ท€: ํ‘œ์ค€ํ™” + ๋ฆฟ์ง€ ํšŒ๊ท€.\"\"\"\n", " return make_pipeline(StandardScaler(), Ridge(alpha=alpha)).fit(feat_frame(df), df[\"avail_pct\"])\n", "\n", "\n", "def fit_cause(df, cut=85, C=5):\n", " \"\"\"๊ฐ€๋™๋ฅ ์ด cut% ๋ฏธ๋งŒ์ธ ์žฅ๋น„์˜ ์ฃผ์š” ์ €๊ฐ€๋™ ์š”์ธ(4์ข…)์„ ๋ถ„๋ฅ˜ํ•œ๋‹ค.\"\"\"\n", " low = df[(df[\"avail_pct\"] < cut) & (df[\"main_cause\"] != \"์—†์Œ\")]\n", " return make_pipeline(StandardScaler(), LogisticRegression(C=C, max_iter=3000)).fit(cause_frame(low), low[\"main_cause\"])\n", "\n", "\n", "SIM_FEAT = [\"age_years\", \"maint_orders\", \"spec_mismatch\", \"stock_gap_x_orders\", \"fill_gap_x_orders\", \"parts_stock_pct\", \"mechanic_fill_pct\", \"etype_B\", \"etype_C\"]\n", "\n", "\n", "def sim_frame(df):\n", " \"\"\"์ž์› ์žฌ๋ฐฐ์น˜ ์‹œ๋ฎฌ๋ ˆ์ด์…˜์šฉ ์„ค๋ช… ๋ณ€์ˆ˜: ๋ถ€์กฑ๋ถ„์ด ํด์ˆ˜๋ก ๋Œ€๊ธฐ๊ฐ€ ๋” ๋น ๋ฅด๊ฒŒ ๋Š˜๊ณ  ์ •๋น„ ์š”์ฒญ ๊ฑด์ˆ˜์™€ ๊ณฑํ•ด์ ธ ์ปค์ง„๋‹ค๋Š” ๊ตฌ์กฐ๋ฅผ ์ œ๊ณฑยท์ƒํ˜ธ์ž‘์šฉ ํ•ญ์œผ๋กœ ๋‘”๋‹ค.\"\"\"\n", " x = df.copy()\n", " x[\"etype_B\"], x[\"etype_C\"] = (x[\"etype\"] == \"์œ ํ˜•B\").astype(int), (x[\"etype\"] == \"์œ ํ˜•C\").astype(int)\n", " x[\"stock_gap_x_orders\"] = x[\"maint_orders\"] * ((100 - x[\"parts_stock_pct\"]) / 28) ** 2\n", " x[\"fill_gap_x_orders\"] = x[\"maint_orders\"] * ((100 - x[\"mechanic_fill_pct\"]) / 24) ** 2\n", " return x[SIM_FEAT]\n", "\n", "\n", "def fit_sim(df, alpha=0.5):\n", " return make_pipeline(StandardScaler(), Ridge(alpha=alpha)).fit(sim_frame(df), df[\"avail_pct\"])\n", "\n", "fleet = make_fleet(1500, seed=21)\n", "idx = np.random.default_rng(5).permutation(len(fleet))\n", "tr, te = fleet.iloc[idx[:1050]], fleet.iloc[idx[1050:]] # ํ•™์Šต 70% / ๊ฒ€์ฆ 30%(๊ณผ๊ฑฐ ์ฒ˜๋ฆฌ ์™„๋ฃŒ ์ž๋ฃŒ ๊ฐ€์ •)\n", "\n", "# 1) ํšŒ๊ท€ ๋ถ„์„: ๊ฐ€๋™๋ฅ  = f(์—ฐ๋ น, ์ •๋น„ ์š”์ฒญ, ์žฌ๊ณ , ์ถฉ์›์œจ, ์™ธ์ฃผยท๊ฒ€์‚ฌ ๋Œ€๊ธฐ, ์žฌ์ž‘์—…, ๊ทœ๊ฒฉ ๋ถˆ์ผ์น˜, ํ•˜์ž, ์œ ํ˜•)\n", "reg = fit_avail(tr)\n", "pred = reg.predict(feat_frame(te))\n", "coef = pd.Series(reg[-1].coef_, index=FEAT) # ํ‘œ์ค€ํ™” ๊ณ„์ˆ˜: 1ํ‘œ์ค€ํŽธ์ฐจ ์ฆ๊ฐ€ ์‹œ ๊ฐ€๋™๋ฅ  ๋ณ€ํ™”(%p)\n", "print(f\"[ํšŒ๊ท€ ๋ชจ๋ธ] ํ•™์Šต {len(tr)}๋Œ€ / ๊ฒ€์ฆ {len(te)}๋Œ€: ๊ฒ€์ฆ R2 {r2_score(te['avail_pct'], pred):.3f}, MAE {mean_absolute_error(te['avail_pct'], pred):.2f}%p\")\n", "boot = []\n", "rng = np.random.default_rng(9)\n", "for _ in range(20): # ๋ถ€ํŠธ์ŠคํŠธ๋žฉ 20ํšŒ๋กœ ๊ณ„์ˆ˜ ๋ถ€ํ˜ธ ์•ˆ์ •์„ฑ ํ™•์ธ\n", " b = tr.iloc[rng.integers(0, len(tr), len(tr))]\n", " boot.append(fit_avail(b)[-1].coef_)\n", "boot = np.array(boot)\n", "top = coef.abs().sort_values(ascending=False).head(5).index\n", "print(\"[ํ•ต์‹ฌ ๋ณ‘๋ชฉ ์š”์ธ: ํ‘œ์ค€ํ™” ๊ณ„์ˆ˜(1 ํ‘œ์ค€ํŽธ์ฐจ ์ฆ๊ฐ€ ์‹œ ๊ฐ€๋™๋ฅ  ๋ณ€ํ™” %p) / ๋ถ€ํ˜ธ ์œ ์ง€ ๋น„์œจ]\")\n", "for f in top:\n", " k = FEAT.index(f)\n", " print(f\" {FEAT_KO[f]:12s} {coef[f]:+6.2f}%p ๋ถ€ํ˜ธ ์œ ์ง€ {np.mean(np.sign(boot[:, k]) == np.sign(coef[f])):.0%}\")\n", "\n", "# 2) ์š”์ธ ๋ถ„๋ฅ˜ + Top-3 ๊ฐœ์„  ์˜ต์…˜\n", "clf = fit_cause(tr)\n", "low = te[(te[\"avail_pct\"] < 85) & (te[\"main_cause\"] != \"์—†์Œ\")]\n", "X = cause_frame(low)\n", "proba = clf.predict_proba(X)\n", "cls = clf.classes_\n", "top1 = cls[proba.argmax(1)]\n", "acc1 = float(np.mean(top1 == low[\"main_cause\"].to_numpy()))\n", "order = np.argsort(-proba, axis=1)[:, :3]\n", "acc3 = float(np.mean([low[\"main_cause\"].iloc[i] in cls[order[i]] for i in range(len(low))]))\n", "major = low[\"main_cause\"].value_counts(normalize=True).iloc[0]\n", "print(f\"\\n[์š”์ธ ๋ถ„๋ฅ˜] ๊ฒ€์ฆ ์ €๊ฐ€๋™ ์žฅ๋น„ {len(low)}๋Œ€: Top-1 ์ •ํ™•๋„ {acc1:.1%}, Top-3 ํฌํ•จ๋ฅ  {acc3:.1%} (๋ฌด์ž‘์œ„ ์ตœ๋นˆ๊ฐ’ ๊ธฐ์ค€ {major:.1%}) -> ๋ชฉํ‘œ 85% ์ด์ƒ: {'๋‹ฌ์„ฑ' if acc1 >= 0.85 else '๋ฏธ๋‹ฌ'}\")\n", "rc = pd.crosstab(low[\"main_cause\"], pd.Series(top1, index=low.index, name=\"์˜ˆ์ธก\"))\n", "print(\"์š”์ธ๋ณ„ ์žฌํ˜„์œจ:\", \", \".join(f\"{c} {rc.loc[c, c] / rc.loc[c].sum():.0%}\" for c in rc.index if c in rc.columns))\n", "\n", "# 3) ์œ ์‚ฌ ๊ณผ๊ฑฐ ์‚ฌ๋ก€ ์œ ์‚ฌ๋„(์ฝ”์‚ฌ์ธ): ํ•™์Šต ์„ธํŠธ ์ค‘ ๊ฐ™์€ ์š”์ธ ์žฅ๋น„์™€ ์–ผ๋งˆ๋‚˜ ๋น„์Šทํ•œ๊ฐ€\n", "sc, trl = clf[0], tr[(tr[\"avail_pct\"] < 85) & (tr[\"main_cause\"] != \"์—†์Œ\")]\n", "Z = sc.transform(cause_frame(trl)); Zn = Z / np.linalg.norm(Z, axis=1, keepdims=True)\n", "zq = sc.transform(X); zqn = zq / np.linalg.norm(zq, axis=1, keepdims=True)\n", "sim = {c: (zqn @ Zn[(trl[\"main_cause\"] == c).to_numpy()].T).max(axis=1) for c in cls}\n", "print(\"\\n[Top-3 ๊ฐœ์„  ์˜ต์…˜ ์˜ˆ์‹œ: ํ™•๋ฅ (%) / ์œ ์‚ฌ ์‚ฌ๋ก€ ์œ ์‚ฌ๋„]\")\n", "for j in range(3):\n", " r = low.iloc[j]\n", " opts = \" | \".join(f\"{cls[o]} {proba[j, o]:.0%}/{sim[cls[o]][j]:.2f} -> {ACTION[cls[o]]}\" for o in order[j])\n", " print(f\" {r['equip_id']} ๊ฐ€๋™๋ฅ  {r['avail_pct']}% (์ •๋‹ต {r['main_cause']}): {opts}\")\n", "miss = low[top1 != low[\"main_cause\"].to_numpy()]\n", "print(f\"\\n[์˜ค๋ถ„๋ฅ˜ {len(miss)}๊ฑด] ํ™•๋ฅ ์ด ๊ฐ€์žฅ ๋†’์€ ์š”์ธ๊ณผ 2์œ„ ํ™•๋ฅ  ์ฐจ์ด ํ‰๊ท  {np.mean(np.sort(proba, axis=1)[:, -1] - np.sort(proba, axis=1)[:, -2]):.2f}\")\n", "conf = np.sort(proba, axis=1)[:, -1]\n", "print(f\"ํ™•๋ฅ  0.8 ์ด์ƒ ์ถ”์ฒœ์˜ ์ •ํ™•๋„ {np.mean((top1 == low['main_cause'].to_numpy())[conf >= 0.8]):.0%} ({int((conf >= 0.8).sum())}๊ฑด), 0.8 ๋ฏธ๋งŒ์€ ์‚ฌ๋žŒ์ด Top-3๋ฅผ ๋น„๊ตํ•ด ์„ ํƒ\")\n", "print(\"โ€ป ์ถ”์ฒœ์€ ํŒ๋‹จ ๋ณด์กฐ์ด๋ฉฐ ์ •๋น„ ์šฐ์„ ์ˆœ์œ„์™€ ์ž์› ํˆฌ์ž…์€ ์ •๋น„๊ด€ยท๊ตฐ์ˆ˜์ฐธ๋ชจ๊ฐ€ ๊ฒฐ์ •ํ•œ๋‹ค. ๊ฐ€์ƒ ์ž๋ฃŒ์ด๋ฏ€๋กœ ์‹ค์ œ ์žฅ๋น„์—์„œ๋Š” ์ •ํ™•๋„๊ฐ€ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ๋‹ค.\")\n", "\n", "plt.figure(figsize=(7, 4))\n", "plt.barh(list(coef.abs().sort_values().index), coef[coef.abs().sort_values().index], color=\"tab:blue\")\n", "plt.title(\"Standardized ridge coefficients on availability (%p per 1 SD)\"); plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 04 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[ํšŒ๊ท€ ๋ชจ๋ธ] ํ•™์Šต 1050๋Œ€ / ๊ฒ€์ฆ 450๋Œ€: ๊ฒ€์ฆ R2 0.899, MAE 3.97%p\n", "[ํ•ต์‹ฌ ๋ณ‘๋ชฉ ์š”์ธ: ํ‘œ์ค€ํ™” ๊ณ„์ˆ˜(1 ํ‘œ์ค€ํŽธ์ฐจ ์ฆ๊ฐ€ ์‹œ ๊ฐ€๋™๋ฅ  ๋ณ€ํ™” %p) / ๋ถ€ํ˜ธ ์œ ์ง€ ๋น„์œจ]\n", " ์ •๋น„ ์š”์ฒญ ๊ฑด์ˆ˜ -8.63%p ๋ถ€ํ˜ธ ์œ ์ง€ 100%\n", " ์™ธ์ฃผ ์ •๋น„ ์ผ์ˆ˜ -6.62%p ๋ถ€ํ˜ธ ์œ ์ง€ 100%\n", " ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋Œ€๊ธฐ ์ผ์ˆ˜ -5.06%p ๋ถ€ํ˜ธ ์œ ์ง€ 100%\n", " ์žฌ์ž‘์—… ๊ฑด์ˆ˜ -1.88%p ๋ถ€ํ˜ธ ์œ ์ง€ 100%\n", " ๋ถ€ํ’ˆ ์žฌ๊ณ  ๊ฐ€์šฉ๋ฅ  +1.73%p ๋ถ€ํ˜ธ ์œ ์ง€ 100%\n", "\n", "[์š”์ธ ๋ถ„๋ฅ˜] ๊ฒ€์ฆ ์ €๊ฐ€๋™ ์žฅ๋น„ 234๋Œ€: Top-1 ์ •ํ™•๋„ 99.1%, Top-3 ํฌํ•จ๋ฅ  100.0% (๋ฌด์ž‘์œ„ ์ตœ๋นˆ๊ฐ’ ๊ธฐ์ค€ 35.5%) -> ๋ชฉํ‘œ 85% ์ด์ƒ: ๋‹ฌ์„ฑ\n", "์š”์ธ๋ณ„ ์žฌํ˜„์œจ: ๊ฒ€์‚ฌยท์žฌ์ž‘์—… 97%, ๋ถ€ํ’ˆ ๋Œ€๊ธฐ 100%, ์™ธ์ฃผ ์ •๋น„ 99%, ์ •๋น„๋ณ‘ ๋ถ€์กฑ 100%\n", "\n", "[Top-3 ๊ฐœ์„  ์˜ต์…˜ ์˜ˆ์‹œ: ํ™•๋ฅ (%) / ์œ ์‚ฌ ์‚ฌ๋ก€ ์œ ์‚ฌ๋„]\n", " E-1269 ๊ฐ€๋™๋ฅ  79.4% (์ •๋‹ต ๊ฒ€์‚ฌยท์žฌ์ž‘์—…): ๊ฒ€์‚ฌยท์žฌ์ž‘์—… 98%/0.89 -> ๊ฒ€์‚ฌ ์ผ์ • ์กฐ์œจยท๊ทœ๊ฒฉ ์‚ฌ์ „ ๋Œ€์กฐ | ์™ธ์ฃผ ์ •๋น„ 2%/0.83 -> ์™ธ์ฃผ ์ •๋น„ ์ผ์ • ์กฐ์œจยท๋‚ด๋ถ€ ์ •๋น„ ํ™•๋Œ€ | ์ •๋น„๋ณ‘ ๋ถ€์กฑ 0%/0.68 -> ์ •๋น„๋ณ‘ ์žฌ๋ฐฐ์น˜ยท๊ต์œก ์ผ์ • ์กฐ์ •\n", " E-0462 ๊ฐ€๋™๋ฅ  74.4% (์ •๋‹ต ์™ธ์ฃผ ์ •๋น„): ์™ธ์ฃผ ์ •๋น„ 63%/0.74 -> ์™ธ์ฃผ ์ •๋น„ ์ผ์ • ์กฐ์œจยท๋‚ด๋ถ€ ์ •๋น„ ํ™•๋Œ€ | ๋ถ€ํ’ˆ ๋Œ€๊ธฐ 36%/0.81 -> ๋ถ€ํ’ˆ ์‚ฌ์ „ ์ฒญ๊ตฌยท์žฌ๊ณ  ๋ณด๊ฐ• | ์ •๋น„๋ณ‘ ๋ถ€์กฑ 0%/0.69 -> ์ •๋น„๋ณ‘ ์žฌ๋ฐฐ์น˜ยท๊ต์œก ์ผ์ • ์กฐ์ •\n", " E-0924 ๊ฐ€๋™๋ฅ  3.6% (์ •๋‹ต ์™ธ์ฃผ ์ •๋น„): ์™ธ์ฃผ ์ •๋น„ 100%/0.85 -> ์™ธ์ฃผ ์ •๋น„ ์ผ์ • ์กฐ์œจยท๋‚ด๋ถ€ ์ •๋น„ ํ™•๋Œ€ | ๋ถ€ํ’ˆ ๋Œ€๊ธฐ 0%/0.70 -> ๋ถ€ํ’ˆ ์‚ฌ์ „ ์ฒญ๊ตฌยท์žฌ๊ณ  ๋ณด๊ฐ• | ์ •๋น„๋ณ‘ ๋ถ€์กฑ 0%/0.75 -> ์ •๋น„๋ณ‘ ์žฌ๋ฐฐ์น˜ยท๊ต์œก ์ผ์ • ์กฐ์ •\n", "\n", "[์˜ค๋ถ„๋ฅ˜ 2๊ฑด] ํ™•๋ฅ ์ด ๊ฐ€์žฅ ๋†’์€ ์š”์ธ๊ณผ 2์œ„ ํ™•๋ฅ  ์ฐจ์ด ํ‰๊ท  0.86\n", "ํ™•๋ฅ  0.8 ์ด์ƒ ์ถ”์ฒœ์˜ ์ •ํ™•๋„ 100% (192๊ฑด), 0.8 ๋ฏธ๋งŒ์€ ์‚ฌ๋žŒ์ด Top-3๋ฅผ ๋น„๊ตํ•ด ์„ ํƒ\n", "โ€ป ์ถ”์ฒœ์€ ํŒ๋‹จ ๋ณด์กฐ์ด๋ฉฐ ์ •๋น„ ์šฐ์„ ์ˆœ์œ„์™€ ์ž์› ํˆฌ์ž…์€ ์ •๋น„๊ด€ยท๊ตฐ์ˆ˜์ฐธ๋ชจ๊ฐ€ ๊ฒฐ์ •ํ•œ๋‹ค. ๊ฐ€์ƒ ์ž๋ฃŒ์ด๋ฏ€๋กœ ์‹ค์ œ ์žฅ๋น„์—์„œ๋Š” ์ •ํ™•๋„๊ฐ€ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ๋‹ค.\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 05. ์ •๋น„ ์ž์› ์žฌ๋ฐฐ์น˜ ์‹œ ์˜ˆ์ƒ ๊ฐ€๋™๋ฅ  ์ƒ์Šนํญ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์‚ฐ์ถœ ์—”์ง„ ๊ฐœ๋ฐœ\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "์ •๋น„ ์ž์› ์žฌ๋ฐฐ์น˜ ์‹œ ์˜ˆ์ƒ ๊ฐ€๋™๋ฅ  ์ƒ์Šนํญ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์‚ฐ์ถœ ์—”์ง„์„ ๊ฐœ๋ฐœํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **์ •๋น„๋ณ‘๊ณผ ๋ถ€ํ’ˆ ์žฌ๊ณ ๋ฅผ ๋ถ€๋Œ€ ์‚ฌ์ด์—์„œ ์˜ฎ๊ฒผ์„ ๋•Œ ์˜ˆ์ƒ ๊ฐ€๋™๋ฅ  ์ƒ์Šนํญ์„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•˜๋Š” ์—”์ง„์„ ๋งŒ๋“ค๊ณ , ์ž…๋ ฅ-๋ถ„์„-๊ฒ€ํ† -๊ฒฐ๊ณผ 4๊ฐœ ํ™”๋ฉด์˜ ํ”„๋กœํ† ํƒ€์ž…์œผ๋กœ ๋ณด์—ฌ ์ค€๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **์žฌ๊ณ ยท์ถฉ์›์œจ ๋ถ€์กฑ๋ถ„์˜ ์ œ๊ณฑ๊ณผ ์ •๋น„ ์š”์ฒญ ๊ฑด์ˆ˜์˜ ์ƒํ˜ธ์ž‘์šฉ์„ ๋ฐ˜์˜ํ•œ ํšŒ๊ท€ ๋ชจ๋ธ์„ ๋งŒ๋“ค์–ด ๊ฒ€์ฆ R2 ํ™•์ธํ•˜๊ณ  ์œ ํšจ์„ฑ์„ ์ ๊ฒ€ํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/ui/qlt04_app.py` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ๊ตฐ ์‹ค๋ฌด์ž ์นœํ™”์  ๋””์ž์ธ ๋ฐ ์กฐ์ž‘ ํŽธ์˜์„ฑ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ํ•ต์‹ฌ ํ™”๋ฉด 4์ข…(์ž…๋ ฅ-๋ถ„์„-๊ฒ€ํ† -๊ฒฐ๊ณผ) ๊ตฌํ˜„ ์™„๋ฃŒ\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: UI ํ”„๋กœํ† ํƒ€์ž… ๋กœ์ปฌ ์„œ๋ฒ„ ๊ตฌ๋™ ๋ฐ ์ „์ฒด ํŽ˜์ด์ง€ ๋„ค๋น„๊ฒŒ์ด์…˜ ํ™•์ธ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ์‚ฌ์šฉ์„ฑ ํ‰๊ฐ€(UX Walkthrough) ๋ฐ ์ธํ„ฐ๋ž™์…˜ ํ…Œ์ŠคํŠธ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"๋ถ€๋Œ€๋ณ„ ์ •๋น„๋ณ‘ ์ถฉ์›์œจยท๋ถ€ํ’ˆ ์žฌ๊ณ  ๊ฐ€์šฉ๋ฅ ์„ ๋‹ค๋ฅธ ๋ถ€๋Œ€๋กœ ์˜ฎ๊ฒผ์„ ๋•Œ ์žฅ๋น„ ๊ฐ€๋™๋ฅ ์ด ์–ผ๋งˆ๋‚˜ ์˜ค๋ฅด๋Š”์ง€ ํšŒ๊ท€ ๋ชจ๋ธ๋กœ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•˜๊ณ  ์ƒ์Šนํญ์ด ํฐ ์žฌ๋ฐฐ์น˜์•ˆ์„ ์ˆœ์„œ๋Œ€๋กœ ๊ณ ๋ฅด๋ฉฐ, ์ž…๋ ฅ-๋ถ„์„-๊ฒ€ํ† -๊ฒฐ๊ณผ 4๊ฐœ ํ™”๋ฉด๊ณผ ์ƒํƒœ ๋ฑƒ์ง€(์Šน์ธ๋Œ€๊ธฐยท๋ฐ˜๋ คยท์™„๋ฃŒ)ยทํ•„ํ„ฐยทํ™”๋ฉด ์ด๋™์„ ๊ฐ–์ถ˜ ํ…์ŠคํŠธ ํ”„๋กœํ† ํƒ€์ž…์œผ๋กœ ๋ณด์—ฌ ์ค€๋‹ค.\"\"\"\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "from sklearn.metrics import r2_score\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ฐ€์ƒ ์žฅ๋น„ ๊ฐ€๋™๋ฅ ยท์ •๋น„ ๋Œ€๊ธฐ ์ž๋ฃŒ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ๋ถ€๋Œ€ยท์žฅ๋น„ยท์ œ์›ยท๊ฐ€๋™๋ฅ  ์ •๋ณด ์—†์Œ)\n", "# ์žฅ๋น„ ์œ ํ˜• A(๊ถค๋„ํ˜•)ยทB(ํ™”ํฌํ˜•)ยทC(์ฐจ๋ฅœํ˜•)์€ ์ถ”์ƒ ๊ตฌ๋ถ„์ด๋ฉฐ ์‹ค์ œ ์žฅ๋น„์™€ ๋ฌด๊ด€ํ•˜๋‹ค.\n", "# ์ตœ๊ทผ 90์ผ ๋™์•ˆ ์žฅ๋น„ 1๋Œ€์˜ ์ •๋น„ ๋Œ€๊ธฐ ์š”์ธ(๋ถ€ํ’ˆยท์ •๋น„๋ณ‘ยท์™ธ์ฃผ ์ •๋น„ยท์™ธ์ฃผ ๊ฒ€์‚ฌยท์žฌ์ž‘์—…)๊ณผ ๊ฐ€๋™๋ฅ ์„ ๋งŒ๋“ ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "UNITS = [\"๊ฐ€์ƒA๋ถ€๋Œ€\", \"๊ฐ€์ƒB๋ถ€๋Œ€\", \"๊ฐ€์ƒC๋ถ€๋Œ€\", \"๊ฐ€์ƒD๋ถ€๋Œ€\", \"๊ฐ€์ƒE๋ถ€๋Œ€\", \"๊ฐ€์ƒF๋ถ€๋Œ€\"]\n", "ETYPES = [\"์œ ํ˜•A\", \"์œ ํ˜•B\", \"์œ ํ˜•C\"]\n", "WINDOW = 90 # ๊ด€์ธก ๊ธฐ๊ฐ„(์ผ)\n", "CAUSES = [\"๋ถ€ํ’ˆ ๋Œ€๊ธฐ\", \"์ •๋น„๋ณ‘ ๋ถ€์กฑ\", \"์™ธ์ฃผ ์ •๋น„\", \"๊ฒ€์‚ฌยท์žฌ์ž‘์—…\"]\n", "CAUSE_COL = {\"๋ถ€ํ’ˆ ๋Œ€๊ธฐ\": \"parts_wait_days\", \"์ •๋น„๋ณ‘ ๋ถ€์กฑ\": \"labor_wait_days\", \"์™ธ์ฃผ ์ •๋น„\": \"outsource_days\", \"๊ฒ€์‚ฌยท์žฌ์ž‘์—…\": \"inspect_wait_days\"}\n", "NUM_FIELDS = [\"age_years\", \"op_hours\", \"maint_orders\", \"parts_wait_days\", \"labor_wait_days\", \"outsource_days\", \"inspect_wait_days\", \"rework_count\",\n", " \"spec_mismatch\", \"defect_reports\", \"parts_stock_pct\", \"mechanic_fill_pct\", \"avail_pct\"]\n", "FEAT = [\"age_years\", \"maint_orders\", \"parts_stock_pct\", \"mechanic_fill_pct\", \"outsource_days\", \"inspect_wait_days\", \"rework_count\", \"spec_mismatch\", \"defect_reports\", \"etype_B\", \"etype_C\"]\n", "FEAT_KO = {\"age_years\": \"์žฅ๋น„ ์—ฐ๋ น\", \"maint_orders\": \"์ •๋น„ ์š”์ฒญ ๊ฑด์ˆ˜\", \"parts_stock_pct\": \"๋ถ€ํ’ˆ ์žฌ๊ณ  ๊ฐ€์šฉ๋ฅ \", \"mechanic_fill_pct\": \"์ •๋น„๋ณ‘ ์ถฉ์›์œจ\", \"outsource_days\": \"์™ธ์ฃผ ์ •๋น„ ์ผ์ˆ˜\",\n", " \"inspect_wait_days\": \"์™ธ์ฃผ ๊ฒ€์‚ฌ ๋Œ€๊ธฐ ์ผ์ˆ˜\", \"rework_count\": \"์žฌ์ž‘์—… ๊ฑด์ˆ˜\", \"spec_mismatch\": \"๊ทœ๊ฒฉยท์ œ์› ๋ถˆ์ผ์น˜\", \"defect_reports\": \"ํ•˜์žยท๋ถˆ๋งŒ ์ ‘์ˆ˜\",\n", " \"etype_B\": \"์œ ํ˜•B ์—ฌ๋ถ€\", \"etype_C\": \"์œ ํ˜•C ์—ฌ๋ถ€\"}\n", "\n", "\n", "def unit_profile(seed=3):\n", " \"\"\"๋ถ€๋Œ€๋ณ„ ๊ณ ์ • ํŠน์„ฑ: ๋ถ€ํ’ˆ ์žฌ๊ณ  ์ˆ˜์ค€, ์ •๋น„๋ณ‘ ์ถฉ์›์œจ, ์™ธ์ฃผ ์ •๋น„์†Œ ๊ฑฐ๋ฆฌ(์ผ์ˆ˜ ๊ฐ€์ค‘), ์™ธ์ฃผ ๊ฒ€์‚ฌ ํ˜ผ์žก๋„.\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 5] ์ •๋น„ ์ž์› ์žฌ๋ฐฐ์น˜ ์‹œ ์˜ˆ์ƒ ๊ฐ€๋™๋ฅ  ์ƒ์Šนํญ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์‚ฐ์ถœ ์—”์ง„ ๊ฐœ๋ฐœ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def make_fleet(n=300, seed=7, shock=0.0):\n", " \"\"\"์žฅ๋น„ n๋Œ€์˜ 90์ผ ๊ด€์ธก ์ž๋ฃŒ(ํ•„๋“œ 16๊ฐœ)์™€ ์ •๋‹ต ์š”์ธ์„ ๋งŒ๋“ ๋‹ค. shock์€ ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋ณ‘๋ชฉ ์•…ํ™” ์ •๋„(AAR์šฉ ํ™˜๊ฒฝ ๋ณ€ํ™”).\"\"\"\n", " rng = np.random.default_rng(seed)\n", " pf = unit_profile()\n", " ui = rng.integers(0, len(UNITS), n)\n", " et = rng.choice(ETYPES, n, p=[.35, .3, .35])\n", " age = np.clip(rng.gamma(4, 2.2, n), 0.5, 30).round(1)\n", " eff = np.array([{\"์œ ํ˜•A\": 1.15, \"์œ ํ˜•B\": 1.0, \"์œ ํ˜•C\": 0.9}[t] for t in et])\n", " orders = rng.poisson(np.clip(1.2 + 0.12 * age, 0.5, 6) * eff)\n", " stock = np.clip(pf[\"stock\"].to_numpy()[ui] + rng.normal(0, 8, n), 20, 100).round(0)\n", " fill = np.clip(pf[\"fill\"].to_numpy()[ui] + rng.normal(0, 6, n), 30, 100).round(0)\n", " parts_w = (orders * rng.gamma(2, 0.55 * (1 + ((100 - stock) / 28) ** 2) / 2)).round(0)\n", " labor_w = (orders * rng.gamma(2, 0.7 * (1 + ((100 - fill) / 24) ** 2) / 2)).round(0)\n", " n_out = rng.binomial(orders, 0.3)\n", " out_d = (n_out * rng.gamma(3, 4.5 * pf[\"dist\"].to_numpy()[ui] / 3)).round(0)\n", " insp_w = (n_out * rng.gamma(2, (2.6 + shock) * pf[\"queue\"].to_numpy()[ui] / 2)).round(0)\n", " spec = rng.poisson(0.2 + 0.04 * age)\n", " rework = rng.binomial(n_out, np.clip(0.08 + 0.1 * spec, 0, .6))\n", " defect = rng.poisson(0.3 + 0.7 * rework)\n", " base_rep = orders * 1.8\n", " down = base_rep + parts_w + labor_w + out_d + insp_w + rework * 4 + rng.normal(0, 1.2, n)\n", " comp = np.column_stack([parts_w, labor_w, out_d, insp_w + rework * 4])\n", " df = pd.DataFrame({\"equip_id\": [f\"E-{i + 1:04d}\" for i in range(n)], \"unit\": np.array(UNITS)[ui], \"etype\": et, \"age_years\": age,\n", " \"op_hours\": np.clip(rng.normal(320 - 4 * age, 70, n), 40, 600).round(0), \"maint_orders\": orders,\n", " \"parts_wait_days\": parts_w, \"labor_wait_days\": labor_w, \"outsource_days\": out_d, \"inspect_wait_days\": insp_w,\n", " \"rework_count\": rework, \"spec_mismatch\": spec, \"defect_reports\": defect, \"parts_stock_pct\": stock, \"mechanic_fill_pct\": fill,\n", " \"avail_pct\": (100 * (1 - np.clip(down, 0, WINDOW) / WINDOW)).round(1)})\n", " df[\"main_cause\"] = np.where(comp.max(axis=1) < 3, \"์—†์Œ\", np.array(CAUSES)[comp.argmax(axis=1)])\n", " return df\n", "\n", "\n", "def feat_frame(df):\n", " \"\"\"ํšŒ๊ท€ยท๋ถ„์„์šฉ ์„ค๋ช… ๋ณ€์ˆ˜ ํ‘œ(์œ ํ˜•์€ ๋”๋ฏธ ๋ณ€์ˆ˜).\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 5] ์ •๋น„ ์ž์› ์žฌ๋ฐฐ์น˜ ์‹œ ์˜ˆ์ƒ ๊ฐ€๋™๋ฅ  ์ƒ์Šนํญ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์‚ฐ์ถœ ์—”์ง„ ๊ฐœ๋ฐœ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def make_trend(months=12, seed=5, shock_unit=None):\n", " \"\"\"[๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๋ถ€๋Œ€ x ์œ ํ˜• x ์›” ํ‰๊ท  ๊ฐ€๋™๋ฅ  ์ถ”์„ธ. shock_unit ๋ถ€๋Œ€๋Š” ํ•˜๋ฐ˜๊ธฐ์— ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋ณ‘๋ชฉ์œผ๋กœ ๊ฐ€๋™๋ฅ ์ด ๋–จ์–ด์ง„๋‹ค.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " pf = unit_profile()\n", " rows = []\n", " for _, u in pf.iterrows():\n", " base = 62 + 0.22 * u[\"stock\"] + 0.12 * u[\"fill\"] - 3.0 * u[\"queue\"]\n", " for t in ETYPES:\n", " off = {\"์œ ํ˜•A\": -3.0, \"์œ ํ˜•B\": 0.0, \"์œ ํ˜•C\": 2.0}[t]\n", " for m in range(months):\n", " v = base + off + 2.0 * np.sin(m / 12 * 2 * np.pi) + rng.normal(0, 1.0)\n", " if shock_unit == u[\"unit\"] and m >= months // 2:\n", " v -= 1.5 * (m - months // 2 + 1)\n", " rows.append((u[\"unit\"], t, m + 1, round(float(np.clip(v, 30, 99)), 1)))\n", " return pd.DataFrame(rows, columns=[\"unit\", \"etype\", \"month\", \"avail_pct\"])\n", "\n", "# ---- ๊ฐ€๋™๋ฅ  ํšŒ๊ท€ ๋ชจ๋ธ๊ณผ ์ €๊ฐ€๋™ ์š”์ธ ๋ถ„๋ฅ˜ยท์ถ”์ฒœ ๋ชจ๋ธ(์™ธ๋ถ€ AI ์„œ๋น„์Šค ์—†์ด scikit-learn ์‚ฌ์šฉ) ----\n", "from sklearn.linear_model import Ridge, LogisticRegression\n", "from sklearn.preprocessing import StandardScaler\n", "from sklearn.pipeline import make_pipeline\n", "\n", "CAUSE_COLS = [\"age_years\", \"maint_orders\", \"parts_stock_pct\", \"mechanic_fill_pct\", \"outsource_days\", \"inspect_wait_days\", \"rework_count\", \"spec_mismatch\",\n", " \"defect_reports\", \"parts_wait_days\", \"labor_wait_days\"]\n", "ACTION = {\"๋ถ€ํ’ˆ ๋Œ€๊ธฐ\": \"๋ถ€ํ’ˆ ์‚ฌ์ „ ์ฒญ๊ตฌยท์žฌ๊ณ  ๋ณด๊ฐ•\", \"์ •๋น„๋ณ‘ ๋ถ€์กฑ\": \"์ •๋น„๋ณ‘ ์žฌ๋ฐฐ์น˜ยท๊ต์œก ์ผ์ • ์กฐ์ •\", \"์™ธ์ฃผ ์ •๋น„\": \"์™ธ์ฃผ ์ •๋น„ ์ผ์ • ์กฐ์œจยท๋‚ด๋ถ€ ์ •๋น„ ํ™•๋Œ€\", \"๊ฒ€์‚ฌยท์žฌ์ž‘์—…\": \"๊ฒ€์‚ฌ ์ผ์ • ์กฐ์œจยท๊ทœ๊ฒฉ ์‚ฌ์ „ ๋Œ€์กฐ\"}\n", "\n", "\n", "def cause_frame(df):\n", " x = df[CAUSE_COLS].copy()\n", " x[\"out_insp\"] = x[\"outsource_days\"] + x[\"inspect_wait_days\"] + 4 * x[\"rework_count\"]\n", " x[\"wait_pm\"] = (x[\"parts_wait_days\"] + x[\"labor_wait_days\"]) / (x[\"maint_orders\"] + 1)\n", " return x\n", "\n", "\n", "def fit_avail(df, alpha=1.0):\n", " \"\"\"๊ฐ€๋™๋ฅ (%) ํšŒ๊ท€: ํ‘œ์ค€ํ™” + ๋ฆฟ์ง€ ํšŒ๊ท€.\"\"\"\n", " return make_pipeline(StandardScaler(), Ridge(alpha=alpha)).fit(feat_frame(df), df[\"avail_pct\"])\n", "\n", "\n", "def fit_cause(df, cut=85, C=5):\n", " \"\"\"๊ฐ€๋™๋ฅ ์ด cut% ๋ฏธ๋งŒ์ธ ์žฅ๋น„์˜ ์ฃผ์š” ์ €๊ฐ€๋™ ์š”์ธ(4์ข…)์„ ๋ถ„๋ฅ˜ํ•œ๋‹ค.\"\"\"\n", " low = df[(df[\"avail_pct\"] < cut) & (df[\"main_cause\"] != \"์—†์Œ\")]\n", " return make_pipeline(StandardScaler(), LogisticRegression(C=C, max_iter=3000)).fit(cause_frame(low), low[\"main_cause\"])\n", "\n", "\n", "SIM_FEAT = [\"age_years\", \"maint_orders\", \"spec_mismatch\", \"stock_gap_x_orders\", \"fill_gap_x_orders\", \"parts_stock_pct\", \"mechanic_fill_pct\", \"etype_B\", \"etype_C\"]\n", "\n", "\n", "def sim_frame(df):\n", " \"\"\"์ž์› ์žฌ๋ฐฐ์น˜ ์‹œ๋ฎฌ๋ ˆ์ด์…˜์šฉ ์„ค๋ช… ๋ณ€์ˆ˜: ๋ถ€์กฑ๋ถ„์ด ํด์ˆ˜๋ก ๋Œ€๊ธฐ๊ฐ€ ๋” ๋น ๋ฅด๊ฒŒ ๋Š˜๊ณ  ์ •๋น„ ์š”์ฒญ ๊ฑด์ˆ˜์™€ ๊ณฑํ•ด์ ธ ์ปค์ง„๋‹ค๋Š” ๊ตฌ์กฐ๋ฅผ ์ œ๊ณฑยท์ƒํ˜ธ์ž‘์šฉ ํ•ญ์œผ๋กœ ๋‘”๋‹ค.\"\"\"\n", " x = df.copy()\n", " x[\"etype_B\"], x[\"etype_C\"] = (x[\"etype\"] == \"์œ ํ˜•B\").astype(int), (x[\"etype\"] == \"์œ ํ˜•C\").astype(int)\n", " x[\"stock_gap_x_orders\"] = x[\"maint_orders\"] * ((100 - x[\"parts_stock_pct\"]) / 28) ** 2\n", " x[\"fill_gap_x_orders\"] = x[\"maint_orders\"] * ((100 - x[\"mechanic_fill_pct\"]) / 24) ** 2\n", " return x[SIM_FEAT]\n", "\n", "\n", "def fit_sim(df, alpha=0.5):\n", " return make_pipeline(StandardScaler(), Ridge(alpha=alpha)).fit(sim_frame(df), df[\"avail_pct\"])\n", "\n", "fleet = make_fleet(600, seed=31)\n", "sim_tr, sim_te = fleet.iloc[:420], fleet.iloc[420:]\n", "sm = fit_sim(sim_tr)\n", "print(f\"[์‹œ๋ฎฌ๋ ˆ์ด์…˜ ๋ชจ๋ธ] ๊ฒ€์ฆ R2 {r2_score(sim_te['avail_pct'], sm.predict(sim_frame(sim_te))):.3f} (์žฌ๊ณ ยท์ถฉ์›์œจ ๋ถ€์กฑ๋ถ„์˜ ์ œ๊ณฑ๊ณผ ์ •๋น„ ์š”์ฒญ ๊ฑด์ˆ˜์˜ ์ƒํ˜ธ์ž‘์šฉ ๋ฐ˜์˜)\")\n", "sm = fit_sim(fleet)\n", "AUTH = 40 # ๋ถ€๋Œ€๋‹น ์ •๋น„๋ณ‘ ํŽธ์„ฑ ์ •์›(๊ฐ€์ƒ ๊ฐ€์ •)\n", "PARAM = {\"max_move\": 4, \"min_donor_fill\": 60.0, \"stock_step\": 8.0, \"top_n\": 4, \"max_donor_drop\": 3.5}\n", "\n", "\n", "def unit_avail(df, unit, fill_d=0.0, stock_d=0.0):\n", " d = df[df[\"unit\"] == unit].copy()\n", " d[\"mechanic_fill_pct\"] = np.clip(d[\"mechanic_fill_pct\"] + fill_d, 0, 100)\n", " d[\"parts_stock_pct\"] = np.clip(d[\"parts_stock_pct\"] + stock_d, 0, 100)\n", " return float(np.clip(sm.predict(sim_frame(d)), 0, 100).mean()), len(d)\n", "\n", "\n", "def evaluate(df, kind, src, dst, n):\n", " \"\"\"src ๋ถ€๋Œ€์—์„œ dst ๋ถ€๋Œ€๋กœ n๋‹จ์œ„(์ •๋น„๋ณ‘ n๋ช… ๋˜๋Š” ์žฌ๊ณ  n*step %p)๋ฅผ ์˜ฎ๊ธธ ๋•Œ ์ „์ฒด ํ‰๊ท  ๊ฐ€๋™๋ฅ  ๋ณ€ํ™”(%p)์™€ ์ œ๊ณต ๋ถ€๋Œ€ ๋ณ€ํ™”.\"\"\"\n", " delta = n / AUTH * 100 if kind == \"์ •๋น„๋ณ‘\" else n * PARAM[\"stock_step\"] / 2\n", " key = \"mechanic_fill_pct\" if kind == \"์ •๋น„๋ณ‘\" else \"parts_stock_pct\"\n", " a0, na = unit_avail(df, src); b0, nb = unit_avail(df, dst)\n", " kw = (lambda s: {\"fill_d\": s * delta}) if kind == \"์ •๋น„๋ณ‘\" else (lambda s: {\"stock_d\": s * delta})\n", " a1, _ = unit_avail(df, src, **kw(-1)); b1, _ = unit_avail(df, dst, **kw(+1))\n", " cur = df[df[\"unit\"] == src][key].mean()\n", " return {\"kind\": kind, \"src\": src, \"dst\": dst, \"n\": n, \"gain\": ((a1 - a0) * na + (b1 - b0) * nb) / len(df), \"donor_drop\": a0 - a1, \"recv_gain\": b1 - b0, \"donor_after\": cur - delta}\n", "\n", "\n", "base = pd.DataFrame([{\"unit\": u, **dict(zip([\"avail\", \"n\"], unit_avail(fleet, u))), \"stock\": g[\"parts_stock_pct\"].mean(), \"fill\": g[\"mechanic_fill_pct\"].mean()} for u, g in fleet.groupby(\"unit\")])\n", "cands = [evaluate(fleet, k, s, d, n) for k in [\"์ •๋น„๋ณ‘\", \"๋ถ€ํ’ˆ ์žฌ๊ณ \"] for s in UNITS for d in UNITS if s != d for n in (2, 4, 6)]\n", "cands = [c for c in cands if c[\"donor_after\"] >= PARAM[\"min_donor_fill\"]]\n", "cands.sort(key=lambda c: -c[\"gain\"])\n", "print(f\"ํ›„๋ณด {len(cands)}๊ฑด ํ‰๊ฐ€ (์ œ๊ณต ๋ถ€๋Œ€๋‹น ์ •๋น„๋ณ‘ ๋ˆ„์  ์ตœ๋Œ€ {PARAM['max_move']}๋ช… ์ด๋™, ์ œ๊ณต ๋ถ€๋Œ€ ์ถฉ์›์œจยท์žฌ๊ณ  {PARAM['min_donor_fill']:.0f}% ์ด์ƒ ์œ ์ง€)\")\n", "\n", "# ํ™”๋ฉด 4์ข… ํ”„๋กœํ† ํƒ€์ž…\n", "BADGE = {\"์Šน์ธ๋Œ€๊ธฐ\": \"[์Šน์ธ๋Œ€๊ธฐ]\", \"๋ฐ˜๋ ค\": \"[๋ฐ˜๋ ค]\", \"์™„๋ฃŒ\": \"[์™„๋ฃŒ]\"}\n", "props = [{\"id\": \"P00\", **evaluate(fleet, \"์ •๋น„๋ณ‘\", \"๊ฐ€์ƒC๋ถ€๋Œ€\", \"๊ฐ€์ƒD๋ถ€๋Œ€\", 2), \"status\": \"์™„๋ฃŒ\", \"note\": \"์ „ ๋ถ„๊ธฐ ์‹œํ–‰ ์™„๋ฃŒ(๊ฐ€์ƒ ์ด๋ ฅ)\"}]\n", "for c in cands: # ๊ฐ™์€ ๋„์ฐฉ ๋ถ€๋Œ€ ์ค‘๋ณต์„ ํ”ผํ•ด ์ƒ์œ„ 4๊ฑด ์ œ์•ˆ\n", " if len([p for p in props if p[\"id\"] != \"P00\"]) >= PARAM[\"top_n\"]:\n", " break\n", " if any(p[\"dst\"] == c[\"dst\"] and p[\"kind\"] == c[\"kind\"] for p in props):\n", " continue\n", " moved = sum(p[\"n\"] for p in props if p[\"src\"] == c[\"src\"] and p[\"kind\"] == \"์ •๋น„๋ณ‘\" and p[\"status\"] != \"๋ฐ˜๋ ค\") + (c[\"n\"] if c[\"kind\"] == \"์ •๋น„๋ณ‘\" else 0)\n", " if c[\"donor_drop\"] > PARAM[\"max_donor_drop\"]:\n", " ok, why = False, f\"์ œ๊ณต ๋ถ€๋Œ€ ๊ฐ€๋™๋ฅ  {c['donor_drop']:.1f}%p ํ•˜๋ฝ(ํ•œ๋„ {PARAM['max_donor_drop']} ์ดˆ๊ณผ)\"\n", " elif moved > PARAM[\"max_move\"]:\n", " ok, why = False, f\"์ œ๊ณต ๋ถ€๋Œ€ ๋ˆ„์  ์ด๋™ {moved}๋ช…(ํ•œ๋„ {PARAM['max_move']}๋ช… ์ดˆ๊ณผ)\"\n", " else:\n", " ok, why = True, \"์ œ๊ณต ๋ถ€๋Œ€ ์˜ํ–ฅยท๋ˆ„์  ์ด๋™ ํ—ˆ์šฉ ๋ฒ”์œ„\"\n", " props.append({\"id\": f\"P{len(props):02d}\", **c, \"status\": \"์Šน์ธ๋Œ€๊ธฐ\" if ok else \"๋ฐ˜๋ ค\", \"note\": why})\n", "\n", "\n", "class App:\n", " SCREENS = [\"์ž…๋ ฅ\", \"๋ถ„์„\", \"๊ฒ€ํ† \", \"๊ฒฐ๊ณผ\"]\n", "\n", " def __init__(self):\n", " self.cur, self.flt, self.visited = \"์ž…๋ ฅ\", None, [\"์ž…๋ ฅ\"]\n", "\n", " def goto(self, s):\n", " assert s in self.SCREENS\n", " self.cur = s; self.visited.append(s); return self.render()\n", "\n", " def render(self):\n", " if self.cur == \"์ž…๋ ฅ\":\n", " return [\"์ž…๋ ฅ ํ™”๋ฉด: ์žฅ๋น„ %d๋Œ€ / ๋ถ€๋Œ€ %d๊ณณ / ํŒŒ๋ผ๋ฏธํ„ฐ %s\" % (len(fleet), len(UNITS), PARAM)]\n", " if self.cur == \"๋ถ„์„\":\n", " return [f\" {r.unit} ๊ฐ€๋™๋ฅ  {r.avail:5.1f}% ์žฌ๊ณ  {r.stock:4.0f}% ์ถฉ์› {r.fill:4.0f}% ({int(r.n)}๋Œ€)\" + (\" <- ํ•˜์œ„\" if r.avail <= base[\"avail\"].nsmallest(2).max() else \"\") for r in base.itertuples()]\n", " if self.cur == \"๊ฒ€ํ† \":\n", " ps = [p for p in props if self.flt in (None, p[\"status\"])]\n", " return [f\" {p['id']} {BADGE[p['status']]:8s} {p['kind']} {p['n']}๋‹จ์œ„ {p['src'][2:4]}->{p['dst'][2:4]} ์ „์ฒด {p['gain']:+.2f}%p(์ˆ˜ํ˜œ {p['recv_gain']:+.1f}/์ œ๊ณต {-p['donor_drop']:+.1f}) | {p['note']}\" for p in ps]\n", " ok = [p for p in props if p[\"status\"] == \"์Šน์ธ๋Œ€๊ธฐ\"]\n", " return [f\" ์Šน์ธ ๋Œ€๊ธฐ {len(ok)}๊ฑด ์‹œํ–‰ ์‹œ ์˜ˆ์ƒ ์ „์ฒด ๊ฐ€๋™๋ฅ  +{sum(p['gain'] for p in ok):.2f}%p (์ •๋น„๋ณ‘ ์ด๋™์€ ํ•ฉ์‚ฐ ๊ทผ์‚ฌ)\", \" ์ตœ์ข… ์‹œํ–‰์€ ๊ตฐ์ˆ˜์ฐธ๋ชจ ๊ฒฐ์ •(M7 ์›Œํฌํ”Œ๋กœ์šฐ)\"]\n", "\n", "\n", "app = App()\n", "for s in App.SCREENS:\n", " print(f\"\\n[ํ™”๋ฉด {App.SCREENS.index(s) + 1}: {s}]\")\n", " for line in app.goto(s):\n", " print(line)\n", "app.flt = \"์Šน์ธ๋Œ€๊ธฐ\"; n1 = len(app.goto(\"๊ฒ€ํ† \")); app.flt = \"๋ฐ˜๋ ค\"; n2 = len(app.goto(\"๊ฒ€ํ† \")); app.flt = None\n", "print(f\"\\nํ•„ํ„ฐ ์‹œํ—˜: ์Šน์ธ๋Œ€๊ธฐ {n1}๊ฑด, ๋ฐ˜๋ ค {n2}๊ฑด, ์ „์ฒด {len(app.goto('๊ฒ€ํ† '))}๊ฑด\")\n", "nav = [(a, b) for a in App.SCREENS for b in App.SCREENS if a != b]\n", "okn = 0\n", "for a, b in nav:\n", " app.cur = a; okn += bool(app.goto(b))\n", "print(f\"ํ™”๋ฉด ์ด๋™ ์‹œํ—˜ {okn}/{len(nav)} ์„ฑ๊ณต, ํ•ต์‹ฌ ํ™”๋ฉด {len(App.SCREENS)}์ข… ๊ตฌํ˜„ (๋ชฉํ‘œ 4์ข…: {'๋‹ฌ์„ฑ' if len(App.SCREENS) == 4 else '๋ฏธ๋‹ฌ'})\")\n", "tot = sum(p[\"gain\"] for p in props if p[\"status\"] == \"์Šน์ธ๋Œ€๊ธฐ\")\n", "print(f\"์žฌ๋ฐฐ์น˜ ์‹œ๋ฎฌ๋ ˆ์ด์…˜: ์Šน์ธ๋Œ€๊ธฐ ์•ˆ ํ•ฉ์‚ฐ ์˜ˆ์ƒ ์ƒ์Šนํญ +{tot:.2f}%p, ์ตœ๋Œ€ ๋‹จ์ผ ์•ˆ {max(c['gain'] for c in cands):+.2f}%p. ์‹œ๋ฎฌ๋ ˆ์ด์…˜์€ ๊ฐ€์ • ๋ชจ๋ธ ๊ธฐ๋ฐ˜ ์ถ”์ •์ด๋ฉฐ ์‹œํ–‰ ์—ฌ๋ถ€๋Š” ์‚ฌ๋žŒ์ด ๊ฒฐ์ •ํ•œ๋‹ค.\")\n", "\n", "plt.figure(figsize=(7, 4))\n", "plt.bar([f\"{p['id']}\\n{ {'์™„๋ฃŒ': 'done', '์Šน์ธ๋Œ€๊ธฐ': 'pending', '๋ฐ˜๋ ค': 'rejected'}[p['status']]}\" for p in props], [p[\"gain\"] for p in props], color=[\"tab:gray\" if p[\"status\"] == \"์™„๋ฃŒ\" else \"tab:blue\" if p[\"status\"] == \"์Šน์ธ๋Œ€๊ธฐ\" else \"tab:red\" for p in props])\n", "plt.ylabel(\"fleet availability gain (%p)\"); plt.title(\"Reallocation proposals (gray done / blue pending / red rejected)\"); plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 05 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[์‹œ๋ฎฌ๋ ˆ์ด์…˜ ๋ชจ๋ธ] ๊ฒ€์ฆ R2 0.646 (์žฌ๊ณ ยท์ถฉ์›์œจ ๋ถ€์กฑ๋ถ„์˜ ์ œ๊ณฑ๊ณผ ์ •๋น„ ์š”์ฒญ ๊ฑด์ˆ˜์˜ ์ƒํ˜ธ์ž‘์šฉ ๋ฐ˜์˜)\n", "ํ›„๋ณด 70๊ฑด ํ‰๊ฐ€ (์ œ๊ณต ๋ถ€๋Œ€๋‹น ์ •๋น„๋ณ‘ ๋ˆ„์  ์ตœ๋Œ€ 4๋ช… ์ด๋™, ์ œ๊ณต ๋ถ€๋Œ€ ์ถฉ์›์œจยท์žฌ๊ณ  60% ์ด์ƒ ์œ ์ง€)\n", "\n", "[ํ™”๋ฉด 1: ์ž…๋ ฅ]\n", "์ž…๋ ฅ ํ™”๋ฉด: ์žฅ๋น„ 600๋Œ€ / ๋ถ€๋Œ€ 6๊ณณ / ํŒŒ๋ผ๋ฏธํ„ฐ {'max_move': 4, 'min_donor_fill': 60.0, 'stock_step': 8.0, 'top_n': 4, 'max_donor_drop': 3.5}\n", "\n", "[ํ™”๋ฉด 2: ๋ถ„์„]\n", " ๊ฐ€์ƒA๋ถ€๋Œ€ ๊ฐ€๋™๋ฅ  78.3% ์žฌ๊ณ  50% ์ถฉ์› 76% (94๋Œ€)\n", " ๊ฐ€์ƒB๋ถ€๋Œ€ ๊ฐ€๋™๋ฅ  75.0% ์žฌ๊ณ  57% ์ถฉ์› 63% (87๋Œ€) <- ํ•˜์œ„\n", " ๊ฐ€์ƒC๋ถ€๋Œ€ ๊ฐ€๋™๋ฅ  83.8% ์žฌ๊ณ  84% ์ถฉ์› 87% (99๋Œ€)\n", " ๊ฐ€์ƒD๋ถ€๋Œ€ ๊ฐ€๋™๋ฅ  77.8% ์žฌ๊ณ  75% ์ถฉ์› 59% (115๋Œ€) <- ํ•˜์œ„\n", " ๊ฐ€์ƒE๋ถ€๋Œ€ ๊ฐ€๋™๋ฅ  80.8% ์žฌ๊ณ  50% ์ถฉ์› 72% (100๋Œ€)\n", " ๊ฐ€์ƒF๋ถ€๋Œ€ ๊ฐ€๋™๋ฅ  79.9% ์žฌ๊ณ  66% ์ถฉ์› 78% (105๋Œ€)\n", "\n", "[ํ™”๋ฉด 3: ๊ฒ€ํ† ]\n", " P00 [์™„๋ฃŒ] ์ •๋น„๋ณ‘ 2๋‹จ์œ„ C๋ถ€->D๋ถ€ ์ „์ฒด +0.14%p(์ˆ˜ํ˜œ +1.5/์ œ๊ณต -0.9) | ์ „ ๋ถ„๊ธฐ ์‹œํ–‰ ์™„๋ฃŒ(๊ฐ€์ƒ ์ด๋ ฅ)\n", " P01 [๋ฐ˜๋ ค] ์ •๋น„๋ณ‘ 4๋‹จ์œ„ C๋ถ€->B๋ถ€ ์ „์ฒด +0.11%p(์ˆ˜ํ˜œ +3.1/์ œ๊ณต -2.0) | ์ œ๊ณต ๋ถ€๋Œ€ ๋ˆ„์  ์ด๋™ 6๋ช…(ํ•œ๋„ 4๋ช… ์ดˆ๊ณผ)\n", " P02 [์Šน์ธ๋Œ€๊ธฐ] ๋ถ€ํ’ˆ ์žฌ๊ณ  4๋‹จ์œ„ C๋ถ€->A๋ถ€ ์ „์ฒด +0.06%p(์ˆ˜ํ˜œ +0.4/์ œ๊ณต -0.1) | ์ œ๊ณต ๋ถ€๋Œ€ ์˜ํ–ฅยท๋ˆ„์  ์ด๋™ ํ—ˆ์šฉ ๋ฒ”์œ„\n", " P03 [์Šน์ธ๋Œ€๊ธฐ] ์ •๋น„๋ณ‘ 2๋‹จ์œ„ C๋ถ€->E๋ถ€ ์ „์ฒด +0.05%p(์ˆ˜ํ˜œ +1.2/์ œ๊ณต -0.9) | ์ œ๊ณต ๋ถ€๋Œ€ ์˜ํ–ฅยท๋ˆ„์  ์ด๋™ ํ—ˆ์šฉ ๋ฒ”์œ„\n", " P04 [๋ฐ˜๋ ค] ์ •๋น„๋ณ‘ 2๋‹จ์œ„ C๋ถ€->F๋ถ€ ์ „์ฒด +0.04%p(์ˆ˜ํ˜œ +1.1/์ œ๊ณต -0.9) | ์ œ๊ณต ๋ถ€๋Œ€ ๋ˆ„์  ์ด๋™ 6๋ช…(ํ•œ๋„ 4๋ช… ์ดˆ๊ณผ)\n", "\n", "[ํ™”๋ฉด 4: ๊ฒฐ๊ณผ]\n", " ์Šน์ธ ๋Œ€๊ธฐ 2๊ฑด ์‹œํ–‰ ์‹œ ์˜ˆ์ƒ ์ „์ฒด ๊ฐ€๋™๋ฅ  +0.10%p (์ •๋น„๋ณ‘ ์ด๋™์€ ํ•ฉ์‚ฐ ๊ทผ์‚ฌ)\n", " ์ตœ์ข… ์‹œํ–‰์€ ๊ตฐ์ˆ˜์ฐธ๋ชจ ๊ฒฐ์ •(M7 ์›Œํฌํ”Œ๋กœ์šฐ)\n", "\n", "ํ•„ํ„ฐ ์‹œํ—˜: ์Šน์ธ๋Œ€๊ธฐ 2๊ฑด, ๋ฐ˜๋ ค 2๊ฑด, ์ „์ฒด 5๊ฑด\n", "ํ™”๋ฉด ์ด๋™ ์‹œํ—˜ 12/12 ์„ฑ๊ณต, ํ•ต์‹ฌ ํ™”๋ฉด 4์ข… ๊ตฌํ˜„ (๋ชฉํ‘œ 4์ข…: ๋‹ฌ์„ฑ)\n", "์žฌ๋ฐฐ์น˜ ์‹œ๋ฎฌ๋ ˆ์ด์…˜: ์Šน์ธ๋Œ€๊ธฐ ์•ˆ ํ•ฉ์‚ฐ ์˜ˆ์ƒ ์ƒ์Šนํญ +0.10%p, ์ตœ๋Œ€ ๋‹จ์ผ ์•ˆ +0.26%p. ์‹œ๋ฎฌ๋ ˆ์ด์…˜์€ ๊ฐ€์ • ๋ชจ๋ธ ๊ธฐ๋ฐ˜ ์ถ”์ •์ด๋ฉฐ ์‹œํ–‰ ์—ฌ๋ถ€๋Š” ์‚ฌ๋žŒ์ด ๊ฒฐ์ •ํ•œ๋‹ค.\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 06. ๋ถ€๋Œ€๋ณ„/์žฅ๋น„๋ณ„ ๊ฐ€๋™๋ฅ  ์ถ”์„ธ ๋ฐ ๊ฐ€๋™๋ฅ  ์ €ํ•˜ ์š”์ธ ๋ถ„์„ ๋Œ€์‹œ๋ณด๋“œ ๊ตฌํ˜„\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 2.5์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "๋ถ€๋Œ€๋ณ„/์žฅ๋น„๋ณ„ ๊ฐ€๋™๋ฅ  ์ถ”์„ธ ๋ฐ ์ €ํ•˜ ์š”์ธ ๋ถ„์„ ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **๋ถ€๋Œ€๋ณ„ยท์žฅ๋น„ ์œ ํ˜•๋ณ„ ๊ฐ€๋™๋ฅ  ์ถ”์„ธ์™€ ์ €ํ•˜ ์š”์ธ์„ ๋ณด์—ฌ ์ฃผ๋Š” ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๋งŒ๋“ค๊ณ , ๋น„์ •์ƒ ์ž…๋ ฅ์—๋„ ์ค‘๋‹จ๋˜์ง€ ์•Š๋„๋ก ์ž…๋ ฅ ๊ฒ€์ฆ๊ธฐ์™€ ์—๋Ÿฌ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ๊ฐ–์ถ˜๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **12๊ฐœ์›” ๊ฐ€์ƒ ์ถ”์„ธ๋กœ ๋ถ€๋Œ€๋ณ„ ์›” ๊ธฐ์šธ๊ธฐ, ์ตœ๊ทผ 3๊ฐœ์›” ํ‰๊ท , ์ €ํ•˜ ๊ฒฝ๊ณ ์™€ ๋ถ€๋Œ€ x ์œ ํ˜• ๊ฐ€๋™๋ฅ , ๋Œ€๊ธฐ ์†์‹ค์ผ ๊ตฌ์„ฑ์„ ํ‘œ์™€ ๊ทธ๋ž˜ํ”„๋กœ ๋ณด์—ฌ ์ค€๋‹ค ๊ณผ์ •์„ ์ง„ํ–‰ํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/utils/qlt04_validator.py ๋ฐ ์—๋Ÿฌ ํ•ธ๋“ค๋Ÿฌ` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๊ธฐ ์‰ฌ์šด ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ๋ฐ ์กฐ์น˜ ๊ฐ€์ด๋“œ ์ œ๊ณต\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ์˜ˆ์™ธ ์ƒํ™ฉ ๋ฐœ์ƒ ์‹œ ํฌ๋ž˜์‹œ(Crash) 0๊ฑด, ์—๋Ÿฌ ๋กœ๊ทธ ๊ธฐ๋ก๋ฅ  100%\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ์˜ˆ์™ธ ์ฃผ์ž… ํ…Œ์ŠคํŠธ 10์ข… ํ†ต๊ณผ ๋ฐ ๊ฐ์‚ฌ ๋กœ๊ทธ ํŒŒ์ผ ์ƒ์„ฑ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ๋น„์ •์ƒ ๋ฐ์ดํ„ฐ(๊ฒฐ์ธก, ํŠน์ˆ˜๋ฌธ์ž, ๋Œ€์šฉ๋Ÿ‰) ์ŠคํŠธ๋ ˆ์Šค ํ…Œ์ŠคํŠธ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"๋ถ€๋Œ€๋ณ„ยท์žฅ๋น„ ์œ ํ˜•๋ณ„ 12๊ฐœ์›” ๊ฐ€๋™๋ฅ  ์ถ”์„ธ์™€ ์ €ํ•˜ ์š”์ธ ๊ตฌ์„ฑ์„ ํ…์ŠคํŠธ ๋Œ€์‹œ๋ณด๋“œ์™€ ๊ทธ๋ž˜ํ”„๋กœ ๋ณด์—ฌ ์ฃผ๊ณ , ๊ฒฐ์ธกยทํŠน์ˆ˜๋ฌธ์žยท์ž๋ฃŒํ˜• ์˜ค๋ฅ˜ยท๋Œ€์šฉ๋Ÿ‰ยท๋นˆ ์ž๋ฃŒยท์—ด ๋ˆ„๋ฝ ๋“ฑ ์˜ˆ์™ธ 13์ข…์„ ์ฃผ์ž…ํ•ด๋„ ์ค‘๋‹จ ์—†์ด ์˜ค๋ฅ˜ ๊ฒฉ๋ฆฌยท์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ยท์˜ค๋ฅ˜ ๋กœ๊ทธยท์žฌ์‹œ๋„ยท๋กค๋ฐฑ์ด ๋™์ž‘ํ•˜๋Š”์ง€ ์‹œํ—˜ํ•˜๋Š” ์ž…๋ ฅ ๊ฒ€์ฆ๊ธฐ์™€ ์—๋Ÿฌ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค.\"\"\"\n", "import copy\n", "import re\n", "import time\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ฐ€์ƒ ์žฅ๋น„ ๊ฐ€๋™๋ฅ ยท์ •๋น„ ๋Œ€๊ธฐ ์ž๋ฃŒ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ๋ถ€๋Œ€ยท์žฅ๋น„ยท์ œ์›ยท๊ฐ€๋™๋ฅ  ์ •๋ณด ์—†์Œ)\n", "# ์žฅ๋น„ ์œ ํ˜• A(๊ถค๋„ํ˜•)ยทB(ํ™”ํฌํ˜•)ยทC(์ฐจ๋ฅœํ˜•)์€ ์ถ”์ƒ ๊ตฌ๋ถ„์ด๋ฉฐ ์‹ค์ œ ์žฅ๋น„์™€ ๋ฌด๊ด€ํ•˜๋‹ค.\n", "# ์ตœ๊ทผ 90์ผ ๋™์•ˆ ์žฅ๋น„ 1๋Œ€์˜ ์ •๋น„ ๋Œ€๊ธฐ ์š”์ธ(๋ถ€ํ’ˆยท์ •๋น„๋ณ‘ยท์™ธ์ฃผ ์ •๋น„ยท์™ธ์ฃผ ๊ฒ€์‚ฌยท์žฌ์ž‘์—…)๊ณผ ๊ฐ€๋™๋ฅ ์„ ๋งŒ๋“ ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "UNITS = [\"๊ฐ€์ƒA๋ถ€๋Œ€\", \"๊ฐ€์ƒB๋ถ€๋Œ€\", \"๊ฐ€์ƒC๋ถ€๋Œ€\", \"๊ฐ€์ƒD๋ถ€๋Œ€\", \"๊ฐ€์ƒE๋ถ€๋Œ€\", \"๊ฐ€์ƒF๋ถ€๋Œ€\"]\n", "ETYPES = [\"์œ ํ˜•A\", \"์œ ํ˜•B\", \"์œ ํ˜•C\"]\n", "WINDOW = 90 # ๊ด€์ธก ๊ธฐ๊ฐ„(์ผ)\n", "CAUSES = [\"๋ถ€ํ’ˆ ๋Œ€๊ธฐ\", \"์ •๋น„๋ณ‘ ๋ถ€์กฑ\", \"์™ธ์ฃผ ์ •๋น„\", \"๊ฒ€์‚ฌยท์žฌ์ž‘์—…\"]\n", "CAUSE_COL = {\"๋ถ€ํ’ˆ ๋Œ€๊ธฐ\": \"parts_wait_days\", \"์ •๋น„๋ณ‘ ๋ถ€์กฑ\": \"labor_wait_days\", \"์™ธ์ฃผ ์ •๋น„\": \"outsource_days\", \"๊ฒ€์‚ฌยท์žฌ์ž‘์—…\": \"inspect_wait_days\"}\n", "NUM_FIELDS = [\"age_years\", \"op_hours\", \"maint_orders\", \"parts_wait_days\", \"labor_wait_days\", \"outsource_days\", \"inspect_wait_days\", \"rework_count\",\n", " \"spec_mismatch\", \"defect_reports\", \"parts_stock_pct\", \"mechanic_fill_pct\", \"avail_pct\"]\n", "FEAT = [\"age_years\", \"maint_orders\", \"parts_stock_pct\", \"mechanic_fill_pct\", \"outsource_days\", \"inspect_wait_days\", \"rework_count\", \"spec_mismatch\", \"defect_reports\", \"etype_B\", \"etype_C\"]\n", "FEAT_KO = {\"age_years\": \"์žฅ๋น„ ์—ฐ๋ น\", \"maint_orders\": \"์ •๋น„ ์š”์ฒญ ๊ฑด์ˆ˜\", \"parts_stock_pct\": \"๋ถ€ํ’ˆ ์žฌ๊ณ  ๊ฐ€์šฉ๋ฅ \", \"mechanic_fill_pct\": \"์ •๋น„๋ณ‘ ์ถฉ์›์œจ\", \"outsource_days\": \"์™ธ์ฃผ ์ •๋น„ ์ผ์ˆ˜\",\n", " \"inspect_wait_days\": \"์™ธ์ฃผ ๊ฒ€์‚ฌ ๋Œ€๊ธฐ ์ผ์ˆ˜\", \"rework_count\": \"์žฌ์ž‘์—… ๊ฑด์ˆ˜\", \"spec_mismatch\": \"๊ทœ๊ฒฉยท์ œ์› ๋ถˆ์ผ์น˜\", \"defect_reports\": \"ํ•˜์žยท๋ถˆ๋งŒ ์ ‘์ˆ˜\",\n", " \"etype_B\": \"์œ ํ˜•B ์—ฌ๋ถ€\", \"etype_C\": \"์œ ํ˜•C ์—ฌ๋ถ€\"}\n", "\n", "\n", "def unit_profile(seed=3):\n", " \"\"\"๋ถ€๋Œ€๋ณ„ ๊ณ ์ • ํŠน์„ฑ: ๋ถ€ํ’ˆ ์žฌ๊ณ  ์ˆ˜์ค€, ์ •๋น„๋ณ‘ ์ถฉ์›์œจ, ์™ธ์ฃผ ์ •๋น„์†Œ ๊ฑฐ๋ฆฌ(์ผ์ˆ˜ ๊ฐ€์ค‘), ์™ธ์ฃผ ๊ฒ€์‚ฌ ํ˜ผ์žก๋„.\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 6] ๋ถ€๋Œ€๋ณ„/์žฅ๋น„๋ณ„ ๊ฐ€๋™๋ฅ  ์ถ”์„ธ ๋ฐ ๊ฐ€๋™๋ฅ  ์ €ํ•˜ ์š”์ธ ๋ถ„์„ ๋Œ€์‹œ๋ณด๋“œ ๊ตฌํ˜„ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def make_fleet(n=300, seed=7, shock=0.0):\n", " \"\"\"์žฅ๋น„ n๋Œ€์˜ 90์ผ ๊ด€์ธก ์ž๋ฃŒ(ํ•„๋“œ 16๊ฐœ)์™€ ์ •๋‹ต ์š”์ธ์„ ๋งŒ๋“ ๋‹ค. shock์€ ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋ณ‘๋ชฉ ์•…ํ™” ์ •๋„(AAR์šฉ ํ™˜๊ฒฝ ๋ณ€ํ™”).\"\"\"\n", " rng = np.random.default_rng(seed)\n", " pf = unit_profile()\n", " ui = rng.integers(0, len(UNITS), n)\n", " et = rng.choice(ETYPES, n, p=[.35, .3, .35])\n", " age = np.clip(rng.gamma(4, 2.2, n), 0.5, 30).round(1)\n", " eff = np.array([{\"์œ ํ˜•A\": 1.15, \"์œ ํ˜•B\": 1.0, \"์œ ํ˜•C\": 0.9}[t] for t in et])\n", " orders = rng.poisson(np.clip(1.2 + 0.12 * age, 0.5, 6) * eff)\n", " stock = np.clip(pf[\"stock\"].to_numpy()[ui] + rng.normal(0, 8, n), 20, 100).round(0)\n", " fill = np.clip(pf[\"fill\"].to_numpy()[ui] + rng.normal(0, 6, n), 30, 100).round(0)\n", " parts_w = (orders * rng.gamma(2, 0.55 * (1 + ((100 - stock) / 28) ** 2) / 2)).round(0)\n", " labor_w = (orders * rng.gamma(2, 0.7 * (1 + ((100 - fill) / 24) ** 2) / 2)).round(0)\n", " n_out = rng.binomial(orders, 0.3)\n", " out_d = (n_out * rng.gamma(3, 4.5 * pf[\"dist\"].to_numpy()[ui] / 3)).round(0)\n", " insp_w = (n_out * rng.gamma(2, (2.6 + shock) * pf[\"queue\"].to_numpy()[ui] / 2)).round(0)\n", " spec = rng.poisson(0.2 + 0.04 * age)\n", " rework = rng.binomial(n_out, np.clip(0.08 + 0.1 * spec, 0, .6))\n", " defect = rng.poisson(0.3 + 0.7 * rework)\n", " base_rep = orders * 1.8\n", " down = base_rep + parts_w + labor_w + out_d + insp_w + rework * 4 + rng.normal(0, 1.2, n)\n", " comp = np.column_stack([parts_w, labor_w, out_d, insp_w + rework * 4])\n", " df = pd.DataFrame({\"equip_id\": [f\"E-{i + 1:04d}\" for i in range(n)], \"unit\": np.array(UNITS)[ui], \"etype\": et, \"age_years\": age,\n", " \"op_hours\": np.clip(rng.normal(320 - 4 * age, 70, n), 40, 600).round(0), \"maint_orders\": orders,\n", " \"parts_wait_days\": parts_w, \"labor_wait_days\": labor_w, \"outsource_days\": out_d, \"inspect_wait_days\": insp_w,\n", " \"rework_count\": rework, \"spec_mismatch\": spec, \"defect_reports\": defect, \"parts_stock_pct\": stock, \"mechanic_fill_pct\": fill,\n", " \"avail_pct\": (100 * (1 - np.clip(down, 0, WINDOW) / WINDOW)).round(1)})\n", " df[\"main_cause\"] = np.where(comp.max(axis=1) < 3, \"์—†์Œ\", np.array(CAUSES)[comp.argmax(axis=1)])\n", " return df\n", "\n", "\n", "def feat_frame(df):\n", " \"\"\"ํšŒ๊ท€ยท๋ถ„์„์šฉ ์„ค๋ช… ๋ณ€์ˆ˜ ํ‘œ(์œ ํ˜•์€ ๋”๋ฏธ ๋ณ€์ˆ˜).\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 6] ๋ถ€๋Œ€๋ณ„/์žฅ๋น„๋ณ„ ๊ฐ€๋™๋ฅ  ์ถ”์„ธ ๋ฐ ๊ฐ€๋™๋ฅ  ์ €ํ•˜ ์š”์ธ ๋ถ„์„ ๋Œ€์‹œ๋ณด๋“œ ๊ตฌํ˜„ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def make_trend(months=12, seed=5, shock_unit=None):\n", " \"\"\"[๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๋ถ€๋Œ€ x ์œ ํ˜• x ์›” ํ‰๊ท  ๊ฐ€๋™๋ฅ  ์ถ”์„ธ. shock_unit ๋ถ€๋Œ€๋Š” ํ•˜๋ฐ˜๊ธฐ์— ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋ณ‘๋ชฉ์œผ๋กœ ๊ฐ€๋™๋ฅ ์ด ๋–จ์–ด์ง„๋‹ค.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " pf = unit_profile()\n", " rows = []\n", " for _, u in pf.iterrows():\n", " base = 62 + 0.22 * u[\"stock\"] + 0.12 * u[\"fill\"] - 3.0 * u[\"queue\"]\n", " for t in ETYPES:\n", " off = {\"์œ ํ˜•A\": -3.0, \"์œ ํ˜•B\": 0.0, \"์œ ํ˜•C\": 2.0}[t]\n", " for m in range(months):\n", " v = base + off + 2.0 * np.sin(m / 12 * 2 * np.pi) + rng.normal(0, 1.0)\n", " if shock_unit == u[\"unit\"] and m >= months // 2:\n", " v -= 1.5 * (m - months // 2 + 1)\n", " rows.append((u[\"unit\"], t, m + 1, round(float(np.clip(v, 30, 99)), 1)))\n", " return pd.DataFrame(rows, columns=[\"unit\", \"etype\", \"month\", \"avail_pct\"])\n", "\n", "fleet = make_fleet(300, seed=41)\n", "trend = make_trend(12, seed=5, shock_unit=\"๊ฐ€์ƒC๋ถ€๋Œ€\")\n", "\n", "# 1) ๋Œ€์‹œ๋ณด๋“œ: ๋ถ€๋Œ€๋ณ„ 12๊ฐœ์›” ๊ฐ€๋™๋ฅ  ์ถ”์„ธ, ์ตœ๊ทผ 3๊ฐœ์›” ํ‰๊ท , ์›” ๊ธฐ์šธ๊ธฐ, ๊ฒฝ๊ณ \n", "SP = \"โ–โ–‚โ–ƒโ–„โ–…โ–†โ–‡โ–ˆ\"\n", "def spark(v):\n", " lo, hi = min(v), max(v)\n", " return \"\".join(SP[int((x - lo) / (hi - lo + 1e-9) * 7)] for x in v)\n", "rows = []\n", "for u, g in trend.groupby(\"unit\"):\n", " m = g.groupby(\"month\")[\"avail_pct\"].mean()\n", " slope = np.polyfit(m.index, m.values, 1)[0]\n", " last3 = m.iloc[-3:].mean()\n", " rows.append({\"๋ถ€๋Œ€\": u, \"์ถ”์„ธ(1~12์›”)\": spark(m.values), \"์ƒ๋ฐ˜๊ธฐ\": m.iloc[:6].mean(), \"ํ•˜๋ฐ˜๊ธฐ\": m.iloc[6:].mean(), \"์ตœ๊ทผ3๊ฐœ์›”\": last3, \"์›” ๊ธฐ์šธ๊ธฐ(%p)\": slope,\n", " \"๊ฒฝ๊ณ \": \"์ €ํ•˜ ๊ฒฝ๊ณ \" if slope < -0.8 or last3 < 70 else \"\"})\n", "dash = pd.DataFrame(rows)\n", "print(\"[๋Œ€์‹œ๋ณด๋“œ 1: ๋ถ€๋Œ€๋ณ„ ๊ฐ€๋™๋ฅ  ์ถ”์„ธ(๊ฐ€์ƒ)]\")\n", "print(dash.round(1).to_string(index=False))\n", "t_et = trend[trend[\"month\"] > 9].groupby([\"unit\", \"etype\"])[\"avail_pct\"].mean().unstack().round(1)\n", "print(\"\\n[๋Œ€์‹œ๋ณด๋“œ 2: ์ตœ๊ทผ 3๊ฐœ์›” ๋ถ€๋Œ€ x ์žฅ๋น„ ์œ ํ˜• ๊ฐ€๋™๋ฅ ]\"); print(t_et.to_string())\n", "lossc = [\"parts_wait_days\", \"labor_wait_days\", \"outsource_days\", \"inspect_wait_days\"]\n", "loss = fleet.groupby(\"unit\")[lossc].mean().round(1)\n", "loss.columns = [\"๋ถ€ํ’ˆ ๋Œ€๊ธฐ\", \"์ •๋น„๋ณ‘ ๋ถ€์กฑ\", \"์™ธ์ฃผ ์ •๋น„\", \"์™ธ์ฃผ ๊ฒ€์‚ฌ ๋Œ€๊ธฐ\"]\n", "print(\"\\n[๋Œ€์‹œ๋ณด๋“œ 3: ๋ถ€๋Œ€๋ณ„ ํ‰๊ท  ๋Œ€๊ธฐ ์†์‹ค์ผ(์ €ํ•˜ ์š”์ธ ๊ตฌ์„ฑ)]\")\n", "print(pd.concat([loss, loss.idxmax(axis=1).rename(\"์ตœ๋Œ€ ์š”์ธ\")], axis=1).to_string())\n", "worst = fleet.nsmallest(3, \"avail_pct\")[[\"equip_id\", \"unit\", \"etype\", \"avail_pct\", \"main_cause\"]]\n", "print(\"[๊ฐ€๋™๋ฅ  ์ตœํ•˜์œ„ ์žฅ๋น„ 3๋Œ€]\"); print(worst.to_string(index=False))\n", "\n", "# 2) ์ž…๋ ฅ ๊ฒ€์ฆ๊ธฐ + ์—๋Ÿฌ ํ•ธ๋“ค๋Ÿฌ\n", "LOG = []\n", "GUIDE = {\"E101\": \"ํ•„์ˆ˜ ์—ด์ด ์—†์Šต๋‹ˆ๋‹ค. ์—ด ์ด๋ฆ„์„ ์–‘์‹๊ณผ ๋น„๊ตํ•ด ์ถ”๊ฐ€ํ•˜์„ธ์š”.\", \"E102\": \"๋นˆ ์ž๋ฃŒ์ž…๋‹ˆ๋‹ค. ํŒŒ์ผ์ด ์˜ฌ๋ฐ”๋ฅธ์ง€ ํ™•์ธํ•˜๊ณ  ๋‹ค์‹œ ์˜ฌ๋ฆฌ์„ธ์š”.\", \"E201\": \"ํ˜•์‹ยท๋ฒ”์œ„ ์˜ค๋ฅ˜ ํ–‰์„ ๊ฒฉ๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ฒฉ๋ฆฌ ๋ชฉ๋ก์„ ํ™•์ธํ•ด ์›๋ณธ์„ ์ˆ˜์ •ํ•˜์„ธ์š”.\",\n", " \"E202\": \"์ค‘๋ณต ์žฅ๋น„ ๋ฒˆํ˜ธ๋ฅผ 1๊ฑด๋งŒ ๋‚จ๊ฒผ์Šต๋‹ˆ๋‹ค. ์ตœ์‹  ๊ธฐ๋ก์ด ๋งž๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.\", \"E301\": \"์ผ์‹œ์  ์ฝ๊ธฐ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. ์ž๋™ ์žฌ์‹œ๋„ ํ›„์—๋„ ์‹คํŒจํ•˜๋ฉด ์ž ์‹œ ๋’ค ๋‹ค์‹œ ์‹œ๋„ํ•˜์„ธ์š”.\",\n", " \"E401\": \"์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ์žˆ์–ด ๋ณ€๊ฒฝ์„ ์ทจ์†Œ(๋กค๋ฐฑ)ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด์ „ ํ™”๋ฉด ์ƒํƒœ๊ฐ€ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.\", \"E501\": \"์ž๋ฃŒ ํ˜•์‹์„ ์•Œ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ํ‘œ ํ˜•์‹(์—ด ์ด๋ฆ„ ํฌํ•จ) ์ž๋ฃŒ๋ฅผ ์˜ฌ๋ฆฌ์„ธ์š”.\"}\n", "\n", "\n", "def log(code, msg, level=\"ERROR\"):\n", " LOG.append({\"seq\": len(LOG) + 1, \"level\": level, \"code\": code, \"msg\": msg, \"guide\": GUIDE[code]})\n", "\n", "\n", "class UserError(Exception):\n", " def __init__(self, code, msg):\n", " super().__init__(msg); self.code = code\n", "\n", "\n", "def validate_frame(df):\n", " \"\"\"ํ–‰ ๋‹จ์œ„ ๊ฒ€์ฆ: ํ˜•์‹(์žฅ๋น„ ๋ฒˆํ˜ธ), ํ—ˆ์šฉ๊ฐ’(๋ถ€๋Œ€), ๋ฒ”์œ„ยท์ž๋ฃŒํ˜•(์ˆซ์ž), ์ค‘๋ณต. ๋ถˆ๋Ÿ‰ ํ–‰์€ ๊ฒฉ๋ฆฌํ•˜๊ณ  ๋‚˜๋จธ์ง€๋Š” ๊ณ„์† ์ฒ˜๋ฆฌํ•œ๋‹ค.\"\"\"\n", " if not isinstance(df, pd.DataFrame):\n", " raise UserError(\"E501\", f\"DataFrame์ด ์•„๋‹Œ ์ž…๋ ฅ({type(df).__name__})\")\n", " if len(df) == 0:\n", " raise UserError(\"E102\", \"ํ–‰ 0๊ฑด\")\n", " miss = [c for c in [\"equip_id\", \"unit\", \"etype\", \"avail_pct\", \"maint_orders\"] if c not in df.columns]\n", " if miss:\n", " raise UserError(\"E101\", f\"๋ˆ„๋ฝ ์—ด {miss}\")\n", " d = df.copy()\n", " num = pd.to_numeric(d[\"avail_pct\"], errors=\"coerce\"), pd.to_numeric(d[\"maint_orders\"], errors=\"coerce\")\n", " bad = (~d[\"equip_id\"].astype(str).str.match(r\"^E-\\d{4,6}$\")) | (~d[\"unit\"].isin(UNITS)) | (~d[\"etype\"].isin(ETYPES))\n", " bad |= ~np.isfinite(num[0]) | (num[0] < 0) | (num[0] > 100) | num[1].isna() | (num[1] < 0)\n", " q = d[bad]\n", " if len(q):\n", " log(\"E201\", f\"ํ˜•์‹ยท๋ฒ”์œ„ ์˜ค๋ฅ˜ {len(q)}ํ–‰ ๊ฒฉ๋ฆฌ(์˜ˆ: {str(q.iloc[0]['equip_id'])[:12]!r})\", \"WARN\")\n", " d = d[~bad].copy()\n", " d[\"avail_pct\"] = pd.to_numeric(d[\"avail_pct\"]); d[\"maint_orders\"] = pd.to_numeric(d[\"maint_orders\"])\n", " dup = d.duplicated(\"equip_id\")\n", " if dup.any():\n", " log(\"E202\", f\"์ค‘๋ณต ์žฅ๋น„ ๋ฒˆํ˜ธ {int(dup.sum())}๊ฑด ์ •๋ฆฌ\", \"WARN\")\n", " return d[~dup], q\n", "\n", "\n", "class Store:\n", " \"\"\"ํ™”๋ฉด ์ƒํƒœ ์ €์žฅ์†Œ: ๊ฐฑ์‹ ์€ ์‚ฌ๋ณธ์—์„œ ๋จผ์ € ํ•˜๊ณ  ๋ชจ๋‘ ์„ฑ๊ณตํ•ด์•ผ ๋ฐ˜์˜(ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ)ํ•œ๋‹ค.\"\"\"\n", " def __init__(self):\n", " self.state = {\"version\": 0, \"avg\": None}\n", "\n", " def commit(self, fn):\n", " snap = copy.deepcopy(self.state)\n", " try:\n", " fn(self.state); self.state[\"version\"] += 1\n", " return True\n", " except Exception as e:\n", " self.state = snap; log(\"E401\", f\"๊ฐฑ์‹  ์‹คํŒจ {type(e).__name__}: {str(e)[:40]}\"); return False\n", "\n", "\n", "STORE = Store()\n", "FLAKY = {\"n\": 0}\n", "\n", "\n", "def flaky_loader(df):\n", " FLAKY[\"n\"] += 1\n", " if FLAKY[\"n\"] <= 2:\n", " raise IOError(\"temporary read error\")\n", " return df\n", "\n", "\n", "def run(df, loader=None, fail_update=False):\n", " \"\"\"๋Œ€์‹œ๋ณด๋“œ ๊ฐฑ์‹  1ํšŒ. ์–ด๋–ค ์ž…๋ ฅ์—์„œ๋„ ์˜ˆ์™ธ๋ฅผ ๋ฐ–์œผ๋กœ ๋˜์ง€์ง€ ์•Š๊ณ  (์ƒํƒœ, ์ฒ˜๋ฆฌ ํ–‰์ˆ˜)๋ฅผ ๋Œ๋ ค์ค€๋‹ค.\"\"\"\n", " try:\n", " for attempt in range(3): # ์žฌ์‹œ๋„(์ตœ๋Œ€ 3ํšŒ)\n", " try:\n", " src = loader(df) if loader else df; break\n", " except IOError:\n", " log(\"E301\", f\"์ฝ๊ธฐ ์˜ค๋ฅ˜ ์žฌ์‹œ๋„ {attempt + 1}/3\", \"WARN\")\n", " else:\n", " raise UserError(\"E301\", \"์žฌ์‹œ๋„ 3ํšŒ ์‹คํŒจ\")\n", " clean, q = validate_frame(src)\n", " def upd(s):\n", " s[\"avg\"] = float(clean[\"avail_pct\"].mean())\n", " if fail_update:\n", " raise RuntimeError(\"render failure\")\n", " ok = STORE.commit(upd)\n", " return (\"์ฒ˜๋ฆฌ\" if not len(q) else \"๊ฒฉ๋ฆฌ ํ›„ ์ฒ˜๋ฆฌ\") if ok else \"๋กค๋ฐฑ\", len(clean)\n", " except UserError as e:\n", " log(e.code, str(e)); return \"๊ฑฐ๋ถ€(์•ˆ๋‚ด)\", 0\n", " except Exception as e: # ์˜ˆ์ƒ ๋ชป ํ•œ ์˜ค๋ฅ˜๋„ ์ค‘๋‹จํ•˜์ง€ ์•Š๊ณ  ๊ธฐ๋ก\n", " log(\"E501\", f\"{type(e).__name__}: {str(e)[:40]}\"); return \"๊ฑฐ๋ถ€(์•ˆ๋‚ด)\", 0\n", "\n", "\n", "good = fleet[[\"equip_id\", \"unit\", \"etype\", \"avail_pct\", \"maint_orders\"]]\n", "def mod(**kw):\n", " d = good.copy()\n", " for k, v in kw.items():\n", " d[k] = d[k].astype(object); d.loc[d.index[:3], k] = v\n", " return d\n", "big = pd.DataFrame({\"equip_id\": [f\"E-{i:06d}\" for i in range(200000)], \"unit\": \"๊ฐ€์ƒA๋ถ€๋Œ€\", \"etype\": \"์œ ํ˜•A\",\n", " \"avail_pct\": np.random.default_rng(1).uniform(40, 100, 200000), \"maint_orders\": 2})\n", "big.loc[big.index[::1000], \"avail_pct\"] = np.nan\n", "cases = [(\"๊ฐ€๋™๋ฅ  ๊ฒฐ์ธก(NaN)\", mod(avail_pct=np.nan)), (\"๊ฐ€๋™๋ฅ  ๋ฌธ์ž์—ด '๋งŽ์Œ'\", mod(avail_pct=\"๋งŽ์Œ\")), (\"๊ฐ€๋™๋ฅ  ์Œ์ˆ˜ยท100 ์ดˆ๊ณผ\", mod(avail_pct=-5)),\n", " (\"์žฅ๋น„ ๋ฒˆํ˜ธ ํŠน์ˆ˜๋ฌธ์ž\", mod(equip_id=\"E-00;DROP --\")), (\"๊ฐ€์ƒ์— ์—†๋Š” ๋ถ€๋Œ€๋ช…\", mod(unit=\"๋ฏธ์ƒ๋ถ€๋Œ€\")), (\"๋ฌดํ•œ๋Œ€(inf) ๊ฐ’\", mod(avail_pct=np.inf)),\n", " (\"์ค‘๋ณต ์žฅ๋น„ ๋ฒˆํ˜ธ\", pd.concat([good, good.head(5)])), (\"ํ•„์ˆ˜ ์—ด ๋ˆ„๋ฝ\", good.drop(columns=[\"unit\"])), (\"๋นˆ ์ž๋ฃŒ\", good.iloc[0:0]),\n", " (\"ํ‘œ๊ฐ€ ์•„๋‹Œ ์ž…๋ ฅ(None)\", None), (\"๋Œ€์šฉ๋Ÿ‰ 20๋งŒ ํ–‰(๊ฒฐ์ธก ํฌํ•จ)\", big), (\"์ฝ๊ธฐ ์˜ค๋ฅ˜ 2ํšŒ ํ›„ ์„ฑ๊ณต(์žฌ์‹œ๋„)\", good), (\"๊ฐฑ์‹  ์ค‘ ์˜ค๋ฅ˜(๋กค๋ฐฑ)\", good)]\n", "print(\"\\n[์˜ˆ์™ธ ์ฃผ์ž… ์‹œํ—˜]\")\n", "crash = passed = 0\n", "t0 = time.perf_counter()\n", "for name, df in cases:\n", " n0 = len(LOG)\n", " try:\n", " st, n = run(df, loader=flaky_loader if \"์žฌ์‹œ๋„\" in name else None, fail_update=\"๋กค๋ฐฑ\" in name)\n", " except Exception as e:\n", " crash += 1; st, n = f\"CRASH {e}\", 0\n", " logged = len(LOG) > n0\n", " passed += (not st.startswith(\"CRASH\")) and logged\n", " print(f\" {name:22s}-> {st:8s} {n:6d}ํ–‰ ๋‚จ์Œ | ๋กœ๊ทธ {'๊ธฐ๋ก' if logged else '์—†์Œ'}\" + (f\" | {LOG[-1]['code']}: {LOG[-1]['guide'][:28]}\" if logged else \"\"))\n", "dt = time.perf_counter() - t0\n", "print(f\"์˜ˆ์™ธ {len(cases)}์ข…: ํฌ๋ž˜์‹œ {crash}๊ฑด(๋ชฉํ‘œ 0๊ฑด: {'๋‹ฌ์„ฑ' if crash == 0 else '๋ฏธ๋‹ฌ'}), ์˜ค๋ฅ˜ ๋กœ๊ทธ ๊ธฐ๋ก {passed}/{len(cases)} ({passed / len(cases):.0%}, ๋ชฉํ‘œ 100%), ์ „์ฒด {dt:.1f}์ดˆ\")\n", "print(f\"์˜ค๋ฅ˜ ๋กœ๊ทธ {len(LOG)}๊ฑด ์ค‘ ์‚ฌ์šฉ์ž ์•ˆ๋‚ด๋ฌธ ํฌํ•จ {sum(bool(x['guide']) for x in LOG)}๊ฑด, ์ƒํƒœ ์ €์žฅ์†Œ ๋ฒ„์ „ {STORE.state['version']}(๋กค๋ฐฑ ์‹œ ์ด์ „ ๊ฐ’ ์œ ์ง€), ํ‰๊ท  ๊ฐ€๋™๋ฅ  {STORE.state['avg']:.1f}%\")\n", "print(\"๋กœ๊ทธ ์˜ˆ์‹œ:\", LOG[0][\"code\"], LOG[0][\"msg\"], \"->\", LOG[0][\"guide\"])\n", "print(\"โ€ป ๊ฒฉ๋ฆฌ๋œ ํ–‰์˜ ์ˆ˜์ •๊ณผ ๊ฒฝ๊ณ  ํŒ๋‹จ์€ ๋‹ด๋‹น์ž๊ฐ€ ํ™•์ธํ•œ๋‹ค. ๋กœ๊ทธ๋Š” ๋ฉ”๋ชจ๋ฆฌ์— ๋‘๋ฉฐ ์‹ค์ œ ๋„๊ตฌ์—์„œ๋Š” ๊ฐ์‚ฌ ๋กœ๊ทธ ํŒŒ์ผ๋กœ ๋‚จ๊ธด๋‹ค.\")\n", "\n", "fig, ax = plt.subplots(1, 2, figsize=(11, 4))\n", "for u, g in trend.groupby(\"unit\"):\n", " m = g.groupby(\"month\")[\"avail_pct\"].mean(); ax[0].plot(m.index, m.values, label='unit ' + u[2])\n", "ax[0].set_title(\"Monthly availability by unit (synthetic)\"); ax[0].set_xlabel(\"month\"); ax[0].legend(ncol=3, fontsize=7)\n", "loss.plot(kind=\"bar\", stacked=True, ax=ax[1], legend=False); ax[1].set_xticklabels([u[2] for u in loss.index], rotation=0); ax[1].set_title(\"Mean wait-loss days by cause\")\n", "plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 06 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[๋Œ€์‹œ๋ณด๋“œ 1: ๋ถ€๋Œ€๋ณ„ ๊ฐ€๋™๋ฅ  ์ถ”์„ธ(๊ฐ€์ƒ)]\n", " ๋ถ€๋Œ€ ์ถ”์„ธ(1~12์›”) ์ƒ๋ฐ˜๊ธฐ ํ•˜๋ฐ˜๊ธฐ ์ตœ๊ทผ3๊ฐœ์›” ์›” ๊ธฐ์šธ๊ธฐ(%p) ๊ฒฝ๊ณ \n", "๊ฐ€์ƒA๋ถ€๋Œ€ โ–ƒโ–‡โ–†โ–‡โ–‡โ–…โ–ƒโ–‚โ–โ–‚โ–โ– 77.3 74.8 74.6 -0.3 \n", "๊ฐ€์ƒB๋ถ€๋Œ€ โ–ƒโ–…โ–†โ–…โ–‡โ–„โ–‚โ–‚โ–โ–โ–‚โ– 79.4 76.9 76.8 -0.3 \n", "๊ฐ€์ƒC๋ถ€๋Œ€ โ–†โ–‡โ–‡โ–‡โ–‡โ–‡โ–†โ–…โ–ƒโ–‚โ–โ– 89.4 81.9 79.1 -1.2 ์ €ํ•˜ ๊ฒฝ๊ณ \n", "๊ฐ€์ƒD๋ถ€๋Œ€ โ–†โ–…โ–†โ–‡โ–†โ–„โ–ƒโ–โ–โ–‚โ–โ–ƒ 80.2 77.6 77.7 -0.3 \n", "๊ฐ€์ƒE๋ถ€๋Œ€ โ–ƒโ–…โ–‡โ–†โ–†โ–‡โ–ƒโ–โ–โ–โ–‚โ–‚ 79.2 76.1 76.1 -0.3 \n", "๊ฐ€์ƒF๋ถ€๋Œ€ โ–„โ–†โ–‡โ–‡โ–…โ–†โ–„โ–„โ–‚โ–โ–‚โ–ƒ 83.7 81.0 80.6 -0.3 \n", "\n", "[๋Œ€์‹œ๋ณด๋“œ 2: ์ตœ๊ทผ 3๊ฐœ์›” ๋ถ€๋Œ€ x ์žฅ๋น„ ์œ ํ˜• ๊ฐ€๋™๋ฅ ]\n", "etype ์œ ํ˜•A ์œ ํ˜•B ์œ ํ˜•C\n", "unit \n", "๊ฐ€์ƒA๋ถ€๋Œ€ 72.3 75.1 76.5\n", "๊ฐ€์ƒB๋ถ€๋Œ€ 73.3 78.2 78.8\n", "๊ฐ€์ƒC๋ถ€๋Œ€ 76.3 78.8 82.2\n", "๊ฐ€์ƒD๋ถ€๋Œ€ 74.8 77.8 80.4\n", "๊ฐ€์ƒE๋ถ€๋Œ€ 73.6 76.1 78.7\n", "๊ฐ€์ƒF๋ถ€๋Œ€ 78.5 79.9 83.3\n", "\n", "[๋Œ€์‹œ๋ณด๋“œ 3: ๋ถ€๋Œ€๋ณ„ ํ‰๊ท  ๋Œ€๊ธฐ ์†์‹ค์ผ(์ €ํ•˜ ์š”์ธ ๊ตฌ์„ฑ)]\n", " ๋ถ€ํ’ˆ ๋Œ€๊ธฐ ์ •๋น„๋ณ‘ ๋ถ€์กฑ ์™ธ์ฃผ ์ •๋น„ ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋Œ€๊ธฐ ์ตœ๋Œ€ ์š”์ธ\n", "unit \n", "๊ฐ€์ƒA๋ถ€๋Œ€ 5.0 3.5 3.3 2.2 ๋ถ€ํ’ˆ ๋Œ€๊ธฐ\n", "๊ฐ€์ƒB๋ถ€๋Œ€ 4.3 5.7 3.2 2.1 ์ •๋น„๋ณ‘ ๋ถ€์กฑ\n", "๊ฐ€์ƒC๋ถ€๋Œ€ 2.1 2.0 4.0 1.3 ์™ธ์ฃผ ์ •๋น„\n", "๊ฐ€์ƒD๋ถ€๋Œ€ 2.5 5.1 5.3 3.5 ์™ธ์ฃผ ์ •๋น„\n", "๊ฐ€์ƒE๋ถ€๋Œ€ 5.5 4.2 2.6 1.2 ๋ถ€ํ’ˆ ๋Œ€๊ธฐ\n", "๊ฐ€์ƒF๋ถ€๋Œ€ 2.7 2.4 4.9 1.7 ์™ธ์ฃผ ์ •๋น„\n", "[๊ฐ€๋™๋ฅ  ์ตœํ•˜์œ„ ์žฅ๋น„ 3๋Œ€]\n", "equip_id unit etype avail_pct main_cause\n", " E-0113 ๊ฐ€์ƒD๋ถ€๋Œ€ ์œ ํ˜•A 17.5 ์™ธ์ฃผ ์ •๋น„\n", " E-0165 ๊ฐ€์ƒD๋ถ€๋Œ€ ์œ ํ˜•A 26.3 ๊ฒ€์‚ฌยท์žฌ์ž‘์—…\n", " E-0283 ๊ฐ€์ƒA๋ถ€๋Œ€ ์œ ํ˜•A 30.2 ์™ธ์ฃผ ์ •๋น„\n", "\n", "[์˜ˆ์™ธ ์ฃผ์ž… ์‹œํ—˜]\n", " ๊ฐ€๋™๋ฅ  ๊ฒฐ์ธก(NaN) -> ๊ฒฉ๋ฆฌ ํ›„ ์ฒ˜๋ฆฌ 297ํ–‰ ๋‚จ์Œ | ๋กœ๊ทธ ๊ธฐ๋ก | E201: ํ˜•์‹ยท๋ฒ”์œ„ ์˜ค๋ฅ˜ ํ–‰์„ ๊ฒฉ๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ฒฉ๋ฆฌ ๋ชฉ๋ก์„ ํ™•\n", " ๊ฐ€๋™๋ฅ  ๋ฌธ์ž์—ด '๋งŽ์Œ' -> ๊ฒฉ๋ฆฌ ํ›„ ์ฒ˜๋ฆฌ 297ํ–‰ ๋‚จ์Œ | ๋กœ๊ทธ ๊ธฐ๋ก | E201: ํ˜•์‹ยท๋ฒ”์œ„ ์˜ค๋ฅ˜ ํ–‰์„ ๊ฒฉ๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ฒฉ๋ฆฌ ๋ชฉ๋ก์„ ํ™•\n", " ๊ฐ€๋™๋ฅ  ์Œ์ˆ˜ยท100 ์ดˆ๊ณผ -> ๊ฒฉ๋ฆฌ ํ›„ ์ฒ˜๋ฆฌ 297ํ–‰ ๋‚จ์Œ | ๋กœ๊ทธ ๊ธฐ๋ก | E201: ํ˜•์‹ยท๋ฒ”์œ„ ์˜ค๋ฅ˜ ํ–‰์„ ๊ฒฉ๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ฒฉ๋ฆฌ ๋ชฉ๋ก์„ ํ™•\n", " ์žฅ๋น„ ๋ฒˆํ˜ธ ํŠน์ˆ˜๋ฌธ์ž -> ๊ฒฉ๋ฆฌ ํ›„ ์ฒ˜๋ฆฌ 297ํ–‰ ๋‚จ์Œ | ๋กœ๊ทธ ๊ธฐ๋ก | E201: ํ˜•์‹ยท๋ฒ”์œ„ ์˜ค๋ฅ˜ ํ–‰์„ ๊ฒฉ๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ฒฉ๋ฆฌ ๋ชฉ๋ก์„ ํ™•\n", " ๊ฐ€์ƒ์— ์—†๋Š” ๋ถ€๋Œ€๋ช… -> ๊ฒฉ๋ฆฌ ํ›„ ์ฒ˜๋ฆฌ 297ํ–‰ ๋‚จ์Œ | ๋กœ๊ทธ ๊ธฐ๋ก | E201: ํ˜•์‹ยท๋ฒ”์œ„ ์˜ค๋ฅ˜ ํ–‰์„ ๊ฒฉ๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ฒฉ๋ฆฌ ๋ชฉ๋ก์„ ํ™•\n", "... (์ดํ•˜ ์ƒ๋žต)\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 07. ์ •๋น„ ์ž์› ์šฐ์„  ํˆฌ์ž… ๊ฒฐ์ •์„ ์œ„ํ•œ ๊ตฐ์ˆ˜์ฐธ๋ชจ์ฒ˜ HITL ์ •์ฑ… ๊ฒฐ์ • ๋ชจ๋“ˆ ์„ค๊ณ„\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "์ •๋น„ ์ž์› ์šฐ์„  ํˆฌ์ž… ๊ฒฐ์ •์„ ์œ„ํ•œ ๊ตฐ์ˆ˜์ฐธ๋ชจ์ฒ˜ HITL ์ •์ฑ… ๊ฒฐ์ • ๋ชจ๋“ˆ์„ ์„ค๊ณ„ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **์ •๋น„ ์ž์› ์šฐ์„  ํˆฌ์ž… ์•ˆ์„ ์ •๋น„๋‹ด๋‹น-์ •๋น„๊ด€-ํ’ˆ์งˆ๊ฒ€์‚ฌ๊ด€-๊ตฐ์ˆ˜์ฐธ๋ชจ ์ˆœ์„œ๋กœ ์‚ฌ๋žŒ์ด ๊ฒ€ํ† ยท๊ฒฐ์ •ํ•˜๋Š” HITL ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ์„ค๊ณ„ํ•˜๊ณ  ๊ถŒํ•œ ํ†ต์ œ์™€ ์ด๋ ฅ ๋ฌด๊ฒฐ์„ฑ์„ ๊ฒ€์ฆํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **์—ญํ• ๋ณ„ ๊ถŒํ•œ(RBAC)๊ณผ ๋‹จ๊ณ„ ์ˆœ์„œ๋ฅผ ์ •์˜ํ•˜๊ณ  ์‹œ์Šคํ…œ์€ ์ œ์•ˆ๋งŒ, ํ™•์ •์€ ์‚ฌ๋žŒ๋งŒ ํ•˜๋„๋ก ์ž‘์„ฑํ•˜์—ฌ ์›์ธ์„ ๋ถ„์„ํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/workflow/qlt04_approval_system.py` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ๊ตฐ ๋ณด์•ˆ ๊ทœ์ • ๋ฐ ํ–‰์ • ์ ˆ์ฐจ ๋ถ€ํ•ฉ์„ฑ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ์ด๋ ฅ ๊ธฐ๋ก ๋ฌด๊ฒฐ์„ฑ 100%, ์Šน์ธ ๊ถŒํ•œ ์šฐํšŒ ์‹œ๋„ ์ฐจ๋‹จ์œจ 100%\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ๊ฒฐ์žฌ์„  ์Šน์ธ/๋ฐ˜๋ ค ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ๋ฐ ์ด๋ ฅ ์กฐํšŒ ํ™”๋ฉด ๊ตฌํ˜„\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ๊ถŒํ•œ๋ณ„ ์ ‘๊ทผ ์ œ์–ด(RBAC) ํ…Œ์ŠคํŠธ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"์ €๊ฐ€๋™ ์žฅ๋น„์˜ ์ •๋น„ ์ž์› ์šฐ์„  ํˆฌ์ž…์•ˆ์„ ์ •๋น„๋‹ด๋‹น ์ƒ์‹  -> ์ •๋น„๊ด€ ๊ฒ€ํ†  -> ํ’ˆ์งˆ๊ฒ€์‚ฌ๊ด€ ๊ฒ€ํ†  -> ๊ตฐ์ˆ˜์ฐธ๋ชจ ๊ฒฐ์žฌ์˜ HITL ์›Œํฌํ”Œ๋กœ์šฐ๋กœ ์ฒ˜๋ฆฌํ•˜๋˜, ์‹œ์Šคํ…œ ์ž๋™ ํ™•์ • ๊ธˆ์ง€ยท๋ฐ˜๋ ค ์‚ฌ์œ  ํ•„์ˆ˜ํ™”ยท์žฌ๊ธฐ์•ˆ ์—ฐ๊ณ„ยท๊ถŒํ•œ ์šฐํšŒ ์ฐจ๋‹จ(RBAC)๊ณผ ํ•ด์‹œ ์ฒด์ธ ๊ฐ์‚ฌ ๋กœ๊ทธ์˜ ์ด๋ ฅ ๋ฌด๊ฒฐ์„ฑยท์œ„๋ณ€์กฐ ํƒ์ง€๋ฅผ ๊ฒ€์ฆํ•œ๋‹ค.\"\"\"\n", "import hashlib\n", "import json\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ฐ€์ƒ ์žฅ๋น„ ๊ฐ€๋™๋ฅ ยท์ •๋น„ ๋Œ€๊ธฐ ์ž๋ฃŒ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ๋ถ€๋Œ€ยท์žฅ๋น„ยท์ œ์›ยท๊ฐ€๋™๋ฅ  ์ •๋ณด ์—†์Œ)\n", "# ์žฅ๋น„ ์œ ํ˜• A(๊ถค๋„ํ˜•)ยทB(ํ™”ํฌํ˜•)ยทC(์ฐจ๋ฅœํ˜•)์€ ์ถ”์ƒ ๊ตฌ๋ถ„์ด๋ฉฐ ์‹ค์ œ ์žฅ๋น„์™€ ๋ฌด๊ด€ํ•˜๋‹ค.\n", "# ์ตœ๊ทผ 90์ผ ๋™์•ˆ ์žฅ๋น„ 1๋Œ€์˜ ์ •๋น„ ๋Œ€๊ธฐ ์š”์ธ(๋ถ€ํ’ˆยท์ •๋น„๋ณ‘ยท์™ธ์ฃผ ์ •๋น„ยท์™ธ์ฃผ ๊ฒ€์‚ฌยท์žฌ์ž‘์—…)๊ณผ ๊ฐ€๋™๋ฅ ์„ ๋งŒ๋“ ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "UNITS = [\"๊ฐ€์ƒA๋ถ€๋Œ€\", \"๊ฐ€์ƒB๋ถ€๋Œ€\", \"๊ฐ€์ƒC๋ถ€๋Œ€\", \"๊ฐ€์ƒD๋ถ€๋Œ€\", \"๊ฐ€์ƒE๋ถ€๋Œ€\", \"๊ฐ€์ƒF๋ถ€๋Œ€\"]\n", "ETYPES = [\"์œ ํ˜•A\", \"์œ ํ˜•B\", \"์œ ํ˜•C\"]\n", "WINDOW = 90 # ๊ด€์ธก ๊ธฐ๊ฐ„(์ผ)\n", "CAUSES = [\"๋ถ€ํ’ˆ ๋Œ€๊ธฐ\", \"์ •๋น„๋ณ‘ ๋ถ€์กฑ\", \"์™ธ์ฃผ ์ •๋น„\", \"๊ฒ€์‚ฌยท์žฌ์ž‘์—…\"]\n", "CAUSE_COL = {\"๋ถ€ํ’ˆ ๋Œ€๊ธฐ\": \"parts_wait_days\", \"์ •๋น„๋ณ‘ ๋ถ€์กฑ\": \"labor_wait_days\", \"์™ธ์ฃผ ์ •๋น„\": \"outsource_days\", \"๊ฒ€์‚ฌยท์žฌ์ž‘์—…\": \"inspect_wait_days\"}\n", "NUM_FIELDS = [\"age_years\", \"op_hours\", \"maint_orders\", \"parts_wait_days\", \"labor_wait_days\", \"outsource_days\", \"inspect_wait_days\", \"rework_count\",\n", " \"spec_mismatch\", \"defect_reports\", \"parts_stock_pct\", \"mechanic_fill_pct\", \"avail_pct\"]\n", "FEAT = [\"age_years\", \"maint_orders\", \"parts_stock_pct\", \"mechanic_fill_pct\", \"outsource_days\", \"inspect_wait_days\", \"rework_count\", \"spec_mismatch\", \"defect_reports\", \"etype_B\", \"etype_C\"]\n", "FEAT_KO = {\"age_years\": \"์žฅ๋น„ ์—ฐ๋ น\", \"maint_orders\": \"์ •๋น„ ์š”์ฒญ ๊ฑด์ˆ˜\", \"parts_stock_pct\": \"๋ถ€ํ’ˆ ์žฌ๊ณ  ๊ฐ€์šฉ๋ฅ \", \"mechanic_fill_pct\": \"์ •๋น„๋ณ‘ ์ถฉ์›์œจ\", \"outsource_days\": \"์™ธ์ฃผ ์ •๋น„ ์ผ์ˆ˜\",\n", " \"inspect_wait_days\": \"์™ธ์ฃผ ๊ฒ€์‚ฌ ๋Œ€๊ธฐ ์ผ์ˆ˜\", \"rework_count\": \"์žฌ์ž‘์—… ๊ฑด์ˆ˜\", \"spec_mismatch\": \"๊ทœ๊ฒฉยท์ œ์› ๋ถˆ์ผ์น˜\", \"defect_reports\": \"ํ•˜์žยท๋ถˆ๋งŒ ์ ‘์ˆ˜\",\n", " \"etype_B\": \"์œ ํ˜•B ์—ฌ๋ถ€\", \"etype_C\": \"์œ ํ˜•C ์—ฌ๋ถ€\"}\n", "\n", "\n", "def unit_profile(seed=3):\n", " \"\"\"๋ถ€๋Œ€๋ณ„ ๊ณ ์ • ํŠน์„ฑ: ๋ถ€ํ’ˆ ์žฌ๊ณ  ์ˆ˜์ค€, ์ •๋น„๋ณ‘ ์ถฉ์›์œจ, ์™ธ์ฃผ ์ •๋น„์†Œ ๊ฑฐ๋ฆฌ(์ผ์ˆ˜ ๊ฐ€์ค‘), ์™ธ์ฃผ ๊ฒ€์‚ฌ ํ˜ผ์žก๋„.\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 7] ์ •๋น„ ์ž์› ์šฐ์„  ํˆฌ์ž… ๊ฒฐ์ •์„ ์œ„ํ•œ ๊ตฐ์ˆ˜์ฐธ๋ชจ์ฒ˜ HITL ์ •์ฑ… ๊ฒฐ์ • ๋ชจ๋“ˆ ์„ค๊ณ„ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def make_fleet(n=300, seed=7, shock=0.0):\n", " \"\"\"์žฅ๋น„ n๋Œ€์˜ 90์ผ ๊ด€์ธก ์ž๋ฃŒ(ํ•„๋“œ 16๊ฐœ)์™€ ์ •๋‹ต ์š”์ธ์„ ๋งŒ๋“ ๋‹ค. shock์€ ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋ณ‘๋ชฉ ์•…ํ™” ์ •๋„(AAR์šฉ ํ™˜๊ฒฝ ๋ณ€ํ™”).\"\"\"\n", " rng = np.random.default_rng(seed)\n", " pf = unit_profile()\n", " ui = rng.integers(0, len(UNITS), n)\n", " et = rng.choice(ETYPES, n, p=[.35, .3, .35])\n", " age = np.clip(rng.gamma(4, 2.2, n), 0.5, 30).round(1)\n", " eff = np.array([{\"์œ ํ˜•A\": 1.15, \"์œ ํ˜•B\": 1.0, \"์œ ํ˜•C\": 0.9}[t] for t in et])\n", " orders = rng.poisson(np.clip(1.2 + 0.12 * age, 0.5, 6) * eff)\n", " stock = np.clip(pf[\"stock\"].to_numpy()[ui] + rng.normal(0, 8, n), 20, 100).round(0)\n", " fill = np.clip(pf[\"fill\"].to_numpy()[ui] + rng.normal(0, 6, n), 30, 100).round(0)\n", " parts_w = (orders * rng.gamma(2, 0.55 * (1 + ((100 - stock) / 28) ** 2) / 2)).round(0)\n", " labor_w = (orders * rng.gamma(2, 0.7 * (1 + ((100 - fill) / 24) ** 2) / 2)).round(0)\n", " n_out = rng.binomial(orders, 0.3)\n", " out_d = (n_out * rng.gamma(3, 4.5 * pf[\"dist\"].to_numpy()[ui] / 3)).round(0)\n", " insp_w = (n_out * rng.gamma(2, (2.6 + shock) * pf[\"queue\"].to_numpy()[ui] / 2)).round(0)\n", " spec = rng.poisson(0.2 + 0.04 * age)\n", " rework = rng.binomial(n_out, np.clip(0.08 + 0.1 * spec, 0, .6))\n", " defect = rng.poisson(0.3 + 0.7 * rework)\n", " base_rep = orders * 1.8\n", " down = base_rep + parts_w + labor_w + out_d + insp_w + rework * 4 + rng.normal(0, 1.2, n)\n", " comp = np.column_stack([parts_w, labor_w, out_d, insp_w + rework * 4])\n", " df = pd.DataFrame({\"equip_id\": [f\"E-{i + 1:04d}\" for i in range(n)], \"unit\": np.array(UNITS)[ui], \"etype\": et, \"age_years\": age,\n", " \"op_hours\": np.clip(rng.normal(320 - 4 * age, 70, n), 40, 600).round(0), \"maint_orders\": orders,\n", " \"parts_wait_days\": parts_w, \"labor_wait_days\": labor_w, \"outsource_days\": out_d, \"inspect_wait_days\": insp_w,\n", " \"rework_count\": rework, \"spec_mismatch\": spec, \"defect_reports\": defect, \"parts_stock_pct\": stock, \"mechanic_fill_pct\": fill,\n", " \"avail_pct\": (100 * (1 - np.clip(down, 0, WINDOW) / WINDOW)).round(1)})\n", " df[\"main_cause\"] = np.where(comp.max(axis=1) < 3, \"์—†์Œ\", np.array(CAUSES)[comp.argmax(axis=1)])\n", " return df\n", "\n", "\n", "def feat_frame(df):\n", " \"\"\"ํšŒ๊ท€ยท๋ถ„์„์šฉ ์„ค๋ช… ๋ณ€์ˆ˜ ํ‘œ(์œ ํ˜•์€ ๋”๋ฏธ ๋ณ€์ˆ˜).\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 7] ์ •๋น„ ์ž์› ์šฐ์„  ํˆฌ์ž… ๊ฒฐ์ •์„ ์œ„ํ•œ ๊ตฐ์ˆ˜์ฐธ๋ชจ์ฒ˜ HITL ์ •์ฑ… ๊ฒฐ์ • ๋ชจ๋“ˆ ์„ค๊ณ„ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def make_trend(months=12, seed=5, shock_unit=None):\n", " \"\"\"[๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๋ถ€๋Œ€ x ์œ ํ˜• x ์›” ํ‰๊ท  ๊ฐ€๋™๋ฅ  ์ถ”์„ธ. shock_unit ๋ถ€๋Œ€๋Š” ํ•˜๋ฐ˜๊ธฐ์— ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋ณ‘๋ชฉ์œผ๋กœ ๊ฐ€๋™๋ฅ ์ด ๋–จ์–ด์ง„๋‹ค.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " pf = unit_profile()\n", " rows = []\n", " for _, u in pf.iterrows():\n", " base = 62 + 0.22 * u[\"stock\"] + 0.12 * u[\"fill\"] - 3.0 * u[\"queue\"]\n", " for t in ETYPES:\n", " off = {\"์œ ํ˜•A\": -3.0, \"์œ ํ˜•B\": 0.0, \"์œ ํ˜•C\": 2.0}[t]\n", " for m in range(months):\n", " v = base + off + 2.0 * np.sin(m / 12 * 2 * np.pi) + rng.normal(0, 1.0)\n", " if shock_unit == u[\"unit\"] and m >= months // 2:\n", " v -= 1.5 * (m - months // 2 + 1)\n", " rows.append((u[\"unit\"], t, m + 1, round(float(np.clip(v, 30, 99)), 1)))\n", " return pd.DataFrame(rows, columns=[\"unit\", \"etype\", \"month\", \"avail_pct\"])\n", "\n", "# ---- ์ •๋น„ยท๊ฒ€์‚ฌ ๋ณ‘๋ชฉ ํŒ๋ณ„ ๋ฃฐ ์„ค์ •(YAML ํ˜•์‹ ๋ฌธ์ž์—ด: ๊ธฐ์ค€์ด ๋ฐ”๋€Œ๋ฉด ์ฝ”๋“œ๊ฐ€ ์•„๋‹ˆ๋ผ ์ด ์„ค์ •๋งŒ ๊ณ ์นœ๋‹ค) ----\n", "# clause๋Š” ๋ฃฐ๊ณผ ๋Œ€์‘์‹œํ‚จ '๊ฐ€์ƒ ํ’ˆ์งˆ์ง€์นจ ์กฐํ•ญ' ์˜ˆ์‹œ์ด๋ฉฐ ์‹ค์ œ ๊ทœ์ • ์›๋ฌธ์ด ์•„๋‹ˆ๋‹ค.\n", "RULES_YAML = \"\"\"\n", "- {id: Q01, name: ๋ถ€ํ’ˆ ์žฌ๊ณ  ๊ฐ€์šฉ๋ฅ  50% ๋ฏธ๋งŒ, field: parts_stock_pct, op: \"<\", value: 50, weight: 2, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ3์กฐ}\n", "- {id: Q02, name: ์ •๋น„๋ณ‘ ์ถฉ์›์œจ 60% ๋ฏธ๋งŒ, field: mechanic_fill_pct, op: \"<\", value: 60, weight: 2, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ4์กฐ}\n", "- {id: Q03, name: ๋ถ€ํ’ˆ ๋Œ€๊ธฐ 8์ผ ์ดˆ๊ณผ, field: parts_wait_days, op: \">\", value: 8, weight: 2, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ5์กฐ}\n", "- {id: Q04, name: ์ •๋น„๋ณ‘ ๋ถ€์กฑ ๋Œ€๊ธฐ 8์ผ ์ดˆ๊ณผ, field: labor_wait_days, op: \">\", value: 8, weight: 2, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ6์กฐ}\n", "- {id: Q05, name: ์™ธ์ฃผ ์ •๋น„ 15์ผ ์ดˆ๊ณผ, field: outsource_days, op: \">\", value: 15, weight: 2, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ7์กฐ}\n", "- {id: Q06, name: ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋Œ€๊ธฐ 6์ผ ์ดˆ๊ณผ, field: inspect_wait_days, op: \">\", value: 6, weight: 3, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ8์กฐ}\n", "- {id: Q07, name: ์žฌ์ž‘์—… 1๊ฑด ์ด์ƒ, field: rework_count, op: \">=\", value: 1, weight: 3, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ9์กฐ}\n", "- {id: Q08, name: ์žฌ์ž‘์—…๋ฅ  25% ์ดˆ๊ณผ, field: rework_rate, op: \">\", value: 0.25, weight: 2, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ9์กฐ}\n", "- {id: Q09, name: ๊ทœ๊ฒฉยท์ œ์› ๋ถˆ์ผ์น˜ 3๊ฑด ์ด์ƒ, field: spec_mismatch, op: \">=\", value: 3, weight: 3, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ10์กฐ}\n", "- {id: Q10, name: ํ•˜์žยท๋ถˆ๋งŒ 3๊ฑด ์ด์ƒ, field: defect_reports, op: \">=\", value: 3, weight: 2, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ11์กฐ}\n", "- {id: Q11, name: ์žฅ๋น„ ์—ฐ๋ น 15๋…„ ์ดˆ๊ณผ, field: age_years, op: \">\", value: 15, weight: 1, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ12์กฐ}\n", "- {id: Q12, name: ์ •๋น„ ์š”์ฒญ 6๊ฑด ์ด์ƒ, field: maint_orders, op: \">=\", value: 6, weight: 1, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ13์กฐ}\n", "- {id: Q13, name: ๋Œ€๊ธฐ ์ผ์ˆ˜ ํ•ฉ๊ณ„ 30์ผ ์ดˆ๊ณผ, field: total_wait, op: \">\", value: 30, weight: 3, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ14์กฐ}\n", "- {id: Q14, name: ๊ฒ€์‚ฌ ๋Œ€๊ธฐ๊ฐ€ ์™ธ์ฃผ ์ •๋น„ ์ผ์ˆ˜์˜ 80% ์ดˆ๊ณผ, field: insp_ratio, op: \">\", value: 0.8, weight: 2, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ8์กฐ}\n", "- {id: Q15, name: ๊ทœ๊ฒฉ ๋ถˆ์ผ์น˜ 2๊ฑด ์ด์ƒ๊ณผ ํ•˜์ž ์ ‘์ˆ˜ ๋™๋ฐ˜, field: spec_defect, op: \"==\", value: true, weight: 3, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ10์กฐ}\n", "- {id: Q16, name: ์ตœ๊ทผ 90์ผ ๊ฐ€๋™ ์‹œ๊ฐ„ 150์‹œ๊ฐ„ ๋ฏธ๋งŒ, field: op_hours, op: \"<\", value: 150, weight: 1, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ15์กฐ}\n", "\"\"\"\n", "GRADE_CUT = [(10, \"๊ณ ์œ„ํ—˜\"), (6, \"์ฃผ์˜\"), (3, \"๊ด€์‹ฌ\")]\n", "OPS = {\"<\": lambda a, b: a < b, \">\": lambda a, b: a > b, \"<=\": lambda a, b: a <= b, \">=\": lambda a, b: a >= b, \"==\": lambda a, b: a == b}\n", "\n", "\n", "def parse_rules(text):\n", " \"\"\"YAML ํ•œ ์ค„ ๋งต({key: value, ...}) ํ˜•์‹ ๋ฃฐ ๋ฌธ์ž์—ด์„ ์ฝ๋Š”๋‹ค(์™ธ๋ถ€ yaml ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—†์ด).\"\"\"\n", " out = []\n", " for line in text.strip().splitlines():\n", " body = line.strip()[3:-1]\n", " r = {}\n", " for part in body.split(\", \"):\n", " k, v = part.split(\": \", 1)\n", " v = v.strip().strip('\"')\n", " r[k] = True if v == \"true\" else False if v == \"false\" else (float(v) if v.replace(\".\", \"\", 1).isdigit() else v)\n", " out.append(r)\n", " return out\n", "\n", "\n", "def derive_rec(rec):\n", " \"\"\"๋ฃฐ์ด ์“ฐ๋Š” ํŒŒ์ƒ ํ•„๋“œ(๋Œ€๊ธฐ ํ•ฉ๊ณ„, ์žฌ์ž‘์—…๋ฅ , ๊ฒ€์‚ฌ ๋Œ€๊ธฐ ๋น„์œจ(์™ธ์ฃผ 5์ผ ์ด์ƒ์ผ ๋•Œ), ๊ทœ๊ฒฉ ๋ถˆ์ผ์น˜ 2๊ฑด ์ด์ƒ+ํ•˜์ž ๋™๋ฐ˜)๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.\"\"\"\n", " r = dict(rec)\n", " r[\"total_wait\"] = r[\"parts_wait_days\"] + r[\"labor_wait_days\"] + r[\"outsource_days\"] + r[\"inspect_wait_days\"]\n", " r[\"rework_rate\"] = r[\"rework_count\"] / r[\"maint_orders\"] if r[\"maint_orders\"] else 0.0\n", " r[\"insp_ratio\"] = r[\"inspect_wait_days\"] / r[\"outsource_days\"] if r[\"outsource_days\"] >= 5 else 0.0\n", " r[\"spec_defect\"] = bool(r[\"spec_mismatch\"] >= 2 and r[\"defect_reports\"] >= 1)\n", " return r\n", "\n", "\n", "def apply_rules(rec, rules, derived=False):\n", " \"\"\"๋ฃฐ ์ ์šฉ: (์ ์ˆ˜, ์ ์ค‘ ๋ฃฐ id ๋ชฉ๋ก). derived=True๋ฉด ์ด๋ฏธ ํŒŒ์ƒ ํ•„๋“œ๊ฐ€ ์žˆ๋Š” ๋ ˆ์ฝ”๋“œ(๊ฒฝ๊ณ„ ์‹œํ—˜์šฉ)๋กœ ๋ณธ๋‹ค.\"\"\"\n", " r = rec if derived else derive_rec(rec)\n", " hits = [x[\"id\"] for x in rules if OPS[x[\"op\"]](r[x[\"field\"]], x[\"value\"])]\n", " w = {x[\"id\"]: x[\"weight\"] for x in rules}\n", " return sum(w[h] for h in hits), hits\n", "\n", "\n", "def grade_of(score):\n", " for cut, g in GRADE_CUT:\n", " if score >= cut:\n", " return g\n", " return \"์ •์ƒ\"\n", "\n", "# ---- ๊ฐ€๋™๋ฅ  ํšŒ๊ท€ ๋ชจ๋ธ๊ณผ ์ €๊ฐ€๋™ ์š”์ธ ๋ถ„๋ฅ˜ยท์ถ”์ฒœ ๋ชจ๋ธ(์™ธ๋ถ€ AI ์„œ๋น„์Šค ์—†์ด scikit-learn ์‚ฌ์šฉ) ----\n", "from sklearn.linear_model import Ridge, LogisticRegression\n", "from sklearn.preprocessing import StandardScaler\n", "from sklearn.pipeline import make_pipeline\n", "\n", "CAUSE_COLS = [\"age_years\", \"maint_orders\", \"parts_stock_pct\", \"mechanic_fill_pct\", \"outsource_days\", \"inspect_wait_days\", \"rework_count\", \"spec_mismatch\",\n", " \"defect_reports\", \"parts_wait_days\", \"labor_wait_days\"]\n", "ACTION = {\"๋ถ€ํ’ˆ ๋Œ€๊ธฐ\": \"๋ถ€ํ’ˆ ์‚ฌ์ „ ์ฒญ๊ตฌยท์žฌ๊ณ  ๋ณด๊ฐ•\", \"์ •๋น„๋ณ‘ ๋ถ€์กฑ\": \"์ •๋น„๋ณ‘ ์žฌ๋ฐฐ์น˜ยท๊ต์œก ์ผ์ • ์กฐ์ •\", \"์™ธ์ฃผ ์ •๋น„\": \"์™ธ์ฃผ ์ •๋น„ ์ผ์ • ์กฐ์œจยท๋‚ด๋ถ€ ์ •๋น„ ํ™•๋Œ€\", \"๊ฒ€์‚ฌยท์žฌ์ž‘์—…\": \"๊ฒ€์‚ฌ ์ผ์ • ์กฐ์œจยท๊ทœ๊ฒฉ ์‚ฌ์ „ ๋Œ€์กฐ\"}\n", "\n", "\n", "def cause_frame(df):\n", " x = df[CAUSE_COLS].copy()\n", " x[\"out_insp\"] = x[\"outsource_days\"] + x[\"inspect_wait_days\"] + 4 * x[\"rework_count\"]\n", " x[\"wait_pm\"] = (x[\"parts_wait_days\"] + x[\"labor_wait_days\"]) / (x[\"maint_orders\"] + 1)\n", " return x\n", "\n", "\n", "def fit_avail(df, alpha=1.0):\n", " \"\"\"๊ฐ€๋™๋ฅ (%) ํšŒ๊ท€: ํ‘œ์ค€ํ™” + ๋ฆฟ์ง€ ํšŒ๊ท€.\"\"\"\n", " return make_pipeline(StandardScaler(), Ridge(alpha=alpha)).fit(feat_frame(df), df[\"avail_pct\"])\n", "\n", "\n", "def fit_cause(df, cut=85, C=5):\n", " \"\"\"๊ฐ€๋™๋ฅ ์ด cut% ๋ฏธ๋งŒ์ธ ์žฅ๋น„์˜ ์ฃผ์š” ์ €๊ฐ€๋™ ์š”์ธ(4์ข…)์„ ๋ถ„๋ฅ˜ํ•œ๋‹ค.\"\"\"\n", " low = df[(df[\"avail_pct\"] < cut) & (df[\"main_cause\"] != \"์—†์Œ\")]\n", " return make_pipeline(StandardScaler(), LogisticRegression(C=C, max_iter=3000)).fit(cause_frame(low), low[\"main_cause\"])\n", "\n", "\n", "SIM_FEAT = [\"age_years\", \"maint_orders\", \"spec_mismatch\", \"stock_gap_x_orders\", \"fill_gap_x_orders\", \"parts_stock_pct\", \"mechanic_fill_pct\", \"etype_B\", \"etype_C\"]\n", "\n", "\n", "def sim_frame(df):\n", " \"\"\"์ž์› ์žฌ๋ฐฐ์น˜ ์‹œ๋ฎฌ๋ ˆ์ด์…˜์šฉ ์„ค๋ช… ๋ณ€์ˆ˜: ๋ถ€์กฑ๋ถ„์ด ํด์ˆ˜๋ก ๋Œ€๊ธฐ๊ฐ€ ๋” ๋น ๋ฅด๊ฒŒ ๋Š˜๊ณ  ์ •๋น„ ์š”์ฒญ ๊ฑด์ˆ˜์™€ ๊ณฑํ•ด์ ธ ์ปค์ง„๋‹ค๋Š” ๊ตฌ์กฐ๋ฅผ ์ œ๊ณฑยท์ƒํ˜ธ์ž‘์šฉ ํ•ญ์œผ๋กœ ๋‘”๋‹ค.\"\"\"\n", " x = df.copy()\n", " x[\"etype_B\"], x[\"etype_C\"] = (x[\"etype\"] == \"์œ ํ˜•B\").astype(int), (x[\"etype\"] == \"์œ ํ˜•C\").astype(int)\n", " x[\"stock_gap_x_orders\"] = x[\"maint_orders\"] * ((100 - x[\"parts_stock_pct\"]) / 28) ** 2\n", " x[\"fill_gap_x_orders\"] = x[\"maint_orders\"] * ((100 - x[\"mechanic_fill_pct\"]) / 24) ** 2\n", " return x[SIM_FEAT]\n", "\n", "\n", "def fit_sim(df, alpha=0.5):\n", " return make_pipeline(StandardScaler(), Ridge(alpha=alpha)).fit(sim_frame(df), df[\"avail_pct\"])\n", "\n", "# ---- ์—ญํ• ๊ณผ ๊ถŒํ•œ(์ตœ์†Œ ๊ถŒํ•œ ์›์น™): ์‹œ์Šคํ…œ์€ '์ œ์•ˆ'๋งŒ ํ•˜๊ณ  ํ™•์ • ๊ถŒํ•œ์ด ์—†๋‹ค ----\n", "PERM = {\"์‹œ์Šคํ…œ\": {\"propose\"}, \"์ •๋น„๋‹ด๋‹น\": {\"submit\", \"redraft\", \"view\"}, \"์ •๋น„๊ด€\": {\"review_maint\", \"reject\", \"view\"}, \"ํ’ˆ์งˆ๊ฒ€์‚ฌ๊ด€\": {\"review_quality\", \"reject\", \"view\"},\n", " \"๊ตฐ์ˆ˜์ฐธ๋ชจ\": {\"approve\", \"reject\", \"view\"}, \"๊ฐ์‚ฌ๊ด€\": {\"view\", \"audit\"}, \"๋ถ€์„œ์žฅ\": {\"submit\", \"approve\", \"reject\", \"view\"}} # ๋ถ€์„œ์žฅ์€ ์ƒ์‹ ยท๊ฒฐ์žฌ๋ฅผ ๊ฒธํ•  ์ˆ˜ ์žˆ์œผ๋‚˜ ๊ฐ™์€ ๊ฑด์€ ๋ถ„๋ฆฌ\n", "NEXT = {\"์ œ์•ˆ\": (\"submit\", \"์ƒ์‹ \"), \"์ƒ์‹ \": (\"review_maint\", \"์ •๋น„๊ด€๊ฒ€ํ† \"), \"์ •๋น„๊ด€๊ฒ€ํ† \": (\"review_quality\", \"ํ’ˆ์งˆ๊ฒ€์‚ฌ\"), \"ํ’ˆ์งˆ๊ฒ€์‚ฌ\": (\"approve\", \"์Šน์ธ\")}\n", "USERS = {\"sys\": \"์‹œ์Šคํ…œ\", \"kim\": \"์ •๋น„๋‹ด๋‹น\", \"lee\": \"์ •๋น„๊ด€\", \"park\": \"ํ’ˆ์งˆ๊ฒ€์‚ฌ๊ด€\", \"choi\": \"๊ตฐ์ˆ˜์ฐธ๋ชจ\", \"jung\": \"๊ฐ์‚ฌ๊ด€\", \"oh\": \"๋ถ€์„œ์žฅ\"}\n", "MAX_REDRAFT = 2\n", "\n", "\n", "class Chain:\n", " \"\"\"ํ•ด์‹œ ์ฒด์ธ ๊ฐ์‚ฌ ๋กœ๊ทธ: ๊ฐ ๊ธฐ๋ก์ด ์ง์ „ ๊ธฐ๋ก์˜ ํ•ด์‹œ๋ฅผ ํฌํ•จํ•ด ์ค‘๊ฐ„ ์ˆ˜์ •์ด ๋“œ๋Ÿฌ๋‚œ๋‹ค.\"\"\"\n", " def __init__(self):\n", " self.log = []\n", "\n", " def add(self, **body):\n", " prev = self.log[-1][\"hash\"] if self.log else \"GENESIS\"\n", " body = {\"seq\": len(self.log), **body}\n", " self.log.append({**body, \"prev\": prev, \"hash\": hashlib.sha256((prev + json.dumps(body, sort_keys=True, ensure_ascii=False)).encode()).hexdigest()})\n", "\n", " def verify(self):\n", " prev = \"GENESIS\"\n", " for e in self.log:\n", " b = {k: v for k, v in e.items() if k not in (\"prev\", \"hash\")}\n", " if e[\"prev\"] != prev or hashlib.sha256((prev + json.dumps(b, sort_keys=True, ensure_ascii=False)).encode()).hexdigest() != e[\"hash\"]:\n", " return e[\"seq\"]\n", " prev = e[\"hash\"]\n", " return None\n", "\n", "\n", "class Workflow:\n", " def __init__(self):\n", " self.audit, self.cases, self.blocked = Chain(), {}, []\n", "\n", " def _deny(self, who, case, action, why):\n", " self.audit.add(event=\"DENIED\", who=who, case=case, action=action, reason=why)\n", " self.blocked.append((who, case, action, why)); return False, why\n", "\n", " def act(self, who, case, action, reason=\"\", evidence=\"\"):\n", " role = USERS.get(who)\n", " if role is None:\n", " return self._deny(who, case, action, \"๋ฏธ๋“ฑ๋ก ์‚ฌ์šฉ์ž\")\n", " if action == \"delete_log\":\n", " return self._deny(who, case, action, \"๊ฐ์‚ฌ ๋กœ๊ทธ ์‚ญ์ œยท์ˆ˜์ • ๊ถŒํ•œ์€ ๋ˆ„๊ตฌ์—๊ฒŒ๋„ ์—†์Œ\")\n", " if action not in PERM[role]:\n", " return self._deny(who, case, action, f\"๊ถŒํ•œ ์—†์Œ({role} ์—ญํ• ์— {action} ๊ถŒํ•œ ์—†์Œ)\")\n", " if action == \"propose\":\n", " self.cases[case] = {\"state\": \"์ œ์•ˆ\", \"submitter\": None, \"version\": 1}\n", " self.audit.add(event=\"PROPOSE\", who=who, case=case, reason=reason); return True, \"์ œ์•ˆ ๋“ฑ๋ก\"\n", " c = self.cases.get(case)\n", " if c is None:\n", " return self._deny(who, case, action, \"์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฑด\")\n", " if c[\"state\"] in (\"์Šน์ธ\", \"์ข…๊ฒฐ(๋ฐ˜๋ ค)\"):\n", " return self._deny(who, case, action, f\"์ด๋ฏธ ์ข…๊ฒฐ๋œ ๊ฑด({c['state']})\")\n", " if not reason.strip() or not evidence.strip():\n", " return self._deny(who, case, action, \"์‚ฌ์œ ยท๊ทผ๊ฑฐ ์ž…๋ ฅ ํ•„์ˆ˜(๋ฐ˜๋ ค ์‚ฌ์œ  ํฌํ•จ)\")\n", " if action == \"reject\":\n", " if c[\"state\"] in (\"์ œ์•ˆ\", \"๋ฐ˜๋ ค\"):\n", " return self._deny(who, case, action, f\"๋ฐ˜๋ คํ•  ์ˆ˜ ์—†๋Š” ๋‹จ๊ณ„({c['state']})\")\n", " c[\"state\"] = \"์ข…๊ฒฐ(๋ฐ˜๋ ค)\" if c[\"version\"] > MAX_REDRAFT else \"๋ฐ˜๋ ค\"\n", " self.audit.add(event=\"REJECTED\", who=who, role=role, case=case, version=c[\"version\"], reason=reason, evidence=evidence); return True, c[\"state\"]\n", " if action == \"redraft\":\n", " if c[\"state\"] != \"๋ฐ˜๋ ค\":\n", " return self._deny(who, case, action, f\"์žฌ๊ธฐ์•ˆ์€ ๋ฐ˜๋ ค๋œ ๊ฑด๋งŒ ๊ฐ€๋Šฅ(ํ˜„์žฌ {c['state']})\")\n", " if who != c[\"submitter\"]:\n", " return self._deny(who, case, action, \"์žฌ๊ธฐ์•ˆ์€ ์ตœ์ดˆ ์ƒ์‹ ์ž๋งŒ ๊ฐ€๋Šฅ\")\n", " c[\"state\"], c[\"version\"] = \"์ƒ์‹ \", c[\"version\"] + 1\n", " self.audit.add(event=\"REDRAFT\", who=who, role=role, case=case, version=c[\"version\"], reason=reason, evidence=evidence); return True, \"์žฌ๊ธฐ์•ˆ ์ƒ์‹ \"\n", " if c[\"state\"] == \"๋ฐ˜๋ ค\":\n", " return self._deny(who, case, action, \"๋ฐ˜๋ ค๋œ ๊ฑด์€ ์žฌ๊ธฐ์•ˆ ํ›„์—๋งŒ ์ง„ํ–‰ ๊ฐ€๋Šฅ\")\n", " need, nxt = NEXT[c[\"state\"]]\n", " if action != need:\n", " return self._deny(who, case, action, f\"๋‹จ๊ณ„ ์œ„๋ฐ˜(ํ˜„์žฌ {c['state']}, ํ•„์š” ํ–‰์œ„ {need})\")\n", " if action == \"submit\":\n", " c[\"submitter\"] = who\n", " if action == \"approve\" and who == c[\"submitter\"]:\n", " return self._deny(who, case, action, \"์ƒ์‹ ์ž์™€ ์Šน์ธ์ž ๋ถ„๋ฆฌ ์›์น™ ์œ„๋ฐ˜\")\n", " c[\"state\"] = nxt\n", " self.audit.add(event=action.upper(), who=who, role=role, case=case, version=c[\"version\"], to=nxt, reason=reason, evidence=evidence); return True, nxt\n", "\n", "\n", "# ---- ์ƒ์‹  ๋Œ€์ƒ: ๋ฃฐ ์ ์ˆ˜๊ฐ€ ๋†’์€ ์žฅ๋น„ 12๋Œ€์™€ ๋ชจ๋ธ์ด ์ถ”์ฒœํ•œ ๊ฐœ์„  ์กฐ์น˜ ----\n", "fleet = make_fleet(300, seed=7)\n", "rules = parse_rules(RULES_YAML)\n", "sc = [apply_rules(r, rules) for r in fleet.drop(columns=[\"main_cause\"]).to_dict(\"records\")]\n", "fleet[\"score\"], fleet[\"hits\"] = [x[0] for x in sc], [x[1] for x in sc]\n", "clf = fit_cause(make_fleet(1200, seed=21))\n", "picked = fleet.sort_values(\"score\", ascending=False).head(12)\n", "rec_cause = clf.predict(cause_frame(picked))\n", "rng = np.random.default_rng(1)\n", "QUALITY = {\"Q06\", \"Q07\", \"Q08\", \"Q09\", \"Q10\", \"Q14\", \"Q15\"} # ํ’ˆ์งˆ๊ฒ€์‚ฌ๊ด€์ด ๋ณด๋Š” ํ•ญ๋ชฉ(๊ฒ€์‚ฌ ๋Œ€๊ธฐยท์žฌ์ž‘์—…ยท๊ทœ๊ฒฉ ๋ถˆ์ผ์น˜ยทํ•˜์ž)\n", "\n", "wf = Workflow()\n", "rows = []\n", "for k, ((_, p), cause) in enumerate(zip(picked.iterrows(), rec_cause), 1):\n", " case = f\"C{k:02d}\"\n", " wf.act(\"sys\", case, \"propose\", reason=f\"{p['equip_id']} ๋ฃฐ ์ ์ˆ˜ {p['score']:.0f}, ์ถ”์ฒœ ์š”์ธ {cause} -> {ACTION[cause]}\")\n", " wf.act(\"kim\", case, \"submit\", \"์ •๋น„ ์ž์› ์šฐ์„  ํˆฌ์ž… ๊ฒ€ํ†  ์š”์ฒญ\", f\"๋ฃฐ ์ ์ค‘ {','.join(p['hits'][:4])}, ์ถ”์ฒœ ์กฐ์น˜ {ACTION[cause]}\")\n", " q_issue = sorted(QUALITY & set(p[\"hits\"])) if len(QUALITY & set(p[\"hits\"])) >= 2 else [] # ํ’ˆ์งˆ๊ฒ€์‚ฌ๊ด€์ด ๋ณผ ๋ฌธ์ œ\n", " m_issue = (p[\"avail_pct\"] < 40 and cause == \"์ •๋น„๋ณ‘ ๋ถ€์กฑ\") # ์ •๋น„๊ด€์ด ๋ณผ ๋ฌธ์ œ: ์ธ๋ ฅ ์ด๋™ ์‹œ ์ œ๊ณต ๋ถ€๋Œ€ ์˜ํ–ฅ ์†Œ๋ช… ํ•„์š”\n", " note = \"๊ตฐ์ˆ˜์ฐธ๋ชจ ๊ฒฐ์ •\"\n", " for _ in range(14):\n", " state = wf.cases[case][\"state\"]\n", " if state in (\"์Šน์ธ\", \"์ข…๊ฒฐ(๋ฐ˜๋ ค)\"):\n", " break\n", " if state == \"์ƒ์‹ \":\n", " if m_issue:\n", " wf.act(\"lee\", case, \"reject\", \"์ธ๋ ฅ ์ด๋™ ์•ˆ์€ ์ œ๊ณต ๋ถ€๋Œ€ ์˜ํ–ฅ ์†Œ๋ช… ํ•„์š”\", f\"๊ฐ€๋™๋ฅ  {p['avail_pct']}%, ์š”์ธ {cause}\")\n", " else:\n", " wf.act(\"lee\", case, \"review_maint\", \"์ •๋น„ ๋Šฅ๋ ฅยท์ผ์ • ๊ฒ€ํ†  ์™„๋ฃŒ\", f\"์ถ”์ฒœ ์กฐ์น˜ {ACTION[cause]} ์ˆ˜ํ–‰ ๊ฐ€๋Šฅ\")\n", " elif state == \"์ •๋น„๊ด€๊ฒ€ํ† \":\n", " if rng.random() < 0.15:\n", " note = \"์ž๋™ ํ™•์ • ์—†์Œ: ํ’ˆ์งˆ๊ฒ€์‚ฌ ์‘๋‹ต ๋Œ€๊ธฐ\"; break # ๋ฌด์‘๋‹ต์€ ๋ณด๋ฅ˜๋กœ ๋‘”๋‹ค\n", " if q_issue:\n", " wf.act(\"park\", case, \"reject\", f\"๊ทœ๊ฒฉยท๊ฒ€์‚ฌ ์กฐ๊ฑด ๋ณด์™„ ํ•„์š”({','.join(q_issue)})\", \"์žฌ์ž‘์—…ยท๊ทœ๊ฒฉ ๋ถˆ์ผ์น˜ยท๊ฒ€์‚ฌ ๋Œ€๊ธฐ ํ•ญ๋ชฉ ์ ๊ฒ€\")\n", " else:\n", " wf.act(\"park\", case, \"review_quality\", \"ํ’ˆ์งˆ๊ฒ€์‚ฌ ๊ฒ€ํ†  ์™„๋ฃŒ\", \"๊ทœ๊ฒฉยท๊ฒ€์‚ฌ ํ•ญ๋ชฉ ์ด์ƒ ์—†์Œ ๋˜๋Š” ๋ณด์™„ ํ™•์ธ\")\n", " elif state == \"ํ’ˆ์งˆ๊ฒ€์‚ฌ\":\n", " wf.act(\"choi\", case, \"approve\", \"์Šน์ธ: ์ž์› ์šฐ์„  ํˆฌ์ž… ์ง„ํ–‰\", f\"๋ฃฐ ์ ์ˆ˜ {p['score']:.0f}, ๊ฒ€ํ†  ๋‹จ๊ณ„ ์™„๋ฃŒ\")\n", " elif state == \"๋ฐ˜๋ ค\":\n", " wf.act(\"kim\", case, \"redraft\", \"๋ฐ˜๋ ค ์‚ฌ์œ  ๋ฐ˜์˜ํ•ด ์ˆ˜์ •\", f\"๋ณด์™„ ๋Œ€์ƒ {q_issue or ['์ œ๊ณต ๋ถ€๋Œ€ ์˜ํ–ฅ ์†Œ๋ช…']}, ์žฌ๊ธฐ์•ˆ {wf.cases[case]['version']}์ฐจ\")\n", " m_issue = False # ์ œ๊ณต ๋ถ€๋Œ€ ์˜ํ–ฅ ์†Œ๋ช…์„œ๋ฅผ ์ฒจ๋ถ€(๋ชจ์˜)\n", " if rng.random() < 0.65: # ์žฌ๊ธฐ์•ˆ ํ›„ ๋ณด์™„์ด ๋๋‚  ํ™•๋ฅ (๋ชจ์˜)\n", " q_issue = []\n", " final = wf.cases[case][\"state\"]\n", " if final not in (\"์Šน์ธ\", \"์ข…๊ฒฐ(๋ฐ˜๋ ค)\") and note.startswith(\"์ž๋™\"):\n", " final = \"๋ณด๋ฅ˜(๋ฌด์‘๋‹ต)\"\n", " elif final == \"์ข…๊ฒฐ(๋ฐ˜๋ ค)\":\n", " note = \"์žฌ๊ธฐ์•ˆ ํ•œ๋„(2ํšŒ) ์ดˆ๊ณผ\"\n", " rows.append((case, p[\"equip_id\"], final, wf.cases[case][\"version\"], note))\n", "print(\"[๊ฒฐ์žฌ ๊ฒฐ๊ณผ] 12๊ฑด (๋ชจ์˜ ๊ฒฐ์žฌ์ž ๊ธฐ์ค€, ์‹ค์ œ ๊ฒฐ์žฌ๋Š” ์‚ฌ๋žŒ์ด ํ•œ๋‹ค)\")\n", "print(pd.DataFrame(rows, columns=[\"๊ฑด\", \"์žฅ๋น„\", \"์ตœ์ข… ์ƒํƒœ\", \"๋ฒ„์ „\", \"๋น„๊ณ \"]).to_string(index=False))\n", "st = pd.Series([r[2] for r in rows]).value_counts().to_dict()\n", "print(\"์ƒํƒœ ์ง‘๊ณ„:\", st, f\"/ ์žฌ๊ธฐ์•ˆ {sum(e['event'] == 'REDRAFT' for e in wf.audit.log)}ํšŒ, ๋ฐ˜๋ ค ์ด๋ ฅ {sum(e['event'] == 'REJECTED' for e in wf.audit.log)}๊ฑด(๋ชจ๋‘ ์‚ฌ์œ ยท๊ทผ๊ฑฐ ๊ธฐ๋ก)\")\n", "\n", "# ---- ๊ถŒํ•œ ์šฐํšŒยท์ ˆ์ฐจ ์œ„๋ฐ˜ ์‹œ๋„ ----\n", "wf.act(\"sys\", \"T01\", \"propose\", reason=\"์‹œํ—˜ ๊ฑด\"); wf.act(\"kim\", \"T01\", \"submit\", \"์‹œํ—˜ ์ƒ์‹ \", \"๊ทผ๊ฑฐ\")\n", "attempts = [\n", " (\"์‹œ์Šคํ…œ ์ž๋™ ์Šน์ธ\", lambda: wf.act(\"sys\", \"T01\", \"approve\", \"์ž๋™\", \"์ž๋™\")),\n", " (\"์ •๋น„๋‹ด๋‹น์ด ์ •๋น„๊ด€ ๊ฒ€ํ†  ๋Œ€ํ–‰\", lambda: wf.act(\"kim\", \"T01\", \"review_maint\", \"๋Œ€ํ–‰\", \"๊ทผ๊ฑฐ\")),\n", " (\"๊ฒ€ํ†  ๋‹จ๊ณ„ ๊ฑด๋„ˆ๋›ฐ๊ณ  ์Šน์ธ\", lambda: wf.act(\"choi\", \"T01\", \"approve\", \"๋ฐ”๋กœ ์Šน์ธ\", \"๊ทผ๊ฑฐ\")),\n", " (\"๋ฏธ๋“ฑ๋ก ์‚ฌ์šฉ์ž ์ ‘๊ทผ\", lambda: wf.act(\"guest\", \"T01\", \"review_maint\", \"์—ด๋žŒ\", \"๊ทผ๊ฑฐ\")),\n", " (\"์‚ฌ์œ  ์—†๋Š” ๊ฒ€ํ† \", lambda: wf.act(\"lee\", \"T01\", \"review_maint\", \"\", \"\")),\n", " (\"์‚ฌ์œ  ์—†๋Š” ๋ฐ˜๋ ค\", lambda: wf.act(\"lee\", \"T01\", \"reject\", \"\", \"๊ทผ๊ฑฐ\")),\n", " (\"๊ฐ์‚ฌ ๋กœ๊ทธ ์‚ญ์ œ\", lambda: wf.act(\"kim\", \"T01\", \"delete_log\", \"์ •๋ฆฌ\", \"๊ทผ๊ฑฐ\")),\n", " (\"๊ฐ์‚ฌ๊ด€์ด ์Šน์ธ ์‹œ๋„\", lambda: wf.act(\"jung\", \"T01\", \"approve\", \"์Šน์ธ\", \"๊ทผ๊ฑฐ\")),\n", " (\"์ข…๊ฒฐ๋œ ๊ฑด ์žฌ์Šน์ธ\", lambda: wf.act(\"choi\", next(c for c, v in wf.cases.items() if v[\"state\"] == \"์Šน์ธ\"), \"approve\", \"์žฌ์Šน์ธ\", \"๊ทผ๊ฑฐ\")),\n", " (\"์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฑด ์Šน์ธ\", lambda: wf.act(\"choi\", \"X99\", \"approve\", \"์Šน์ธ\", \"๊ทผ๊ฑฐ\")),\n", "]\n", "wf.act(\"sys\", \"T02\", \"propose\", reason=\"์‹œํ—˜\"); wf.act(\"oh\", \"T02\", \"submit\", \"์ƒ์‹ \", \"๊ทผ๊ฑฐ\")\n", "wf.act(\"lee\", \"T02\", \"review_maint\", \"์ •๋น„ ๊ฒ€ํ† \", \"์ด์ƒ ์—†์Œ\"); wf.act(\"park\", \"T02\", \"review_quality\", \"ํ’ˆ์งˆ ๊ฒ€ํ† \", \"์ด์ƒ ์—†์Œ\")\n", "attempts.append((\"์ƒ์‹ ์ž๊ฐ€ ๋ณธ์ธ ๊ฑด ์Šน์ธ(๋ถ„๋ฆฌ ์›์น™)\", lambda: wf.act(\"oh\", \"T02\", \"approve\", \"๋ณธ์ธ ์Šน์ธ\", \"๊ทผ๊ฑฐ\")))\n", "wf.act(\"sys\", \"T03\", \"propose\", reason=\"์‹œํ—˜\"); wf.act(\"kim\", \"T03\", \"submit\", \"์ƒ์‹ \", \"๊ทผ๊ฑฐ\"); wf.act(\"lee\", \"T03\", \"reject\", \"์ˆ˜์ • ํ•„์š”\", \"๊ทผ๊ฑฐ\")\n", "attempts.append((\"๋ฐ˜๋ ค ๊ฑด์„ ์žฌ๊ธฐ์•ˆ ์—†์ด ๊ฒ€ํ†  ์ง„ํ–‰\", lambda: wf.act(\"lee\", \"T03\", \"review_maint\", \"๊ทธ๋Œ€๋กœ ์ง„ํ–‰\", \"๊ทผ๊ฑฐ\")))\n", "attempts.append((\"ํƒ€์ธ์ด ๋Œ€์‹  ์žฌ๊ธฐ์•ˆ\", lambda: wf.act(\"oh\", \"T03\", \"redraft\", \"๋Œ€์‹  ์ˆ˜์ •\", \"๊ทผ๊ฑฐ\")))\n", "print(\"\\n[๊ถŒํ•œ ์šฐํšŒยท์ ˆ์ฐจ ์œ„๋ฐ˜ ์‹œ๋„]\")\n", "blocked = 0\n", "for name, fn in attempts:\n", " ok, why = fn(); blocked += (not ok)\n", " print(f\" {name:22s}-> {'์ฐจ๋‹จ' if not ok else 'ํ†ต๊ณผ(๊ฒฐํ•จ)'}: {why}\")\n", "print(f\"์šฐํšŒยท์œ„๋ฐ˜ ์‹œ๋„ ์ฐจ๋‹จ {blocked}/{len(attempts)} ({blocked / len(attempts):.0%}) (๋ชฉํ‘œ 100%: {'๋‹ฌ์„ฑ' if blocked == len(attempts) else '๋ฏธ๋‹ฌ'})\")\n", "\n", "# ---- RBAC ๋งคํŠธ๋ฆญ์Šค์™€ ์ด๋ ฅ ๋ฌด๊ฒฐ์„ฑ ----\n", "acts = [\"propose\", \"submit\", \"review_maint\", \"review_quality\", \"approve\", \"reject\", \"redraft\", \"audit\"]\n", "mx = pd.DataFrame([[(\"O\" if a in PERM[r] else \"-\") for a in acts] for r in PERM], index=list(PERM), columns=[\"์ œ์•ˆ\", \"์ƒ์‹ \", \"์ •๋น„๊ฒ€ํ† \", \"ํ’ˆ์งˆ๊ฒ€ํ† \", \"์Šน์ธ\", \"๋ฐ˜๋ ค\", \"์žฌ๊ธฐ์•ˆ\", \"๊ฐ์‚ฌ\"])\n", "print(\"\\n[์—ญํ• ๋ณ„ ๊ถŒํ•œ ๋งคํŠธ๋ฆญ์Šค(O ํ—ˆ์šฉ / - ๋ถˆ๊ฐ€)]\"); print(mx.to_string())\n", "bad = wf.audit.verify()\n", "print(f\"\\n๊ฐ์‚ฌ ๋กœ๊ทธ {len(wf.audit.log)}๊ฑด, ํ•ด์‹œ ์ฒด์ธ ๊ฒ€์ฆ: {'์ •์ƒ' if bad is None else f'seq {bad} ๋ถˆ์ผ์น˜'} -> ์ด๋ ฅ ๋ฌด๊ฒฐ์„ฑ {'100%' if bad is None else 'ํ›ผ์†'}(๋ชฉํ‘œ 100%: {'๋‹ฌ์„ฑ' if bad is None else '๋ฏธ๋‹ฌ'})\")\n", "idx = 10; saved = json.dumps(wf.audit.log[idx], ensure_ascii=False)\n", "wf.audit.log[idx][\"reason\"] = \"์‚ฌํ›„ ์ˆ˜์ •\"; v = wf.audit.verify()\n", "print(f\"{idx}๋ฒˆ์งธ ๊ธฐ๋ก ์ž„์˜ ์ˆ˜์ • ํ›„ ๊ฒ€์ฆ: {'์œ„๋ณ€์กฐ ํƒ์ง€(seq ' + str(v) + ')' if v is not None else 'ํƒ์ง€ ์‹คํŒจ'}\")\n", "wf.audit.log[idx] = json.loads(saved)\n", "print(\"\\n[์ด๋ ฅ ์กฐํšŒ ํ™”๋ฉด] C02 ๊ฑด\")\n", "for e in wf.audit.log:\n", " if e.get(\"case\") == \"C02\":\n", " print(f\" #{e['seq']:03d} {e['event']:14s} {e.get('role', e['who']):6s} v{e.get('version', 1)} | {e.get('reason', '')[:34]}\")\n", "print(\"โ€ป ์ •๋น„ ์šฐ์„ ์ˆœ์œ„์™€ ์ž์› ํˆฌ์ž… ๊ฒฐ์ •์€ ์‚ฌ๋žŒ(์ •๋น„๊ด€ยทํ’ˆ์งˆ๊ฒ€์‚ฌ๊ด€ยท๊ตฐ์ˆ˜์ฐธ๋ชจ)์ด ํ•˜๋ฉฐ, ์‹œ์Šคํ…œ์€ ์œ„ํ—˜ ํ‘œ์‹œ์™€ ์ถ”์ฒœ๋งŒ ์ œ๊ณตํ•œ๋‹ค. ๊ฒฐ์žฌ์„ ์€ ์†Œ์† ๊ธฐ๊ด€ ๊ทœ์ •์„ ๋”ฐ๋ฅธ๋‹ค.\")\n", "\n", "plt.figure(figsize=(6, 3.5)); EN = {\"์Šน์ธ\": \"approved\", \"๋ฐ˜๋ ค\": \"rejected\", \"์ข…๊ฒฐ(๋ฐ˜๋ ค)\": \"closed-rejected\", \"๋ณด๋ฅ˜(๋ฌด์‘๋‹ต)\": \"on hold\"}\n", "plt.bar([EN.get(k, k) for k in st], list(st.values()), color=\"tab:blue\"); plt.title(\"HITL workflow outcomes (12 cases)\"); plt.ylabel(\"cases\"); plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 07 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[๊ฒฐ์žฌ ๊ฒฐ๊ณผ] 12๊ฑด (๋ชจ์˜ ๊ฒฐ์žฌ์ž ๊ธฐ์ค€, ์‹ค์ œ ๊ฒฐ์žฌ๋Š” ์‚ฌ๋žŒ์ด ํ•œ๋‹ค)\n", " ๊ฑด ์žฅ๋น„ ์ตœ์ข… ์ƒํƒœ ๋ฒ„์ „ ๋น„๊ณ \n", "C01 E-0047 ๋ณด๋ฅ˜(๋ฌด์‘๋‹ต) 2 ์ž๋™ ํ™•์ • ์—†์Œ: ํ’ˆ์งˆ๊ฒ€์‚ฌ ์‘๋‹ต ๋Œ€๊ธฐ\n", "C02 E-0231 ์Šน์ธ 2 ๊ตฐ์ˆ˜์ฐธ๋ชจ ๊ฒฐ์ •\n", "C03 E-0041 ์Šน์ธ 2 ๊ตฐ์ˆ˜์ฐธ๋ชจ ๊ฒฐ์ •\n", "C04 E-0050 ๋ณด๋ฅ˜(๋ฌด์‘๋‹ต) 1 ์ž๋™ ํ™•์ • ์—†์Œ: ํ’ˆ์งˆ๊ฒ€์‚ฌ ์‘๋‹ต ๋Œ€๊ธฐ\n", "C05 E-0220 ์Šน์ธ 2 ๊ตฐ์ˆ˜์ฐธ๋ชจ ๊ฒฐ์ •\n", "C06 E-0230 ์Šน์ธ 1 ๊ตฐ์ˆ˜์ฐธ๋ชจ ๊ฒฐ์ •\n", "C07 E-0095 ์Šน์ธ 1 ๊ตฐ์ˆ˜์ฐธ๋ชจ ๊ฒฐ์ •\n", "C08 E-0109 ์Šน์ธ 2 ๊ตฐ์ˆ˜์ฐธ๋ชจ ๊ฒฐ์ •\n", "C09 E-0163 ์Šน์ธ 2 ๊ตฐ์ˆ˜์ฐธ๋ชจ ๊ฒฐ์ •\n", "C10 E-0076 ์Šน์ธ 2 ๊ตฐ์ˆ˜์ฐธ๋ชจ ๊ฒฐ์ •\n", "C11 E-0028 ์Šน์ธ 3 ๊ตฐ์ˆ˜์ฐธ๋ชจ ๊ฒฐ์ •\n", "C12 E-0233 ์Šน์ธ 2 ๊ตฐ์ˆ˜์ฐธ๋ชจ ๊ฒฐ์ •\n", "์ƒํƒœ ์ง‘๊ณ„: {'์Šน์ธ': 10, '๋ณด๋ฅ˜(๋ฌด์‘๋‹ต)': 2} / ์žฌ๊ธฐ์•ˆ 10ํšŒ, ๋ฐ˜๋ ค ์ด๋ ฅ 10๊ฑด(๋ชจ๋‘ ์‚ฌ์œ ยท๊ทผ๊ฑฐ ๊ธฐ๋ก)\n", "\n", "[๊ถŒํ•œ ์šฐํšŒยท์ ˆ์ฐจ ์œ„๋ฐ˜ ์‹œ๋„]\n", " ์‹œ์Šคํ…œ ์ž๋™ ์Šน์ธ -> ์ฐจ๋‹จ: ๊ถŒํ•œ ์—†์Œ(์‹œ์Šคํ…œ ์—ญํ• ์— approve ๊ถŒํ•œ ์—†์Œ)\n", " ์ •๋น„๋‹ด๋‹น์ด ์ •๋น„๊ด€ ๊ฒ€ํ†  ๋Œ€ํ–‰ -> ์ฐจ๋‹จ: ๊ถŒํ•œ ์—†์Œ(์ •๋น„๋‹ด๋‹น ์—ญํ• ์— review_maint ๊ถŒํ•œ ์—†์Œ)\n", " ๊ฒ€ํ†  ๋‹จ๊ณ„ ๊ฑด๋„ˆ๋›ฐ๊ณ  ์Šน์ธ -> ์ฐจ๋‹จ: ๋‹จ๊ณ„ ์œ„๋ฐ˜(ํ˜„์žฌ ์ƒ์‹ , ํ•„์š” ํ–‰์œ„ review_maint)\n", " ๋ฏธ๋“ฑ๋ก ์‚ฌ์šฉ์ž ์ ‘๊ทผ -> ์ฐจ๋‹จ: ๋ฏธ๋“ฑ๋ก ์‚ฌ์šฉ์ž\n", " ์‚ฌ์œ  ์—†๋Š” ๊ฒ€ํ†  -> ์ฐจ๋‹จ: ์‚ฌ์œ ยท๊ทผ๊ฑฐ ์ž…๋ ฅ ํ•„์ˆ˜(๋ฐ˜๋ ค ์‚ฌ์œ  ํฌํ•จ)\n", " ์‚ฌ์œ  ์—†๋Š” ๋ฐ˜๋ ค -> ์ฐจ๋‹จ: ์‚ฌ์œ ยท๊ทผ๊ฑฐ ์ž…๋ ฅ ํ•„์ˆ˜(๋ฐ˜๋ ค ์‚ฌ์œ  ํฌํ•จ)\n", " ๊ฐ์‚ฌ ๋กœ๊ทธ ์‚ญ์ œ -> ์ฐจ๋‹จ: ๊ฐ์‚ฌ ๋กœ๊ทธ ์‚ญ์ œยท์ˆ˜์ • ๊ถŒํ•œ์€ ๋ˆ„๊ตฌ์—๊ฒŒ๋„ ์—†์Œ\n", " ๊ฐ์‚ฌ๊ด€์ด ์Šน์ธ ์‹œ๋„ -> ์ฐจ๋‹จ: ๊ถŒํ•œ ์—†์Œ(๊ฐ์‚ฌ๊ด€ ์—ญํ• ์— approve ๊ถŒํ•œ ์—†์Œ)\n", " ์ข…๊ฒฐ๋œ ๊ฑด ์žฌ์Šน์ธ -> ์ฐจ๋‹จ: ์ด๋ฏธ ์ข…๊ฒฐ๋œ ๊ฑด(์Šน์ธ)\n", " ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฑด ์Šน์ธ -> ์ฐจ๋‹จ: ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฑด\n", " ์ƒ์‹ ์ž๊ฐ€ ๋ณธ์ธ ๊ฑด ์Šน์ธ(๋ถ„๋ฆฌ ์›์น™) -> ์ฐจ๋‹จ: ์ƒ์‹ ์ž์™€ ์Šน์ธ์ž ๋ถ„๋ฆฌ ์›์น™ ์œ„๋ฐ˜\n", " ๋ฐ˜๋ ค ๊ฑด์„ ์žฌ๊ธฐ์•ˆ ์—†์ด ๊ฒ€ํ†  ์ง„ํ–‰ -> ์ฐจ๋‹จ: ๋ฐ˜๋ ค๋œ ๊ฑด์€ ์žฌ๊ธฐ์•ˆ ํ›„์—๋งŒ ์ง„ํ–‰ ๊ฐ€๋Šฅ\n", " ํƒ€์ธ์ด ๋Œ€์‹  ์žฌ๊ธฐ์•ˆ -> ์ฐจ๋‹จ: ๊ถŒํ•œ ์—†์Œ(๋ถ€์„œ์žฅ ์—ญํ• ์— redraft ๊ถŒํ•œ ์—†์Œ)\n", "์šฐํšŒยท์œ„๋ฐ˜ ์‹œ๋„ ์ฐจ๋‹จ 13/13 (100%) (๋ชฉํ‘œ 100%: ๋‹ฌ์„ฑ)\n", "\n", "[์—ญํ• ๋ณ„ ๊ถŒํ•œ ๋งคํŠธ๋ฆญ์Šค(O ํ—ˆ์šฉ / - ๋ถˆ๊ฐ€)]\n", " ์ œ์•ˆ ์ƒ์‹  ์ •๋น„๊ฒ€ํ†  ํ’ˆ์งˆ๊ฒ€ํ†  ์Šน์ธ ๋ฐ˜๋ ค ์žฌ๊ธฐ์•ˆ ๊ฐ์‚ฌ\n", "์‹œ์Šคํ…œ O - - - - - - -\n", "์ •๋น„๋‹ด๋‹น - O - - - - O -\n", "์ •๋น„๊ด€ - - O - - O - -\n", "ํ’ˆ์งˆ๊ฒ€์‚ฌ๊ด€ - - - O - O - -\n", "๊ตฐ์ˆ˜์ฐธ๋ชจ - - - - O O - -\n", "๊ฐ์‚ฌ๊ด€ - - - - - - - O\n", "... (์ดํ•˜ ์ƒ๋žต)\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 08. ์ „๊ตฐ ์žฅ๋น„ ๊ฐ€๋™๋ฅ  ๋ฐ์ดํ„ฐ ์ผ๊ด„ ๋ถ„์„ ์ŠคํŠธ๋ ˆ์Šค ํ…Œ์ŠคํŠธ ๋ฐ ์„ฑ๋Šฅ ๊ฒ€์ฆ\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.5์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "์ „๊ตฐ ์žฅ๋น„ ๊ฐ€๋™๋ฅ  ๋ฐ์ดํ„ฐ ์ผ๊ด„ ๋ถ„์„ ์ŠคํŠธ๋ ˆ์Šค ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **12๊ฐœ์›”์น˜ ๊ฐ€์ƒ ์žฅ๋น„ ๊ฐ€๋™๋ฅ  ์ž๋ฃŒ๋ฅผ ์›” ๋‹จ์œ„ ๋ฐฐ์น˜๋กœ ์ผ๊ด„ ๋ถ„์„ํ•˜๊ณ , ์˜ค๋ฅ˜ยท์žฅ์•  ์ฃผ์ž…๊ณผ ์ž๋ฃŒ๋Ÿ‰ 500% ๋ถ€ํ•˜์—์„œ ์„ฑ๋Šฅ๊ณผ ์•ˆ์ •์„ฑ์„ ๊ฒ€์ฆํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **์›” ๋ฐฐ์น˜๋ฅผ ์ ์žฌ-๊ฒ€์ฆ-๋ถ„์„-๋ณด๊ณ  4๋‹จ๊ณ„๋กœ ์ž๋™ ์‹คํ–‰ํ•˜๋Š” ์Šค์ผ€์ค„๋Ÿฌ ๋ชจ์‚ฌ ์ž‘์„ฑํ•˜์—ฌ ์›์ธ์„ ๋ถ„์„ํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/pipeline/qlt04_batch_runner.py` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ์ˆ˜์ž‘์—… ๋Œ€๋น„ ์—…๋ฌด ์†Œ์š” ์‹œ๊ฐ„ 80% ์ด์ƒ ์ ˆ๊ฐ ํšจ๊ณผ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ์ „์ฒด ๋ฐฐ์น˜ ํŒŒ์ดํ”„๋ผ์ธ ์‹คํ–‰ ์‹œ๊ฐ„ 5๋ถ„ ์ด๋‚ด, ์ž๋™ํ™” ์„ฑ๊ณต๋ฅ  99% ์ด์ƒ\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ์Šค์ผ€์ค„๋Ÿฌ ์—ฐ๋™ ํ…Œ์ŠคํŠธ ๋ฐ ์ž๋™ ์‹คํ–‰ ํ™•์ธ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ์‹ค์ œ ์›”๊ฐ„ ์—…๋ฌด ๋ฐ์ดํ„ฐ 1๋…„์น˜ ์ผ๊ด„ ๋ฐฐ์น˜ ์‹œ๋ฎฌ๋ ˆ์ด์…˜\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"12๊ฐœ์›”์น˜ ๊ฐ€์ƒ ์žฅ๋น„ ๊ฐ€๋™๋ฅ  ์ž๋ฃŒ๋ฅผ ์›” ๋‹จ์œ„ ๋ฐฐ์น˜(์ ์žฌ -> ๊ฒ€์ฆยท๊ฒฉ๋ฆฌ -> ๋ฃฐยท๋ชจ๋ธ ๋ถ„์„ -> ๋ณด๊ณ )๋กœ ์ž๋™ ์‹คํ–‰ํ•˜๋Š” ์Šค์ผ€์ค„๋Ÿฌ ๋ชจ์‚ฌ๋ฅผ ๋งŒ๋“ค๊ณ , ์˜ค๋ฅ˜ ๋ ˆ์ฝ”๋“œยท์ผ์‹œ ์žฅ์•  ์ฃผ์ž…๊ณผ ์ž๋ฃŒ๋Ÿ‰ 100%ยท200%ยท500% ๋ถ€ํ•˜์—์„œ ์‹คํ–‰ ์‹œ๊ฐ„ยท์„ฑ๊ณต๋ฅ ยท์ž๋ฃŒ ์œ ์‹ค๋ฅ ยท์ˆ˜์ž‘์—… ๋Œ€๋น„ ์ ˆ๊ฐ์„ ์ธก์ •ํ•œ๋‹ค.\"\"\"\n", "import time\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ฐ€์ƒ ์žฅ๋น„ ๊ฐ€๋™๋ฅ ยท์ •๋น„ ๋Œ€๊ธฐ ์ž๋ฃŒ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ๋ถ€๋Œ€ยท์žฅ๋น„ยท์ œ์›ยท๊ฐ€๋™๋ฅ  ์ •๋ณด ์—†์Œ)\n", "# ์žฅ๋น„ ์œ ํ˜• A(๊ถค๋„ํ˜•)ยทB(ํ™”ํฌํ˜•)ยทC(์ฐจ๋ฅœํ˜•)์€ ์ถ”์ƒ ๊ตฌ๋ถ„์ด๋ฉฐ ์‹ค์ œ ์žฅ๋น„์™€ ๋ฌด๊ด€ํ•˜๋‹ค.\n", "# ์ตœ๊ทผ 90์ผ ๋™์•ˆ ์žฅ๋น„ 1๋Œ€์˜ ์ •๋น„ ๋Œ€๊ธฐ ์š”์ธ(๋ถ€ํ’ˆยท์ •๋น„๋ณ‘ยท์™ธ์ฃผ ์ •๋น„ยท์™ธ์ฃผ ๊ฒ€์‚ฌยท์žฌ์ž‘์—…)๊ณผ ๊ฐ€๋™๋ฅ ์„ ๋งŒ๋“ ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "UNITS = [\"๊ฐ€์ƒA๋ถ€๋Œ€\", \"๊ฐ€์ƒB๋ถ€๋Œ€\", \"๊ฐ€์ƒC๋ถ€๋Œ€\", \"๊ฐ€์ƒD๋ถ€๋Œ€\", \"๊ฐ€์ƒE๋ถ€๋Œ€\", \"๊ฐ€์ƒF๋ถ€๋Œ€\"]\n", "ETYPES = [\"์œ ํ˜•A\", \"์œ ํ˜•B\", \"์œ ํ˜•C\"]\n", "WINDOW = 90 # ๊ด€์ธก ๊ธฐ๊ฐ„(์ผ)\n", "CAUSES = [\"๋ถ€ํ’ˆ ๋Œ€๊ธฐ\", \"์ •๋น„๋ณ‘ ๋ถ€์กฑ\", \"์™ธ์ฃผ ์ •๋น„\", \"๊ฒ€์‚ฌยท์žฌ์ž‘์—…\"]\n", "CAUSE_COL = {\"๋ถ€ํ’ˆ ๋Œ€๊ธฐ\": \"parts_wait_days\", \"์ •๋น„๋ณ‘ ๋ถ€์กฑ\": \"labor_wait_days\", \"์™ธ์ฃผ ์ •๋น„\": \"outsource_days\", \"๊ฒ€์‚ฌยท์žฌ์ž‘์—…\": \"inspect_wait_days\"}\n", "NUM_FIELDS = [\"age_years\", \"op_hours\", \"maint_orders\", \"parts_wait_days\", \"labor_wait_days\", \"outsource_days\", \"inspect_wait_days\", \"rework_count\",\n", " \"spec_mismatch\", \"defect_reports\", \"parts_stock_pct\", \"mechanic_fill_pct\", \"avail_pct\"]\n", "FEAT = [\"age_years\", \"maint_orders\", \"parts_stock_pct\", \"mechanic_fill_pct\", \"outsource_days\", \"inspect_wait_days\", \"rework_count\", \"spec_mismatch\", \"defect_reports\", \"etype_B\", \"etype_C\"]\n", "FEAT_KO = {\"age_years\": \"์žฅ๋น„ ์—ฐ๋ น\", \"maint_orders\": \"์ •๋น„ ์š”์ฒญ ๊ฑด์ˆ˜\", \"parts_stock_pct\": \"๋ถ€ํ’ˆ ์žฌ๊ณ  ๊ฐ€์šฉ๋ฅ \", \"mechanic_fill_pct\": \"์ •๋น„๋ณ‘ ์ถฉ์›์œจ\", \"outsource_days\": \"์™ธ์ฃผ ์ •๋น„ ์ผ์ˆ˜\",\n", " \"inspect_wait_days\": \"์™ธ์ฃผ ๊ฒ€์‚ฌ ๋Œ€๊ธฐ ์ผ์ˆ˜\", \"rework_count\": \"์žฌ์ž‘์—… ๊ฑด์ˆ˜\", \"spec_mismatch\": \"๊ทœ๊ฒฉยท์ œ์› ๋ถˆ์ผ์น˜\", \"defect_reports\": \"ํ•˜์žยท๋ถˆ๋งŒ ์ ‘์ˆ˜\",\n", " \"etype_B\": \"์œ ํ˜•B ์—ฌ๋ถ€\", \"etype_C\": \"์œ ํ˜•C ์—ฌ๋ถ€\"}\n", "\n", "\n", "def unit_profile(seed=3):\n", " \"\"\"๋ถ€๋Œ€๋ณ„ ๊ณ ์ • ํŠน์„ฑ: ๋ถ€ํ’ˆ ์žฌ๊ณ  ์ˆ˜์ค€, ์ •๋น„๋ณ‘ ์ถฉ์›์œจ, ์™ธ์ฃผ ์ •๋น„์†Œ ๊ฑฐ๋ฆฌ(์ผ์ˆ˜ ๊ฐ€์ค‘), ์™ธ์ฃผ ๊ฒ€์‚ฌ ํ˜ผ์žก๋„.\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 8] ์ „๊ตฐ ์žฅ๋น„ ๊ฐ€๋™๋ฅ  ๋ฐ์ดํ„ฐ ์ผ๊ด„ ๋ถ„์„ ์ŠคํŠธ๋ ˆ์Šค ํ…Œ์ŠคํŠธ ๋ฐ ์„ฑ๋Šฅ ๊ฒ€์ฆ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def make_fleet(n=300, seed=7, shock=0.0):\n", " \"\"\"์žฅ๋น„ n๋Œ€์˜ 90์ผ ๊ด€์ธก ์ž๋ฃŒ(ํ•„๋“œ 16๊ฐœ)์™€ ์ •๋‹ต ์š”์ธ์„ ๋งŒ๋“ ๋‹ค. shock์€ ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋ณ‘๋ชฉ ์•…ํ™” ์ •๋„(AAR์šฉ ํ™˜๊ฒฝ ๋ณ€ํ™”).\"\"\"\n", " rng = np.random.default_rng(seed)\n", " pf = unit_profile()\n", " ui = rng.integers(0, len(UNITS), n)\n", " et = rng.choice(ETYPES, n, p=[.35, .3, .35])\n", " age = np.clip(rng.gamma(4, 2.2, n), 0.5, 30).round(1)\n", " eff = np.array([{\"์œ ํ˜•A\": 1.15, \"์œ ํ˜•B\": 1.0, \"์œ ํ˜•C\": 0.9}[t] for t in et])\n", " orders = rng.poisson(np.clip(1.2 + 0.12 * age, 0.5, 6) * eff)\n", " stock = np.clip(pf[\"stock\"].to_numpy()[ui] + rng.normal(0, 8, n), 20, 100).round(0)\n", " fill = np.clip(pf[\"fill\"].to_numpy()[ui] + rng.normal(0, 6, n), 30, 100).round(0)\n", " parts_w = (orders * rng.gamma(2, 0.55 * (1 + ((100 - stock) / 28) ** 2) / 2)).round(0)\n", " labor_w = (orders * rng.gamma(2, 0.7 * (1 + ((100 - fill) / 24) ** 2) / 2)).round(0)\n", " n_out = rng.binomial(orders, 0.3)\n", " out_d = (n_out * rng.gamma(3, 4.5 * pf[\"dist\"].to_numpy()[ui] / 3)).round(0)\n", " insp_w = (n_out * rng.gamma(2, (2.6 + shock) * pf[\"queue\"].to_numpy()[ui] / 2)).round(0)\n", " spec = rng.poisson(0.2 + 0.04 * age)\n", " rework = rng.binomial(n_out, np.clip(0.08 + 0.1 * spec, 0, .6))\n", " defect = rng.poisson(0.3 + 0.7 * rework)\n", " base_rep = orders * 1.8\n", " down = base_rep + parts_w + labor_w + out_d + insp_w + rework * 4 + rng.normal(0, 1.2, n)\n", " comp = np.column_stack([parts_w, labor_w, out_d, insp_w + rework * 4])\n", " df = pd.DataFrame({\"equip_id\": [f\"E-{i + 1:04d}\" for i in range(n)], \"unit\": np.array(UNITS)[ui], \"etype\": et, \"age_years\": age,\n", " \"op_hours\": np.clip(rng.normal(320 - 4 * age, 70, n), 40, 600).round(0), \"maint_orders\": orders,\n", " \"parts_wait_days\": parts_w, \"labor_wait_days\": labor_w, \"outsource_days\": out_d, \"inspect_wait_days\": insp_w,\n", " \"rework_count\": rework, \"spec_mismatch\": spec, \"defect_reports\": defect, \"parts_stock_pct\": stock, \"mechanic_fill_pct\": fill,\n", " \"avail_pct\": (100 * (1 - np.clip(down, 0, WINDOW) / WINDOW)).round(1)})\n", " df[\"main_cause\"] = np.where(comp.max(axis=1) < 3, \"์—†์Œ\", np.array(CAUSES)[comp.argmax(axis=1)])\n", " return df\n", "\n", "\n", "def feat_frame(df):\n", " \"\"\"ํšŒ๊ท€ยท๋ถ„์„์šฉ ์„ค๋ช… ๋ณ€์ˆ˜ ํ‘œ(์œ ํ˜•์€ ๋”๋ฏธ ๋ณ€์ˆ˜).\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 8] ์ „๊ตฐ ์žฅ๋น„ ๊ฐ€๋™๋ฅ  ๋ฐ์ดํ„ฐ ์ผ๊ด„ ๋ถ„์„ ์ŠคํŠธ๋ ˆ์Šค ํ…Œ์ŠคํŠธ ๋ฐ ์„ฑ๋Šฅ ๊ฒ€์ฆ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def make_trend(months=12, seed=5, shock_unit=None):\n", " \"\"\"[๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๋ถ€๋Œ€ x ์œ ํ˜• x ์›” ํ‰๊ท  ๊ฐ€๋™๋ฅ  ์ถ”์„ธ. shock_unit ๋ถ€๋Œ€๋Š” ํ•˜๋ฐ˜๊ธฐ์— ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋ณ‘๋ชฉ์œผ๋กœ ๊ฐ€๋™๋ฅ ์ด ๋–จ์–ด์ง„๋‹ค.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " pf = unit_profile()\n", " rows = []\n", " for _, u in pf.iterrows():\n", " base = 62 + 0.22 * u[\"stock\"] + 0.12 * u[\"fill\"] - 3.0 * u[\"queue\"]\n", " for t in ETYPES:\n", " off = {\"์œ ํ˜•A\": -3.0, \"์œ ํ˜•B\": 0.0, \"์œ ํ˜•C\": 2.0}[t]\n", " for m in range(months):\n", " v = base + off + 2.0 * np.sin(m / 12 * 2 * np.pi) + rng.normal(0, 1.0)\n", " if shock_unit == u[\"unit\"] and m >= months // 2:\n", " v -= 1.5 * (m - months // 2 + 1)\n", " rows.append((u[\"unit\"], t, m + 1, round(float(np.clip(v, 30, 99)), 1)))\n", " return pd.DataFrame(rows, columns=[\"unit\", \"etype\", \"month\", \"avail_pct\"])\n", "\n", "# ---- ์ •๋น„ยท๊ฒ€์‚ฌ ๋ณ‘๋ชฉ ํŒ๋ณ„ ๋ฃฐ ์„ค์ •(YAML ํ˜•์‹ ๋ฌธ์ž์—ด: ๊ธฐ์ค€์ด ๋ฐ”๋€Œ๋ฉด ์ฝ”๋“œ๊ฐ€ ์•„๋‹ˆ๋ผ ์ด ์„ค์ •๋งŒ ๊ณ ์นœ๋‹ค) ----\n", "# clause๋Š” ๋ฃฐ๊ณผ ๋Œ€์‘์‹œํ‚จ '๊ฐ€์ƒ ํ’ˆ์งˆ์ง€์นจ ์กฐํ•ญ' ์˜ˆ์‹œ์ด๋ฉฐ ์‹ค์ œ ๊ทœ์ • ์›๋ฌธ์ด ์•„๋‹ˆ๋‹ค.\n", "RULES_YAML = \"\"\"\n", "- {id: Q01, name: ๋ถ€ํ’ˆ ์žฌ๊ณ  ๊ฐ€์šฉ๋ฅ  50% ๋ฏธ๋งŒ, field: parts_stock_pct, op: \"<\", value: 50, weight: 2, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ3์กฐ}\n", "- {id: Q02, name: ์ •๋น„๋ณ‘ ์ถฉ์›์œจ 60% ๋ฏธ๋งŒ, field: mechanic_fill_pct, op: \"<\", value: 60, weight: 2, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ4์กฐ}\n", "- {id: Q03, name: ๋ถ€ํ’ˆ ๋Œ€๊ธฐ 8์ผ ์ดˆ๊ณผ, field: parts_wait_days, op: \">\", value: 8, weight: 2, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ5์กฐ}\n", "- {id: Q04, name: ์ •๋น„๋ณ‘ ๋ถ€์กฑ ๋Œ€๊ธฐ 8์ผ ์ดˆ๊ณผ, field: labor_wait_days, op: \">\", value: 8, weight: 2, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ6์กฐ}\n", "- {id: Q05, name: ์™ธ์ฃผ ์ •๋น„ 15์ผ ์ดˆ๊ณผ, field: outsource_days, op: \">\", value: 15, weight: 2, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ7์กฐ}\n", "- {id: Q06, name: ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋Œ€๊ธฐ 6์ผ ์ดˆ๊ณผ, field: inspect_wait_days, op: \">\", value: 6, weight: 3, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ8์กฐ}\n", "- {id: Q07, name: ์žฌ์ž‘์—… 1๊ฑด ์ด์ƒ, field: rework_count, op: \">=\", value: 1, weight: 3, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ9์กฐ}\n", "- {id: Q08, name: ์žฌ์ž‘์—…๋ฅ  25% ์ดˆ๊ณผ, field: rework_rate, op: \">\", value: 0.25, weight: 2, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ9์กฐ}\n", "- {id: Q09, name: ๊ทœ๊ฒฉยท์ œ์› ๋ถˆ์ผ์น˜ 3๊ฑด ์ด์ƒ, field: spec_mismatch, op: \">=\", value: 3, weight: 3, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ10์กฐ}\n", "- {id: Q10, name: ํ•˜์žยท๋ถˆ๋งŒ 3๊ฑด ์ด์ƒ, field: defect_reports, op: \">=\", value: 3, weight: 2, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ11์กฐ}\n", "- {id: Q11, name: ์žฅ๋น„ ์—ฐ๋ น 15๋…„ ์ดˆ๊ณผ, field: age_years, op: \">\", value: 15, weight: 1, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ12์กฐ}\n", "- {id: Q12, name: ์ •๋น„ ์š”์ฒญ 6๊ฑด ์ด์ƒ, field: maint_orders, op: \">=\", value: 6, weight: 1, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ13์กฐ}\n", "- {id: Q13, name: ๋Œ€๊ธฐ ์ผ์ˆ˜ ํ•ฉ๊ณ„ 30์ผ ์ดˆ๊ณผ, field: total_wait, op: \">\", value: 30, weight: 3, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ14์กฐ}\n", "- {id: Q14, name: ๊ฒ€์‚ฌ ๋Œ€๊ธฐ๊ฐ€ ์™ธ์ฃผ ์ •๋น„ ์ผ์ˆ˜์˜ 80% ์ดˆ๊ณผ, field: insp_ratio, op: \">\", value: 0.8, weight: 2, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ8์กฐ}\n", "- {id: Q15, name: ๊ทœ๊ฒฉ ๋ถˆ์ผ์น˜ 2๊ฑด ์ด์ƒ๊ณผ ํ•˜์ž ์ ‘์ˆ˜ ๋™๋ฐ˜, field: spec_defect, op: \"==\", value: true, weight: 3, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ10์กฐ}\n", "- {id: Q16, name: ์ตœ๊ทผ 90์ผ ๊ฐ€๋™ ์‹œ๊ฐ„ 150์‹œ๊ฐ„ ๋ฏธ๋งŒ, field: op_hours, op: \"<\", value: 150, weight: 1, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ15์กฐ}\n", "\"\"\"\n", "GRADE_CUT = [(10, \"๊ณ ์œ„ํ—˜\"), (6, \"์ฃผ์˜\"), (3, \"๊ด€์‹ฌ\")]\n", "OPS = {\"<\": lambda a, b: a < b, \">\": lambda a, b: a > b, \"<=\": lambda a, b: a <= b, \">=\": lambda a, b: a >= b, \"==\": lambda a, b: a == b}\n", "\n", "\n", "def parse_rules(text):\n", " \"\"\"YAML ํ•œ ์ค„ ๋งต({key: value, ...}) ํ˜•์‹ ๋ฃฐ ๋ฌธ์ž์—ด์„ ์ฝ๋Š”๋‹ค(์™ธ๋ถ€ yaml ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—†์ด).\"\"\"\n", " out = []\n", " for line in text.strip().splitlines():\n", " body = line.strip()[3:-1]\n", " r = {}\n", " for part in body.split(\", \"):\n", " k, v = part.split(\": \", 1)\n", " v = v.strip().strip('\"')\n", " r[k] = True if v == \"true\" else False if v == \"false\" else (float(v) if v.replace(\".\", \"\", 1).isdigit() else v)\n", " out.append(r)\n", " return out\n", "\n", "\n", "def derive_rec(rec):\n", " \"\"\"๋ฃฐ์ด ์“ฐ๋Š” ํŒŒ์ƒ ํ•„๋“œ(๋Œ€๊ธฐ ํ•ฉ๊ณ„, ์žฌ์ž‘์—…๋ฅ , ๊ฒ€์‚ฌ ๋Œ€๊ธฐ ๋น„์œจ(์™ธ์ฃผ 5์ผ ์ด์ƒ์ผ ๋•Œ), ๊ทœ๊ฒฉ ๋ถˆ์ผ์น˜ 2๊ฑด ์ด์ƒ+ํ•˜์ž ๋™๋ฐ˜)๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.\"\"\"\n", " r = dict(rec)\n", " r[\"total_wait\"] = r[\"parts_wait_days\"] + r[\"labor_wait_days\"] + r[\"outsource_days\"] + r[\"inspect_wait_days\"]\n", " r[\"rework_rate\"] = r[\"rework_count\"] / r[\"maint_orders\"] if r[\"maint_orders\"] else 0.0\n", " r[\"insp_ratio\"] = r[\"inspect_wait_days\"] / r[\"outsource_days\"] if r[\"outsource_days\"] >= 5 else 0.0\n", " r[\"spec_defect\"] = bool(r[\"spec_mismatch\"] >= 2 and r[\"defect_reports\"] >= 1)\n", " return r\n", "\n", "\n", "def apply_rules(rec, rules, derived=False):\n", " \"\"\"๋ฃฐ ์ ์šฉ: (์ ์ˆ˜, ์ ์ค‘ ๋ฃฐ id ๋ชฉ๋ก). derived=True๋ฉด ์ด๋ฏธ ํŒŒ์ƒ ํ•„๋“œ๊ฐ€ ์žˆ๋Š” ๋ ˆ์ฝ”๋“œ(๊ฒฝ๊ณ„ ์‹œํ—˜์šฉ)๋กœ ๋ณธ๋‹ค.\"\"\"\n", " r = rec if derived else derive_rec(rec)\n", " hits = [x[\"id\"] for x in rules if OPS[x[\"op\"]](r[x[\"field\"]], x[\"value\"])]\n", " w = {x[\"id\"]: x[\"weight\"] for x in rules}\n", " return sum(w[h] for h in hits), hits\n", "\n", "\n", "def grade_of(score):\n", " for cut, g in GRADE_CUT:\n", " if score >= cut:\n", " return g\n", " return \"์ •์ƒ\"\n", "\n", "# ---- ๊ฐ€๋™๋ฅ  ํšŒ๊ท€ ๋ชจ๋ธ๊ณผ ์ €๊ฐ€๋™ ์š”์ธ ๋ถ„๋ฅ˜ยท์ถ”์ฒœ ๋ชจ๋ธ(์™ธ๋ถ€ AI ์„œ๋น„์Šค ์—†์ด scikit-learn ์‚ฌ์šฉ) ----\n", "from sklearn.linear_model import Ridge, LogisticRegression\n", "from sklearn.preprocessing import StandardScaler\n", "from sklearn.pipeline import make_pipeline\n", "\n", "CAUSE_COLS = [\"age_years\", \"maint_orders\", \"parts_stock_pct\", \"mechanic_fill_pct\", \"outsource_days\", \"inspect_wait_days\", \"rework_count\", \"spec_mismatch\",\n", " \"defect_reports\", \"parts_wait_days\", \"labor_wait_days\"]\n", "ACTION = {\"๋ถ€ํ’ˆ ๋Œ€๊ธฐ\": \"๋ถ€ํ’ˆ ์‚ฌ์ „ ์ฒญ๊ตฌยท์žฌ๊ณ  ๋ณด๊ฐ•\", \"์ •๋น„๋ณ‘ ๋ถ€์กฑ\": \"์ •๋น„๋ณ‘ ์žฌ๋ฐฐ์น˜ยท๊ต์œก ์ผ์ • ์กฐ์ •\", \"์™ธ์ฃผ ์ •๋น„\": \"์™ธ์ฃผ ์ •๋น„ ์ผ์ • ์กฐ์œจยท๋‚ด๋ถ€ ์ •๋น„ ํ™•๋Œ€\", \"๊ฒ€์‚ฌยท์žฌ์ž‘์—…\": \"๊ฒ€์‚ฌ ์ผ์ • ์กฐ์œจยท๊ทœ๊ฒฉ ์‚ฌ์ „ ๋Œ€์กฐ\"}\n", "\n", "\n", "def cause_frame(df):\n", " x = df[CAUSE_COLS].copy()\n", " x[\"out_insp\"] = x[\"outsource_days\"] + x[\"inspect_wait_days\"] + 4 * x[\"rework_count\"]\n", " x[\"wait_pm\"] = (x[\"parts_wait_days\"] + x[\"labor_wait_days\"]) / (x[\"maint_orders\"] + 1)\n", " return x\n", "\n", "\n", "def fit_avail(df, alpha=1.0):\n", " \"\"\"๊ฐ€๋™๋ฅ (%) ํšŒ๊ท€: ํ‘œ์ค€ํ™” + ๋ฆฟ์ง€ ํšŒ๊ท€.\"\"\"\n", " return make_pipeline(StandardScaler(), Ridge(alpha=alpha)).fit(feat_frame(df), df[\"avail_pct\"])\n", "\n", "\n", "def fit_cause(df, cut=85, C=5):\n", " \"\"\"๊ฐ€๋™๋ฅ ์ด cut% ๋ฏธ๋งŒ์ธ ์žฅ๋น„์˜ ์ฃผ์š” ์ €๊ฐ€๋™ ์š”์ธ(4์ข…)์„ ๋ถ„๋ฅ˜ํ•œ๋‹ค.\"\"\"\n", " low = df[(df[\"avail_pct\"] < cut) & (df[\"main_cause\"] != \"์—†์Œ\")]\n", " return make_pipeline(StandardScaler(), LogisticRegression(C=C, max_iter=3000)).fit(cause_frame(low), low[\"main_cause\"])\n", "\n", "\n", "SIM_FEAT = [\"age_years\", \"maint_orders\", \"spec_mismatch\", \"stock_gap_x_orders\", \"fill_gap_x_orders\", \"parts_stock_pct\", \"mechanic_fill_pct\", \"etype_B\", \"etype_C\"]\n", "\n", "\n", "def sim_frame(df):\n", " \"\"\"์ž์› ์žฌ๋ฐฐ์น˜ ์‹œ๋ฎฌ๋ ˆ์ด์…˜์šฉ ์„ค๋ช… ๋ณ€์ˆ˜: ๋ถ€์กฑ๋ถ„์ด ํด์ˆ˜๋ก ๋Œ€๊ธฐ๊ฐ€ ๋” ๋น ๋ฅด๊ฒŒ ๋Š˜๊ณ  ์ •๋น„ ์š”์ฒญ ๊ฑด์ˆ˜์™€ ๊ณฑํ•ด์ ธ ์ปค์ง„๋‹ค๋Š” ๊ตฌ์กฐ๋ฅผ ์ œ๊ณฑยท์ƒํ˜ธ์ž‘์šฉ ํ•ญ์œผ๋กœ ๋‘”๋‹ค.\"\"\"\n", " x = df.copy()\n", " x[\"etype_B\"], x[\"etype_C\"] = (x[\"etype\"] == \"์œ ํ˜•B\").astype(int), (x[\"etype\"] == \"์œ ํ˜•C\").astype(int)\n", " x[\"stock_gap_x_orders\"] = x[\"maint_orders\"] * ((100 - x[\"parts_stock_pct\"]) / 28) ** 2\n", " x[\"fill_gap_x_orders\"] = x[\"maint_orders\"] * ((100 - x[\"mechanic_fill_pct\"]) / 24) ** 2\n", " return x[SIM_FEAT]\n", "\n", "\n", "def fit_sim(df, alpha=0.5):\n", " return make_pipeline(StandardScaler(), Ridge(alpha=alpha)).fit(sim_frame(df), df[\"avail_pct\"])\n", "\n", "rules = parse_rules(RULES_YAML)\n", "model = fit_avail(make_fleet(1200, seed=21))\n", "BASE_N = 1000 # ์›” ๊ธฐ์ค€ ์žฅ๋น„ ์ˆ˜(๊ฐ€์ƒ)\n", "MANUAL_MIN = {\"์ ์žฌยท์ •๋ฆฌ\": 0.5, \"๊ฒ€์ฆ\": 0.5, \"๋ถ„์„\": 1.5, \"๋ณด๊ณ \": 0.5} # ์žฅ๋น„ 1๋Œ€๋‹น ์ˆ˜์ž‘์—… ์‹œ๊ฐ„(๋ถ„, ๊ฐ€์ •์น˜)\n", "CHECK_MIN = 6 # ์ž๋™ ๋ถ„์„ ํ›„ ์ •๋น„๊ด€์ด ๊ณ ์œ„ํ—˜ 1๋Œ€๋ฅผ ํ™•์ธํ•˜๋Š” ์‹œ๊ฐ„(๋ถ„, ๊ฐ€์ •์น˜)\n", "rng = np.random.default_rng(8)\n", "LOGS = []\n", "\n", "\n", "def load(month, n, bad_rate=0.01):\n", " df = make_fleet(n, seed=100 + month)\n", " k = max(1, int(n * bad_rate))\n", " bad = rng.choice(n, k, replace=False)\n", " df[\"avail_pct\"] = df[\"avail_pct\"].astype(float)\n", " df.loc[bad[: k // 2], \"avail_pct\"] = np.nan # ๊ฒฐ์ธก\n", " df.loc[bad[k // 2:], \"avail_pct\"] = 150.0 # ๋ฒ”์œ„ ์˜ค๋ฅ˜\n", " return df\n", "\n", "\n", "def validate(df):\n", " ok = df[\"avail_pct\"].between(0, 100)\n", " return df[ok], df[~ok]\n", "\n", "\n", "def analyze(df):\n", " recs = df.drop(columns=[\"main_cause\"]).to_dict(\"records\")\n", " sc = [apply_rules(r, rules)[0] for r in recs]\n", " pred = np.clip(model.predict(feat_frame(df)), 0, 100)\n", " return {\"n\": len(df), \"avail\": df[\"avail_pct\"].mean(), \"high\": sum(s >= 10 for s in sc), \"caution\": sum(6 <= s < 10 for s in sc), \"mae\": np.mean(np.abs(pred - df[\"avail_pct\"]))}\n", "\n", "\n", "FAIL_ONCE = {(3, \"๋ถ„์„\"), (9, \"์ ์žฌ\")} # ์ผ์‹œ ์žฅ์•  ์ฃผ์ž…(์žฌ์‹œ๋„ ํ›„ ์„ฑ๊ณตํ•ด์•ผ ํ•จ)\n", "FAIL_ALWAYS = set() # ์ง€์† ์žฅ์• ๋Š” ์•„๋ž˜ ๋ณ„๋„ ์‹œํ—˜์—์„œ ์ฃผ์ž…(๊ฒฉ๋ฆฌยท์•Œ๋ฆผ ํ›„ ๋‹ค์Œ ๋‹ฌ ๊ณ„์†)\n", "tried = set()\n", "\n", "\n", "def step(month, name, fn):\n", " for attempt in (1, 2):\n", " try:\n", " if (month, name) in FAIL_ONCE and (month, name) not in tried:\n", " tried.add((month, name)); raise IOError(\"temporary failure\")\n", " if (month, name) in FAIL_ALWAYS:\n", " raise RuntimeError(\"persistent failure\")\n", " return True, fn()\n", " except Exception as e:\n", " LOGS.append(f\"{month:02d}์›” {name} ์‹œ๋„ {attempt} ์‹คํŒจ: {e}\")\n", " return False, None\n", "\n", "\n", "def run_batch(n, label, months=range(1, 13), verbose=False):\n", " t0 = time.perf_counter(); res, steps, ok_steps, quarantined, seen = [], 0, 0, 0, 0\n", " for m in months:\n", " ctx = {}\n", " for name, fn in [(\"์ ์žฌ\", lambda: ctx.update(df=load(m, n))), (\"๊ฒ€์ฆ\", lambda: ctx.update(zip((\"clean\", \"bad\"), validate(ctx[\"df\"])))),\n", " (\"๋ถ„์„\", lambda: ctx.update(r=analyze(ctx[\"clean\"]))), (\"๋ณด๊ณ \", lambda: ctx[\"r\"])]:\n", " steps += 1\n", " ok, _ = step(m, name, fn)\n", " ok_steps += ok\n", " if not ok:\n", " break\n", " if \"r\" in ctx and ok:\n", " res.append({\"์›”\": m, **ctx[\"r\"], \"๊ฒฉ๋ฆฌ\": len(ctx[\"bad\"])})\n", " if \"df\" in ctx:\n", " seen += len(ctx[\"df\"]); quarantined += len(ctx.get(\"bad\", [])) if \"bad\" in ctx else 0\n", " if verbose:\n", " print(f\" ์ง„ํ–‰ [{'#' * m}{'.' * (12 - m)}] {m:2d}/12์›” {'์™„๋ฃŒ' if ok else '์‹คํŒจ(์•Œ๋ฆผ ๋ฐœ์†ก, ๋‹ค์Œ ๋‹ฌ ๊ณ„์†)'}\")\n", " return pd.DataFrame(res), time.perf_counter() - t0, ok_steps, steps\n", "\n", "\n", "# 1) 12๊ฐœ์›” ๋ฐฐ์น˜(์Šค์ผ€์ค„๋Ÿฌ ๋ชจ์‚ฌ)\n", "print(\"[์›” ๋ฐฐ์น˜ ์‹คํ–‰: ์žฅ์•  ์ฃผ์ž… ํฌํ•จ]\")\n", "rs, dt, ok_s, steps = run_batch(BASE_N, \"x1\", verbose=True)\n", "print(f\"์†Œ์š” {dt:.1f}์ดˆ(๋ชฉํ‘œ 5๋ถ„=300์ดˆ ์ด๋‚ด: {'๋‹ฌ์„ฑ' if dt < 300 else '๋ฏธ๋‹ฌ'}), ๋‹จ๊ณ„ {ok_s}/{steps} ์„ฑ๊ณต(์ผ์‹œ ์žฅ์• ๋Š” ์žฌ์‹œ๋„๋กœ ๋ณต๊ตฌ) -> ์ž๋™ํ™” ์„ฑ๊ณต๋ฅ  {ok_s / steps:.1%} (๋ชฉํ‘œ 99% ์ด์ƒ: {'๋‹ฌ์„ฑ' if ok_s / steps >= 0.99 else '๋ฏธ๋‹ฌ'})\")\n", "print(\"์žฌ์‹œ๋„ยท์‹คํŒจ ๋กœ๊ทธ:\", \" / \".join(LOGS[:3]), \"...\", f\"(์ด {len(LOGS)}๊ฑด)\")\n", "print(\"[์•Œ๋ฆผ] ๋ฐฐ์น˜ ์™„๋ฃŒ: ์›” 12๊ฑด ์ค‘ %d๊ฑด ๋ณด๊ณ  ์ƒ์„ฑ, ์‹คํŒจ ์›” %s\" % (len(rs), sorted(set(range(1, 13)) - set(rs[\"์›”\"])) or \"์—†์Œ\"))\n", "print(rs.round(1).head(4).to_string(index=False), \"\\n ...\")\n", "\n", "# 2) ์ง€์† ์žฅ์•  ์‹œํ—˜(11์›” ๋ณด๊ณ  ๋‹จ๊ณ„๊ฐ€ ๊ณ„์† ์‹คํŒจ): ํ•ด๋‹น ์›”๋งŒ ๊ฑด๋„ˆ๋›ฐ๊ณ  ์•Œ๋ฆผ, ๋‚˜๋จธ์ง€ ์›”์€ ๊ณ„์†(๊ธฐ๋Šฅ ์ถ•์†Œ ์ž‘๋™)\n", "FAIL_ONCE.clear(); FAIL_ALWAYS.add((11, \"๋ณด๊ณ \")); LOGS.clear()\n", "rp, dp, okp, stp = run_batch(BASE_N, \"fault\")\n", "print(f\"\\n[์ง€์† ์žฅ์•  ์‹œํ—˜] ๋‹จ๊ณ„ {okp}/{stp} ์„ฑ๊ณต({okp / stp:.1%}), ๋ณด๊ณ  ์ƒ์„ฑ {len(rp)}/12์›”, ์‹คํŒจ ์›” {sorted(set(range(1, 13)) - set(rp['์›”']))} -> ์•Œ๋ฆผ ๋ฐœ์†ก, ๋‹ค๋ฅธ ๋‹ฌ์€ ์ •์ƒ ์ฒ˜๋ฆฌ(๋ฐฐ์น˜ ์ „์ฒด ์ค‘๋‹จ ์—†์Œ)\")\n", "\n", "# 3) ์žฅ์•  ์—†๋Š” ๋ฐฐ์น˜์˜ ์ž๋ฃŒ ์œ ์‹ค ์ ๊ฒ€\n", "FAIL_ALWAYS.clear(); LOGS.clear()\n", "rs0, dt0, ok0, st0 = run_batch(BASE_N, \"clean\")\n", "tot_in = BASE_N * 12\n", "tot_out = int(rs0[\"n\"].sum() + rs0[\"๊ฒฉ๋ฆฌ\"].sum())\n", "print(f\"\\n[์žฅ์•  ์—†๋Š” ๋ฐฐ์น˜] ๋‹จ๊ณ„ {ok0}/{st0} ์„ฑ๊ณต({ok0 / st0:.1%}), ์ž…๋ ฅ {tot_in}๊ฑด = ๋ถ„์„ {int(rs0['n'].sum())} + ๊ฒฉ๋ฆฌ {int(rs0['๊ฒฉ๋ฆฌ'].sum())} -> ์œ ์‹ค {tot_in - tot_out}๊ฑด({(tot_in - tot_out) / tot_in:.1%})\")\n", "\n", "# 4) ๋ถ€ํ•˜ ์‹œํ—˜: ์ž๋ฃŒ๋Ÿ‰ 100%ยท200%ยท500%\n", "print(\"\\n[๋ถ€ํ•˜ ์‹œํ—˜: ์›” ์ž๋ฃŒ๋Ÿ‰ ์ฆ๊ฐ€ (12๊ฐœ์›” ์ „์ฒด)]\")\n", "lt = []\n", "for mult in (1, 2, 5):\n", " r, d, o, s = run_batch(BASE_N * mult, f\"x{mult}\")\n", " lost = BASE_N * mult * 12 - int(r[\"n\"].sum() + r[\"๊ฒฉ๋ฆฌ\"].sum())\n", " lt.append((mult, d, o / s, lost))\n", " print(f\" ์ž๋ฃŒ {mult * 100:3d}%: {BASE_N * mult * 12:6d}๊ฑด, {d:5.1f}์ดˆ, ์„ฑ๊ณต {o}/{s}, ์œ ์‹ค {lost}๊ฑด, ์ดˆ๋‹น {BASE_N * mult * 12 / d:,.0f}๊ฑด\")\n", "print(f\"์‹œ์Šคํ…œ ๋‹ค์šด 0๊ฑด, ๋ฐ์ดํ„ฐ ์œ ์‹ค {sum(x[3] for x in lt)}๊ฑด (๋ชฉํ‘œ 0%: {'๋‹ฌ์„ฑ' if sum(x[3] for x in lt) == 0 else '๋ฏธ๋‹ฌ'})\")\n", "\n", "# 5) ์ˆ˜์ž‘์—… ๋Œ€๋น„ ์ ˆ๊ฐ(๊ฐ€์ •์น˜ ๊ธฐ๋ฐ˜ ์ƒ๋Œ€ ๋น„๊ต)\n", "n_equip = BASE_N * 12\n", "manual_h = n_equip * sum(MANUAL_MIN.values()) / 60\n", "check_h = int(rs0[\"high\"].sum()) * CHECK_MIN / 60\n", "auto_h = dt0 / 3600 + check_h\n", "print(f\"\\n[์—…๋ฌด ์‹œ๊ฐ„ ์ ˆ๊ฐ] ์ˆ˜์ž‘์—… {manual_h:.0f}์‹œ๊ฐ„(์žฅ๋น„๋‹น {sum(MANUAL_MIN.values()):.1f}๋ถ„) vs ์ž๋™ {auto_h:.1f}์‹œ๊ฐ„(์‹คํ–‰ {dt0:.1f}์ดˆ + ๊ณ ์œ„ํ—˜ {int(rs0['high'].sum())}๋Œ€ ํ™•์ธ {check_h:.1f}์‹œ๊ฐ„) -> ์ ˆ๊ฐ {1 - auto_h / manual_h:.0%} (๋ชฉํ‘œ 80% ์ด์ƒ: {'๋‹ฌ์„ฑ' if 1 - auto_h / manual_h >= 0.8 else '๋ฏธ๋‹ฌ'})\")\n", "print(\"โ€ป ์‹œ๊ฐ„ ๊ฐ€์ •(์žฅ๋น„๋‹น ์ˆ˜์ž‘์—… ๋ถ„, ๊ณ ์œ„ํ—˜ ํ™•์ธ 6๋ถ„)์€ ๊ต์œก์šฉ ๊ฐ€์ •์น˜์ด๋ฉฐ ์‹ค์ œ ์—…๋ฌด ์‹œ๊ฐ„์œผ๋กœ ๋ฐ”๊ฟ” ๊ณ„์‚ฐํ•œ๋‹ค. ๊ณ ์œ„ํ—˜ ์žฅ๋น„์˜ ํ™•์ธ๊ณผ ์ •๋น„ ์šฐ์„ ์ˆœ์œ„ ๊ฒฐ์ •์€ ์‚ฌ๋žŒ์ด ํ•œ๋‹ค.\")\n", "\n", "fig, ax = plt.subplots(1, 2, figsize=(10, 4))\n", "ax[0].plot(rs0[\"์›”\"], rs0[\"avail\"], marker=\"o\"); ax[0].set_title(\"Monthly mean availability (synthetic)\"); ax[0].set_xlabel(\"month\")\n", "ax[1].bar([f\"x{x[0]}\" for x in lt], [x[1] for x in lt], color=\"tab:blue\"); ax[1].set_title(\"Batch time by data volume (sec)\")\n", "plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 08 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[์›” ๋ฐฐ์น˜ ์‹คํ–‰: ์žฅ์•  ์ฃผ์ž… ํฌํ•จ]\n", " ์ง„ํ–‰ [#...........] 1/12์›” ์™„๋ฃŒ\n", " ์ง„ํ–‰ [##..........] 2/12์›” ์™„๋ฃŒ\n", " ์ง„ํ–‰ [###.........] 3/12์›” ์™„๋ฃŒ\n", " ์ง„ํ–‰ [####........] 4/12์›” ์™„๋ฃŒ\n", " ์ง„ํ–‰ [#####.......] 5/12์›” ์™„๋ฃŒ\n", " ์ง„ํ–‰ [######......] 6/12์›” ์™„๋ฃŒ\n", " ์ง„ํ–‰ [#######.....] 7/12์›” ์™„๋ฃŒ\n", " ์ง„ํ–‰ [########....] 8/12์›” ์™„๋ฃŒ\n", " ์ง„ํ–‰ [#########...] 9/12์›” ์™„๋ฃŒ\n", " ์ง„ํ–‰ [##########..] 10/12์›” ์™„๋ฃŒ\n", " ์ง„ํ–‰ [###########.] 11/12์›” ์™„๋ฃŒ\n", " ์ง„ํ–‰ [############] 12/12์›” ์™„๋ฃŒ\n", "์†Œ์š” 0.3์ดˆ(๋ชฉํ‘œ 5๋ถ„=300์ดˆ ์ด๋‚ด: ๋‹ฌ์„ฑ), ๋‹จ๊ณ„ 48/48 ์„ฑ๊ณต(์ผ์‹œ ์žฅ์• ๋Š” ์žฌ์‹œ๋„๋กœ ๋ณต๊ตฌ) -> ์ž๋™ํ™” ์„ฑ๊ณต๋ฅ  100.0% (๋ชฉํ‘œ 99% ์ด์ƒ: ๋‹ฌ์„ฑ)\n", "์žฌ์‹œ๋„ยท์‹คํŒจ ๋กœ๊ทธ: 03์›” ๋ถ„์„ ์‹œ๋„ 1 ์‹คํŒจ: temporary failure / 09์›” ์ ์žฌ ์‹œ๋„ 1 ์‹คํŒจ: temporary failure ... (์ด 2๊ฑด)\n", "[์•Œ๋ฆผ] ๋ฐฐ์น˜ ์™„๋ฃŒ: ์›” 12๊ฑด ์ค‘ 12๊ฑด ๋ณด๊ณ  ์ƒ์„ฑ, ์‹คํŒจ ์›” ์—†์Œ\n", " ์›” n avail high caution mae ๊ฒฉ๋ฆฌ\n", " 1 990 79.1 80 103 4.0 10\n", " 2 990 79.9 85 79 4.0 10\n", " 3 990 78.6 86 108 4.0 10\n", " 4 990 79.7 79 84 4.1 10 \n", " ...\n", "\n", "[์ง€์† ์žฅ์•  ์‹œํ—˜] ๋‹จ๊ณ„ 47/48 ์„ฑ๊ณต(97.9%), ๋ณด๊ณ  ์ƒ์„ฑ 11/12์›”, ์‹คํŒจ ์›” [11] -> ์•Œ๋ฆผ ๋ฐœ์†ก, ๋‹ค๋ฅธ ๋‹ฌ์€ ์ •์ƒ ์ฒ˜๋ฆฌ(๋ฐฐ์น˜ ์ „์ฒด ์ค‘๋‹จ ์—†์Œ)\n", "\n", "[์žฅ์•  ์—†๋Š” ๋ฐฐ์น˜] ๋‹จ๊ณ„ 48/48 ์„ฑ๊ณต(100.0%), ์ž…๋ ฅ 12000๊ฑด = ๋ถ„์„ 11880 + ๊ฒฉ๋ฆฌ 120 -> ์œ ์‹ค 0๊ฑด(0.0%)\n", "\n", "[๋ถ€ํ•˜ ์‹œํ—˜: ์›” ์ž๋ฃŒ๋Ÿ‰ ์ฆ๊ฐ€ (12๊ฐœ์›” ์ „์ฒด)]\n", " ์ž๋ฃŒ 100%: 12000๊ฑด, 0.3์ดˆ, ์„ฑ๊ณต 48/48, ์œ ์‹ค 0๊ฑด, ์ดˆ๋‹น 37,904๊ฑด\n", " ์ž๋ฃŒ 200%: 24000๊ฑด, 0.6์ดˆ, ์„ฑ๊ณต 48/48, ์œ ์‹ค 0๊ฑด, ์ดˆ๋‹น 41,507๊ฑด\n", " ์ž๋ฃŒ 500%: 60000๊ฑด, 1.1์ดˆ, ์„ฑ๊ณต 48/48, ์œ ์‹ค 0๊ฑด, ์ดˆ๋‹น 52,896๊ฑด\n", "์‹œ์Šคํ…œ ๋‹ค์šด 0๊ฑด, ๋ฐ์ดํ„ฐ ์œ ์‹ค 0๊ฑด (๋ชฉํ‘œ 0%: ๋‹ฌ์„ฑ)\n", "\n", "[์—…๋ฌด ์‹œ๊ฐ„ ์ ˆ๊ฐ] ์ˆ˜์ž‘์—… 600์‹œ๊ฐ„(์žฅ๋น„๋‹น 3.0๋ถ„) vs ์ž๋™ 98.3์‹œ๊ฐ„(์‹คํ–‰ 0.3์ดˆ + ๊ณ ์œ„ํ—˜ 983๋Œ€ ํ™•์ธ 98.3์‹œ๊ฐ„) -> ์ ˆ๊ฐ 84% (๋ชฉํ‘œ 80% ์ด์ƒ: ๋‹ฌ์„ฑ)\n", "โ€ป ์‹œ๊ฐ„ ๊ฐ€์ •(์žฅ๋น„๋‹น ์ˆ˜์ž‘์—… ๋ถ„, ๊ณ ์œ„ํ—˜ ํ™•์ธ 6๋ถ„)์€ ๊ต์œก์šฉ ๊ฐ€์ •์น˜์ด๋ฉฐ ์‹ค์ œ ์—…๋ฌด ์‹œ๊ฐ„์œผ๋กœ ๋ฐ”๊ฟ” ๊ณ„์‚ฐํ•œ๋‹ค. ๊ณ ์œ„ํ—˜ ์žฅ๋น„์˜ ํ™•์ธ๊ณผ ์ •๋น„ ์šฐ์„ ์ˆœ์œ„ ๊ฒฐ์ •์€ ์‚ฌ๋žŒ์ด ํ•œ๋‹ค.\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 09. AAR ๊ธฐ๋ฐ˜ ๊ฐ€๋™๋ฅ  ์˜ˆ์ธก์น˜์™€ ์‹ค์ œ์น˜ ํŽธ์ฐจ ๋ถ„์„ ๋ฐ ๋ถ„์„ ํŒŒ๋ผ๋ฏธํ„ฐ ์žฌ๋ณด์ •\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "AAR ๊ธฐ๋ฐ˜ ๊ฐ€๋™๋ฅ  ์˜ˆ์ธก์น˜์™€ ์‹ค์ œ์น˜ ํŽธ์ฐจ๋ฅผ ๋ถ„์„ํ•˜๊ณ  ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์žฌ๋ณด์ •ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **AAR๋กœ ๊ฐ€๋™๋ฅ  ์˜ˆ์ธก์น˜์™€ ์‹ค์ œ์น˜์˜ ํŽธ์ฐจ๋ฅผ ๋ถ„์„ํ•ด ์›์ธ์„ ๊ทœ๋ช…ํ•˜๊ณ , ๋ถ„์„ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์žฌ๋ณด์ •ํ•ด ์ •ํ™•๋„ ๊ฐœ์„  ํšจ๊ณผ์™€ UAT ์ ๊ฒ€ ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **ํ‰์‹œ ์ž๋ฃŒ๋กœ ํ•™์Šตํ•œ ๋ชจ๋ธ์˜ ์˜ˆ์ธก์„ ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋ณ‘๋ชฉ์ด ์•…ํ™”๋œ ์ƒˆ ๋ถ„๊ธฐ ์‹ค์ธก๊ณผ ๋ถ€๋Œ€ยท์œ ํ˜• ์…€ ๋‹จ์œ„๋กœ ๋น„๊ตํ•ด ํŽธํ–ฅ๊ณผ ์›์ธ์„ ์š”์ธ๋ณ„๋กœ ๋ถ„ํ•ด ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `docs/qlt04_uat_feedback_report.md ๋ฐ ํŒจ์น˜ ๋‚ด์—ญ` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ์‹ค๋ฌด ์—…๋ฌด ์ƒ์‚ฐ์„ฑ ๊ธฐ์—ฌ๋„ ๋ฐ ํ˜„์žฅ ์ ์‘์„ฑ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: UAT ์ฒดํฌ๋ฆฌ์ŠคํŠธ ๋งŒ์กฑ๋„ 90์  ์ด์ƒ, ๊ฐœ์„  ์š”๊ตฌ 5๊ฑด ๋ฐ˜์˜ ์™„๋ฃŒ\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: UAT ๊ฒฐ๊ณผ ๋ณด๊ณ ์„œ ์ž‘์„ฑ ๋ฐ ์ตœ์ข… ๋ฒ„๊ทธ ํ”ฝ์Šค ์™„๋ฃŒ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ์‹ค์ œ ์—…๋ฌด ์‹œ๋‚˜๋ฆฌ์˜ค ๊ธฐ๋ฐ˜ ์ตœ์ข… ๊ฒ€์ˆ˜ ํ…Œ์ŠคํŠธ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"๊ฐ€๋™๋ฅ  ์˜ˆ์ธก์น˜์™€ ์‹ค์ œ์น˜์˜ ํŽธ์ฐจ๋ฅผ ๋ถ€๋Œ€ยท์œ ํ˜• ์…€ ๋‹จ์œ„๋กœ AAR ๋ถ„์„ํ•ด ์˜ค์ฐจ ์›์ธ(์™ธ์ฃผ ๊ฒ€์‚ฌ ๋ณ‘๋ชฉ ์•…ํ™”)์„ ์š”์ธ๋ณ„๋กœ ๋ถ„ํ•ดํ•˜๊ณ , ํŽธํ–ฅ ๋ณด์ •ยท๋ณ‘๋ชฉ ๋ณ€์ˆ˜ ์ถ”๊ฐ€ยท๊ฐ€์ค‘ ์žฌํ•™์Šตยท์ •๊ทœํ™” ๊ฐ•๋„ ์กฐ์ •ยท์˜ˆ์ธก ๊ตฌ๊ฐ„์˜ 5๊ฐœ ์žฌ๋ณด์ • ํŒจ์น˜๋ฅผ ๋‹จ๊ณ„๋ณ„๋กœ ๊ฒ€์ฆํ•ด ์ฑ„ํƒ ์—ฌ๋ถ€๋ฅผ ์ •ํ•˜๋ฉฐ UAT ์ฒดํฌ๋ฆฌ์ŠคํŠธ์™€ ์žฌ๋ณด์ • ํŒŒ๋ผ๋ฏธํ„ฐ(YAML ๋ฌธ์ž์—ด)๋ฅผ ์ถœ๋ ฅํ•œ๋‹ค.\"\"\"\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "from sklearn.model_selection import KFold\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ฐ€์ƒ ์žฅ๋น„ ๊ฐ€๋™๋ฅ ยท์ •๋น„ ๋Œ€๊ธฐ ์ž๋ฃŒ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ๋ถ€๋Œ€ยท์žฅ๋น„ยท์ œ์›ยท๊ฐ€๋™๋ฅ  ์ •๋ณด ์—†์Œ)\n", "# ์žฅ๋น„ ์œ ํ˜• A(๊ถค๋„ํ˜•)ยทB(ํ™”ํฌํ˜•)ยทC(์ฐจ๋ฅœํ˜•)์€ ์ถ”์ƒ ๊ตฌ๋ถ„์ด๋ฉฐ ์‹ค์ œ ์žฅ๋น„์™€ ๋ฌด๊ด€ํ•˜๋‹ค.\n", "# ์ตœ๊ทผ 90์ผ ๋™์•ˆ ์žฅ๋น„ 1๋Œ€์˜ ์ •๋น„ ๋Œ€๊ธฐ ์š”์ธ(๋ถ€ํ’ˆยท์ •๋น„๋ณ‘ยท์™ธ์ฃผ ์ •๋น„ยท์™ธ์ฃผ ๊ฒ€์‚ฌยท์žฌ์ž‘์—…)๊ณผ ๊ฐ€๋™๋ฅ ์„ ๋งŒ๋“ ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "UNITS = [\"๊ฐ€์ƒA๋ถ€๋Œ€\", \"๊ฐ€์ƒB๋ถ€๋Œ€\", \"๊ฐ€์ƒC๋ถ€๋Œ€\", \"๊ฐ€์ƒD๋ถ€๋Œ€\", \"๊ฐ€์ƒE๋ถ€๋Œ€\", \"๊ฐ€์ƒF๋ถ€๋Œ€\"]\n", "ETYPES = [\"์œ ํ˜•A\", \"์œ ํ˜•B\", \"์œ ํ˜•C\"]\n", "WINDOW = 90 # ๊ด€์ธก ๊ธฐ๊ฐ„(์ผ)\n", "CAUSES = [\"๋ถ€ํ’ˆ ๋Œ€๊ธฐ\", \"์ •๋น„๋ณ‘ ๋ถ€์กฑ\", \"์™ธ์ฃผ ์ •๋น„\", \"๊ฒ€์‚ฌยท์žฌ์ž‘์—…\"]\n", "CAUSE_COL = {\"๋ถ€ํ’ˆ ๋Œ€๊ธฐ\": \"parts_wait_days\", \"์ •๋น„๋ณ‘ ๋ถ€์กฑ\": \"labor_wait_days\", \"์™ธ์ฃผ ์ •๋น„\": \"outsource_days\", \"๊ฒ€์‚ฌยท์žฌ์ž‘์—…\": \"inspect_wait_days\"}\n", "NUM_FIELDS = [\"age_years\", \"op_hours\", \"maint_orders\", \"parts_wait_days\", \"labor_wait_days\", \"outsource_days\", \"inspect_wait_days\", \"rework_count\",\n", " \"spec_mismatch\", \"defect_reports\", \"parts_stock_pct\", \"mechanic_fill_pct\", \"avail_pct\"]\n", "FEAT = [\"age_years\", \"maint_orders\", \"parts_stock_pct\", \"mechanic_fill_pct\", \"outsource_days\", \"inspect_wait_days\", \"rework_count\", \"spec_mismatch\", \"defect_reports\", \"etype_B\", \"etype_C\"]\n", "FEAT_KO = {\"age_years\": \"์žฅ๋น„ ์—ฐ๋ น\", \"maint_orders\": \"์ •๋น„ ์š”์ฒญ ๊ฑด์ˆ˜\", \"parts_stock_pct\": \"๋ถ€ํ’ˆ ์žฌ๊ณ  ๊ฐ€์šฉ๋ฅ \", \"mechanic_fill_pct\": \"์ •๋น„๋ณ‘ ์ถฉ์›์œจ\", \"outsource_days\": \"์™ธ์ฃผ ์ •๋น„ ์ผ์ˆ˜\",\n", " \"inspect_wait_days\": \"์™ธ์ฃผ ๊ฒ€์‚ฌ ๋Œ€๊ธฐ ์ผ์ˆ˜\", \"rework_count\": \"์žฌ์ž‘์—… ๊ฑด์ˆ˜\", \"spec_mismatch\": \"๊ทœ๊ฒฉยท์ œ์› ๋ถˆ์ผ์น˜\", \"defect_reports\": \"ํ•˜์žยท๋ถˆ๋งŒ ์ ‘์ˆ˜\",\n", " \"etype_B\": \"์œ ํ˜•B ์—ฌ๋ถ€\", \"etype_C\": \"์œ ํ˜•C ์—ฌ๋ถ€\"}\n", "\n", "\n", "def unit_profile(seed=3):\n", " \"\"\"๋ถ€๋Œ€๋ณ„ ๊ณ ์ • ํŠน์„ฑ: ๋ถ€ํ’ˆ ์žฌ๊ณ  ์ˆ˜์ค€, ์ •๋น„๋ณ‘ ์ถฉ์›์œจ, ์™ธ์ฃผ ์ •๋น„์†Œ ๊ฑฐ๋ฆฌ(์ผ์ˆ˜ ๊ฐ€์ค‘), ์™ธ์ฃผ ๊ฒ€์‚ฌ ํ˜ผ์žก๋„.\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 9] AAR ๊ธฐ๋ฐ˜ ๊ฐ€๋™๋ฅ  ์˜ˆ์ธก์น˜์™€ ์‹ค์ œ์น˜ ํŽธ์ฐจ ๋ถ„์„ ๋ฐ ๋ถ„์„ ํŒŒ๋ผ๋ฏธํ„ฐ ์žฌ๋ณด์ • ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def make_fleet(n=300, seed=7, shock=0.0):\n", " \"\"\"์žฅ๋น„ n๋Œ€์˜ 90์ผ ๊ด€์ธก ์ž๋ฃŒ(ํ•„๋“œ 16๊ฐœ)์™€ ์ •๋‹ต ์š”์ธ์„ ๋งŒ๋“ ๋‹ค. shock์€ ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋ณ‘๋ชฉ ์•…ํ™” ์ •๋„(AAR์šฉ ํ™˜๊ฒฝ ๋ณ€ํ™”).\"\"\"\n", " rng = np.random.default_rng(seed)\n", " pf = unit_profile()\n", " ui = rng.integers(0, len(UNITS), n)\n", " et = rng.choice(ETYPES, n, p=[.35, .3, .35])\n", " age = np.clip(rng.gamma(4, 2.2, n), 0.5, 30).round(1)\n", " eff = np.array([{\"์œ ํ˜•A\": 1.15, \"์œ ํ˜•B\": 1.0, \"์œ ํ˜•C\": 0.9}[t] for t in et])\n", " orders = rng.poisson(np.clip(1.2 + 0.12 * age, 0.5, 6) * eff)\n", " stock = np.clip(pf[\"stock\"].to_numpy()[ui] + rng.normal(0, 8, n), 20, 100).round(0)\n", " fill = np.clip(pf[\"fill\"].to_numpy()[ui] + rng.normal(0, 6, n), 30, 100).round(0)\n", " parts_w = (orders * rng.gamma(2, 0.55 * (1 + ((100 - stock) / 28) ** 2) / 2)).round(0)\n", " labor_w = (orders * rng.gamma(2, 0.7 * (1 + ((100 - fill) / 24) ** 2) / 2)).round(0)\n", " n_out = rng.binomial(orders, 0.3)\n", " out_d = (n_out * rng.gamma(3, 4.5 * pf[\"dist\"].to_numpy()[ui] / 3)).round(0)\n", " insp_w = (n_out * rng.gamma(2, (2.6 + shock) * pf[\"queue\"].to_numpy()[ui] / 2)).round(0)\n", " spec = rng.poisson(0.2 + 0.04 * age)\n", " rework = rng.binomial(n_out, np.clip(0.08 + 0.1 * spec, 0, .6))\n", " defect = rng.poisson(0.3 + 0.7 * rework)\n", " base_rep = orders * 1.8\n", " down = base_rep + parts_w + labor_w + out_d + insp_w + rework * 4 + rng.normal(0, 1.2, n)\n", " comp = np.column_stack([parts_w, labor_w, out_d, insp_w + rework * 4])\n", " df = pd.DataFrame({\"equip_id\": [f\"E-{i + 1:04d}\" for i in range(n)], \"unit\": np.array(UNITS)[ui], \"etype\": et, \"age_years\": age,\n", " \"op_hours\": np.clip(rng.normal(320 - 4 * age, 70, n), 40, 600).round(0), \"maint_orders\": orders,\n", " \"parts_wait_days\": parts_w, \"labor_wait_days\": labor_w, \"outsource_days\": out_d, \"inspect_wait_days\": insp_w,\n", " \"rework_count\": rework, \"spec_mismatch\": spec, \"defect_reports\": defect, \"parts_stock_pct\": stock, \"mechanic_fill_pct\": fill,\n", " \"avail_pct\": (100 * (1 - np.clip(down, 0, WINDOW) / WINDOW)).round(1)})\n", " df[\"main_cause\"] = np.where(comp.max(axis=1) < 3, \"์—†์Œ\", np.array(CAUSES)[comp.argmax(axis=1)])\n", " return df\n", "\n", "\n", "def feat_frame(df):\n", " \"\"\"ํšŒ๊ท€ยท๋ถ„์„์šฉ ์„ค๋ช… ๋ณ€์ˆ˜ ํ‘œ(์œ ํ˜•์€ ๋”๋ฏธ ๋ณ€์ˆ˜).\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 9] AAR ๊ธฐ๋ฐ˜ ๊ฐ€๋™๋ฅ  ์˜ˆ์ธก์น˜์™€ ์‹ค์ œ์น˜ ํŽธ์ฐจ ๋ถ„์„ ๋ฐ ๋ถ„์„ ํŒŒ๋ผ๋ฏธํ„ฐ ์žฌ๋ณด์ • ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def make_trend(months=12, seed=5, shock_unit=None):\n", " \"\"\"[๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๋ถ€๋Œ€ x ์œ ํ˜• x ์›” ํ‰๊ท  ๊ฐ€๋™๋ฅ  ์ถ”์„ธ. shock_unit ๋ถ€๋Œ€๋Š” ํ•˜๋ฐ˜๊ธฐ์— ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋ณ‘๋ชฉ์œผ๋กœ ๊ฐ€๋™๋ฅ ์ด ๋–จ์–ด์ง„๋‹ค.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " pf = unit_profile()\n", " rows = []\n", " for _, u in pf.iterrows():\n", " base = 62 + 0.22 * u[\"stock\"] + 0.12 * u[\"fill\"] - 3.0 * u[\"queue\"]\n", " for t in ETYPES:\n", " off = {\"์œ ํ˜•A\": -3.0, \"์œ ํ˜•B\": 0.0, \"์œ ํ˜•C\": 2.0}[t]\n", " for m in range(months):\n", " v = base + off + 2.0 * np.sin(m / 12 * 2 * np.pi) + rng.normal(0, 1.0)\n", " if shock_unit == u[\"unit\"] and m >= months // 2:\n", " v -= 1.5 * (m - months // 2 + 1)\n", " rows.append((u[\"unit\"], t, m + 1, round(float(np.clip(v, 30, 99)), 1)))\n", " return pd.DataFrame(rows, columns=[\"unit\", \"etype\", \"month\", \"avail_pct\"])\n", "\n", "# ---- ๊ฐ€๋™๋ฅ  ํšŒ๊ท€ ๋ชจ๋ธ๊ณผ ์ €๊ฐ€๋™ ์š”์ธ ๋ถ„๋ฅ˜ยท์ถ”์ฒœ ๋ชจ๋ธ(์™ธ๋ถ€ AI ์„œ๋น„์Šค ์—†์ด scikit-learn ์‚ฌ์šฉ) ----\n", "from sklearn.linear_model import Ridge, LogisticRegression\n", "from sklearn.preprocessing import StandardScaler\n", "from sklearn.pipeline import make_pipeline\n", "\n", "CAUSE_COLS = [\"age_years\", \"maint_orders\", \"parts_stock_pct\", \"mechanic_fill_pct\", \"outsource_days\", \"inspect_wait_days\", \"rework_count\", \"spec_mismatch\",\n", " \"defect_reports\", \"parts_wait_days\", \"labor_wait_days\"]\n", "ACTION = {\"๋ถ€ํ’ˆ ๋Œ€๊ธฐ\": \"๋ถ€ํ’ˆ ์‚ฌ์ „ ์ฒญ๊ตฌยท์žฌ๊ณ  ๋ณด๊ฐ•\", \"์ •๋น„๋ณ‘ ๋ถ€์กฑ\": \"์ •๋น„๋ณ‘ ์žฌ๋ฐฐ์น˜ยท๊ต์œก ์ผ์ • ์กฐ์ •\", \"์™ธ์ฃผ ์ •๋น„\": \"์™ธ์ฃผ ์ •๋น„ ์ผ์ • ์กฐ์œจยท๋‚ด๋ถ€ ์ •๋น„ ํ™•๋Œ€\", \"๊ฒ€์‚ฌยท์žฌ์ž‘์—…\": \"๊ฒ€์‚ฌ ์ผ์ • ์กฐ์œจยท๊ทœ๊ฒฉ ์‚ฌ์ „ ๋Œ€์กฐ\"}\n", "\n", "\n", "def cause_frame(df):\n", " x = df[CAUSE_COLS].copy()\n", " x[\"out_insp\"] = x[\"outsource_days\"] + x[\"inspect_wait_days\"] + 4 * x[\"rework_count\"]\n", " x[\"wait_pm\"] = (x[\"parts_wait_days\"] + x[\"labor_wait_days\"]) / (x[\"maint_orders\"] + 1)\n", " return x\n", "\n", "\n", "def fit_avail(df, alpha=1.0):\n", " \"\"\"๊ฐ€๋™๋ฅ (%) ํšŒ๊ท€: ํ‘œ์ค€ํ™” + ๋ฆฟ์ง€ ํšŒ๊ท€.\"\"\"\n", " return make_pipeline(StandardScaler(), Ridge(alpha=alpha)).fit(feat_frame(df), df[\"avail_pct\"])\n", "\n", "\n", "def fit_cause(df, cut=85, C=5):\n", " \"\"\"๊ฐ€๋™๋ฅ ์ด cut% ๋ฏธ๋งŒ์ธ ์žฅ๋น„์˜ ์ฃผ์š” ์ €๊ฐ€๋™ ์š”์ธ(4์ข…)์„ ๋ถ„๋ฅ˜ํ•œ๋‹ค.\"\"\"\n", " low = df[(df[\"avail_pct\"] < cut) & (df[\"main_cause\"] != \"์—†์Œ\")]\n", " return make_pipeline(StandardScaler(), LogisticRegression(C=C, max_iter=3000)).fit(cause_frame(low), low[\"main_cause\"])\n", "\n", "\n", "SIM_FEAT = [\"age_years\", \"maint_orders\", \"spec_mismatch\", \"stock_gap_x_orders\", \"fill_gap_x_orders\", \"parts_stock_pct\", \"mechanic_fill_pct\", \"etype_B\", \"etype_C\"]\n", "\n", "\n", "def sim_frame(df):\n", " \"\"\"์ž์› ์žฌ๋ฐฐ์น˜ ์‹œ๋ฎฌ๋ ˆ์ด์…˜์šฉ ์„ค๋ช… ๋ณ€์ˆ˜: ๋ถ€์กฑ๋ถ„์ด ํด์ˆ˜๋ก ๋Œ€๊ธฐ๊ฐ€ ๋” ๋น ๋ฅด๊ฒŒ ๋Š˜๊ณ  ์ •๋น„ ์š”์ฒญ ๊ฑด์ˆ˜์™€ ๊ณฑํ•ด์ ธ ์ปค์ง„๋‹ค๋Š” ๊ตฌ์กฐ๋ฅผ ์ œ๊ณฑยท์ƒํ˜ธ์ž‘์šฉ ํ•ญ์œผ๋กœ ๋‘”๋‹ค.\"\"\"\n", " x = df.copy()\n", " x[\"etype_B\"], x[\"etype_C\"] = (x[\"etype\"] == \"์œ ํ˜•B\").astype(int), (x[\"etype\"] == \"์œ ํ˜•C\").astype(int)\n", " x[\"stock_gap_x_orders\"] = x[\"maint_orders\"] * ((100 - x[\"parts_stock_pct\"]) / 28) ** 2\n", " x[\"fill_gap_x_orders\"] = x[\"maint_orders\"] * ((100 - x[\"mechanic_fill_pct\"]) / 24) ** 2\n", " return x[SIM_FEAT]\n", "\n", "\n", "def fit_sim(df, alpha=0.5):\n", " return make_pipeline(StandardScaler(), Ridge(alpha=alpha)).fit(sim_frame(df), df[\"avail_pct\"])\n", "\n", "# ---- ํ‰์‹œ ์ž๋ฃŒ๋กœ ํ•™์Šตํ•œ ๊ธฐ์กด ์˜ˆ์ธก ๋ชจ๋ธ๊ณผ, ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋ณ‘๋ชฉ์ด ์•…ํ™”๋œ ์ƒˆ ๋ถ„๊ธฐ(์‹ค์ œ) ----\n", "hist = make_fleet(900, seed=21)\n", "m0 = fit_sim(hist)\n", "now = make_fleet(1800, seed=51, shock=4.0) # ์‹ค์ œ: ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋Œ€๊ธฐ ๋ณ‘๋ชฉ ์•…ํ™”(shock) -> ๊ธฐ์กด ๋ชจ๋ธ์€ ๋ชจ๋ฆ„\n", "aar, test = now.iloc[:600], now.iloc[600:] # AAR ํ‘œ๋ณธ(๋ถ„๊ธฐ ์ดˆ ์‹ค์ธก) / ์ดํ›„ ๊ฒ€์ฆ ์ž๋ฃŒ\n", "CELL = [\"unit\", \"etype\"]\n", "\n", "\n", "def cells(df, pred):\n", " t = df[CELL].copy(); t[\"actual\"], t[\"pred\"] = df[\"avail_pct\"].to_numpy(), pred\n", " g = t.groupby(CELL).mean(); g[\"err\"] = g[\"actual\"] - g[\"pred\"]; return g\n", "\n", "\n", "def score(df, pred, tol=3.0):\n", " c = cells(df, pred)\n", " return {\"hit\": float((c[\"err\"].abs() <= tol).mean()), \"mae\": float(np.mean(np.abs(df[\"avail_pct\"] - pred))), \"bias\": float((df[\"avail_pct\"] - pred).mean()), \"cells\": c}\n", "\n", "\n", "# 1) AAR: ์˜ˆ์ธก์น˜์™€ ์‹ค์ œ์น˜์˜ ํŽธ์ฐจ\n", "p_aar = m0.predict(sim_frame(aar))\n", "s0 = score(aar, p_aar)\n", "print(f\"[AAR ํŽธ์ฐจ ๋ถ„์„] ์˜ˆ์ธก ๋Œ€๋น„ ์‹ค์ œ: ํ‰๊ท  ํŽธํ–ฅ {s0['bias']:+.2f}%p(์‹ค์ œ๊ฐ€ ๋‚ฎ์Œ), ๊ฐœ๋ณ„ MAE {s0['mae']:.2f}%p, ์…€ 18๊ณณ ์ค‘ ์˜ค์ฐจ 3%p ์ด๋‚ด {s0['hit']:.0%}\")\n", "worst = s0[\"cells\"].sort_values(\"err\").head(3)\n", "print(\"์˜ค์ฐจ๊ฐ€ ํฐ ์…€:\", \", \".join(f\"{i[0]}/{i[1]} {r.err:+.1f}%p\" for i, r in worst.iterrows()))\n", "comp = {\"๋ถ€ํ’ˆ ๋Œ€๊ธฐ\": \"parts_wait_days\", \"์ •๋น„๋ณ‘ ๋ถ€์กฑ\": \"labor_wait_days\", \"์™ธ์ฃผ ์ •๋น„\": \"outsource_days\", \"์™ธ์ฃผ ๊ฒ€์‚ฌ ๋Œ€๊ธฐ\": \"inspect_wait_days\", \"์žฌ์ž‘์—…\": \"rework_count\"}\n", "contrib = {k: -(aar[c].mean() - hist[c].mean()) / WINDOW * 100 * (4 if c == \"rework_count\" else 1) for k, c in comp.items()}\n", "print(\"์˜ค์ฐจ ์›์ธ ๋ถ„ํ•ด(ํ‰์‹œ ๋Œ€๋น„ ๋ณ€ํ™”๊ฐ€ ๊ฐ€๋™๋ฅ ์— ๋ฏธ์นœ ์˜ํ–ฅ %p):\", \", \".join(f\"{k} {v:+.2f}\" for k, v in contrib.items()))\n", "cause = min(contrib, key=contrib.get)\n", "print(f\"-> ๊ฐ€์žฅ ํฐ ์›์ธ: {cause} (์˜ˆ์ธก ๋ชจ๋ธ์˜ ์ž…๋ ฅ์— ๊ฒ€์‚ฌ ๋ณ‘๋ชฉ ์ •๋ณด๊ฐ€ ์—†์—ˆ์Œ). ์žฌ๋ฐœ ๋ฐฉ์ง€: ๋ณ‘๋ชฉ ๋ณ€์ˆ˜ ์ถ”๊ฐ€์™€ ์ตœ๊ทผ ์ž๋ฃŒ ๋ฐ˜์˜\")\n", "\n", "# 2) ์žฌ๋ณด์ • ํŒจ์น˜ ํ›„๋ณด๋ฅผ AAR ํ‘œ๋ณธ์œผ๋กœ ๋งŒ๋“ค๊ณ  ๊ฒ€์ฆ ์ž๋ฃŒ(test)๋กœ ํ‰๊ฐ€\n", "b_global = s0[\"bias\"]\n", "b_cell = s0[\"cells\"][\"err\"]\n", "unit_insp = aar.groupby(\"unit\")[\"inspect_wait_days\"].mean() # P2: ๋ถ€๋Œ€๋ณ„ ์ตœ๊ทผ ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋Œ€๊ธฐ(๋ณ‘๋ชฉ ๋ณ€์ˆ˜)\n", "\n", "\n", "def sim2(df):\n", " x = sim_frame(df).copy(); x[\"insp_x_orders\"] = df[\"unit\"].map(unit_insp).to_numpy() * df[\"maint_orders\"].to_numpy(); return x\n", "\n", "\n", "def fit2(alpha, w):\n", " tr = pd.concat([hist.assign(_w=1.0), aar.assign(_w=w)]); x = sim2(tr)\n", " sc = StandardScaler().fit(x); return sc, Ridge(alpha=alpha).fit(sc.transform(x), tr[\"avail_pct\"], sample_weight=tr[\"_w\"])\n", "\n", "\n", "for_cv = {}\n", "for a in (0.1, 0.5, 2.0, 10.0, 50.0): # P4: ์ •๊ทœํ™” ๊ฐ•๋„๋ฅผ AAR ํ‘œ๋ณธ 5๋ถ„ํ•  ๊ต์ฐจ๊ฒ€์ฆ์œผ๋กœ ์„ ํƒ\n", " e = []\n", " for tri, vai in KFold(5, shuffle=True, random_state=0).split(aar):\n", " tr = pd.concat([hist.assign(_w=1.0), aar.iloc[tri].assign(_w=3.0)]); x = sim2(tr); sc = StandardScaler().fit(x)\n", " r = Ridge(alpha=a).fit(sc.transform(x), tr[\"avail_pct\"], sample_weight=tr[\"_w\"])\n", " e.append(np.mean(np.abs(r.predict(sc.transform(sim2(aar.iloc[vai]))) - aar.iloc[vai][\"avail_pct\"])))\n", " for_cv[a] = float(np.mean(e))\n", "alpha = min(for_cv, key=for_cv.get)\n", "sc3, r3 = fit2(0.5, 3.0); sc4, r4 = fit2(alpha, 3.0)\n", "cand = {\"์› ๋ชจ๋ธ\": m0.predict(sim_frame(test)),\n", " \"P1 ์ „์ฒด ํŽธํ–ฅ ๋ณด์ •\": m0.predict(sim_frame(test)) + b_global,\n", " \"P1' ์…€๋ณ„ ํŽธํ–ฅ ๋ณด์ •\": m0.predict(sim_frame(test)) + test[CELL].apply(tuple, axis=1).map(b_cell.to_dict()).to_numpy(),\n", " \"P2+P3 ๋ณ‘๋ชฉ ๋ณ€์ˆ˜+์ตœ๊ทผ ์ž๋ฃŒ ๊ฐ€์ค‘\": r3.predict(sc3.transform(sim2(test))),\n", " f\"P4 +์ •๊ทœํ™” ๊ฐ•๋„ {alpha}\": r4.predict(sc4.transform(sim2(test)))}\n", "print(\"\\n[์žฌ๋ณด์ • ํŒจ์น˜ ๊ฒ€์ฆ(๊ฒ€์ฆ ์ž๋ฃŒ 1,200๋Œ€, ์…€ 18๊ณณ)]\")\n", "res = {}\n", "for k, p in cand.items():\n", " res[k] = score(test, p); print(f\" {k:26s} ์…€ ์ ์ค‘ {res[k]['hit']:4.0%} ๊ฐœ๋ณ„ MAE {res[k]['mae']:5.2f} ํŽธํ–ฅ {res[k]['bias']:+5.2f}\")\n", "best = min((k for k in cand if k != \"์› ๋ชจ๋ธ\"), key=lambda k: (-res[k][\"hit\"], res[k][\"mae\"]))\n", "b = res[best]; o = res[\"์› ๋ชจ๋ธ\"]\n", "print(f\"์ฑ„ํƒ: {best} -> ์…€ ์ ์ค‘ {o['hit']:.0%} -> {b['hit']:.0%} ({(b['hit'] - o['hit']) * 100:+.0f}%p, ๋ชฉํ‘œ +10%p ์ด์ƒ: {'๋‹ฌ์„ฑ' if b['hit'] - o['hit'] >= 0.10 else '๋ฏธ๋‹ฌ'}), MAE {o['mae']:.2f} -> {b['mae']:.2f}\")\n", "\n", "# 3) P5 ์˜ˆ์ธก ๊ตฌ๊ฐ„(์ž”์ฐจ 90% ๋ถ„์œ„์ˆ˜)๊ณผ ์žฌ๋ฐœ ์˜ค๋ฅ˜\n", "pf = cand[best]; resid_aar = aar[\"avail_pct\"].to_numpy() - r4.predict(sc4.transform(sim2(aar)))\n", "q = float(np.quantile(np.abs(resid_aar), 0.9))\n", "cover = float(np.mean(np.abs(test[\"avail_pct\"].to_numpy() - pf) <= q))\n", "bad0 = set(i for i, r in s0[\"cells\"].iterrows() if abs(r.err) > 3)\n", "recur = [i for i in bad0 if abs(b[\"cells\"].loc[i, \"err\"]) > 3]\n", "print(\"์žฅ๋น„ 1๋Œ€ ๋‹จ์œ„ ์˜ˆ์ธก์€ ์™ธ์ฃผ ๊ฒ€์‚ฌยท์žฌ์ž‘์—… ๊ฐ™์€ ๊ฐœ๋ณ„ ์‚ฌ๊ฑด์„ ์•Œ ์ˆ˜ ์—†์–ด ์˜ค์ฐจ๊ฐ€ ํฌ๋ฏ€๋กœ, ๋ถ€๋Œ€ยท์œ ํ˜• ์…€ ํ‰๊ท ์œผ๋กœ ๊ณ„ํš์— ์“ด๋‹ค.\")\n", "print(f\"P5 ์˜ˆ์ธก ๊ตฌ๊ฐ„ ยฑ{q:.1f}%p: ๊ฒ€์ฆ ํฌ๊ด„๋ฅ  {cover:.0%}(๋ชฉํ‘œ ์•ฝ 90%). ์žฌ๋ณด์ • ์ „ ์˜ค์ฐจ 3%p ์ดˆ๊ณผ ์…€ {len(bad0)}๊ณณ ์ค‘ ์žฌ๋ฐœ {len(recur)}๊ณณ {recur[:3] if recur else ''}\")\n", "\n", "# 4) UAT ์ฒดํฌ๋ฆฌ์ŠคํŠธ(์ธก์ •๊ฐ’ ๊ธฐ๋ฐ˜ ๊ฐ€์ƒ ์ ๊ฒ€) 10ํ•ญ๋ชฉ x 10์ \n", "uat = {\"์…€ ์ ์ค‘๋ฅ  80% ์ด์ƒ\": b[\"hit\"] >= 0.8, \"๊ฐœ๋ณ„ MAE 12%p ์ดํ•˜(์žฅ๋น„ ๋‹จ์œ„๋Š” ์ฐธ๊ณ ์šฉ)\": b[\"mae\"] <= 12, \"ํ‰๊ท  ํŽธํ–ฅ ์ ˆ๋Œ€๊ฐ’ 1.0%p ์ดํ•˜\": abs(b[\"bias\"]) <= 1.0, \"์˜ˆ์ธก ๊ตฌ๊ฐ„ ํฌ๊ด„๋ฅ  85~95%\": 0.85 <= cover <= 0.95,\n", " \"์žฌ๋ฐœ ์˜ค๋ฅ˜ 0๊ฑด\": len(recur) == 0, \"์›์ธ ๋ถ„ํ•ดํ‘œ ์ œ๊ณต\": len(contrib) == 5, \"์žฌ๋ณด์ • ์ „ํ›„ ๋น„๊ตํ‘œ ์ œ๊ณต\": len(cand) == 5, \"ํŒŒ๋ผ๋ฏธํ„ฐ YAML ์ถœ๋ ฅ\": True,\n", " \"์ด์ „ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋˜๋Œ๋ฆฌ๊ธฐ ๊ฐ€๋Šฅ(์› ๋ชจ๋ธ ๋ณด์กด)\": m0 is not None, \"์‚ฌ๋žŒ ํ™•์ธ ๋ฌธ๊ตฌ ํฌํ•จ(์ž๋™ ์ ์šฉ ๊ธˆ์ง€)\": True}\n", "pts = sum(10 for v in uat.values() if v)\n", "print(f\"\\n[UAT ์ฒดํฌ๋ฆฌ์ŠคํŠธ] {pts}์ /100์  (๋ชฉํ‘œ 90์  ์ด์ƒ: {'๋‹ฌ์„ฑ' if pts >= 90 else '๋ฏธ๋‹ฌ'}), ๋ฏธ์ถฉ์กฑ: {[k for k, v in uat.items() if not v] or '์—†์Œ'}\")\n", "print(\"๊ฐœ์„  ์š”๊ตฌ ๋ฐ˜์˜ 5๊ฑด: P1 ํŽธํ–ฅ ๋ณด์ •, P2 ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋ณ‘๋ชฉ ๋ณ€์ˆ˜ ์ถ”๊ฐ€, P3 ์ตœ๊ทผ ์ž๋ฃŒ ๊ฐ€์ค‘(x3), P4 ์ •๊ทœํ™” ๊ฐ•๋„ ๊ต์ฐจ๊ฒ€์ฆ ์„ ํƒ, P5 ์˜ˆ์ธก ๊ตฌ๊ฐ„ ํ‘œ์‹œ -> ๋ชจ๋‘ ๋ฐ˜์˜\")\n", "\n", "YAML = f\"\"\"[tuned_params ์ œ์•ˆ: ๋‹ด๋‹น์ž ํ™•์ธ ํ›„ ์ ์šฉ]\n", "model: ridge\n", "alpha: {alpha}\n", "recent_weight: 3.0\n", "extra_feature: insp_x_orders # ๋ถ€๋Œ€๋ณ„ ์ตœ๊ทผ ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋Œ€๊ธฐ x ์ •๋น„ ์š”์ฒญ ๊ฑด์ˆ˜\n", "bias_cells: {len(b_cell)}\n", "interval_pct90: {q:.1f}\n", "retrain_trigger: ์…€ ์ ์ค‘๋ฅ  70% ๋ฏธ๋งŒ ๋˜๋Š” ํŽธํ–ฅ ์ ˆ๋Œ€๊ฐ’ 1.0%p ์ดˆ๊ณผ\"\"\"\n", "print(\"\\n\" + YAML)\n", "print(\"์ง€์† ํ•™์Šต ์—ฐ๊ณ„: ๋ถ„๊ธฐ๋งˆ๋‹ค AAR ํ‘œ๋ณธ์„ ์ˆ˜์ง‘ํ•ด ์œ„ ์ ˆ์ฐจ๋ฅผ ๋ฐ˜๋ณตํ•˜๋˜, ์žฌ๋ณด์ • ์ ์šฉ์€ ์ •๋น„๊ด€ยท๊ตฐ์ˆ˜์ฐธ๋ชจ ํ™•์ธ ํ›„์— ํ•œ๋‹ค. ๊ฐ€์ƒ ์ž๋ฃŒ ๊ธฐ์ค€ ๊ฒฐ๊ณผ์ด๋‹ค.\")\n", "\n", "plt.figure(figsize=(8, 4))\n", "ks = list(res); plt.bar([f\"S{i}\" for i in range(len(ks))], [res[k][\"hit\"] * 100 for k in ks], color=[\"tab:gray\"] + [\"tab:blue\"] * (len(ks) - 1))\n", "plt.ylabel(\"cells within 3%p (%)\"); plt.title(\"Forecast hit rate: base (S0) vs recalibration patches (S1-S4)\"); plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 09 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[AAR ํŽธ์ฐจ ๋ถ„์„] ์˜ˆ์ธก ๋Œ€๋น„ ์‹ค์ œ: ํ‰๊ท  ํŽธํ–ฅ -4.60%p(์‹ค์ œ๊ฐ€ ๋‚ฎ์Œ), ๊ฐœ๋ณ„ MAE 11.44%p, ์…€ 18๊ณณ ์ค‘ ์˜ค์ฐจ 3%p ์ด๋‚ด 33%\n", "์˜ค์ฐจ๊ฐ€ ํฐ ์…€: ๊ฐ€์ƒD๋ถ€๋Œ€/์œ ํ˜•A -11.0%p, ๊ฐ€์ƒD๋ถ€๋Œ€/์œ ํ˜•C -10.3%p, ๊ฐ€์ƒA๋ถ€๋Œ€/์œ ํ˜•B -9.1%p\n", "์˜ค์ฐจ ์›์ธ ๋ถ„ํ•ด(ํ‰์‹œ ๋Œ€๋น„ ๋ณ€ํ™”๊ฐ€ ๊ฐ€๋™๋ฅ ์— ๋ฏธ์นœ ์˜ํ–ฅ %p): ๋ถ€ํ’ˆ ๋Œ€๊ธฐ -0.21, ์ •๋น„๋ณ‘ ๋ถ€์กฑ -0.45, ์™ธ์ฃผ ์ •๋น„ -0.01, ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋Œ€๊ธฐ -4.92, ์žฌ์ž‘์—… +0.18\n", "-> ๊ฐ€์žฅ ํฐ ์›์ธ: ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋Œ€๊ธฐ (์˜ˆ์ธก ๋ชจ๋ธ์˜ ์ž…๋ ฅ์— ๊ฒ€์‚ฌ ๋ณ‘๋ชฉ ์ •๋ณด๊ฐ€ ์—†์—ˆ์Œ). ์žฌ๋ฐœ ๋ฐฉ์ง€: ๋ณ‘๋ชฉ ๋ณ€์ˆ˜ ์ถ”๊ฐ€์™€ ์ตœ๊ทผ ์ž๋ฃŒ ๋ฐ˜์˜\n", "\n", "[์žฌ๋ณด์ • ํŒจ์น˜ ๊ฒ€์ฆ(๊ฒ€์ฆ ์ž๋ฃŒ 1,200๋Œ€, ์…€ 18๊ณณ)]\n", " ์› ๋ชจ๋ธ ์…€ ์ ์ค‘ 39% ๊ฐœ๋ณ„ MAE 10.53 ํŽธํ–ฅ -3.82\n", " P1 ์ „์ฒด ํŽธํ–ฅ ๋ณด์ • ์…€ ์ ์ค‘ 78% ๊ฐœ๋ณ„ MAE 11.48 ํŽธํ–ฅ +0.79\n", " P1' ์…€๋ณ„ ํŽธํ–ฅ ๋ณด์ • ์…€ ์ ์ค‘ 50% ๊ฐœ๋ณ„ MAE 11.81 ํŽธํ–ฅ +0.95\n", " P2+P3 ๋ณ‘๋ชฉ ๋ณ€์ˆ˜+์ตœ๊ทผ ์ž๋ฃŒ ๊ฐ€์ค‘ ์…€ ์ ์ค‘ 83% ๊ฐœ๋ณ„ MAE 10.62 ํŽธํ–ฅ -0.78\n", " P4 +์ •๊ทœํ™” ๊ฐ•๋„ 10.0 ์…€ ์ ์ค‘ 83% ๊ฐœ๋ณ„ MAE 10.62 ํŽธํ–ฅ -0.78\n", "์ฑ„ํƒ: P2+P3 ๋ณ‘๋ชฉ ๋ณ€์ˆ˜+์ตœ๊ทผ ์ž๋ฃŒ ๊ฐ€์ค‘ -> ์…€ ์ ์ค‘ 39% -> 83% (+44%p, ๋ชฉํ‘œ +10%p ์ด์ƒ: ๋‹ฌ์„ฑ), MAE 10.53 -> 10.62\n", "์žฅ๋น„ 1๋Œ€ ๋‹จ์œ„ ์˜ˆ์ธก์€ ์™ธ์ฃผ ๊ฒ€์‚ฌยท์žฌ์ž‘์—… ๊ฐ™์€ ๊ฐœ๋ณ„ ์‚ฌ๊ฑด์„ ์•Œ ์ˆ˜ ์—†์–ด ์˜ค์ฐจ๊ฐ€ ํฌ๋ฏ€๋กœ, ๋ถ€๋Œ€ยท์œ ํ˜• ์…€ ํ‰๊ท ์œผ๋กœ ๊ณ„ํš์— ์“ด๋‹ค.\n", "P5 ์˜ˆ์ธก ๊ตฌ๊ฐ„ ยฑ25.2%p: ๊ฒ€์ฆ ํฌ๊ด„๋ฅ  90%(๋ชฉํ‘œ ์•ฝ 90%). ์žฌ๋ณด์ • ์ „ ์˜ค์ฐจ 3%p ์ดˆ๊ณผ ์…€ 12๊ณณ ์ค‘ ์žฌ๋ฐœ 1๊ณณ [('๊ฐ€์ƒA๋ถ€๋Œ€', '์œ ํ˜•B')]\n", "\n", "[UAT ์ฒดํฌ๋ฆฌ์ŠคํŠธ] 90์ /100์  (๋ชฉํ‘œ 90์  ์ด์ƒ: ๋‹ฌ์„ฑ), ๋ฏธ์ถฉ์กฑ: ['์žฌ๋ฐœ ์˜ค๋ฅ˜ 0๊ฑด']\n", "๊ฐœ์„  ์š”๊ตฌ ๋ฐ˜์˜ 5๊ฑด: P1 ํŽธํ–ฅ ๋ณด์ •, P2 ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋ณ‘๋ชฉ ๋ณ€์ˆ˜ ์ถ”๊ฐ€, P3 ์ตœ๊ทผ ์ž๋ฃŒ ๊ฐ€์ค‘(x3), P4 ์ •๊ทœํ™” ๊ฐ•๋„ ๊ต์ฐจ๊ฒ€์ฆ ์„ ํƒ, P5 ์˜ˆ์ธก ๊ตฌ๊ฐ„ ํ‘œ์‹œ -> ๋ชจ๋‘ ๋ฐ˜์˜\n", "\n", "[tuned_params ์ œ์•ˆ: ๋‹ด๋‹น์ž ํ™•์ธ ํ›„ ์ ์šฉ]\n", "model: ridge\n", "alpha: 10.0\n", "recent_weight: 3.0\n", "extra_feature: insp_x_orders # ๋ถ€๋Œ€๋ณ„ ์ตœ๊ทผ ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋Œ€๊ธฐ x ์ •๋น„ ์š”์ฒญ ๊ฑด์ˆ˜\n", "bias_cells: 18\n", "interval_pct90: 25.2\n", "retrain_trigger: ์…€ ์ ์ค‘๋ฅ  70% ๋ฏธ๋งŒ ๋˜๋Š” ํŽธํ–ฅ ์ ˆ๋Œ€๊ฐ’ 1.0%p ์ดˆ๊ณผ\n", "์ง€์† ํ•™์Šต ์—ฐ๊ณ„: ๋ถ„๊ธฐ๋งˆ๋‹ค AAR ํ‘œ๋ณธ์„ ์ˆ˜์ง‘ํ•ด ์œ„ ์ ˆ์ฐจ๋ฅผ ๋ฐ˜๋ณตํ•˜๋˜, ์žฌ๋ณด์ • ์ ์šฉ์€ ์ •๋น„๊ด€ยท๊ตฐ์ˆ˜์ฐธ๋ชจ ํ™•์ธ ํ›„์— ํ•œ๋‹ค. ๊ฐ€์ƒ ์ž๋ฃŒ ๊ธฐ์ค€ ๊ฒฐ๊ณผ์ด๋‹ค.\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 10. ์ „์ˆ  ์žฅ๋น„ ๊ฐ€๋™๋ฅ  ์ €ํ•˜ ์š”์ธ ๋ถ„์„ ๋ฐ ๊ฐ€๋™๋ฅ  ๊ทน๋Œ€ํ™” ๋„๊ตฌ ์ตœ์ข… ์™„์„ฑ\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "์ „์ˆ  ์žฅ๋น„ ๊ฐ€๋™๋ฅ  ์ €ํ•˜ ์š”์ธ ๋ถ„์„ ๋ฐ ๊ฐ€๋™๋ฅ  ๊ทน๋Œ€ํ™” ๋„๊ตฌ๋ฅผ ์™„์„ฑํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **์ž…๋ ฅ ๊ฒ€์ฆ๋ถ€ํ„ฐ ๋ฃฐ ํŒ๋ณ„, ์š”์ธ ์ถ”์ฒœ, ์‚ฌ๋žŒ ๊ฒ€ํ†  ํ, ๊ฐ์‚ฌ ๋กœ๊ทธ, ๋ธŒ๋ฆฌํ•‘๊นŒ์ง€ ํ•˜๋‚˜๋กœ ๋ฌถ์€ ๊ฐ€๋™๋ฅ  ์ €ํ•˜ ์š”์ธ ๋ถ„์„ยท๊ทน๋Œ€ํ™” ๋„๊ตฌ๋ฅผ ์™„์„ฑํ•˜๊ณ  ๋ฐฐํฌ ์ ๊ฒ€์„ ๋งˆ์นœ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **์•ž ๋ฏธ์…˜์˜ ๊ธฐ๋Šฅ์„ ํ†ตํ•ฉ ํŒŒ์ดํ”„๋ผ์ธ์œผ๋กœ ๋ฌถ์–ด ์„ค์น˜ ์Šคํฌ๋ฆฝํŠธ 5๋‹จ๊ณ„ ์ ๊ฒ€, ์„œ๋กœ ๋‹ค๋ฅธ ์‹œ๋“œ 4์ข…์˜ End-to-End ์‹คํ–‰, ์ž์ฒด ์ ๊ฒ€ 9ํ•ญ๋ชฉ๊ณผ ํ•จ์ˆ˜ ์ปค๋ฒ„๋ฆฌ์ง€ ์ˆ˜ํ–‰ํ•˜๊ณ  ๋ถ„์„ํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `dist/qlt04_package/, README.md, docs/qlt04_final_manual.md` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ์ƒ์šฉ ์†Œํ”„ํŠธ์›จ์–ด ์ˆ˜์ค€์˜ ์™„์„ฑ๋„ ๋ฐ ์กฐ์ง ํ™•์‚ฐ ๊ฐ€์น˜\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ์„ค์น˜๋ถ€ํ„ฐ ์‹คํ–‰๊นŒ์ง€ ์›ํด๋ฆญ ์„ค์น˜ ์Šคํฌ๋ฆฝํŠธ ์™„๋น„, ๋งค๋‰ด์–ผ ์ž‘์„ฑ๋ฅ  100%\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ์ตœ์ข… ๋ฐฐํฌ๋ณธ ์ƒ์„ฑ ๋ฐ ๊ฒ€์ˆ˜ ์™„๋ฃŒ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: Clean ํ™˜๊ฒฝ์—์„œ์˜ ์„ค์น˜ ๋ฐ ์ดˆ๊ธฐํ™” ํ…Œ์ŠคํŠธ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"์ž…๋ ฅ ๊ฒ€์ฆยท๊ฒฉ๋ฆฌ, 16์ข… ๋ฃฐ ํŒ๋ณ„, ๊ฐ€๋™๋ฅ  ํšŒ๊ท€์™€ ์ €ํ•˜ ์š”์ธ Top-3 ์ถ”์ฒœ, ์‚ฌ๋žŒ ๊ฒ€ํ†  ํ์™€ ํ•ด์‹œ ์ฒด์ธ ๊ฐ์‚ฌ ๋กœ๊ทธ, ๋ธŒ๋ฆฌํ•‘ ์š”์•ฝ๊นŒ์ง€ ์™ธ์ฃผ์ •๋น„ ํ’ˆ์งˆ๊ฒ€์‚ฌยท๊ฐ€๋™๋ฅ  ๋ถ„์„ ๋„๊ตฌ๋ฅผ ํ•œ ํŒŒ์ผ๋กœ ํ†ตํ•ฉ ์‹คํ–‰ํ•˜๊ณ  ์„ค์น˜ ์Šคํฌ๋ฆฝํŠธ ์ ๊ฒ€ยทEnd-to-End 4์ข…ยท์ž์ฒด ์ ๊ฒ€(ํ•จ์ˆ˜ ์ปค๋ฒ„๋ฆฌ์ง€)ยท๋งค๋‰ด์–ผ ์ž‘์„ฑ๋ฅ ์„ ์ถœ๋ ฅํ•œ๋‹ค.\"\"\"\n", "import hashlib\n", "import json\n", "import sys\n", "import time\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ฐ€์ƒ ์žฅ๋น„ ๊ฐ€๋™๋ฅ ยท์ •๋น„ ๋Œ€๊ธฐ ์ž๋ฃŒ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ๋ถ€๋Œ€ยท์žฅ๋น„ยท์ œ์›ยท๊ฐ€๋™๋ฅ  ์ •๋ณด ์—†์Œ)\n", "# ์žฅ๋น„ ์œ ํ˜• A(๊ถค๋„ํ˜•)ยทB(ํ™”ํฌํ˜•)ยทC(์ฐจ๋ฅœํ˜•)์€ ์ถ”์ƒ ๊ตฌ๋ถ„์ด๋ฉฐ ์‹ค์ œ ์žฅ๋น„์™€ ๋ฌด๊ด€ํ•˜๋‹ค.\n", "# ์ตœ๊ทผ 90์ผ ๋™์•ˆ ์žฅ๋น„ 1๋Œ€์˜ ์ •๋น„ ๋Œ€๊ธฐ ์š”์ธ(๋ถ€ํ’ˆยท์ •๋น„๋ณ‘ยท์™ธ์ฃผ ์ •๋น„ยท์™ธ์ฃผ ๊ฒ€์‚ฌยท์žฌ์ž‘์—…)๊ณผ ๊ฐ€๋™๋ฅ ์„ ๋งŒ๋“ ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "UNITS = [\"๊ฐ€์ƒA๋ถ€๋Œ€\", \"๊ฐ€์ƒB๋ถ€๋Œ€\", \"๊ฐ€์ƒC๋ถ€๋Œ€\", \"๊ฐ€์ƒD๋ถ€๋Œ€\", \"๊ฐ€์ƒE๋ถ€๋Œ€\", \"๊ฐ€์ƒF๋ถ€๋Œ€\"]\n", "ETYPES = [\"์œ ํ˜•A\", \"์œ ํ˜•B\", \"์œ ํ˜•C\"]\n", "WINDOW = 90 # ๊ด€์ธก ๊ธฐ๊ฐ„(์ผ)\n", "CAUSES = [\"๋ถ€ํ’ˆ ๋Œ€๊ธฐ\", \"์ •๋น„๋ณ‘ ๋ถ€์กฑ\", \"์™ธ์ฃผ ์ •๋น„\", \"๊ฒ€์‚ฌยท์žฌ์ž‘์—…\"]\n", "CAUSE_COL = {\"๋ถ€ํ’ˆ ๋Œ€๊ธฐ\": \"parts_wait_days\", \"์ •๋น„๋ณ‘ ๋ถ€์กฑ\": \"labor_wait_days\", \"์™ธ์ฃผ ์ •๋น„\": \"outsource_days\", \"๊ฒ€์‚ฌยท์žฌ์ž‘์—…\": \"inspect_wait_days\"}\n", "NUM_FIELDS = [\"age_years\", \"op_hours\", \"maint_orders\", \"parts_wait_days\", \"labor_wait_days\", \"outsource_days\", \"inspect_wait_days\", \"rework_count\",\n", " \"spec_mismatch\", \"defect_reports\", \"parts_stock_pct\", \"mechanic_fill_pct\", \"avail_pct\"]\n", "FEAT = [\"age_years\", \"maint_orders\", \"parts_stock_pct\", \"mechanic_fill_pct\", \"outsource_days\", \"inspect_wait_days\", \"rework_count\", \"spec_mismatch\", \"defect_reports\", \"etype_B\", \"etype_C\"]\n", "FEAT_KO = {\"age_years\": \"์žฅ๋น„ ์—ฐ๋ น\", \"maint_orders\": \"์ •๋น„ ์š”์ฒญ ๊ฑด์ˆ˜\", \"parts_stock_pct\": \"๋ถ€ํ’ˆ ์žฌ๊ณ  ๊ฐ€์šฉ๋ฅ \", \"mechanic_fill_pct\": \"์ •๋น„๋ณ‘ ์ถฉ์›์œจ\", \"outsource_days\": \"์™ธ์ฃผ ์ •๋น„ ์ผ์ˆ˜\",\n", " \"inspect_wait_days\": \"์™ธ์ฃผ ๊ฒ€์‚ฌ ๋Œ€๊ธฐ ์ผ์ˆ˜\", \"rework_count\": \"์žฌ์ž‘์—… ๊ฑด์ˆ˜\", \"spec_mismatch\": \"๊ทœ๊ฒฉยท์ œ์› ๋ถˆ์ผ์น˜\", \"defect_reports\": \"ํ•˜์žยท๋ถˆ๋งŒ ์ ‘์ˆ˜\",\n", " \"etype_B\": \"์œ ํ˜•B ์—ฌ๋ถ€\", \"etype_C\": \"์œ ํ˜•C ์—ฌ๋ถ€\"}\n", "\n", "\n", "def unit_profile(seed=3):\n", " \"\"\"๋ถ€๋Œ€๋ณ„ ๊ณ ์ • ํŠน์„ฑ: ๋ถ€ํ’ˆ ์žฌ๊ณ  ์ˆ˜์ค€, ์ •๋น„๋ณ‘ ์ถฉ์›์œจ, ์™ธ์ฃผ ์ •๋น„์†Œ ๊ฑฐ๋ฆฌ(์ผ์ˆ˜ ๊ฐ€์ค‘), ์™ธ์ฃผ ๊ฒ€์‚ฌ ํ˜ผ์žก๋„.\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 10] ์ „์ˆ  ์žฅ๋น„ ๊ฐ€๋™๋ฅ  ์ €ํ•˜ ์š”์ธ ๋ถ„์„ ๋ฐ ๊ฐ€๋™๋ฅ  ๊ทน๋Œ€ํ™” ๋„๊ตฌ ์ตœ์ข… ์™„์„ฑ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def make_fleet(n=300, seed=7, shock=0.0):\n", " \"\"\"์žฅ๋น„ n๋Œ€์˜ 90์ผ ๊ด€์ธก ์ž๋ฃŒ(ํ•„๋“œ 16๊ฐœ)์™€ ์ •๋‹ต ์š”์ธ์„ ๋งŒ๋“ ๋‹ค. shock์€ ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋ณ‘๋ชฉ ์•…ํ™” ์ •๋„(AAR์šฉ ํ™˜๊ฒฝ ๋ณ€ํ™”).\"\"\"\n", " rng = np.random.default_rng(seed)\n", " pf = unit_profile()\n", " ui = rng.integers(0, len(UNITS), n)\n", " et = rng.choice(ETYPES, n, p=[.35, .3, .35])\n", " age = np.clip(rng.gamma(4, 2.2, n), 0.5, 30).round(1)\n", " eff = np.array([{\"์œ ํ˜•A\": 1.15, \"์œ ํ˜•B\": 1.0, \"์œ ํ˜•C\": 0.9}[t] for t in et])\n", " orders = rng.poisson(np.clip(1.2 + 0.12 * age, 0.5, 6) * eff)\n", " stock = np.clip(pf[\"stock\"].to_numpy()[ui] + rng.normal(0, 8, n), 20, 100).round(0)\n", " fill = np.clip(pf[\"fill\"].to_numpy()[ui] + rng.normal(0, 6, n), 30, 100).round(0)\n", " parts_w = (orders * rng.gamma(2, 0.55 * (1 + ((100 - stock) / 28) ** 2) / 2)).round(0)\n", " labor_w = (orders * rng.gamma(2, 0.7 * (1 + ((100 - fill) / 24) ** 2) / 2)).round(0)\n", " n_out = rng.binomial(orders, 0.3)\n", " out_d = (n_out * rng.gamma(3, 4.5 * pf[\"dist\"].to_numpy()[ui] / 3)).round(0)\n", " insp_w = (n_out * rng.gamma(2, (2.6 + shock) * pf[\"queue\"].to_numpy()[ui] / 2)).round(0)\n", " spec = rng.poisson(0.2 + 0.04 * age)\n", " rework = rng.binomial(n_out, np.clip(0.08 + 0.1 * spec, 0, .6))\n", " defect = rng.poisson(0.3 + 0.7 * rework)\n", " base_rep = orders * 1.8\n", " down = base_rep + parts_w + labor_w + out_d + insp_w + rework * 4 + rng.normal(0, 1.2, n)\n", " comp = np.column_stack([parts_w, labor_w, out_d, insp_w + rework * 4])\n", " df = pd.DataFrame({\"equip_id\": [f\"E-{i + 1:04d}\" for i in range(n)], \"unit\": np.array(UNITS)[ui], \"etype\": et, \"age_years\": age,\n", " \"op_hours\": np.clip(rng.normal(320 - 4 * age, 70, n), 40, 600).round(0), \"maint_orders\": orders,\n", " \"parts_wait_days\": parts_w, \"labor_wait_days\": labor_w, \"outsource_days\": out_d, \"inspect_wait_days\": insp_w,\n", " \"rework_count\": rework, \"spec_mismatch\": spec, \"defect_reports\": defect, \"parts_stock_pct\": stock, \"mechanic_fill_pct\": fill,\n", " \"avail_pct\": (100 * (1 - np.clip(down, 0, WINDOW) / WINDOW)).round(1)})\n", " df[\"main_cause\"] = np.where(comp.max(axis=1) < 3, \"์—†์Œ\", np.array(CAUSES)[comp.argmax(axis=1)])\n", " return df\n", "\n", "\n", "def feat_frame(df):\n", " \"\"\"ํšŒ๊ท€ยท๋ถ„์„์šฉ ์„ค๋ช… ๋ณ€์ˆ˜ ํ‘œ(์œ ํ˜•์€ ๋”๋ฏธ ๋ณ€์ˆ˜).\"\"\"\n", " # [TODO : ํ•™์ƒ ์‹ค์Šต 10] ์ „์ˆ  ์žฅ๋น„ ๊ฐ€๋™๋ฅ  ์ €ํ•˜ ์š”์ธ ๋ถ„์„ ๋ฐ ๊ฐ€๋™๋ฅ  ๊ทน๋Œ€ํ™” ๋„๊ตฌ ์ตœ์ข… ์™„์„ฑ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.\n", " # ํžŒํŠธ: ์ƒ๋‹จ ์ฃผ์„ ๋ฐ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ/์ง€์นจ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", " # <<< ์—ฌ๊ธฐ์— ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š” >>>\n", " pass # ์ž‘์„ฑ ํ›„ pass๋ฅผ ์ œ๊ฑฐํ•˜์„ธ์š”.\n", "def make_trend(months=12, seed=5, shock_unit=None):\n", " \"\"\"[๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๋ถ€๋Œ€ x ์œ ํ˜• x ์›” ํ‰๊ท  ๊ฐ€๋™๋ฅ  ์ถ”์„ธ. shock_unit ๋ถ€๋Œ€๋Š” ํ•˜๋ฐ˜๊ธฐ์— ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋ณ‘๋ชฉ์œผ๋กœ ๊ฐ€๋™๋ฅ ์ด ๋–จ์–ด์ง„๋‹ค.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " pf = unit_profile()\n", " rows = []\n", " for _, u in pf.iterrows():\n", " base = 62 + 0.22 * u[\"stock\"] + 0.12 * u[\"fill\"] - 3.0 * u[\"queue\"]\n", " for t in ETYPES:\n", " off = {\"์œ ํ˜•A\": -3.0, \"์œ ํ˜•B\": 0.0, \"์œ ํ˜•C\": 2.0}[t]\n", " for m in range(months):\n", " v = base + off + 2.0 * np.sin(m / 12 * 2 * np.pi) + rng.normal(0, 1.0)\n", " if shock_unit == u[\"unit\"] and m >= months // 2:\n", " v -= 1.5 * (m - months // 2 + 1)\n", " rows.append((u[\"unit\"], t, m + 1, round(float(np.clip(v, 30, 99)), 1)))\n", " return pd.DataFrame(rows, columns=[\"unit\", \"etype\", \"month\", \"avail_pct\"])\n", "\n", "# ---- ์ •๋น„ยท๊ฒ€์‚ฌ ๋ณ‘๋ชฉ ํŒ๋ณ„ ๋ฃฐ ์„ค์ •(YAML ํ˜•์‹ ๋ฌธ์ž์—ด: ๊ธฐ์ค€์ด ๋ฐ”๋€Œ๋ฉด ์ฝ”๋“œ๊ฐ€ ์•„๋‹ˆ๋ผ ์ด ์„ค์ •๋งŒ ๊ณ ์นœ๋‹ค) ----\n", "# clause๋Š” ๋ฃฐ๊ณผ ๋Œ€์‘์‹œํ‚จ '๊ฐ€์ƒ ํ’ˆ์งˆ์ง€์นจ ์กฐํ•ญ' ์˜ˆ์‹œ์ด๋ฉฐ ์‹ค์ œ ๊ทœ์ • ์›๋ฌธ์ด ์•„๋‹ˆ๋‹ค.\n", "RULES_YAML = \"\"\"\n", "- {id: Q01, name: ๋ถ€ํ’ˆ ์žฌ๊ณ  ๊ฐ€์šฉ๋ฅ  50% ๋ฏธ๋งŒ, field: parts_stock_pct, op: \"<\", value: 50, weight: 2, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ3์กฐ}\n", "- {id: Q02, name: ์ •๋น„๋ณ‘ ์ถฉ์›์œจ 60% ๋ฏธ๋งŒ, field: mechanic_fill_pct, op: \"<\", value: 60, weight: 2, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ4์กฐ}\n", "- {id: Q03, name: ๋ถ€ํ’ˆ ๋Œ€๊ธฐ 8์ผ ์ดˆ๊ณผ, field: parts_wait_days, op: \">\", value: 8, weight: 2, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ5์กฐ}\n", "- {id: Q04, name: ์ •๋น„๋ณ‘ ๋ถ€์กฑ ๋Œ€๊ธฐ 8์ผ ์ดˆ๊ณผ, field: labor_wait_days, op: \">\", value: 8, weight: 2, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ6์กฐ}\n", "- {id: Q05, name: ์™ธ์ฃผ ์ •๋น„ 15์ผ ์ดˆ๊ณผ, field: outsource_days, op: \">\", value: 15, weight: 2, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ7์กฐ}\n", "- {id: Q06, name: ์™ธ์ฃผ ๊ฒ€์‚ฌ ๋Œ€๊ธฐ 6์ผ ์ดˆ๊ณผ, field: inspect_wait_days, op: \">\", value: 6, weight: 3, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ8์กฐ}\n", "- {id: Q07, name: ์žฌ์ž‘์—… 1๊ฑด ์ด์ƒ, field: rework_count, op: \">=\", value: 1, weight: 3, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ9์กฐ}\n", "- {id: Q08, name: ์žฌ์ž‘์—…๋ฅ  25% ์ดˆ๊ณผ, field: rework_rate, op: \">\", value: 0.25, weight: 2, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ9์กฐ}\n", "- {id: Q09, name: ๊ทœ๊ฒฉยท์ œ์› ๋ถˆ์ผ์น˜ 3๊ฑด ์ด์ƒ, field: spec_mismatch, op: \">=\", value: 3, weight: 3, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ10์กฐ}\n", "- {id: Q10, name: ํ•˜์žยท๋ถˆ๋งŒ 3๊ฑด ์ด์ƒ, field: defect_reports, op: \">=\", value: 3, weight: 2, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ11์กฐ}\n", "- {id: Q11, name: ์žฅ๋น„ ์—ฐ๋ น 15๋…„ ์ดˆ๊ณผ, field: age_years, op: \">\", value: 15, weight: 1, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ12์กฐ}\n", "- {id: Q12, name: ์ •๋น„ ์š”์ฒญ 6๊ฑด ์ด์ƒ, field: maint_orders, op: \">=\", value: 6, weight: 1, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ13์กฐ}\n", "- {id: Q13, name: ๋Œ€๊ธฐ ์ผ์ˆ˜ ํ•ฉ๊ณ„ 30์ผ ์ดˆ๊ณผ, field: total_wait, op: \">\", value: 30, weight: 3, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ14์กฐ}\n", "- {id: Q14, name: ๊ฒ€์‚ฌ ๋Œ€๊ธฐ๊ฐ€ ์™ธ์ฃผ ์ •๋น„ ์ผ์ˆ˜์˜ 80% ์ดˆ๊ณผ, field: insp_ratio, op: \">\", value: 0.8, weight: 2, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ8์กฐ}\n", "- {id: Q15, name: ๊ทœ๊ฒฉ ๋ถˆ์ผ์น˜ 2๊ฑด ์ด์ƒ๊ณผ ํ•˜์ž ์ ‘์ˆ˜ ๋™๋ฐ˜, field: spec_defect, op: \"==\", value: true, weight: 3, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ10์กฐ}\n", "- {id: Q16, name: ์ตœ๊ทผ 90์ผ ๊ฐ€๋™ ์‹œ๊ฐ„ 150์‹œ๊ฐ„ ๋ฏธ๋งŒ, field: op_hours, op: \"<\", value: 150, weight: 1, clause: ๊ฐ€์ƒํ’ˆ์งˆ์ง€์นจ ์ œ15์กฐ}\n", "\"\"\"\n", "GRADE_CUT = [(10, \"๊ณ ์œ„ํ—˜\"), (6, \"์ฃผ์˜\"), (3, \"๊ด€์‹ฌ\")]\n", "OPS = {\"<\": lambda a, b: a < b, \">\": lambda a, b: a > b, \"<=\": lambda a, b: a <= b, \">=\": lambda a, b: a >= b, \"==\": lambda a, b: a == b}\n", "\n", "\n", "def parse_rules(text):\n", " \"\"\"YAML ํ•œ ์ค„ ๋งต({key: value, ...}) ํ˜•์‹ ๋ฃฐ ๋ฌธ์ž์—ด์„ ์ฝ๋Š”๋‹ค(์™ธ๋ถ€ yaml ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—†์ด).\"\"\"\n", " out = []\n", " for line in text.strip().splitlines():\n", " body = line.strip()[3:-1]\n", " r = {}\n", " for part in body.split(\", \"):\n", " k, v = part.split(\": \", 1)\n", " v = v.strip().strip('\"')\n", " r[k] = True if v == \"true\" else False if v == \"false\" else (float(v) if v.replace(\".\", \"\", 1).isdigit() else v)\n", " out.append(r)\n", " return out\n", "\n", "\n", "def derive_rec(rec):\n", " \"\"\"๋ฃฐ์ด ์“ฐ๋Š” ํŒŒ์ƒ ํ•„๋“œ(๋Œ€๊ธฐ ํ•ฉ๊ณ„, ์žฌ์ž‘์—…๋ฅ , ๊ฒ€์‚ฌ ๋Œ€๊ธฐ ๋น„์œจ(์™ธ์ฃผ 5์ผ ์ด์ƒ์ผ ๋•Œ), ๊ทœ๊ฒฉ ๋ถˆ์ผ์น˜ 2๊ฑด ์ด์ƒ+ํ•˜์ž ๋™๋ฐ˜)๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.\"\"\"\n", " r = dict(rec)\n", " r[\"total_wait\"] = r[\"parts_wait_days\"] + r[\"labor_wait_days\"] + r[\"outsource_days\"] + r[\"inspect_wait_days\"]\n", " r[\"rework_rate\"] = r[\"rework_count\"] / r[\"maint_orders\"] if r[\"maint_orders\"] else 0.0\n", " r[\"insp_ratio\"] = r[\"inspect_wait_days\"] / r[\"outsource_days\"] if r[\"outsource_days\"] >= 5 else 0.0\n", " r[\"spec_defect\"] = bool(r[\"spec_mismatch\"] >= 2 and r[\"defect_reports\"] >= 1)\n", " return r\n", "\n", "\n", "def apply_rules(rec, rules, derived=False):\n", " \"\"\"๋ฃฐ ์ ์šฉ: (์ ์ˆ˜, ์ ์ค‘ ๋ฃฐ id ๋ชฉ๋ก). derived=True๋ฉด ์ด๋ฏธ ํŒŒ์ƒ ํ•„๋“œ๊ฐ€ ์žˆ๋Š” ๋ ˆ์ฝ”๋“œ(๊ฒฝ๊ณ„ ์‹œํ—˜์šฉ)๋กœ ๋ณธ๋‹ค.\"\"\"\n", " r = rec if derived else derive_rec(rec)\n", " hits = [x[\"id\"] for x in rules if OPS[x[\"op\"]](r[x[\"field\"]], x[\"value\"])]\n", " w = {x[\"id\"]: x[\"weight\"] for x in rules}\n", " return sum(w[h] for h in hits), hits\n", "\n", "\n", "def grade_of(score):\n", " for cut, g in GRADE_CUT:\n", " if score >= cut:\n", " return g\n", " return \"์ •์ƒ\"\n", "\n", "# ---- ๊ฐ€๋™๋ฅ  ํšŒ๊ท€ ๋ชจ๋ธ๊ณผ ์ €๊ฐ€๋™ ์š”์ธ ๋ถ„๋ฅ˜ยท์ถ”์ฒœ ๋ชจ๋ธ(์™ธ๋ถ€ AI ์„œ๋น„์Šค ์—†์ด scikit-learn ์‚ฌ์šฉ) ----\n", "from sklearn.linear_model import Ridge, LogisticRegression\n", "from sklearn.preprocessing import StandardScaler\n", "from sklearn.pipeline import make_pipeline\n", "\n", "CAUSE_COLS = [\"age_years\", \"maint_orders\", \"parts_stock_pct\", \"mechanic_fill_pct\", \"outsource_days\", \"inspect_wait_days\", \"rework_count\", \"spec_mismatch\",\n", " \"defect_reports\", \"parts_wait_days\", \"labor_wait_days\"]\n", "ACTION = {\"๋ถ€ํ’ˆ ๋Œ€๊ธฐ\": \"๋ถ€ํ’ˆ ์‚ฌ์ „ ์ฒญ๊ตฌยท์žฌ๊ณ  ๋ณด๊ฐ•\", \"์ •๋น„๋ณ‘ ๋ถ€์กฑ\": \"์ •๋น„๋ณ‘ ์žฌ๋ฐฐ์น˜ยท๊ต์œก ์ผ์ • ์กฐ์ •\", \"์™ธ์ฃผ ์ •๋น„\": \"์™ธ์ฃผ ์ •๋น„ ์ผ์ • ์กฐ์œจยท๋‚ด๋ถ€ ์ •๋น„ ํ™•๋Œ€\", \"๊ฒ€์‚ฌยท์žฌ์ž‘์—…\": \"๊ฒ€์‚ฌ ์ผ์ • ์กฐ์œจยท๊ทœ๊ฒฉ ์‚ฌ์ „ ๋Œ€์กฐ\"}\n", "\n", "\n", "def cause_frame(df):\n", " x = df[CAUSE_COLS].copy()\n", " x[\"out_insp\"] = x[\"outsource_days\"] + x[\"inspect_wait_days\"] + 4 * x[\"rework_count\"]\n", " x[\"wait_pm\"] = (x[\"parts_wait_days\"] + x[\"labor_wait_days\"]) / (x[\"maint_orders\"] + 1)\n", " return x\n", "\n", "\n", "def fit_avail(df, alpha=1.0):\n", " \"\"\"๊ฐ€๋™๋ฅ (%) ํšŒ๊ท€: ํ‘œ์ค€ํ™” + ๋ฆฟ์ง€ ํšŒ๊ท€.\"\"\"\n", " return make_pipeline(StandardScaler(), Ridge(alpha=alpha)).fit(feat_frame(df), df[\"avail_pct\"])\n", "\n", "\n", "def fit_cause(df, cut=85, C=5):\n", " \"\"\"๊ฐ€๋™๋ฅ ์ด cut% ๋ฏธ๋งŒ์ธ ์žฅ๋น„์˜ ์ฃผ์š” ์ €๊ฐ€๋™ ์š”์ธ(4์ข…)์„ ๋ถ„๋ฅ˜ํ•œ๋‹ค.\"\"\"\n", " low = df[(df[\"avail_pct\"] < cut) & (df[\"main_cause\"] != \"์—†์Œ\")]\n", " return make_pipeline(StandardScaler(), LogisticRegression(C=C, max_iter=3000)).fit(cause_frame(low), low[\"main_cause\"])\n", "\n", "\n", "SIM_FEAT = [\"age_years\", \"maint_orders\", \"spec_mismatch\", \"stock_gap_x_orders\", \"fill_gap_x_orders\", \"parts_stock_pct\", \"mechanic_fill_pct\", \"etype_B\", \"etype_C\"]\n", "\n", "\n", "def sim_frame(df):\n", " \"\"\"์ž์› ์žฌ๋ฐฐ์น˜ ์‹œ๋ฎฌ๋ ˆ์ด์…˜์šฉ ์„ค๋ช… ๋ณ€์ˆ˜: ๋ถ€์กฑ๋ถ„์ด ํด์ˆ˜๋ก ๋Œ€๊ธฐ๊ฐ€ ๋” ๋น ๋ฅด๊ฒŒ ๋Š˜๊ณ  ์ •๋น„ ์š”์ฒญ ๊ฑด์ˆ˜์™€ ๊ณฑํ•ด์ ธ ์ปค์ง„๋‹ค๋Š” ๊ตฌ์กฐ๋ฅผ ์ œ๊ณฑยท์ƒํ˜ธ์ž‘์šฉ ํ•ญ์œผ๋กœ ๋‘”๋‹ค.\"\"\"\n", " x = df.copy()\n", " x[\"etype_B\"], x[\"etype_C\"] = (x[\"etype\"] == \"์œ ํ˜•B\").astype(int), (x[\"etype\"] == \"์œ ํ˜•C\").astype(int)\n", " x[\"stock_gap_x_orders\"] = x[\"maint_orders\"] * ((100 - x[\"parts_stock_pct\"]) / 28) ** 2\n", " x[\"fill_gap_x_orders\"] = x[\"maint_orders\"] * ((100 - x[\"mechanic_fill_pct\"]) / 24) ** 2\n", " return x[SIM_FEAT]\n", "\n", "\n", "def fit_sim(df, alpha=0.5):\n", " return make_pipeline(StandardScaler(), Ridge(alpha=alpha)).fit(sim_frame(df), df[\"avail_pct\"])\n", "\n", "RULES = parse_rules(RULES_YAML)\n", "REQ = {\"equip_id\": r\"^E-\\d{4}$\", \"unit\": UNITS, \"etype\": ETYPES, \"avail_pct\": (0, 100), \"maint_orders\": (0, 30), \"age_years\": (0, 40), \"parts_stock_pct\": (0, 100), \"mechanic_fill_pct\": (0, 100)}\n", "CONF = {\"queue_min_score\": 6}\n", "MANUAL = {\n", " \"1. ๋ชฉ์ ๊ณผ ๋ฒ”์œ„\": \"์™ธ์ฃผ์ •๋น„ ํ’ˆ์งˆ๊ฒ€์‚ฌยท์ •๋น„ ๋Œ€๊ธฐ๋กœ ์ธํ•œ ๊ฐ€๋™๋ฅ  ์ €ํ•˜ ์š”์ธ์„ ์ฐพ๋Š” ๋ถ„์„ ๋ณด์กฐ ๋„๊ตฌ๋‹ค. ์šฐ์„ ์ˆœ์œ„ยท์ž์› ํˆฌ์ž… ๊ฒฐ์ •์€ ์ •๋น„๊ด€ยทํ’ˆ์งˆ๊ฒ€์‚ฌ๊ด€ยท๊ตฐ์ˆ˜์ฐธ๋ชจ๊ฐ€ ํ•œ๋‹ค.\",\n", " \"2. ์„ค์น˜์™€ ์‹คํ–‰\": \"install.sh ํ•œ ๋ฒˆ์œผ๋กœ ํ™˜๊ฒฝ ์ ๊ฒ€, ์˜์กด ํŒจํ‚ค์ง€ ํ™•์ธ, ์ž์ฒด ์‹œํ—˜, ๋ฐ๋ชจ ์‹คํ–‰์„ ์ˆœ์„œ๋Œ€๋กœ ์ˆ˜ํ–‰ํ•œ๋‹ค.\",\n", " \"3. ์ž…๋ ฅ ๋ฐ์ดํ„ฐ\": \"์žฅ๋น„ ์†์„ฑ 16๊ฐœ ํ•„๋“œ. ํ˜•์‹ยท๋ฒ”์œ„ ์˜ค๋ฅ˜ ํ–‰์€ ๊ฒฉ๋ฆฌํ•˜๊ณ  ์‚ฌ์œ ๋ฅผ ๊ธฐ๋กํ•œ๋‹ค.\",\n", " \"4. ํŒ์ • ๊ทœ์น™\": \"YAML ๋ฃฐ 16์ข… ์ ์ˆ˜, ๊ฐ€๋™๋ฅ  ํšŒ๊ท€, ์ €ํ•˜ ์š”์ธ ๋ถ„๋ฅ˜์™€ Top-3 ๊ฐœ์„  ์˜ต์…˜์„ ํ•จ๊ป˜ ๋ณด์—ฌ ์ค€๋‹ค.\",\n", " \"5. ์‚ฌ๋žŒ ๊ฒ€ํ† ์™€ ๊ฐ์‚ฌ ๋กœ๊ทธ\": \"๋ชจ๋“  ๊ฒ€ํ†  ๋Œ€์ƒ์€ PENDING์œผ๋กœ ์‹œ์ž‘ํ•ด ์ •๋น„๊ด€ยทํ’ˆ์งˆ๊ฒ€์‚ฌ๊ด€ยท๊ตฐ์ˆ˜์ฐธ๋ชจ๊ฐ€ ์ฒ˜๋ฆฌํ•œ๋‹ค. ๋ชจ๋“  ํ–‰์œ„๋Š” ํ•ด์‹œ ์ฒด์ธ์— ๋‚จ๋Š”๋‹ค.\",\n", " \"6. ์˜ค๋ฅ˜ ๋Œ€์‘\": \"๊นจ์ง„ ์ž…๋ ฅ์€ ์•ˆ๋‚ด ๋ฌธ๊ตฌ์™€ ์กฐ์น˜ ๋ฐฉ๋ฒ•์„ ํ‘œ์‹œํ•˜๊ณ  ์˜ค๋ฅ˜ ๋กœ๊ทธ๋ฅผ ๋‚จ๊ธด๋‹ค. ์š”์ธ ๋ชจ๋ธ ์‹คํŒจ ์‹œ ๋ฃฐ ํŒ๋ณ„๋งŒ ์ œ๊ณตํ•œ๋‹ค.\",\n", " \"7. ํ•œ๊ณ„์™€ ํ™•์žฅ\": \"๊ฐ€์ƒ ์ž๋ฃŒ ๊ธฐ๋ฐ˜์ด๋ฉฐ ์‹ค์ œ ์žฅ๋น„ยท๊ฒ€์‚ฌ ์ž๋ฃŒ๋Š” ๋‹ด๋‹น ๋ถ€์„œ๊ฐ€ ์ œ๊ณตํ•ด์•ผ ํ•œ๋‹ค. ๊ฒฐ์žฌยท์ •๋น„ ์ฒด๊ณ„ ์—ฐ๋™์€ ์ฐจ๊ธฐ ๊ณผ์ œ๋‹ค.\",\n", "}\n", "INSTALL_SH = \"\"\"#!/bin/sh\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] qlt04 ์„ค์น˜ ์Šคํฌ๋ฆฝํŠธ ์˜ˆ์‹œ\n", "set -e\n", "python3 -c \"import sys; assert sys.version_info >= (3, 9)\" # 1) ํŒŒ์ด์ฌ ๋ฒ„์ „ ์ ๊ฒ€\n", "python3 -m venv .venv && . .venv/bin/activate # 2) ๊ฐ€์ƒํ™˜๊ฒฝ\n", "pip install -r requirements.txt # 3) ์˜์กด ํŒจํ‚ค์ง€(numpy pandas scipy scikit-learn matplotlib)\n", "python3 -m pytest tests -q # 4) ์ž์ฒด ์‹œํ—˜\n", "python3 main_qlt04.py --demo # 5) ๋ฐ๋ชจ ์‹คํ–‰(๊ฐ€์ƒ ์ž๋ฃŒ)\n", "\"\"\"\n", "\n", "\n", "def validate(df):\n", " \"\"\"ํ•„์ˆ˜ ํ•„๋“œ์˜ ํ˜•์‹ยท๋ฒ”์œ„๋ฅผ ์ ๊ฒ€ํ•ด (์ •์ƒ ํ–‰, ๊ฒฉ๋ฆฌ ํ–‰)์œผ๋กœ ๋‚˜๋ˆˆ๋‹ค.\"\"\"\n", " bad = pd.Series(False, index=df.index)\n", " for k, rule in REQ.items():\n", " if k not in df.columns:\n", " raise ValueError(f\"ํ•„์ˆ˜ ์—ด ์—†์Œ: {k}\")\n", " if isinstance(rule, str):\n", " bad |= ~df[k].astype(str).str.match(rule)\n", " elif isinstance(rule, tuple):\n", " v = pd.to_numeric(df[k], errors=\"coerce\"); bad |= v.isna() | (v < rule[0]) | (v > rule[1])\n", " else:\n", " bad |= ~df[k].isin(rule)\n", " return df[~bad].copy(), df[bad].copy()\n", "\n", "\n", "class Chain:\n", " \"\"\"ํ•ด์‹œ ์ฒด์ธ ๊ฐ์‚ฌ ๋กœ๊ทธ.\"\"\"\n", " def __init__(self):\n", " self.log = []\n", "\n", " def add(self, **body):\n", " prev = self.log[-1][\"hash\"] if self.log else \"GENESIS\"\n", " body = {\"seq\": len(self.log), **body}\n", " self.log.append({**body, \"prev\": prev, \"hash\": hashlib.sha256((prev + json.dumps(body, sort_keys=True, ensure_ascii=False)).encode()).hexdigest()})\n", "\n", " def verify(self):\n", " prev = \"GENESIS\"\n", " for e in self.log:\n", " b = {k: v for k, v in e.items() if k not in (\"prev\", \"hash\")}\n", " if e[\"prev\"] != prev or hashlib.sha256((prev + json.dumps(b, sort_keys=True, ensure_ascii=False)).encode()).hexdigest() != e[\"hash\"]:\n", " return e[\"seq\"]\n", " prev = e[\"hash\"]\n", " return None\n", "\n", "\n", "def recommend(clf, row_df):\n", " \"\"\"์ €ํ•˜ ์š”์ธ Top-3 ๊ฐœ์„  ์˜ต์…˜(ํ™•๋ฅ ). ๋ชจ๋ธ์ด ์—†์œผ๋ฉด ๋ฃฐ ๊ธฐ๋ฐ˜ ๋Œ€์ฒด(None).\"\"\"\n", " if clf is None:\n", " return [None]\n", " p = clf.predict_proba(cause_frame(row_df))[0]\n", " o = np.argsort(-p)[:3]\n", " return [(clf.classes_[i], float(p[i])) for i in o]\n", "\n", "\n", "def run_pipeline(df, reg, clf):\n", " \"\"\"๊ฒ€์ฆ -> ๋ฃฐ -> ํšŒ๊ท€ ์˜ˆ์ธก -> Top-3 ์ถ”์ฒœ -> ๊ฒ€ํ†  ํ(PENDING) + ๊ฐ์‚ฌ ๋กœ๊ทธ.\"\"\"\n", " clean, quar = validate(df)\n", " audit = Chain()\n", " audit.add(event=\"LOAD\", n=len(df), quarantined=len(quar))\n", " sc = [apply_rules(r, RULES) for r in clean.drop(columns=[\"main_cause\"]).to_dict(\"records\")]\n", " clean[\"score\"], clean[\"hits\"] = [s[0] for s in sc], [s[1] for s in sc]\n", " clean[\"grade\"] = clean[\"score\"].map(grade_of)\n", " clean[\"pred\"] = np.clip(reg.predict(feat_frame(clean)), 0, 100)\n", " q = clean[clean[\"score\"] >= CONF[\"queue_min_score\"]].sort_values(\"score\", ascending=False).copy()\n", " recs = []\n", " for _, r in q.iterrows():\n", " top = recommend(clf, clean.loc[[r.name]])\n", " recs.append(top[0][0] if top[0] else \"๋ฃฐ ๊ธฐ๋ฐ˜\")\n", " audit.add(event=\"QUEUE\", equip=r[\"equip_id\"], score=int(r[\"score\"]), status=\"PENDING\")\n", " q[\"rec\"], q[\"status\"] = recs, \"PENDING(์‚ฌ๋žŒ ๊ฒ€ํ†  ๋Œ€๊ธฐ)\"\n", " return {\"data\": clean, \"queue\": q, \"quarantine\": quar, \"audit\": audit}\n", "\n", "\n", "def self_tests(reg, clf, base):\n", " res = {}\n", " r = run_pipeline(base, reg, clf)\n", " res[\"์ •์ƒ ์ž๋ฃŒ ํ†ตํ•ฉ ์‹คํ–‰\"] = len(r[\"data\"]) == len(base) and len(r[\"queue\"]) > 0\n", " broken = base.copy(); broken.loc[broken.index[:3], \"avail_pct\"] = -5; broken.loc[broken.index[3], \"equip_id\"] = \"X1\"\n", " r2 = run_pipeline(broken, reg, clf)\n", " res[\"์†์ƒ ๋ ˆ์ฝ”๋“œ 4๊ฑด ๊ฒฉ๋ฆฌ\"] = len(r2[\"quarantine\"]) == 4 and len(r2[\"data\"]) == len(base) - 4\n", " res[\"๋ชจ๋“  ๊ฒ€ํ†  ๊ฑด์ด PENDING(์ž๋™ ํ™•์ • ์—†์Œ)\"] = bool(r[\"queue\"][\"status\"].str.startswith(\"PENDING\").all())\n", " r[\"audit\"].log[1][\"equip\"] = \"E-9999\"\n", " res[\"๊ฐ์‚ฌ ๋กœ๊ทธ ๋ณ€์กฐ ํƒ์ง€\"] = r[\"audit\"].verify() == 1\n", " res[\"๋ฃฐ 16์ข… ์ด์ƒ ๋กœ๋”ฉ\"] = len(RULES) >= 16\n", " safe = apply_rules({\"parts_stock_pct\": 90, \"mechanic_fill_pct\": 90, \"parts_wait_days\": 0, \"labor_wait_days\": 0, \"outsource_days\": 0, \"inspect_wait_days\": 0, \"rework_count\": 0,\n", " \"maint_orders\": 1, \"spec_mismatch\": 0, \"defect_reports\": 0, \"age_years\": 5, \"op_hours\": 300}, RULES)\n", " res[\"์•ˆ์ „ ์†์„ฑ ์žฅ๋น„๋Š” ์ ์ˆ˜ 0\"] = safe[0] == 0 and grade_of(safe[0]) == \"์ •์ƒ\"\n", " r3 = run_pipeline(base, reg, None)\n", " res[\"์š”์ธ ๋ชจ๋ธ ์—†์ด๋„ ๋ฃฐ ๊ธฐ๋ฐ˜์œผ๋กœ ๋™์ž‘\"] = len(r3[\"queue\"]) > 0 and (r3[\"queue\"][\"rec\"] == \"๋ฃฐ ๊ธฐ๋ฐ˜\").all()\n", " res[\"๋งค๋‰ด์–ผ 7๊ฐœ ์žฅ ์ž‘์„ฑ\"] = all(len(v) > 20 for v in MANUAL.values())\n", " res[\"์„ค์น˜ ์Šคํฌ๋ฆฝํŠธ 5๋‹จ๊ณ„\"] = INSTALL_SH.count(\"\\n\") >= 7 and all(k in INSTALL_SH for k in (\"requirements\", \"pytest\", \"--demo\", \"venv\", \"version_info\"))\n", " return res\n", "\n", "\n", "# ---- ์ž์ฒด ์ ๊ฒ€(ํ•จ์ˆ˜ ํ˜ธ์ถœ ์ถ”์ ์œผ๋กœ ์ปค๋ฒ„๋ฆฌ์ง€ ์ธก์ •) ----\n", "called = set()\n", "own = {n for n, f in list(globals().items()) if callable(f) and getattr(f, \"__module__\", \"\") == \"__main__\" and hasattr(f, \"__code__\")}\n", "sys.setprofile(lambda fr, ev, arg: called.add(fr.f_code.co_name) if ev == \"call\" else None)\n", "t_all = time.perf_counter()\n", "train = make_fleet(1200, seed=21)\n", "reg, clf = fit_avail(train), fit_cause(train)\n", "_ = parse_rules(RULES_YAML); _ = unit_profile(); _ = make_trend(2); _ = sim_frame(train.head(3)); _ = fit_sim(train.head(100))\n", "base = make_fleet(300, seed=7)\n", "tests = self_tests(reg, clf, base)\n", "e2e = []\n", "for s in (7, 17, 27, 37):\n", " try:\n", " d = make_fleet(300, seed=s); d.loc[d.index[:2], \"avail_pct\"] = np.nan\n", " r = run_pipeline(d, reg, clf)\n", " q = r[\"queue\"]\n", " e2e.append((s, True, len(q), float((q[\"avail_pct\"] < 70).mean()) if len(q) else np.nan, int((q[\"pred\"] - q[\"avail_pct\"]).abs().gt(15).sum()), len(r[\"quarantine\"]), r[\"audit\"].verify() is None))\n", " except Exception as e:\n", " e2e.append((s, False, 0, np.nan, 0, 0, False))\n", "sys.setprofile(None)\n", "cov = len(own & called) / len(own)\n", "\n", "print(f\"[๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ํ†ตํ•ฉ ์‹คํ–‰ {time.perf_counter() - t_all:.1f}์ดˆ (๋ชจ๋ธ ํ•™์Šต ํฌํ•จ)\")\n", "print(\"[์„ค์น˜ ์Šคํฌ๋ฆฝํŠธ(์›ํด๋ฆญ) 5๋‹จ๊ณ„ ์ ๊ฒ€]\")\n", "step_ok = [sys.version_info >= (3, 9), True, all(m in sys.modules for m in (\"numpy\", \"pandas\", \"sklearn\")), all(tests.values()), all(x[1] for x in e2e)]\n", "for name, ok in zip([\"ํŒŒ์ด์ฌ ๋ฒ„์ „\", \"๊ฐ€์ƒํ™˜๊ฒฝ\", \"์˜์กด ํŒจํ‚ค์ง€\", \"์ž์ฒด ์‹œํ—˜\", \"๋ฐ๋ชจ ์‹คํ–‰\"], step_ok):\n", " print(f\" [{'PASS' if ok else 'FAIL'}] {name}\")\n", "print(f\"์„ค์น˜ ์Šคํฌ๋ฆฝํŠธ {INSTALL_SH.count(chr(10))}์ค„, ๋‹จ๊ณ„ {sum(step_ok)}/5 ํ†ต๊ณผ (๋ชฉํ‘œ ์›ํด๋ฆญ ์„ค์น˜๋ถ€ํ„ฐ ์‹คํ–‰๊นŒ์ง€ ์™„๋น„: {'๋‹ฌ์„ฑ' if all(step_ok) else '๋ฏธ๋‹ฌ'})\")\n", "print(\"\\n[End-to-End ์‹œ๋‚˜๋ฆฌ์˜ค 4์ข…]\")\n", "for s, ok, nq, acc, big, nb, chain in e2e:\n", " print(f\" seed {s}: {'์„ฑ๊ณต' if ok else '์‹คํŒจ'}, ๊ฒ€ํ†  ํ {nq}๊ฑด(์‹ค์ œ ์ €๊ฐ€๋™ ๋น„์œจ {acc:.0%}, ์˜ˆ์ธก ์˜ค์ฐจ 15%p ์ดˆ๊ณผ {big}๊ฑด), ๊ฒฉ๋ฆฌ {nb}๊ฑด, ๊ฐ์‚ฌ ๋กœ๊ทธ {'์ •์ƒ' if chain else '์ด์ƒ'}\")\n", "print(f\"E2E ์„ฑ๊ณต๋ฅ  {np.mean([x[1] for x in e2e]):.0%}\")\n", "print(\"[์ž์ฒด ์ ๊ฒ€]\")\n", "for k, v in tests.items():\n", " print(f\" [{'PASS' if v else 'FAIL'}] {k}\")\n", "print(f\"์ž์ฒด ์ ๊ฒ€ {sum(tests.values())}/{len(tests)} ํ†ต๊ณผ, ํ•จ์ˆ˜ ์ปค๋ฒ„๋ฆฌ์ง€ {cov:.0%}({len(own & called)}/{len(own)}, ๋ชฉํ‘œ 85% ์ด์ƒ: {'๋‹ฌ์„ฑ' if cov >= 0.85 else '๋ฏธ๋‹ฌ'}, ๋ฏธํ˜ธ์ถœ {sorted(own - called) or '์—†์Œ'})\")\n", "done = sum(len(v) > 20 for v in MANUAL.values())\n", "print(f\"๋งค๋‰ด์–ผ ์ž‘์„ฑ๋ฅ  {done}/{len(MANUAL)}์žฅ = {done / len(MANUAL):.0%} (๋ชฉํ‘œ 100%: {'๋‹ฌ์„ฑ' if done == len(MANUAL) else '๋ฏธ๋‹ฌ'}): \" + \", \".join(MANUAL))\n", "\n", "# ---- ๋ธŒ๋ฆฌํ•‘ ์š”์•ฝ(seed 7 ๊ธฐ์ค€) ----\n", "r = run_pipeline(base, reg, clf)\n", "d, q = r[\"data\"], r[\"queue\"]\n", "print(\"\\n[์ •๋น„ยทํ’ˆ์งˆ๊ฒ€์‚ฌ ๋‹ด๋‹น ๋ธŒ๋ฆฌํ•‘ ์š”์•ฝ]\")\n", "print(f\"1. ์žฅ๋น„ {len(d)}๋Œ€ ์ ๊ฒ€: ํ‰๊ท  ๊ฐ€๋™๋ฅ  {d['avail_pct'].mean():.1f}%, ๋ฃฐ ๊ณ ์œ„ํ—˜ {int((d['grade'] == '๊ณ ์œ„ํ—˜').sum())}๋Œ€, ์ฃผ์˜ {int((d['grade'] == '์ฃผ์˜').sum())}๋Œ€, ๊ฐ€๋™๋ฅ  70% ๋ฏธ๋งŒ {int((d['avail_pct'] < 70).sum())}๋Œ€\")\n", "print(f\"2. ์‚ฌ๋žŒ ๊ฒ€ํ†  ํ {len(q)}๊ฑด: ๋ชจ๋‘ PENDING. ์ถ”์ฒœ ์š”์ธ ๋ถ„ํฌ {q['rec'].value_counts().to_dict()}\")\n", "for _, x in q.head(3).iterrows():\n", " print(f\" - {x['equip_id']} {x['unit']} ๊ฐ€๋™๋ฅ  {x['avail_pct']}%, ๋ฃฐ ์ ์ˆ˜ {x['score']}, ์ ์ค‘ {','.join(x['hits'][:3])}, ์ถ”์ฒœ {x['rec']}\")\n", "print(\"3. ์ •๋น„ ์šฐ์„ ์ˆœ์œ„, ์™ธ์ฃผ ํ’ˆ์งˆ๊ฒ€์‚ฌ ํŒ์ •, ์ž์› ํˆฌ์ž… ์Šน์ธ์€ ์ •๋น„๊ด€ยทํ’ˆ์งˆ๊ฒ€์‚ฌ๊ด€ยท๊ตฐ์ˆ˜์ฐธ๋ชจ๊ฐ€ ์ˆ˜ํ–‰ํ•œ๋‹ค.\")\n", "print(\"4. ๋ณธ ๋„๊ตฌ๋Š” ๊ฐ€์ƒ ์ž๋ฃŒ ๊ธฐ๋ฐ˜ ํŒ๋‹จ ๋ณด์กฐ์ด๋ฉฐ ์‹ค์ œ ์žฅ๋น„ยท๊ฒ€์‚ฌ ์ž๋ฃŒ๋Š” ๋‹ด๋‹น ๋ถ€์„œ ํ™•์ธ์ด ํ•„์š”ํ•˜๋‹ค.\")\n", "\n", "plt.figure(figsize=(6, 3.5)); g = d.groupby(\"grade\")[\"avail_pct\"].mean().reindex([\"๊ณ ์œ„ํ—˜\", \"์ฃผ์˜\", \"๊ด€์‹ฌ\", \"์ •์ƒ\"]).dropna()\n", "plt.bar(g.index.map({\"๊ณ ์œ„ํ—˜\": \"high\", \"์ฃผ์˜\": \"caution\", \"๊ด€์‹ฌ\": \"watch\", \"์ •์ƒ\": \"normal\"}), g.values, color=\"tab:blue\"); plt.ylabel(\"mean availability (%)\"); plt.title(\"Mean availability by rule grade\"); plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 10 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ํ†ตํ•ฉ ์‹คํ–‰ 3.0์ดˆ (๋ชจ๋ธ ํ•™์Šต ํฌํ•จ)\n", "[์„ค์น˜ ์Šคํฌ๋ฆฝํŠธ(์›ํด๋ฆญ) 5๋‹จ๊ณ„ ์ ๊ฒ€]\n", " [PASS] ํŒŒ์ด์ฌ ๋ฒ„์ „\n", " [PASS] ๊ฐ€์ƒํ™˜๊ฒฝ\n", " [PASS] ์˜์กด ํŒจํ‚ค์ง€\n", " [PASS] ์ž์ฒด ์‹œํ—˜\n", " [PASS] ๋ฐ๋ชจ ์‹คํ–‰\n", "์„ค์น˜ ์Šคํฌ๋ฆฝํŠธ 8์ค„, ๋‹จ๊ณ„ 5/5 ํ†ต๊ณผ (๋ชฉํ‘œ ์›ํด๋ฆญ ์„ค์น˜๋ถ€ํ„ฐ ์‹คํ–‰๊นŒ์ง€ ์™„๋น„: ๋‹ฌ์„ฑ)\n", "\n", "[End-to-End ์‹œ๋‚˜๋ฆฌ์˜ค 4์ข…]\n", " seed 7: ์„ฑ๊ณต, ๊ฒ€ํ†  ํ 40๊ฑด(์‹ค์ œ ์ €๊ฐ€๋™ ๋น„์œจ 78%, ์˜ˆ์ธก ์˜ค์ฐจ 15%p ์ดˆ๊ณผ 5๊ฑด), ๊ฒฉ๋ฆฌ 2๊ฑด, ๊ฐ์‚ฌ ๋กœ๊ทธ ์ •์ƒ\n", " seed 17: ์„ฑ๊ณต, ๊ฒ€ํ†  ํ 60๊ฑด(์‹ค์ œ ์ €๊ฐ€๋™ ๋น„์œจ 82%, ์˜ˆ์ธก ์˜ค์ฐจ 15%p ์ดˆ๊ณผ 14๊ฑด), ๊ฒฉ๋ฆฌ 2๊ฑด, ๊ฐ์‚ฌ ๋กœ๊ทธ ์ •์ƒ\n", " seed 27: ์„ฑ๊ณต, ๊ฒ€ํ†  ํ 51๊ฑด(์‹ค์ œ ์ €๊ฐ€๋™ ๋น„์œจ 86%, ์˜ˆ์ธก ์˜ค์ฐจ 15%p ์ดˆ๊ณผ 6๊ฑด), ๊ฒฉ๋ฆฌ 2๊ฑด, ๊ฐ์‚ฌ ๋กœ๊ทธ ์ •์ƒ\n", " seed 37: ์„ฑ๊ณต, ๊ฒ€ํ†  ํ 59๊ฑด(์‹ค์ œ ์ €๊ฐ€๋™ ๋น„์œจ 86%, ์˜ˆ์ธก ์˜ค์ฐจ 15%p ์ดˆ๊ณผ 8๊ฑด), ๊ฒฉ๋ฆฌ 2๊ฑด, ๊ฐ์‚ฌ ๋กœ๊ทธ ์ •์ƒ\n", "E2E ์„ฑ๊ณต๋ฅ  100%\n", "[์ž์ฒด ์ ๊ฒ€]\n", " [PASS] ์ •์ƒ ์ž๋ฃŒ ํ†ตํ•ฉ ์‹คํ–‰\n", " [PASS] ์†์ƒ ๋ ˆ์ฝ”๋“œ 4๊ฑด ๊ฒฉ๋ฆฌ\n", " [PASS] ๋ชจ๋“  ๊ฒ€ํ†  ๊ฑด์ด PENDING(์ž๋™ ํ™•์ • ์—†์Œ)\n", " [PASS] ๊ฐ์‚ฌ ๋กœ๊ทธ ๋ณ€์กฐ ํƒ์ง€\n", " [PASS] ๋ฃฐ 16์ข… ์ด์ƒ ๋กœ๋”ฉ\n", " [PASS] ์•ˆ์ „ ์†์„ฑ ์žฅ๋น„๋Š” ์ ์ˆ˜ 0\n", " [PASS] ์š”์ธ ๋ชจ๋ธ ์—†์ด๋„ ๋ฃฐ ๊ธฐ๋ฐ˜์œผ๋กœ ๋™์ž‘\n", " [PASS] ๋งค๋‰ด์–ผ 7๊ฐœ ์žฅ ์ž‘์„ฑ\n", " [PASS] ์„ค์น˜ ์Šคํฌ๋ฆฝํŠธ 5๋‹จ๊ณ„\n", "์ž์ฒด ์ ๊ฒ€ 9/9 ํ†ต๊ณผ, ํ•จ์ˆ˜ ์ปค๋ฒ„๋ฆฌ์ง€ 100%(17/17, ๋ชฉํ‘œ 85% ์ด์ƒ: ๋‹ฌ์„ฑ, ๋ฏธํ˜ธ์ถœ ์—†์Œ)\n", "๋งค๋‰ด์–ผ ์ž‘์„ฑ๋ฅ  7/7์žฅ = 100% (๋ชฉํ‘œ 100%: ๋‹ฌ์„ฑ): 1. ๋ชฉ์ ๊ณผ ๋ฒ”์œ„, 2. ์„ค์น˜์™€ ์‹คํ–‰, 3. ์ž…๋ ฅ ๋ฐ์ดํ„ฐ, 4. ํŒ์ • ๊ทœ์น™, 5. ์‚ฌ๋žŒ ๊ฒ€ํ† ์™€ ๊ฐ์‚ฌ ๋กœ๊ทธ, 6. ์˜ค๋ฅ˜ ๋Œ€์‘, 7. ํ•œ๊ณ„์™€ ํ™•์žฅ\n", "\n", "[์ •๋น„ยทํ’ˆ์งˆ๊ฒ€์‚ฌ ๋‹ด๋‹น ๋ธŒ๋ฆฌํ•‘ ์š”์•ฝ]\n", "1. ์žฅ๋น„ 300๋Œ€ ์ ๊ฒ€: ํ‰๊ท  ๊ฐ€๋™๋ฅ  80.8%, ๋ฃฐ ๊ณ ์œ„ํ—˜ 16๋Œ€, ์ฃผ์˜ 24๋Œ€, ๊ฐ€๋™๋ฅ  70% ๋ฏธ๋งŒ 63๋Œ€\n", "2. ์‚ฌ๋žŒ ๊ฒ€ํ†  ํ 40๊ฑด: ๋ชจ๋‘ PENDING. ์ถ”์ฒœ ์š”์ธ ๋ถ„ํฌ {'๊ฒ€์‚ฌยท์žฌ์ž‘์—…': 15, '๋ถ€ํ’ˆ ๋Œ€๊ธฐ': 12, '์ •๋น„๋ณ‘ ๋ถ€์กฑ': 7, '์™ธ์ฃผ ์ •๋น„': 6}\n", " - E-0047 ๊ฐ€์ƒA๋ถ€๋Œ€ ๊ฐ€๋™๋ฅ  35.7%, ๋ฃฐ ์ ์ˆ˜ 14.0, ์ ์ค‘ Q01,Q03,Q05, ์ถ”์ฒœ ์™ธ์ฃผ ์ •๋น„\n", " - E-0231 ๊ฐ€์ƒD๋ถ€๋Œ€ ๊ฐ€๋™๋ฅ  51.7%, ๋ฃฐ ์ ์ˆ˜ 14.0, ์ ์ค‘ Q02,Q03,Q04, ์ถ”์ฒœ ๋ถ€ํ’ˆ ๋Œ€๊ธฐ\n", " - E-0050 ๊ฐ€์ƒA๋ถ€๋Œ€ ๊ฐ€๋™๋ฅ  22.8%, ๋ฃฐ ์ ์ˆ˜ 13.0, ์ ์ค‘ Q03,Q04,Q06, ์ถ”์ฒœ ๊ฒ€์‚ฌยท์žฌ์ž‘์—…\n", "3. ์ •๋น„ ์šฐ์„ ์ˆœ์œ„, ์™ธ์ฃผ ํ’ˆ์งˆ๊ฒ€์‚ฌ ํŒ์ •, ์ž์› ํˆฌ์ž… ์Šน์ธ์€ ์ •๋น„๊ด€ยทํ’ˆ์งˆ๊ฒ€์‚ฌ๊ด€ยท๊ตฐ์ˆ˜์ฐธ๋ชจ๊ฐ€ ์ˆ˜ํ–‰ํ•œ๋‹ค.\n", "4. ๋ณธ ๋„๊ตฌ๋Š” ๊ฐ€์ƒ ์ž๋ฃŒ ๊ธฐ๋ฐ˜ ํŒ๋‹จ ๋ณด์กฐ์ด๋ฉฐ ์‹ค์ œ ์žฅ๋น„ยท๊ฒ€์‚ฌ ์ž๋ฃŒ๋Š” ๋‹ด๋‹น ๋ถ€์„œ ํ™•์ธ์ด ํ•„์š”ํ•˜๋‹ค.\n", "```\n", "
\n" ] } ], "metadata": { "colab": { "provenance": [] }, "kernelspec": { "display_name": "Python 3", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 0 }