{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# ๐ฏ [QLT04] Data ๊ธฐ๋ฐ ์ธ์ฃผ์ ๋น ํ์ง๊ฒ์ฌ ํตํฉ ์๋ฃจ์
\n",
"### Google Colab ์ค์ต ํ๊ฒฝ (ํ์งยท๊ท๊ฒฉยท๊ฒ์ฌ / Level 2)\n",
"\n",
"[](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
}