{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# ๐ฏ [MNT03] ๊ณ ์ฅ๋ฅ ๋ถ์์ ํตํ ๋ค๋น๋ ๊ณ ์ฅํ๋ชฉ ์๋ณ\n",
"### Google Colab ์ค์ต ํ๊ฒฝ (์ฅ๋น์ ๋นยท์๋ฆฌ๋ถ์ / Level 3)\n",
"\n",
"[](https://colab.research.google.com/github/edunlab/milicontent2/blob/main/uploads/MNT03/MNT03_solution.ipynb)\n",
"\n",
"---\n",
"\n",
"## ๐ 1. ํ๋ก์ ํธ ๊ฐ์ ๋ฐ ๋ฐฐ๊ฒฝ\n",
"\n",
"| ํญ๋ชฉ | ๋ด์ฉ |\n",
"| :--- | :--- |\n",
"| **๊ณผ์ ๋ถ๋ฅ** | ์ฅ๋น์ ๋นยท์๋ฆฌ๋ถ์ (Level 3) |\n",
"| **ํ์ต ์์ ์๊ฐ** | 10๊ฐ ๋ฏธ์
(5์ผ / ์ด 28.5์๊ฐ) |\n",
"| **ํ๊ฒฝ ์ฌ์** | Python 3.10+, numpy, pandas, matplotlib, scipy, scikit-learn |\n",
"\n",
"### ๐ ํ๋ก์ ํธ ๋ชฉํ\n",
"์ผ์ ์ ๋น ํ์ฅ์์๋ ์ด๋ค ๋ถํ์ด ์์ฃผ ๊ณ ์ฅ ๋๋์ง ์ ํํ ์์์ผ ์๋ฆฌ๋ถ์์ ๋จผ์ ํ๋ณดํ๊ณ ์๋ฐฉ์ ๋น ์ฃผ๊ธฐ๋ฅผ ์กฐ์ ํ ์ ์์ง๋ง, ์ฐจ๋ ์ ์์ ์ด์ฅ์น(ECU)๊ฐ ๋จ๊ธฐ๋ ๊ณ ์ฅ ์ง๋จ ์ฝ๋(DTC)๋ ํ ๋ฒ์ ์ฌ๋ฌ ๊ฐ๊ฐ ๊ฒน์ณ ๊ธฐ๋ก๋๊ณ ๋ถํ ๊ฐ ๊ณต์ ์ฝ๋๋ ์์ด ์ฒซ ๋ฒ์งธ ์ฝ๋๋ง ๋ณด๋ฉด ์์ธ ๋ถํ์ ์์ฃผ ๋์น๋ค. ๋ณธ ํ๋ก์ ํธ๋ ๊ณ ์ฅยท์ ๋น ์ด๋ ฅ์ ์ฐ๊ฒฐํ๊ณ ๊ฐ๋์๊ฐ ๋๋น ๊ณ ์ฅ๋ฅ ์ ๊ณ์ฐํด ๋ค๋น๋ ๊ณ ์ฅํ๋ชฉ์ ์๋ณํ๋ ๋ฐ์ดํฐ๋ถ์ ๊ธฐ๋ณธ๊ธฐ๋ฅผ ์ตํ๋ ๊ฒ์ ๋ชฉํ๋ก, ๊ต์ก์ฉ ๊ฐ์ ์ฐจ๋ 100๋์ 18๊ฐ์ DTCยท์ ๋น ์ด๋ ฅ์ ์ฝ๋๋ก ๋ง๋ค์ด ๋ถ์ํ๋ค. ๋ฐ์ดํฐ ์คํค๋ง ์ ์, ๋ก๊ทธ ์ ์ , ๋จ์ผ ์ฝ๋ Baseline๊ณผ ํ๋ ํ ยท์๊ด ๋ถ์, ์ฐ๊ด ๊ท์น ๊ธฐ๋ฐ ๊ทผ๋ณธ ์์ธ ์์ธก, ์ ๋น ์กฐ์น ์ถ์ฒ๊ณผ ์ ๋ขฐ๋ ์ ์, ์ค๋ช
๊ฐ๋ฅ์ฑ๊ณผ ์ ๋น๋ณ์ฉ ์ง๋จ ํ๋ฉด, ์๋ จ ์ ๋น๊ด 2์ฐจ ์ง๋จ(HITL) ์๋ฎฌ๋ ์ดํฐ, 1,000๊ฑด ๋์ ์ฃผ์
๋ถํ ์ํ, AAR ๋ฃฐ ์ฌํ๋๊ณผ ํจ๊ณผ ์ฐ์ถ, ์ฌํ์ต์ ํฌํจํ ํตํฉ ๋๊ตฌ๊น์ง ๋จ๊ณ๋ณ๋ก ๊ตฌํํ๋ค. ์ต์ข
๋ชฉํ๋ ๋ค๋น๋ ๊ณ ์ฅํ๋ชฉ์ ๊ทผ๊ฑฐ์ ํจ๊ป ์๋ณํด ์๋ฆฌ๋ถ์ ํ๋ณด์ ์๋ฐฉ์ ๋น ์ฐ์ ์์๋ฅผ ์ ํ๋, ๋ณตํฉ ๊ณ ์ฅ์ ์ต์ข
ํ๋จ์ ์๋ จ ์ ๋น๊ด์ด ๋ด๋ฆฌ๋๋ก ์ง์ํ๋ ๊ฒ์ด๋ค.\n",
"\n",
"> **โ ๏ธ [๊ต์ก์ฉ ๊ฐ์ ๋ฐ์ดํฐ์
์์น]**\n",
"> - ๋ณธ ๋
ธํธ๋ถ์ ๋ชจ๋ ๋ฐ์ดํฐ(์ ์, ๋ก๊ทธ, ์๋๋ฆฌ์ค ๋ฑ)๋ ๋ณด์ ๊ท์ ์ ์ค์ํ์ฌ **์ฝ๋ ๋ด ๋์ ๊ธฐ๋ฐ ๊ฐ์ ๋ฐ์ดํฐ๋ก ์์ฑ**๋ฉ๋๋ค.\n",
"> - ์ค์ ๋ถ๋ ์์น, ์์ ์ธ๊ฐ, ์ค์ ์ ์ฅ ์ ๋ณด, ๊ตฐ์ฌ ๊ธฐ๋ฐ ๋ฐ์ดํฐ๋ ์ผ์ฒด ํฌํจ๋์ง ์์ต๋๋ค.\n",
"\n",
"---\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# ============================================================================\n",
"# [Google Colab ํ๊ฒฝ ์ค์ ] ํ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค์น ๋ฐ ์ํฌํธ\n",
"# ============================================================================\n",
"!pip install -q numpy pandas matplotlib scipy scikit-learn\n",
"\n",
"import os\n",
"import sys\n",
"import time\n",
"import math\n",
"import json\n",
"import re\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"import warnings\n",
"warnings.filterwarnings('ignore')\n",
"\n",
"# ๊ทธ๋ํ ์๋ฌธ ํฐํธ ๊ธฐ๋ณธ ์ค์ (ํ๊ฒฝ ๋
๋ฆฝ์ฑ ๋ณด์ฅ)\n",
"plt.rcParams['font.sans-serif'] = 'DejaVu Sans'\n",
"plt.rcParams['axes.unicode_minus'] = False\n",
"\n",
"print(f\"์คํ ํ๊ฒฝ ์ค๋น ์๋ฃ (Python {sys.version.split()[0]})\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
01. ์ ์ฐจ/์ฅ๊ฐ์ฐจ ์ ์์ ์ด์ฅ์น(ECU) ๊ณ ์ฅ ์ง๋จ ์ฝ๋(DTC) ๋ฐ ์ ๋น ๋งค๋ด์ผ ๋ฐ์ดํฐ์
์ ์\n",
"**์์์๊ฐ**: ์ฝ 1.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ ์ฐจ/์ฅ๊ฐ์ฐจ ์ ์์ ์ด์ฅ์น(ECU) ๊ณ ์ฅ ์ง๋จ ์ฝ๋(DTC) ๋ฐ์ดํฐ์
์ ์ ์ํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ ์ฐจ/์ฅ๊ฐ์ฐจ ๊ณ์ด ๊ฐ์ ์ฐจ๋์ ECU ๊ณ ์ฅ ์ง๋จ ์ฝ๋(DTC) ๋ฐ์ ๋ก๊ทธ, ์ ๋น ์ด๋ ฅ, ์ ๋น ๋งค๋ด์ผ ๋ฐ์ดํฐ์
์ ์คํค๋ง์ ๋ฐ์ดํฐ ๋์
๋๋ฆฌ, ๊ณ ์ฅ๋ฅ ๋ถ์ ์
๋ฌด ํ๋ก์ธ์ค ๋งต์ ์ ์ํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **DTC ๋ฐ์๋ถํฐ ์ ๋น ์ด๋ ฅ ์ฐ๊ฒฐ, ๊ณ ์ฅ๋ฅ ์ง๊ณ, ๋ค๋น๋ ํ๋ชฉ ์๋ณ, ์ฌํ๊ฒํ ๊น์ง ์
๋ฌด ํ๋ฆ์ ๋จ๊ณ๋ณ๋ก ์ ๋ฆฌํ๊ณ , DTC ๋ก๊ทธยท์ ๋น ๊ฑดยท์ฐจ๋ยท์์ธก ๋์ ๋ฌถ์์ผ๋ก ํผ์ฒ 20๊ฐ ์ด์ ์ ์ํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `docs/mnt03_logistics_domain_spec.md ๋ฐ ๋ฐ์ดํฐ ๋์
๋๋ฆฌ` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ตฐ์ ์ค๋ฌด ์
๋ฌด ํ๋ก์ธ์ค์์ ์ผ์น์ฑ ๋ฐ ๋ถ์ ๋ชฉํ์ ๋ช
ํ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์
๋ฌด ํ๋ก์ธ์ค ๋งต ์์ฑ ์๋ฃ, ํ์ ํผ์ฒ(Feature) ๋ชฉ๋ก 20๊ฐ ์ด์ ์ ์\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ๋๋ฉ์ธ ๋ถ์์ ๋ฐ ๋ฐ์ดํฐ ์๊ตฌ๋ช
์ธ์ ์น์ธ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ๊ตฐ์ ๋ด๋น ์ค๋ฌด์ ์ธํฐ๋ทฐ ๋ฐ ๊ธฐ์กด ์ ์ฐ์ฒด๊ณ ๋ฐ์ดํฐ ๊ตฌ์กฐ ๋์กฐ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์ ์ฐจ/์ฅ๊ฐ์ฐจ ๊ณ์ด ๊ฐ์ ์ฐจ๋์ ์ ์์ ์ด์ฅ์น(ECU) ๊ณ ์ฅ ์ง๋จ ์ฝ๋(DTC) ๋ฐ์ ๋ก๊ทธ, ์ ๋น ์ด๋ ฅ, ์ ๋น ๋งค๋ด์ผ ๋ฐ์ดํฐ์
์ ์คํค๋งยท๋ฐ์ดํฐ ๋์
๋๋ฆฌ(ํผ์ฒ 20๊ฐ ์ด์)์ ๊ณ ์ฅ๋ฅ ๋ถ์ ์
๋ฌด ํ๋ก์ธ์ค ๋งต์ ์ ์ํ๊ณ , ๊ฐ์ ๋ฐ์ดํฐ๋ก ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import json\n",
"import re\n",
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ฐจ๋ 100๋์ 18๊ฐ์ ๊ณ ์ฅ ์ง๋จ ์ฝ๋(DTC)ยท์ ๋น ์ด๋ ฅ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์ฅ๋นยท์ ๋น ์ค์ ์์)\n",
"# ๋ถํ 16์ข
(1000๊ฐ๋์๊ฐ๋น ๊ณ ์ฅ๋ฅ ์ฐจ๋ฑ), DTC 28์ข
(๋ถํ ๊ณ ์ ์ฝ๋, ์ง ๋ถํ ๊ณต์ ์ฝ๋, ๊ณํต ์ฆ์ ์ฝ๋)\n",
"# ๊ณ ์ฅ 1๊ฑด(์ ๋น ์ด๋ ฅ)๋ง๋ค DTC๊ฐ 1~5๊ฐ ๋์ ๋ฐ์ํ๊ณ , ์ฐ์ ๊ณ ์ฅ ์ฝ๋์ ์ผ์ ์ก์ ์ฝ๋๊ฐ ์์ธ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"PARTS = [ # (ID, ์ด๋ฆ, ๊ณํต, 1000๊ฐ๋์๊ฐ๋น ๊ณ ์ฅ๋ฅ , ๊ฐ์ ๋จ๊ฐ์ง์, ์๋ฆฌ์๊ฐ h)\n",
" (\"P01\", \"์ฐ๋ฃ๋ถ์ฌ๊ธฐ\", \"engine\", 12.0, 3.0, 2.0), (\"P02\", \"ํฐ๋ณด์ฐจ์ \", \"engine\", 9.0, 8.0, 5.0),\n",
" (\"P03\", \"์ถ์ ์ง\", \"electric\", 6.5, 4.0, 1.5), (\"P04\", \"๋ฐ์ ๊ธฐ\", \"electric\", 4.0, 9.0, 3.0),\n",
" (\"P05\", \"๋ณ์๊ธฐ ์ ์ด๋ชจ๋\", \"drive\", 3.0, 12.0, 6.0), (\"P06\", \"๋๊ฐ์ ํํ\", \"engine\", 2.2, 3.5, 3.0),\n",
" (\"P07\", \"์ ์ ํํ\", \"hydraulic\", 1.6, 10.0, 4.0), (\"P08\", \"์๋ ์ผ์\", \"drive\", 1.2, 2.0, 1.5),\n",
" (\"P09\", \"ECU ์ปค๋ฅํฐ\", \"electric\", 0.9, 2.5, 1.5), (\"P10\", \"์ฐ๋ฃ ํํฐ\", \"engine\", 0.7, 0.8, 0.7),\n",
" (\"P11\", \"์ ๋ ๋ฐธ๋ธ\", \"brake\", 0.5, 6.0, 3.0), (\"P12\", \"์กฐํฅ ์ก์ถ์์ดํฐ\", \"drive\", 0.4, 9.0, 5.0),\n",
" (\"P13\", \"๊ณต๊ธฐ ํํฐ\", \"engine\", 0.3, 0.5, 0.5), (\"P14\", \"ํต์ ์ธํฐํ์ด์ค ๋ชจ๋\", \"electric\", 0.25, 3.0, 1.5),\n",
" (\"P15\", \"์คํ์ ์๋ ฅ ์ผ์\", \"engine\", 0.2, 1.0, 1.0), (\"P16\", \"ํฌ ๊ตฌ๋ ๋ชจํฐ\", \"cooling\", 0.15, 2.0, 2.0)]\n",
"PID = [p[0] for p in PARTS]\n",
"PNAME = {p[0]: p[1] for p in PARTS}; PSYS = {p[0]: p[2] for p in PARTS}; RATE = {p[0]: p[3] for p in PARTS}\n",
"COST = {p[0]: p[4] for p in PARTS}; REPH = {p[0]: p[5] for p in PARTS}\n",
"PAIRS = [(\"P01\", \"P02\"), (\"P03\", \"P04\"), (\"P05\", \"P08\"), (\"P06\", \"P10\"), (\"P09\", \"P14\"), (\"P13\", \"P15\")]\n",
"SHARED = {p: f\"S{k + 1:02d}\" for k, pr in enumerate(PAIRS) for p in pr} # ์ง ๋ถํ์ด ํจ๊ป ์ฐ๋ ๋ชจํธํ ์ฝ๋\n",
"SPEC = {p: f\"C{p[1:]}1\" for p in PID} # ๋ถํ ๊ณ ์ ์ฝ๋\n",
"SYS = [\"engine\", \"electric\", \"drive\", \"hydraulic\", \"brake\", \"cooling\"]\n",
"SYMP = {s: f\"G{i + 1:02d}\" for i, s in enumerate(SYS)} # ๊ณํต ์ฆ์ ์ฝ๋\n",
"CODES = sorted(set(SPEC.values()) | set(SHARED.values()) | set(SYMP.values()))\n",
"CASCADE = {\"P01\": [(\"P10\", 0.20)], \"P02\": [(\"P06\", 0.20)], \"P03\": [(\"P04\", 0.25)], \"P04\": [(\"P09\", 0.25)],\n",
" \"P05\": [(\"P08\", 0.30)], \"P06\": [(\"P16\", 0.30)], \"P09\": [(\"P14\", 0.20)], \"P12\": [(\"P07\", 0.20)]}\n",
"CRIT_SYS = {\"engine\", \"drive\", \"brake\"} # ๊ธฐ๋ยท์ ๋ ๋ถ๋ฅ ์ํ ๊ณํต(๊ฒฝ๋ณด ๋์)\n",
"MONTHS = 18\n",
"\n",
"\n",
"def make_fleet(n=100, seed=0):\n",
" rng = np.random.default_rng(seed)\n",
" f = pd.DataFrame({\"veh\": [f\"V{i:03d}\" for i in range(n)], \"age\": rng.integers(1, 26, n).astype(float),\n",
" \"terrain\": rng.uniform(0, 1, n), \"load\": rng.uniform(0, 1, n), \"pm_gap\": rng.uniform(20, 200, n),\n",
" \"humid\": rng.uniform(0, 1, n), \"base_h\": rng.uniform(55, 125, n)})\n",
" f[\"mult\"] = np.exp(0.045 * (f[\"age\"] - 13) + 0.9 * (f[\"terrain\"] - .5) + 0.8 * (f[\"load\"] - .5)\n",
" + 0.006 * (f[\"pm_gap\"] - 110) + 0.7 * (f[\"humid\"] - .5))\n",
" return f\n",
"\n",
"\n",
"def make_data(seed=1, months=MONTHS):\n",
" \"\"\"๋ฐํ: fleet(์ฐจ๋ ์์ฑ), ev(์ ๋น ์ด๋ ฅ 1๊ฑด=1ํ: ๋ฐ์ DTC ๋ชฉ๋ก, ์ค์ ๊ณ ์ฅ ๋ถํ), log(DTC ๋ฐ์ ๋ก๊ทธ, ํ=์ฝ๋ 1๊ฑด)\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" fleet = make_fleet()\n",
" V = len(fleet)\n",
" temp = 12 + 14 * np.sin(2 * np.pi * (np.arange(months) - 3) / 12) # ์ํ๊ท ๊ธฐ์จ(๊ฐ์)\n",
" hours = fleet[\"base_h\"].values[:, None] * (1 + 0.25 * np.sin(2 * np.pi * np.arange(months) / 12 + 1)) * rng.lognormal(0, .12, (V, months))\n",
" fleet[\"hours\"] = hours.sum(axis=1) # ์ฐจ๋๋ณ 18๊ฐ์ ์ด ๊ฐ๋์๊ฐ\n",
" fleet.attrs[\"month_h\"] = hours.sum(axis=0) # ์๋ณ ์ ์ฒด ๊ฐ๋์๊ฐ(๊ณ ์ฅ๋ฅ ๋ถ๋ชจ)\n",
" evs = []\n",
" for p in PID:\n",
" s = np.ones(months)\n",
" if p in (\"P03\", \"P04\"): s = np.exp(-0.06 * (temp - 12)) # ํ๋ญ๊ธฐ ์ ๊ธฐ๊ณํต ์ฆ๊ฐ\n",
" if p in (\"P06\", \"P16\"): s = np.exp(0.05 * (temp - 12)) # ํน์๊ธฐ ๋๊ฐ๊ณํต ์ฆ๊ฐ\n",
" cnt = rng.poisson(RATE[p] * hours / 1000 * fleet[\"mult\"].values[:, None] * s[None, :])\n",
" for v, m in zip(*np.nonzero(cnt)):\n",
" for _ in range(cnt[v, m]):\n",
" evs.append((fleet[\"veh\"][v], int(m), m * 720 + rng.uniform(0, 720), p, float(temp[m] + rng.normal(0, 3))))\n",
" ev = pd.DataFrame(evs, columns=[\"veh\", \"month\", \"t\", \"part\", \"temp\"]).sort_values(\"t\").reset_index(drop=True)\n",
" ev[\"ev_id\"] = [f\"W{i:05d}\" for i in range(len(ev))]\n",
" rows, codes = [], []\n",
" for e in ev.itertuples():\n",
" c, r = [], rng.random(3)\n",
" if r[0] < 0.6: c.append(SPEC[e.part])\n",
" if e.part in SHARED and r[1] < 0.6: c.append(SHARED[e.part])\n",
" if r[2] < 0.5: c.append(SYMP[PSYS[e.part]])\n",
" if not c: c.append(SHARED.get(e.part, SPEC[e.part]))\n",
" for q, pr in CASCADE.get(e.part, []):\n",
" if rng.random() < pr: c.append(SPEC[q])\n",
" if rng.random() < 0.12: c.append(SPEC[PID[rng.integers(0, 16)]])\n",
" c = list(dict.fromkeys(c))\n",
" rng.shuffle(c) # ๋์ ๋ฐ์ ์ฝ๋์ ๊ธฐ๋ก ์์๋ ์์\n",
" codes.append(c)\n",
" for k, cd in enumerate(c):\n",
" rows.append((e.ev_id, e.veh, e.t + k * 0.1, cd))\n",
" ev[\"codes\"] = codes\n",
" log = pd.DataFrame(rows, columns=[\"ev_id\", \"veh\", \"ts\", \"code\"])\n",
" return fleet, ev, log\n",
"\n",
"# 1) ์
๋ฌด ํ๋ก์ธ์ค ๋งต: DTC ๋ฐ์๋ถํฐ ๊ณ ์ฅ๋ฅ ์ง๊ณยทํ๋ฅ๊น์ง (๊ฐ ๋จ๊ณ์์ ์๊ธฐ๋ ๋ฐ์ดํฐ)\n",
"PROCESS = [(\"DTC ๋ฐ์\", \"์ฐจ๋ ID, ๋ฐ์ ์๊ฐ, DTC ์ฝ๋, ๊ฐ๋์๊ฐ ๊ณ๊ธฐ๊ฐ\"), (\"๋ก๊ทธ ์์งยท์ ์ \", \"์ค๋ณต ์ ๊ฑฐ, ์ฝ๋ยท์๊ฐ ํ๊ธฐ ํต์ผ\"),\n",
" (\"๋์ ๋ฐ์ ๋ฌถ์\", \"๊ฐ์ ์ ๋น ๊ฑด์์ ๋ฐ์ํ DTC ์ธํธ(์ ๊ทํ)\"), (\"๊ณ ์ฅ ์ง๋จ\", \"์ถ์ ๊ณ ์ฅ ๋ถํ, ์ ๋ขฐ๋\"),\n",
" (\"์ ๋น ์กฐ์น\", \"์ ๋น ๋งค๋ด์ผ ์ ์ฐจ, ์๋ฆฌ์๊ฐ, ๊ต์ฒด ๋ถํ\"), (\"์ ๋น ์ด๋ ฅ ์ฐ๊ฒฐ\", \"์ ๋น ๊ฑด ID๋ก DTC์ ์ค์ ๊ณ ์ฅ ๋ถํ ์ฐ๊ฒฐ\"),\n",
" (\"๊ณ ์ฅ๋ฅ ์ง๊ณ\", \"๋ถํ๋ณ 1000๊ฐ๋์๊ฐ๋น ๊ณ ์ฅ ๊ฑด์\"), (\"๋ค๋น๋ ํ๋ชฉ ์๋ณ\", \"ํ๋ ํ (ABC) ๋ฑ๊ธ, ๊ด๋ฆฌ ๋์ ์ ์ \"),\n",
" (\"์ฌํ๊ฒํ (AAR)\", \"์ค์ง๋จ ์ฌ๋ก, ๊ท์น ์กฐ์ ์ด๋ ฅ\")]\n",
"\n",
"# 2) ์คํค๋ง(๋ฐ์ดํฐ ๋์
๋๋ฆฌ): ๊ทธ๋ฃน / ํ๋ / ์๋ฃํ / ๊ท์น / ์ค๋ช
\n",
"S = {\n",
" \"dtc_log\": {\n",
" \"ev_id\": (str, {\"pattern\": r\"^W\\d{5}$\"}, \"์ ๋น ๊ฑด ID(DTC์ ์ ๋น ์ด๋ ฅ์ ์๋ ํค)\"),\n",
" \"veh_id\": (str, {\"pattern\": r\"^V\\d{3}$\"}, \"์ฐจ๋ ID(๊ฐ์)\"),\n",
" \"ts\": (float, {\"min\": 0, \"max\": 13000}, \"DTC ๋ฐ์ ์๊ฐ(๋ชจ์ ์์ ํ ๊ฒฝ๊ณผ ์๊ฐ, h)\"),\n",
" \"code\": (str, {\"enum\": CODES}, \"DTC ์ฝ๋(๊ณ ์ ยท๊ณต์ ยท๊ณํต ์ฆ์ 28์ข
)\"),\n",
" \"co_count\": (int, {\"min\": 1, \"max\": 8}, \"๊ฐ์ ์ ๋น ๊ฑด์์ ๋์ ๋ฐ์ํ DTC ์\"),\n",
" },\n",
" \"work_order\": {\n",
" \"part_id\": (str, {\"enum\": PID}, \"์ค์ ๊ณ ์ฅ ๋ถํ(์ ๋น ์ด๋ ฅ) - ์์ธก๋์ 1\"),\n",
" \"system\": (str, {\"enum\": SYS}, \"๊ณ ์ฅ ๊ณํต\"),\n",
" \"repair_h\": (float, {\"min\": 0.1, \"max\": 24}, \"์๋ฆฌ ์์์๊ฐ(h)\"),\n",
" \"unit_cost\": (float, {\"min\": 0, \"max\": 50}, \"๋ถํ ๋จ๊ฐ์ง์(๊ฐ์)\"),\n",
" \"unplanned\": (bool, {}, \"๋น๊ณํยท๊ธด๊ธ ์ ๋น ์ฌ๋ถ\"),\n",
" \"month\": (int, {\"min\": 0, \"max\": 17}, \"์ ๋น ์(๋ชจ์ ์์ ๊ธฐ์ค)\"),\n",
" },\n",
" \"vehicle\": {\n",
" \"age\": (float, {\"min\": 0, \"max\": 40}, \"์ฐจ๋ น(๋
)\"),\n",
" \"op_hours\": (float, {\"min\": 0, \"max\": 400}, \"์ ๊ฐ๋์๊ฐ(h)\"),\n",
" \"pm_gap\": (float, {\"min\": 0, \"max\": 500}, \"๋ง์ง๋ง ์๋ฐฉ์ ๋น ํ ๊ฒฝ๊ณผ ๊ฐ๋์๊ฐ(h)\"),\n",
" \"terrain\": (float, {\"min\": 0, \"max\": 1}, \"์ด์ฉ ์งํ ๊ฐํน๋(0~1)\"),\n",
" \"load\": (float, {\"min\": 0, \"max\": 1}, \"์ด์ฉ ๋ถํ ์์ค(0~1)\"),\n",
" \"temp\": (float, {\"min\": -40, \"max\": 60}, \"์ ๋น ์์ ๊ธฐ์จ(๋)\"),\n",
" },\n",
" \"manual\": {\n",
" \"code_ref\": (str, {\"enum\": CODES}, \"๋งค๋ด์ผ ๊ธฐ์ค DTC ์ฝ๋\"),\n",
" \"cand_parts\": (list, {}, \"์ฝ๋๋ณ ํ๋ณด ๊ณ ์ฅ ๋ถํ ๋ชฉ๋ก\"),\n",
" \"step_no\": (int, {\"min\": 1, \"max\": 20}, \"์ ๋น ์กฐ์น ์ ์ฐจ ๋จ๊ณ ๋ฒํธ\"),\n",
" \"safety_flag\": (bool, {}, \"์์ ์ฃผ์ ํญ๋ชฉ ํฌํจ ์ฌ๋ถ\"),\n",
" },\n",
" \"target\": {\n",
" \"fail_rate\": (float, {\"min\": 0, \"max\": 100}, \"๋ถํ๋ณ 1000๊ฐ๋์๊ฐ๋น ๊ณ ์ฅ๋ฅ - ์์ธก๋์ 2\"),\n",
" \"confidence\": (float, {\"min\": 0, \"max\": 1}, \"์ง๋จ ์ถ์ฒ ์ ๋ขฐ๋ ์ ์ - ์์ธก๋์ 3\"),\n",
" },\n",
"}\n",
"\n",
"\n",
"def check(name, v, typ, rule):\n",
" if not isinstance(v, typ) or (typ in (int, float) and isinstance(v, bool)):\n",
" return [f\"{name}: ์๋ฃํ ์ค๋ฅ({type(v).__name__})\"]\n",
" e = []\n",
" if \"enum\" in rule and v not in rule[\"enum\"]:\n",
" e.append(f\"{name}: ํ์ฉ๋์ง ์์ ๊ฐ '{v}'\")\n",
" if \"min\" in rule and v < rule[\"min\"]:\n",
" e.append(f\"{name}: ์ต์๊ฐ {rule['min']} ๋ฏธ๋ง\")\n",
" if \"max\" in rule and v > rule[\"max\"]:\n",
" e.append(f\"{name}: ์ต๋๊ฐ {rule['max']} ์ด๊ณผ\")\n",
" if \"pattern\" in rule and not re.match(rule[\"pattern\"], v):\n",
" e.append(f\"{name}: ํ์ ๋ถ์ผ์น '{v}'\")\n",
" return e\n",
"\n",
"\n",
"def validate(rec):\n",
" errs = []\n",
" for g, fields in S.items():\n",
" for k, (typ, rule, _) in fields.items():\n",
" if k not in rec.get(g, {}):\n",
" errs.append(f\"{g}.{k}: ํ์ ํ๋ ๋๋ฝ\")\n",
" else:\n",
" errs += check(f\"{g}.{k}\", rec[g][k], typ, rule)\n",
" return errs\n",
"\n",
"\n",
"# 3) [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์์ฑ ๋ฐ์ดํฐ์์ 20๊ฑด ์ถ์ถํด ๋ ์ฝ๋ ํํ๋ก ๋ง๋ค๊ณ ๊ฒ์ฆ\n",
"fleet, ev, log = make_data()\n",
"f = fleet.set_index(\"veh\")\n",
"recs = []\n",
"for e in ev.sample(20, random_state=0).itertuples():\n",
" first = log[log[\"ev_id\"] == e.ev_id].iloc[0]\n",
" cs = e.codes\n",
" recs.append({\n",
" \"dtc_log\": {\"ev_id\": e.ev_id, \"veh_id\": e.veh, \"ts\": float(round(first[\"ts\"], 1)), \"code\": cs[0], \"co_count\": len(cs)},\n",
" \"work_order\": {\"part_id\": e.part, \"system\": PSYS[e.part], \"repair_h\": float(REPH[e.part]), \"unit_cost\": float(COST[e.part]),\n",
" \"unplanned\": bool(len(cs) >= 3), \"month\": int(e.month)},\n",
" \"vehicle\": {\"age\": float(f.loc[e.veh, \"age\"]), \"op_hours\": float(round(f.loc[e.veh, \"base_h\"], 1)), \"pm_gap\": float(round(f.loc[e.veh, \"pm_gap\"], 1)),\n",
" \"terrain\": float(round(f.loc[e.veh, \"terrain\"], 2)), \"load\": float(round(f.loc[e.veh, \"load\"], 2)), \"temp\": float(round(e.temp, 1))},\n",
" \"manual\": {\"code_ref\": cs[0], \"cand_parts\": [p for p in PID if cs[0] in (SPEC[p], SHARED.get(p))] or [e.part], \"step_no\": 1,\n",
" \"safety_flag\": PSYS[e.part] in CRIT_SYS},\n",
" \"target\": {\"fail_rate\": float(RATE[e.part]), \"confidence\": 0.8}})\n",
"ok = sum(not validate(r) for r in recs)\n",
"print(f\"๊ฐ์ ๋ฐ์ดํฐ {len(ev)}๊ฑด(DTC ๋ก๊ทธ {len(log)}ํ) ์์ฑ, ํ๋ณธ {len(recs)}๊ฑด ์ค ๊ฒ์ฆ ํต๊ณผ {ok}๊ฑด, ์ค๋ฅ {len(recs) - ok}๊ฑด\")\n",
"print(f\"์ ์ฒด DTC ๋ก๊ทธ ์ฝ๋ ํ์ฉ๊ฐ ๊ฒ์ฆ: {'ํต๊ณผ' if log['code'].isin(CODES).all() else '์คํจ'}\")\n",
"\n",
"# 4) ์ค๋ฅ ์ฃผ์
์ํ\n",
"bad = json.loads(json.dumps(recs[0]))\n",
"bad[\"dtc_log\"][\"code\"] = \"C999\" # ์ ์๋์ง ์์ DTC ์ฝ๋\n",
"bad[\"dtc_log\"][\"veh_id\"] = \"TANK-7\" # ID ํ์ ์ค๋ฅ\n",
"bad[\"vehicle\"][\"load\"] = 1.8 # ๋ฒ์ ์ด๊ณผ\n",
"del bad[\"work_order\"][\"repair_h\"] # ํ์ ํ๋ ๋๋ฝ\n",
"bad[\"target\"][\"confidence\"] = \"๋์\" # ์๋ฃํ ์ค๋ฅ\n",
"print(\"\\n[์ค๋ฅ ์ฃผ์
๊ฒฐ๊ณผ]\")\n",
"for e in validate(bad):\n",
" print(\" -\", e)\n",
"\n",
"# 5) ํ๋ก์ธ์ค ๋งต๊ณผ ๋ฐ์ดํฐ ๋์
๋๋ฆฌ\n",
"print(\"\\n[์
๋ฌด ํ๋ก์ธ์ค ๋งต]\")\n",
"print(\" -> \".join(f\"{i}.{p}\" for i, (p, _) in enumerate(PROCESS, 1)))\n",
"for p, d in PROCESS:\n",
" print(f\" {p:10s}: {d}\")\n",
"rows = [(g, k, t.__name__, d) for g, fl in S.items() for k, (t, r, d) in fl.items()]\n",
"dd = pd.DataFrame(rows, columns=[\"๊ทธ๋ฃน\", \"ํ๋\", \"์๋ฃํ\", \"์ค๋ช
\"])\n",
"print(f\"\\n[๋ฐ์ดํฐ ๋์
๋๋ฆฌ] ํผ์ฒ {len(dd)}๊ฐ (๋ชฉํ 20๊ฐ ์ด์: {'๋ฌ์ฑ' if len(dd) >= 20 else '๋ฏธ๋ฌ'})\")\n",
"print(dd.to_string(index=False))\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
01 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"๊ฐ์ ๋ฐ์ดํฐ 10681๊ฑด(DTC ๋ก๊ทธ 22079ํ) ์์ฑ, ํ๋ณธ 20๊ฑด ์ค ๊ฒ์ฆ ํต๊ณผ 20๊ฑด, ์ค๋ฅ 0๊ฑด\n",
"์ ์ฒด DTC ๋ก๊ทธ ์ฝ๋ ํ์ฉ๊ฐ ๊ฒ์ฆ: ํต๊ณผ\n",
"\n",
"[์ค๋ฅ ์ฃผ์
๊ฒฐ๊ณผ]\n",
" - dtc_log.veh_id: ํ์ ๋ถ์ผ์น 'TANK-7'\n",
" - dtc_log.code: ํ์ฉ๋์ง ์์ ๊ฐ 'C999'\n",
" - work_order.repair_h: ํ์ ํ๋ ๋๋ฝ\n",
" - vehicle.load: ์ต๋๊ฐ 1 ์ด๊ณผ\n",
" - target.confidence: ์๋ฃํ ์ค๋ฅ(str)\n",
"\n",
"[์
๋ฌด ํ๋ก์ธ์ค ๋งต]\n",
"1.DTC ๋ฐ์ -> 2.๋ก๊ทธ ์์งยท์ ์ -> 3.๋์ ๋ฐ์ ๋ฌถ์ -> 4.๊ณ ์ฅ ์ง๋จ -> 5.์ ๋น ์กฐ์น -> 6.์ ๋น ์ด๋ ฅ ์ฐ๊ฒฐ -> 7.๊ณ ์ฅ๋ฅ ์ง๊ณ -> 8.๋ค๋น๋ ํ๋ชฉ ์๋ณ -> 9.์ฌํ๊ฒํ (AAR)\n",
" DTC ๋ฐ์ : ์ฐจ๋ ID, ๋ฐ์ ์๊ฐ, DTC ์ฝ๋, ๊ฐ๋์๊ฐ ๊ณ๊ธฐ๊ฐ\n",
" ๋ก๊ทธ ์์งยท์ ์ : ์ค๋ณต ์ ๊ฑฐ, ์ฝ๋ยท์๊ฐ ํ๊ธฐ ํต์ผ\n",
" ๋์ ๋ฐ์ ๋ฌถ์ : ๊ฐ์ ์ ๋น ๊ฑด์์ ๋ฐ์ํ DTC ์ธํธ(์ ๊ทํ)\n",
" ๊ณ ์ฅ ์ง๋จ : ์ถ์ ๊ณ ์ฅ ๋ถํ, ์ ๋ขฐ๋\n",
" ์ ๋น ์กฐ์น : ์ ๋น ๋งค๋ด์ผ ์ ์ฐจ, ์๋ฆฌ์๊ฐ, ๊ต์ฒด ๋ถํ\n",
" ์ ๋น ์ด๋ ฅ ์ฐ๊ฒฐ : ์ ๋น ๊ฑด ID๋ก DTC์ ์ค์ ๊ณ ์ฅ ๋ถํ ์ฐ๊ฒฐ\n",
" ๊ณ ์ฅ๋ฅ ์ง๊ณ : ๋ถํ๋ณ 1000๊ฐ๋์๊ฐ๋น ๊ณ ์ฅ ๊ฑด์\n",
" ๋ค๋น๋ ํ๋ชฉ ์๋ณ : ํ๋ ํ (ABC) ๋ฑ๊ธ, ๊ด๋ฆฌ ๋์ ์ ์ \n",
" ์ฌํ๊ฒํ (AAR) : ์ค์ง๋จ ์ฌ๋ก, ๊ท์น ์กฐ์ ์ด๋ ฅ\n",
"\n",
"[๋ฐ์ดํฐ ๋์
๋๋ฆฌ] ํผ์ฒ 23๊ฐ (๋ชฉํ 20๊ฐ ์ด์: ๋ฌ์ฑ)\n",
" ๊ทธ๋ฃน ํ๋ ์๋ฃํ ์ค๋ช
\n",
" dtc_log ev_id str ์ ๋น ๊ฑด ID(DTC์ ์ ๋น ์ด๋ ฅ์ ์๋ ํค)\n",
" dtc_log veh_id str ์ฐจ๋ ID(๊ฐ์)\n",
" dtc_log ts float DTC ๋ฐ์ ์๊ฐ(๋ชจ์ ์์ ํ ๊ฒฝ๊ณผ ์๊ฐ, h)\n",
" dtc_log code str DTC ์ฝ๋(๊ณ ์ ยท๊ณต์ ยท๊ณํต ์ฆ์ 28์ข
)\n",
" dtc_log co_count int ๊ฐ์ ์ ๋น ๊ฑด์์ ๋์ ๋ฐ์ํ DTC ์\n",
"work_order part_id str ์ค์ ๊ณ ์ฅ ๋ถํ(์ ๋น ์ด๋ ฅ) - ์์ธก๋์ 1\n",
"work_order system str ๊ณ ์ฅ ๊ณํต\n",
"work_order repair_h float ์๋ฆฌ ์์์๊ฐ(h)\n",
"work_order unit_cost float ๋ถํ ๋จ๊ฐ์ง์(๊ฐ์)\n",
"work_order unplanned bool ๋น๊ณํยท๊ธด๊ธ ์ ๋น ์ฌ๋ถ\n",
"work_order month int ์ ๋น ์(๋ชจ์ ์์ ๊ธฐ์ค)\n",
" vehicle age float ์ฐจ๋ น(๋
)\n",
" vehicle op_hours float ์ ๊ฐ๋์๊ฐ(h)\n",
" vehicle pm_gap float ๋ง์ง๋ง ์๋ฐฉ์ ๋น ํ ๊ฒฝ๊ณผ ๊ฐ๋์๊ฐ(h)\n",
" vehicle terrain float ์ด์ฉ ์งํ ๊ฐํน๋(0~1)\n",
" vehicle load float ์ด์ฉ ๋ถํ ์์ค(0~1)\n",
"... (์ดํ ์๋ต)\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
02. DTC ์๊ณ์ด ๋ฐ์ ๋ก๊ทธ ์ ์ , ๋์ ๋ฐ์ ์ฝ๋ ์ ๊ทํ ์ ์ฒ๋ฆฌ ๋ชจ๋ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"DTC ๋ฐ์ ๋ก๊ทธ ์ ์ ๋ฐ ๋์ ๋ฐ์ ์ฝ๋ ์ ๊ทํ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **DTC ์๊ณ์ด ๋ฐ์ ๋ก๊ทธ๋ฅผ ์ ์ ํ๊ณ ๊ฐ์ ์ ๋น ๊ฑด์์ ๋์ ๋ฐ์ํ ์ฝ๋๋ฅผ ์ ๊ทํํ๋ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ค๋ณต ํ ์ ๊ฑฐ, ์ฐจ๋ IDยทDTC ์ฝ๋ยท์๊ฐ ํ๊ธฐ ํต์ผ, ๊ฐ๋์๊ฐ ๋จ์ ํต์ผ, ์์ ๊ฐ๋์๊ฐ ๊ฒฐ์ธก ์ฒ๋ฆฌ, ๋ฏธ๋ฑ๋ก ์ฐจ๋ยท๋ฏธ์ ์ ์ฝ๋ ๊ฒฉ๋ฆฌ, KNN ๊ฒฐ์ธก ๋ณด์ , ๋์ ๋ฐ์ ์ฝ๋์ ์ ๋น ๊ฑด ๋ฌถ์ยท์ ๋ ฌ ๋ฑ ์ ์ ๊ท์น 10์ข
์ ์ ์ฉ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/etl/mnt03_data_cleansing.py ๋ฐ ์ ์ ํ์ง ๋ณด๊ณ ์` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ตฐ์ ํน์ ์์ธ(๋น๊ณํ ์ ๋น, ๊ธด๊ธ ๋ถ์ถ ๋ฑ) ๋ฐ์ดํฐ ๋ณด์กด์ ํ๋น์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๋ฐ์ดํฐ ์ ์ ๊ท์น 10์ข
์ ์ฉ, ๋ฐ์ดํฐ ์ ํฉ์ฑ ๊ฒ์ฆ ํต๊ณผ์จ 100%\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ETL ํ์ดํ๋ผ์ธ ๊ตฌํ ๋ฐ ์ ์ ๋ ๋ง์คํฐ ๋ฐ์ดํฐ์
๊ตฌ์ถ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ์ ํ ๋ฐ์ดํฐ ํต๊ณ๋(๊ธฐ์ ํต๊ณ) ๋น๊ต ๋ฐ ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"ํ๊ธฐ ๋ถ์ผ์นยท์ค๋ณตยท๋จ์ ํผ์ฌยท๊ฒฐ์ธกยท๋ฏธ๋ฑ๋ก ์ฝ๋๊ฐ ์์ธ ๊ฐ์ DTC ๋ฐ์ ๋ก๊ทธ์ ์ ์ ๊ท์น 10์ข
์ ์ ์ฉํ๊ณ , ๊ฐ์ ์๊ฐ๋์ ํจ๊ป ๋ฐ์ํ DTC๋ฅผ ์ ๋น ๊ฑด ๋จ์๋ก ๋ฌถ์ด ์ ๊ทํ(์ค๋ณต ์ ๊ฑฐยท์ ๋ ฌ)ํ ๋ง์คํฐ ๋ฐ์ดํฐ๋ฅผ ๋ง๋ ๋ค. ๊ธด๊ธยท๋น๊ณํ ์ ๋น๋ ๋ณด์กดํ๋ฉฐ ์ ํฉ์ฑ ๊ฒ์ฆ ํต๊ณผ์จ๊ณผ ์ ์ ์ ํ ํต๊ณ๋ฅผ ํ์ธํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.impute import KNNImputer\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ฐจ๋ 100๋์ 18๊ฐ์ ๊ณ ์ฅ ์ง๋จ ์ฝ๋(DTC)ยท์ ๋น ์ด๋ ฅ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์ฅ๋นยท์ ๋น ์ค์ ์์)\n",
"# ๋ถํ 16์ข
(1000๊ฐ๋์๊ฐ๋น ๊ณ ์ฅ๋ฅ ์ฐจ๋ฑ), DTC 28์ข
(๋ถํ ๊ณ ์ ์ฝ๋, ์ง ๋ถํ ๊ณต์ ์ฝ๋, ๊ณํต ์ฆ์ ์ฝ๋)\n",
"# ๊ณ ์ฅ 1๊ฑด(์ ๋น ์ด๋ ฅ)๋ง๋ค DTC๊ฐ 1~5๊ฐ ๋์ ๋ฐ์ํ๊ณ , ์ฐ์ ๊ณ ์ฅ ์ฝ๋์ ์ผ์ ์ก์ ์ฝ๋๊ฐ ์์ธ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"PARTS = [ # (ID, ์ด๋ฆ, ๊ณํต, 1000๊ฐ๋์๊ฐ๋น ๊ณ ์ฅ๋ฅ , ๊ฐ์ ๋จ๊ฐ์ง์, ์๋ฆฌ์๊ฐ h)\n",
" (\"P01\", \"์ฐ๋ฃ๋ถ์ฌ๊ธฐ\", \"engine\", 12.0, 3.0, 2.0), (\"P02\", \"ํฐ๋ณด์ฐจ์ \", \"engine\", 9.0, 8.0, 5.0),\n",
" (\"P03\", \"์ถ์ ์ง\", \"electric\", 6.5, 4.0, 1.5), (\"P04\", \"๋ฐ์ ๊ธฐ\", \"electric\", 4.0, 9.0, 3.0),\n",
" (\"P05\", \"๋ณ์๊ธฐ ์ ์ด๋ชจ๋\", \"drive\", 3.0, 12.0, 6.0), (\"P06\", \"๋๊ฐ์ ํํ\", \"engine\", 2.2, 3.5, 3.0),\n",
" (\"P07\", \"์ ์ ํํ\", \"hydraulic\", 1.6, 10.0, 4.0), (\"P08\", \"์๋ ์ผ์\", \"drive\", 1.2, 2.0, 1.5),\n",
" (\"P09\", \"ECU ์ปค๋ฅํฐ\", \"electric\", 0.9, 2.5, 1.5), (\"P10\", \"์ฐ๋ฃ ํํฐ\", \"engine\", 0.7, 0.8, 0.7),\n",
" (\"P11\", \"์ ๋ ๋ฐธ๋ธ\", \"brake\", 0.5, 6.0, 3.0), (\"P12\", \"์กฐํฅ ์ก์ถ์์ดํฐ\", \"drive\", 0.4, 9.0, 5.0),\n",
" (\"P13\", \"๊ณต๊ธฐ ํํฐ\", \"engine\", 0.3, 0.5, 0.5), (\"P14\", \"ํต์ ์ธํฐํ์ด์ค ๋ชจ๋\", \"electric\", 0.25, 3.0, 1.5),\n",
" (\"P15\", \"์คํ์ ์๋ ฅ ์ผ์\", \"engine\", 0.2, 1.0, 1.0), (\"P16\", \"ํฌ ๊ตฌ๋ ๋ชจํฐ\", \"cooling\", 0.15, 2.0, 2.0)]\n",
"PID = [p[0] for p in PARTS]\n",
"PNAME = {p[0]: p[1] for p in PARTS}; PSYS = {p[0]: p[2] for p in PARTS}; RATE = {p[0]: p[3] for p in PARTS}\n",
"COST = {p[0]: p[4] for p in PARTS}; REPH = {p[0]: p[5] for p in PARTS}\n",
"PAIRS = [(\"P01\", \"P02\"), (\"P03\", \"P04\"), (\"P05\", \"P08\"), (\"P06\", \"P10\"), (\"P09\", \"P14\"), (\"P13\", \"P15\")]\n",
"SHARED = {p: f\"S{k + 1:02d}\" for k, pr in enumerate(PAIRS) for p in pr} # ์ง ๋ถํ์ด ํจ๊ป ์ฐ๋ ๋ชจํธํ ์ฝ๋\n",
"SPEC = {p: f\"C{p[1:]}1\" for p in PID} # ๋ถํ ๊ณ ์ ์ฝ๋\n",
"SYS = [\"engine\", \"electric\", \"drive\", \"hydraulic\", \"brake\", \"cooling\"]\n",
"SYMP = {s: f\"G{i + 1:02d}\" for i, s in enumerate(SYS)} # ๊ณํต ์ฆ์ ์ฝ๋\n",
"CODES = sorted(set(SPEC.values()) | set(SHARED.values()) | set(SYMP.values()))\n",
"CASCADE = {\"P01\": [(\"P10\", 0.20)], \"P02\": [(\"P06\", 0.20)], \"P03\": [(\"P04\", 0.25)], \"P04\": [(\"P09\", 0.25)],\n",
" \"P05\": [(\"P08\", 0.30)], \"P06\": [(\"P16\", 0.30)], \"P09\": [(\"P14\", 0.20)], \"P12\": [(\"P07\", 0.20)]}\n",
"CRIT_SYS = {\"engine\", \"drive\", \"brake\"} # ๊ธฐ๋ยท์ ๋ ๋ถ๋ฅ ์ํ ๊ณํต(๊ฒฝ๋ณด ๋์)\n",
"MONTHS = 18\n",
"\n",
"\n",
"def make_fleet(n=100, seed=0):\n",
" rng = np.random.default_rng(seed)\n",
" f = pd.DataFrame({\"veh\": [f\"V{i:03d}\" for i in range(n)], \"age\": rng.integers(1, 26, n).astype(float),\n",
" \"terrain\": rng.uniform(0, 1, n), \"load\": rng.uniform(0, 1, n), \"pm_gap\": rng.uniform(20, 200, n),\n",
" \"humid\": rng.uniform(0, 1, n), \"base_h\": rng.uniform(55, 125, n)})\n",
" f[\"mult\"] = np.exp(0.045 * (f[\"age\"] - 13) + 0.9 * (f[\"terrain\"] - .5) + 0.8 * (f[\"load\"] - .5)\n",
" + 0.006 * (f[\"pm_gap\"] - 110) + 0.7 * (f[\"humid\"] - .5))\n",
" return f\n",
"\n",
"\n",
"def make_data(seed=1, months=MONTHS):\n",
" \"\"\"๋ฐํ: fleet(์ฐจ๋ ์์ฑ), ev(์ ๋น ์ด๋ ฅ 1๊ฑด=1ํ: ๋ฐ์ DTC ๋ชฉ๋ก, ์ค์ ๊ณ ์ฅ ๋ถํ), log(DTC ๋ฐ์ ๋ก๊ทธ, ํ=์ฝ๋ 1๊ฑด)\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" fleet = make_fleet()\n",
" V = len(fleet)\n",
" temp = 12 + 14 * np.sin(2 * np.pi * (np.arange(months) - 3) / 12) # ์ํ๊ท ๊ธฐ์จ(๊ฐ์)\n",
" hours = fleet[\"base_h\"].values[:, None] * (1 + 0.25 * np.sin(2 * np.pi * np.arange(months) / 12 + 1)) * rng.lognormal(0, .12, (V, months))\n",
" fleet[\"hours\"] = hours.sum(axis=1) # ์ฐจ๋๋ณ 18๊ฐ์ ์ด ๊ฐ๋์๊ฐ\n",
" fleet.attrs[\"month_h\"] = hours.sum(axis=0) # ์๋ณ ์ ์ฒด ๊ฐ๋์๊ฐ(๊ณ ์ฅ๋ฅ ๋ถ๋ชจ)\n",
" evs = []\n",
" for p in PID:\n",
" s = np.ones(months)\n",
" if p in (\"P03\", \"P04\"): s = np.exp(-0.06 * (temp - 12)) # ํ๋ญ๊ธฐ ์ ๊ธฐ๊ณํต ์ฆ๊ฐ\n",
" if p in (\"P06\", \"P16\"): s = np.exp(0.05 * (temp - 12)) # ํน์๊ธฐ ๋๊ฐ๊ณํต ์ฆ๊ฐ\n",
" cnt = rng.poisson(RATE[p] * hours / 1000 * fleet[\"mult\"].values[:, None] * s[None, :])\n",
" for v, m in zip(*np.nonzero(cnt)):\n",
" for _ in range(cnt[v, m]):\n",
" evs.append((fleet[\"veh\"][v], int(m), m * 720 + rng.uniform(0, 720), p, float(temp[m] + rng.normal(0, 3))))\n",
" ev = pd.DataFrame(evs, columns=[\"veh\", \"month\", \"t\", \"part\", \"temp\"]).sort_values(\"t\").reset_index(drop=True)\n",
" ev[\"ev_id\"] = [f\"W{i:05d}\" for i in range(len(ev))]\n",
" rows, codes = [], []\n",
" for e in ev.itertuples():\n",
" c, r = [], rng.random(3)\n",
" if r[0] < 0.6: c.append(SPEC[e.part])\n",
" if e.part in SHARED and r[1] < 0.6: c.append(SHARED[e.part])\n",
" if r[2] < 0.5: c.append(SYMP[PSYS[e.part]])\n",
" if not c: c.append(SHARED.get(e.part, SPEC[e.part]))\n",
" for q, pr in CASCADE.get(e.part, []):\n",
" if rng.random() < pr: c.append(SPEC[q])\n",
" if rng.random() < 0.12: c.append(SPEC[PID[rng.integers(0, 16)]])\n",
" c = list(dict.fromkeys(c))\n",
" rng.shuffle(c) # ๋์ ๋ฐ์ ์ฝ๋์ ๊ธฐ๋ก ์์๋ ์์\n",
" codes.append(c)\n",
" for k, cd in enumerate(c):\n",
" rows.append((e.ev_id, e.veh, e.t + k * 0.1, cd))\n",
" ev[\"codes\"] = codes\n",
" log = pd.DataFrame(rows, columns=[\"ev_id\", \"veh\", \"ts\", \"code\"])\n",
" return fleet, ev, log\n",
"\n",
"# 1) [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์์ฒ ๋ก๊ทธ: ์์ฑ ๋ก๊ทธ์ ์ผ๋ถ๋ฌ ์ค๋ฅ๋ฅผ ์๋๋ค\n",
"fleet, ev, log = make_data()\n",
"rng = np.random.default_rng(2)\n",
"clean_n = len(log)\n",
"start = pd.Timestamp(\"2025-01-01\")\n",
"raw = pd.DataFrame({\"veh\": log[\"veh\"].values, \"ts\": [(start + pd.Timedelta(hours=t)).strftime(\"%Y-%m-%d %H:%M\") for t in log[\"ts\"]],\n",
" \"code\": log[\"code\"].values, \"emergency\": False})\n",
"vn = raw[\"veh\"].str[1:].astype(int).values\n",
"raw[\"op_h\"] = (vn * 37 % 900) + log[\"ts\"].values * 0.15 # ์ฐจ๋๋ณ ๋์ ๊ฐ๋์๊ฐ ๊ณ๊ธฐ๊ฐ(๊ฐ์)\n",
"raw[\"op_unit\"] = \"h\"\n",
"raw[\"op_h\"] = raw[\"op_h\"].round(1)\n",
"idx = rng.choice(len(raw), 2600, replace=False)\n",
"a, b, c, d_, e, f, g, h_, k = np.split(idx, [400, 900, 1500, 1800, 1880, 1960, 2040, 2300])\n",
"raw.loc[a, \"veh\"] = rng.choice([\"v\", \" V\"], len(a)) + raw.loc[a, \"veh\"].str[1:] # ์ฐจ๋ ID ํ๊ธฐ ๋ถ์ผ์น\n",
"raw.loc[b, \"code\"] = [x.lower() if i % 2 else x[0] + \"-\" + x[1:] + \" \" for i, x in enumerate(raw.loc[b, \"code\"])] # ์ฝ๋ ํ๊ธฐ ๋ถ์ผ์น\n",
"raw.loc[c, \"ts\"] = pd.to_datetime(raw.loc[c, \"ts\"]).dt.strftime(\"%Y/%m/%d %H:%M:%S\") # ์๊ฐ ํ์ ํผ์ฌ\n",
"raw.loc[d_, \"op_h\"] = (raw.loc[d_, \"op_h\"] * 60).round(0); raw.loc[d_, \"op_unit\"] = \"min\" # ๋จ์ ํผ์ฌ\n",
"raw.loc[e, \"op_h\"] = -1.0 # ๋ถ๊ฐ๋ฅ ๊ฐ\n",
"raw.loc[f, \"veh\"] = \"V999\" # ๋ฏธ๋ฑ๋ก ์ฐจ๋\n",
"raw.loc[g, \"code\"] = \"ZZ9\" # ๋ฏธ์ ์ ์ฝ๋\n",
"raw.loc[h_, \"op_h\"] = np.nan # ๊ฒฐ์ธก\n",
"burst = raw.iloc[k[:40]].copy() # ๊ธด๊ธยท๋น๊ณํ ์ ๋น: DTC 6๊ฐ ํญ์ฃผ\n",
"bursts = []\n",
"for _, r in burst.iterrows():\n",
" for j, cd in enumerate(rng.choice(CODES, 6, replace=False)):\n",
" bursts.append({**r, \"code\": cd, \"emergency\": True,\n",
" \"ts\": (pd.to_datetime(r[\"ts\"]) + pd.Timedelta(minutes=j)).strftime(\"%Y-%m-%d %H:%M\")})\n",
"raw = pd.concat([raw, pd.DataFrame(bursts), raw.sample(150, random_state=1)], ignore_index=True) # ํญ์ฃผ ๊ฑด + ์ค๋ณต 150๊ฑด\n",
"raw = raw.sample(frac=1, random_state=3).reset_index(drop=True)\n",
"print(f\"์์ฒ ๋ก๊ทธ {len(raw)}ํ (์ ์ {clean_n}ํ + ๊ธด๊ธ ํญ์ฃผ {len(bursts)}ํ + ์ค๋ณต 150ํ, ์ค๋ฅ ์ฃผ์
{len(idx)}ํ)\")\n",
"\n",
"# 2) ์ ์ ๊ท์น 10์ข
\n",
"df, applied = raw.copy(), []\n",
"def rule(name, n):\n",
" applied.append((name, int(n)))\n",
"\n",
"rule(\"R1 ์ค๋ณต ํ ์ ๊ฑฐ\", df.duplicated().sum()); df = df.drop_duplicates().reset_index(drop=True)\n",
"v = df[\"veh\"].str.strip().str.upper(); rule(\"R2 ์ฐจ๋ ID ํ๊ธฐ ํต์ผ\", (v != df[\"veh\"]).sum()); df[\"veh\"] = v\n",
"cd = df[\"code\"].str.strip().str.upper().str.replace(\"-\", \"\", regex=False); rule(\"R3 DTC ์ฝ๋ ํ๊ธฐ ํต์ผ\", (cd != df[\"code\"]).sum()); df[\"code\"] = cd\n",
"t1 = pd.to_datetime(df[\"ts\"], format=\"%Y-%m-%d %H:%M\", errors=\"coerce\")\n",
"t2 = pd.to_datetime(df[\"ts\"], format=\"%Y/%m/%d %H:%M:%S\", errors=\"coerce\"); rule(\"R4 ๋ฐ์ ์๊ฐ ํ์ ํต์ผ\", t1.isna().sum()); df[\"ts\"] = t1.fillna(t2)\n",
"mn = df[\"op_unit\"] == \"min\"; df.loc[mn, \"op_h\"] = df.loc[mn, \"op_h\"] / 60; df[\"op_unit\"] = \"h\"; rule(\"R5 ๊ฐ๋์๊ฐ ๋จ์ ํต์ผ(๋ถ -> ์๊ฐ)\", mn.sum())\n",
"bad = df[\"op_h\"] < 0; df.loc[bad, \"op_h\"] = np.nan; rule(\"R6 ์์ ๊ฐ๋์๊ฐ ๊ฒฐ์ธก ์ฒ๋ฆฌ\", bad.sum())\n",
"unk = ~df[\"veh\"].isin(fleet[\"veh\"]) | ~df[\"code\"].isin(CODES); quarantine = df[unk].copy(); df = df[~unk].reset_index(drop=True)\n",
"rule(\"R7 ๋ฏธ๋ฑ๋ก ์ฐจ๋ยท๋ฏธ์ ์ ์ฝ๋ ๊ฒฉ๋ฆฌ(์ญ์ ์๋)\", unk.sum())\n",
"df[\"hrs\"] = (df[\"ts\"] - start).dt.total_seconds() / 3600; df[\"vn\"] = df[\"veh\"].str[1:].astype(int)\n",
"miss = df[\"op_h\"].isna().sum()\n",
"df[\"op_h\"] = KNNImputer(n_neighbors=5).fit_transform(df[[\"hrs\", \"vn\", \"op_h\"]].assign(vn=df[\"vn\"] / 10))[:, 2]; rule(\"R8 ๊ฐ๋์๊ฐ ๊ฒฐ์ธก KNN ๋ณด์ \", miss)\n",
"df = df.sort_values([\"veh\", \"ts\"]).reset_index(drop=True)\n",
"df[\"case\"] = (df[\"veh\"] != df[\"veh\"].shift()) | (df[\"ts\"].diff() > pd.Timedelta(hours=1)); df[\"case_id\"] = df[\"case\"].cumsum()\n",
"before = len(df); df = df.drop_duplicates([\"case_id\", \"code\"]).reset_index(drop=True)\n",
"cases = df.groupby(\"case_id\").agg(veh=(\"veh\", \"first\"), t=(\"ts\", \"first\"), codes=(\"code\", lambda s: tuple(sorted(s))), n=(\"code\", \"size\"), emergency=(\"emergency\", \"max\"))\n",
"print(f\"(๋์ ๋ฐ์ ๋ฌถ์ ๊ณผ์ ์์ ์ค๋ณต ์ฝ๋ {before - len(df)}ํ ์ ๊ฑฐ)\")\n",
"rule(\"R9 ๋์ ๋ฐ์ ์ฝ๋ ์ ๋น ๊ฑด ๋ฌถ์ยท์ ๋ ฌ(์์ฑ ๊ฑด์)\", len(cases))\n",
"cases[\"outlier_flag\"] = (cases[\"n\"] > 4) & ~cases[\"emergency\"]\n",
"rule(\"R10 ๊ธด๊ธยท๋น๊ณํ ์ ๋น ๋ณด์กด(์ผ๋ฐ ์ด์ ๊ฑด์ ํ์๋ง)\", cases[\"emergency\"].sum())\n",
"print(\"\\n[์ ์ฉ ๊ท์น๊ณผ ์ฒ๋ฆฌ ๊ฑด์]\")\n",
"for nme, n in applied:\n",
" print(f\" {nme:40s} {n:5d}๊ฑด\")\n",
"\n",
"# 3) ์ ํฉ์ฑ ๊ฒ์ฆ\n",
"checks = {\"์ค๋ณต 0๊ฑด\": not df.duplicated([\"case_id\", \"code\"]).any(), \"์ฐจ๋ ID ํ์\": df[\"veh\"].str.match(r\"^V\\d{3}$\").all(),\n",
" \"์ฝ๋ ํ์ฉ ๋ฒ์\": df[\"code\"].isin(CODES).all(), \"์๊ฐ ๋ณํ\": df[\"ts\"].notna().all(), \"๊ฐ๋์๊ฐ ๋จ์ h\": (df[\"op_unit\"] == \"h\").all(),\n",
" \"๊ฐ๋์๊ฐ 0 ์ด์\": (df[\"op_h\"] >= 0).all(), \"๋ฏธ๋ฑ๋ก ๊ฑด ๊ฒฉ๋ฆฌ ๋ณด๊ด\": len(quarantine) == unk.sum(),\n",
" \"๊ฒฐ์ธก 0๊ฑด\": df[[\"veh\", \"code\", \"ts\", \"op_h\"]].notna().all().all(), \"๋ฌถ์ ๋ด ์ฝ๋ ์ ๋ ฌ\": all(list(c) == sorted(c) for c in cases[\"codes\"]),\n",
" f\"๊ธด๊ธ ์ ๋น {len(bursts) // 6}๊ฑด ๋ณด์กด\": int(cases[\"emergency\"].sum()) >= len(bursts) // 6}\n",
"rate = np.mean(list(checks.values()))\n",
"print(f\"\\n์ ํฉ์ฑ ๊ฒ์ฆ {sum(checks.values())}/{len(checks)} ํต๊ณผ, ํต๊ณผ์จ {rate:.0%} (๋ชฉํ 100%: {'๋ฌ์ฑ' if rate == 1 else '๋ฏธ๋ฌ'})\")\n",
"for kx, vx in checks.items():\n",
" if not vx:\n",
" print(\" ์คํจ:\", kx)\n",
"print(f\"์ ๋น ๊ฑด {len(cases)}๊ฑด ์์ฑ(ํ๊ท DTC {cases['n'].mean():.2f}๊ฐ), ์ด์ ํ์ {int(cases['outlier_flag'].sum())}๊ฑด\")\n",
"\n",
"# 4) ์ ์ ์ ํ ๊ธฐ์ ํต๊ณ ๋น๊ต\n",
"num = pd.to_numeric(raw[\"op_h\"], errors=\"coerce\")\n",
"cmp = pd.DataFrame({\"์ ์ ์ \": [len(raw), raw[\"veh\"].nunique(), raw[\"code\"].nunique(), num.mean(), num.min(), num.isna().sum()],\n",
" \"์ ์ ํ\": [len(df), df[\"veh\"].nunique(), df[\"code\"].nunique(), df[\"op_h\"].mean(), df[\"op_h\"].min(), df[\"op_h\"].isna().sum()]},\n",
" index=[\"ํ ์\", \"์ฐจ๋ ์ข
๋ฅ ์\", \"์ฝ๋ ์ข
๋ฅ ์\", \"๊ฐ๋์๊ฐ ํ๊ท (h)\", \"๊ฐ๋์๊ฐ ์ต์(h)\", \"๊ฒฐ์ธก ์\"]).round(1)\n",
"print(\"\\n[์ ์ ์ ํ ๊ธฐ์ ํต๊ณ]\"); print(cmp.to_string())\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(10, 3.5))\n",
"ax[0].hist(num.dropna(), bins=40, alpha=0.6, label=\"raw\"); ax[0].hist(df[\"op_h\"], bins=40, alpha=0.6, label=\"cleaned\")\n",
"ax[0].set_title(\"Operating hours before/after unit fix\"); ax[0].legend()\n",
"ax[1].bar([n.split()[0] for n, _ in applied], [n for _, n in applied]); ax[1].set_title(\"Records changed by rule\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
02 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"์์ฒ ๋ก๊ทธ 22469ํ (์ ์ 22079ํ + ๊ธด๊ธ ํญ์ฃผ 240ํ + ์ค๋ณต 150ํ, ์ค๋ฅ ์ฃผ์
2600ํ)\n",
"(๋์ ๋ฐ์ ๋ฌถ์ ๊ณผ์ ์์ ์ค๋ณต ์ฝ๋ 68ํ ์ ๊ฑฐ)\n",
"\n",
"[์ ์ฉ ๊ท์น๊ณผ ์ฒ๋ฆฌ ๊ฑด์]\n",
" R1 ์ค๋ณต ํ ์ ๊ฑฐ 150๊ฑด\n",
" R2 ์ฐจ๋ ID ํ๊ธฐ ํต์ผ 400๊ฑด\n",
" R3 DTC ์ฝ๋ ํ๊ธฐ ํต์ผ 500๊ฑด\n",
" R4 ๋ฐ์ ์๊ฐ ํ์ ํต์ผ 600๊ฑด\n",
" R5 ๊ฐ๋์๊ฐ ๋จ์ ํต์ผ(๋ถ -> ์๊ฐ) 300๊ฑด\n",
" R6 ์์ ๊ฐ๋์๊ฐ ๊ฒฐ์ธก ์ฒ๋ฆฌ 80๊ฑด\n",
" R7 ๋ฏธ๋ฑ๋ก ์ฐจ๋ยท๋ฏธ์ ์ ์ฝ๋ ๊ฒฉ๋ฆฌ(์ญ์ ์๋) 160๊ฑด\n",
" R8 ๊ฐ๋์๊ฐ ๊ฒฐ์ธก KNN ๋ณด์ 340๊ฑด\n",
" R9 ๋์ ๋ฐ์ ์ฝ๋ ์ ๋น ๊ฑด ๋ฌถ์ยท์ ๋ ฌ(์์ฑ ๊ฑด์) 10504๊ฑด\n",
" R10 ๊ธด๊ธยท๋น๊ณํ ์ ๋น ๋ณด์กด(์ผ๋ฐ ์ด์ ๊ฑด์ ํ์๋ง) 40๊ฑด\n",
"\n",
"์ ํฉ์ฑ ๊ฒ์ฆ 10/10 ํต๊ณผ, ํต๊ณผ์จ 100% (๋ชฉํ 100%: ๋ฌ์ฑ)\n",
"์ ๋น ๊ฑด 10504๊ฑด ์์ฑ(ํ๊ท DTC 2.10๊ฐ), ์ด์ ํ์ 85๊ฑด\n",
"\n",
"[์ ์ ์ ํ ๊ธฐ์ ํต๊ณ]\n",
" ์ ์ ์ ์ ์ ํ\n",
"ํ ์ 22469.0 22091.0\n",
"์ฐจ๋ ์ข
๋ฅ ์ 252.0 100.0\n",
"์ฝ๋ ์ข
๋ฅ ์ 82.0 28.0\n",
"๊ฐ๋์๊ฐ ํ๊ท (h) 2496.2 1372.4\n",
"๊ฐ๋์๊ฐ ์ต์(h) -1.0 3.8\n",
"๊ฒฐ์ธก ์ 261.0 0.0\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
03. ๋จ์ผ ๊ณ ์ฅ ์ฝ๋ ๊ธฐ๋ฐ 1์ฐจ ๊ณ ์ฅ ๋ถํ ๋งคํ Baseline ์๊ณ ๋ฆฌ์ฆ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋จ์ผ ๊ณ ์ฅ ์ฝ๋ ๊ธฐ๋ฐ 1์ฐจ ๊ณ ์ฅ ๋ถํ ๋งคํ Baseline ์๊ณ ๋ฆฌ์ฆ์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋จ์ผ ๊ณ ์ฅ ์ฝ๋ ๊ธฐ๋ฐ 1์ฐจ ๊ณ ์ฅ ๋ถํ ๋งคํ Baseline(์ฒซ ๊ธฐ๋ก ์ฝ๋์ ๊ฐ์ฅ ์์ฃผ ํจ๊ป ๋์จ ๋ถํ)์ ๊ตฌํํ๊ณ , ํ์์ ๋ถ์์ผ๋ก ๋ค๋น๋ ๊ณ ์ฅํ๋ชฉ๊ณผ ๊ณ ์ฅ ์ฆ๊ฐ ์์ธ์ ์ฐพ๋๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **ํ์ต ๊ธฐ๊ฐ ์ด๋ ฅ์ผ๋ก ์ฝ๋์ ๋ถํ์ ๋์ํ๋ฅผ ๋ง๋ค์ด ๊ฒ์ฆ ๊ธฐ๊ฐ์ ๋ถํ ์ ํ๋๋ฅผ ์ธก์ ํ๊ณ , ๊ณต์ ยท์ฆ์ ์ฝ๋์ฒ๋ผ ๋ชจํธํ ์ฝ๋๊ฐ ์ฒซ ๊ธฐ๋ก์ผ ๋ ์ ํ๋๊ฐ ๋จ์ด์ง๋์ง ํ์ธํ๊ณ ์ ํจ์ฑ์ ์ ๊ฒํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `notebooks/mnt03_eda_analysis.ipynb ๋ฐ EDA ์์ฝ ๋ฆฌํฌํธ` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ตฐ์ ์์ฌ๊ฒฐ์ ์ ์ง๊ด์ ์ธ ์ธ์ฌ์ดํธ ์ ๊ณต ์์ค\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: EDA ์๊ฐํ ์ฐจํธ 8์ข
์ด์ ์์ฑ, ์ ์๋ฏธํ ์๊ด๊ด๊ณ ํผ์ฒ 5๊ฐ ์ด์ ๋์ถ\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: EDA ๋
ธํธ๋ถ ์คํ ์๋ฃ ๋ฐ ํต์ฌ ๋ฐ๊ฒฌ์ (Key Findings) ๋ฌธ์ํ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ํต๊ณ์ ๊ฐ์ค ๊ฒ์ (p-value, ์๊ด๊ณ์)์ ํตํ ์ ์์ฑ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"๋จ์ผ DTC ์ฝ๋๋ง ๋ณด๊ณ ๊ฐ์ฅ ํํ ๊ณ ์ฅ ๋ถํ์ ๊ณ ๋ฅด๋ 1์ฐจ Baseline ๋งคํ ์๊ณ ๋ฆฌ์ฆ์ ๊ตฌํํด ์ ํ๋์ ํ๊ณ๋ฅผ ํ์ธํ๊ณ , ํ์์ ๋ถ์(EDA ์ฐจํธ 8์ข
, ๊ณ ์ฅ ๊ฑด์์ ํผ์ฒ์ ์๊ด๊ณ์ยทp-value, ํ๋ ํ ABC ๋ถ๋ฅ)์ผ๋ก 1000๊ฐ๋์๊ฐ๋น ๊ณ ์ฅ๋ฅ ์ด ๋์ ๋ค๋น๋ ๊ณ ์ฅํ๋ชฉ์ ์๋ณํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy.stats import pearsonr\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ฐจ๋ 100๋์ 18๊ฐ์ ๊ณ ์ฅ ์ง๋จ ์ฝ๋(DTC)ยท์ ๋น ์ด๋ ฅ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์ฅ๋นยท์ ๋น ์ค์ ์์)\n",
"# ๋ถํ 16์ข
(1000๊ฐ๋์๊ฐ๋น ๊ณ ์ฅ๋ฅ ์ฐจ๋ฑ), DTC 28์ข
(๋ถํ ๊ณ ์ ์ฝ๋, ์ง ๋ถํ ๊ณต์ ์ฝ๋, ๊ณํต ์ฆ์ ์ฝ๋)\n",
"# ๊ณ ์ฅ 1๊ฑด(์ ๋น ์ด๋ ฅ)๋ง๋ค DTC๊ฐ 1~5๊ฐ ๋์ ๋ฐ์ํ๊ณ , ์ฐ์ ๊ณ ์ฅ ์ฝ๋์ ์ผ์ ์ก์ ์ฝ๋๊ฐ ์์ธ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"PARTS = [ # (ID, ์ด๋ฆ, ๊ณํต, 1000๊ฐ๋์๊ฐ๋น ๊ณ ์ฅ๋ฅ , ๊ฐ์ ๋จ๊ฐ์ง์, ์๋ฆฌ์๊ฐ h)\n",
" (\"P01\", \"์ฐ๋ฃ๋ถ์ฌ๊ธฐ\", \"engine\", 12.0, 3.0, 2.0), (\"P02\", \"ํฐ๋ณด์ฐจ์ \", \"engine\", 9.0, 8.0, 5.0),\n",
" (\"P03\", \"์ถ์ ์ง\", \"electric\", 6.5, 4.0, 1.5), (\"P04\", \"๋ฐ์ ๊ธฐ\", \"electric\", 4.0, 9.0, 3.0),\n",
" (\"P05\", \"๋ณ์๊ธฐ ์ ์ด๋ชจ๋\", \"drive\", 3.0, 12.0, 6.0), (\"P06\", \"๋๊ฐ์ ํํ\", \"engine\", 2.2, 3.5, 3.0),\n",
" (\"P07\", \"์ ์ ํํ\", \"hydraulic\", 1.6, 10.0, 4.0), (\"P08\", \"์๋ ์ผ์\", \"drive\", 1.2, 2.0, 1.5),\n",
" (\"P09\", \"ECU ์ปค๋ฅํฐ\", \"electric\", 0.9, 2.5, 1.5), (\"P10\", \"์ฐ๋ฃ ํํฐ\", \"engine\", 0.7, 0.8, 0.7),\n",
" (\"P11\", \"์ ๋ ๋ฐธ๋ธ\", \"brake\", 0.5, 6.0, 3.0), (\"P12\", \"์กฐํฅ ์ก์ถ์์ดํฐ\", \"drive\", 0.4, 9.0, 5.0),\n",
" (\"P13\", \"๊ณต๊ธฐ ํํฐ\", \"engine\", 0.3, 0.5, 0.5), (\"P14\", \"ํต์ ์ธํฐํ์ด์ค ๋ชจ๋\", \"electric\", 0.25, 3.0, 1.5),\n",
" (\"P15\", \"์คํ์ ์๋ ฅ ์ผ์\", \"engine\", 0.2, 1.0, 1.0), (\"P16\", \"ํฌ ๊ตฌ๋ ๋ชจํฐ\", \"cooling\", 0.15, 2.0, 2.0)]\n",
"PID = [p[0] for p in PARTS]\n",
"PNAME = {p[0]: p[1] for p in PARTS}; PSYS = {p[0]: p[2] for p in PARTS}; RATE = {p[0]: p[3] for p in PARTS}\n",
"COST = {p[0]: p[4] for p in PARTS}; REPH = {p[0]: p[5] for p in PARTS}\n",
"PAIRS = [(\"P01\", \"P02\"), (\"P03\", \"P04\"), (\"P05\", \"P08\"), (\"P06\", \"P10\"), (\"P09\", \"P14\"), (\"P13\", \"P15\")]\n",
"SHARED = {p: f\"S{k + 1:02d}\" for k, pr in enumerate(PAIRS) for p in pr} # ์ง ๋ถํ์ด ํจ๊ป ์ฐ๋ ๋ชจํธํ ์ฝ๋\n",
"SPEC = {p: f\"C{p[1:]}1\" for p in PID} # ๋ถํ ๊ณ ์ ์ฝ๋\n",
"SYS = [\"engine\", \"electric\", \"drive\", \"hydraulic\", \"brake\", \"cooling\"]\n",
"SYMP = {s: f\"G{i + 1:02d}\" for i, s in enumerate(SYS)} # ๊ณํต ์ฆ์ ์ฝ๋\n",
"CODES = sorted(set(SPEC.values()) | set(SHARED.values()) | set(SYMP.values()))\n",
"CASCADE = {\"P01\": [(\"P10\", 0.20)], \"P02\": [(\"P06\", 0.20)], \"P03\": [(\"P04\", 0.25)], \"P04\": [(\"P09\", 0.25)],\n",
" \"P05\": [(\"P08\", 0.30)], \"P06\": [(\"P16\", 0.30)], \"P09\": [(\"P14\", 0.20)], \"P12\": [(\"P07\", 0.20)]}\n",
"CRIT_SYS = {\"engine\", \"drive\", \"brake\"} # ๊ธฐ๋ยท์ ๋ ๋ถ๋ฅ ์ํ ๊ณํต(๊ฒฝ๋ณด ๋์)\n",
"MONTHS = 18\n",
"\n",
"\n",
"def make_fleet(n=100, seed=0):\n",
" rng = np.random.default_rng(seed)\n",
" f = pd.DataFrame({\"veh\": [f\"V{i:03d}\" for i in range(n)], \"age\": rng.integers(1, 26, n).astype(float),\n",
" \"terrain\": rng.uniform(0, 1, n), \"load\": rng.uniform(0, 1, n), \"pm_gap\": rng.uniform(20, 200, n),\n",
" \"humid\": rng.uniform(0, 1, n), \"base_h\": rng.uniform(55, 125, n)})\n",
" f[\"mult\"] = np.exp(0.045 * (f[\"age\"] - 13) + 0.9 * (f[\"terrain\"] - .5) + 0.8 * (f[\"load\"] - .5)\n",
" + 0.006 * (f[\"pm_gap\"] - 110) + 0.7 * (f[\"humid\"] - .5))\n",
" return f\n",
"\n",
"\n",
"def make_data(seed=1, months=MONTHS):\n",
" \"\"\"๋ฐํ: fleet(์ฐจ๋ ์์ฑ), ev(์ ๋น ์ด๋ ฅ 1๊ฑด=1ํ: ๋ฐ์ DTC ๋ชฉ๋ก, ์ค์ ๊ณ ์ฅ ๋ถํ), log(DTC ๋ฐ์ ๋ก๊ทธ, ํ=์ฝ๋ 1๊ฑด)\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" fleet = make_fleet()\n",
" V = len(fleet)\n",
" temp = 12 + 14 * np.sin(2 * np.pi * (np.arange(months) - 3) / 12) # ์ํ๊ท ๊ธฐ์จ(๊ฐ์)\n",
" hours = fleet[\"base_h\"].values[:, None] * (1 + 0.25 * np.sin(2 * np.pi * np.arange(months) / 12 + 1)) * rng.lognormal(0, .12, (V, months))\n",
" fleet[\"hours\"] = hours.sum(axis=1) # ์ฐจ๋๋ณ 18๊ฐ์ ์ด ๊ฐ๋์๊ฐ\n",
" fleet.attrs[\"month_h\"] = hours.sum(axis=0) # ์๋ณ ์ ์ฒด ๊ฐ๋์๊ฐ(๊ณ ์ฅ๋ฅ ๋ถ๋ชจ)\n",
" evs = []\n",
" for p in PID:\n",
" s = np.ones(months)\n",
" if p in (\"P03\", \"P04\"): s = np.exp(-0.06 * (temp - 12)) # ํ๋ญ๊ธฐ ์ ๊ธฐ๊ณํต ์ฆ๊ฐ\n",
" if p in (\"P06\", \"P16\"): s = np.exp(0.05 * (temp - 12)) # ํน์๊ธฐ ๋๊ฐ๊ณํต ์ฆ๊ฐ\n",
" cnt = rng.poisson(RATE[p] * hours / 1000 * fleet[\"mult\"].values[:, None] * s[None, :])\n",
" for v, m in zip(*np.nonzero(cnt)):\n",
" for _ in range(cnt[v, m]):\n",
" evs.append((fleet[\"veh\"][v], int(m), m * 720 + rng.uniform(0, 720), p, float(temp[m] + rng.normal(0, 3))))\n",
" ev = pd.DataFrame(evs, columns=[\"veh\", \"month\", \"t\", \"part\", \"temp\"]).sort_values(\"t\").reset_index(drop=True)\n",
" ev[\"ev_id\"] = [f\"W{i:05d}\" for i in range(len(ev))]\n",
" rows, codes = [], []\n",
" for e in ev.itertuples():\n",
" c, r = [], rng.random(3)\n",
" if r[0] < 0.6: c.append(SPEC[e.part])\n",
" if e.part in SHARED and r[1] < 0.6: c.append(SHARED[e.part])\n",
" if r[2] < 0.5: c.append(SYMP[PSYS[e.part]])\n",
" if not c: c.append(SHARED.get(e.part, SPEC[e.part]))\n",
" for q, pr in CASCADE.get(e.part, []):\n",
" if rng.random() < pr: c.append(SPEC[q])\n",
" if rng.random() < 0.12: c.append(SPEC[PID[rng.integers(0, 16)]])\n",
" c = list(dict.fromkeys(c))\n",
" rng.shuffle(c) # ๋์ ๋ฐ์ ์ฝ๋์ ๊ธฐ๋ก ์์๋ ์์\n",
" codes.append(c)\n",
" for k, cd in enumerate(c):\n",
" rows.append((e.ev_id, e.veh, e.t + k * 0.1, cd))\n",
" ev[\"codes\"] = codes\n",
" log = pd.DataFrame(rows, columns=[\"ev_id\", \"veh\", \"ts\", \"code\"])\n",
" return fleet, ev, log\n",
"\n",
"fleet, ev, log = make_data()\n",
"ev[\"first\"] = ev[\"codes\"].str[0]\n",
"train, test = ev[ev[\"month\"] < 12], ev[ev[\"month\"] >= 12]\n",
"\n",
"# 1) Baseline: ์ฒซ ๋ฒ์งธ๋ก ๊ธฐ๋ก๋ ์ฝ๋ -> ํ์ต ๊ธฐ๊ฐ์ ๊ทธ ์ฝ๋์ ๊ฐ์ฅ ์์ฃผ ํจ๊ป ๋์จ ๋ถํ\n",
"mp = train.groupby(\"first\")[\"part\"].agg(lambda s: s.value_counts().index[0])\n",
"pred = test[\"first\"].map(mp)\n",
"acc = (pred == test[\"part\"]).mean()\n",
"sys_acc = (pred.map(PSYS) == test[\"part\"].map(PSYS)).mean()\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ ๋น ๊ฑด {len(ev)}๊ฑด, DTC ๋ก๊ทธ {len(log)}ํ, ์ฐจ๋ {len(fleet)}๋, 18๊ฐ์\")\n",
"print(f\"Baseline ๋ถํ ์ ํ๋ {acc:.1%}, ๊ณ ์ฅ ๊ณํต ์ ํ๋ {sys_acc:.1%} (๊ฒ์ฆ {len(test)}๊ฑด)\")\n",
"amb = test[\"first\"].isin(list(SHARED.values()) + list(SYMP.values()))\n",
"print(f\" ๋ชจํธํ ์ฝ๋(๊ณต์ ยท์ฆ์) ์ฒซ ๊ธฐ๋ก ๊ฑด ์ ํ๋ {(pred[amb] == test['part'][amb]).mean():.1%} vs ๊ณ ์ ์ฝ๋ {(pred[~amb] == test['part'][~amb]).mean():.1%}\")\n",
"\n",
"# 2) ๋ถํ๋ณ ๊ณ ์ฅ๋ฅ (1000๊ฐ๋์๊ฐ๋น)๊ณผ ํ๋ ํ (ABC)\n",
"tot_h = fleet[\"hours\"].sum()\n",
"cnt = ev[\"part\"].value_counts().reindex(PID)\n",
"rate = (cnt / tot_h * 1000).round(3)\n",
"par = pd.DataFrame({\"๋ถํ\": [PNAME[p] for p in PID], \"๊ฑด์\": cnt.values, \"๊ณ ์ฅ๋ฅ \": rate.values}, index=PID).sort_values(\"๊ฑด์\", ascending=False)\n",
"par[\"๋์ ๋น์จ\"] = par[\"๊ฑด์\"].cumsum() / par[\"๊ฑด์\"].sum()\n",
"par[\"๋ฑ๊ธ\"] = np.where(par[\"๋์ ๋น์จ\"] <= 0.80, \"A\", np.where(par[\"๋์ ๋น์จ\"] <= 0.95, \"B\", \"C\"))\n",
"par.loc[par.index[0], \"๋ฑ๊ธ\"] = \"A\"\n",
"print(f\"\\n[ํ๋ ํ ] ์ด ๊ฐ๋ {tot_h:,.0f}์๊ฐ, ์ ์ฒด ๊ณ ์ฅ๋ฅ {cnt.sum() / tot_h * 1000:.1f}๊ฑด/1000h\")\n",
"print(par.head(7).assign(๋์ ๋น์จ=lambda d: d[\"๋์ ๋น์จ\"].map(\"{:.0%}\".format)).to_string())\n",
"top20 = par.head(int(np.ceil(len(par) * 0.2)))\n",
"print(f\"์์ 20% ํ๋ชฉ({len(top20)}์ข
: {', '.join(top20['๋ถํ'])})์ด ์ ์ฒด ๊ณ ์ฅ์ {top20['๊ฑด์'].sum() / cnt.sum():.0%}, A๋ฑ๊ธ {int((par['๋ฑ๊ธ'] == 'A').sum())}์ข
\")\n",
"\n",
"# 3) ์๊ด๋ถ์: ์ฐจ๋๋ณ ์ ๋น ๊ฑด์์ ํผ์ฒ\n",
"vc = ev.groupby(\"veh\").size().reindex(fleet[\"veh\"]).fillna(0).values\n",
"rows = []\n",
"for col, lab in [(\"age\", \"์ฐจ๋ น\"), (\"pm_gap\", \"์๋ฐฉ์ ๋น ๊ฒฝ๊ณผ์๊ฐ\"), (\"terrain\", \"์งํ ๊ฐํน๋\"), (\"load\", \"์ด์ฉ ๋ถํ\"), (\"hours\", \"์ด ๊ฐ๋์๊ฐ\"), (\"humid\", \"์ต๋ ์ง์\")]:\n",
" r, p = pearsonr(fleet[col], vc); rows.append((lab, r, p))\n",
"mc = ev.groupby([\"month\", \"part\"]).size().unstack(fill_value=0)\n",
"tm = ev.groupby(\"month\")[\"temp\"].mean()\n",
"r, p = pearsonr(tm, mc[\"P03\"]); rows.append((\"์ํ๊ท ๊ธฐ์จ-์ถ์ ์ง ๊ณ ์ฅ\", r, p))\n",
"cor = pd.DataFrame(rows, columns=[\"ํผ์ฒ\", \"r\", \"p\"])\n",
"cor[\"์ ์\"] = cor[\"p\"] < 0.05\n",
"print(\"\\n[์๊ด๋ถ์] ํผ์ฒ | r | p-value | ์ ์(p<0.05)\")\n",
"for x in cor.itertuples():\n",
" print(f\" {x.ํผ์ฒ:14s} {x.r:+.2f} {x.p:.4f} {'์ ์' if x.์ ์ else '-'}\")\n",
"nsig = int(cor[\"์ ์\"].sum())\n",
"\n",
"# 4) EDA ์ฐจํธ 8์ข
\n",
"fig, ax = plt.subplots(2, 4, figsize=(16, 7))\n",
"ax[0, 0].bar(par.index, par[\"๊ณ ์ฅ๋ฅ \"]); ax[0, 0].set_title(\"1 Failure rate per 1000h\")\n",
"ax[0, 1].plot(range(len(par)), par[\"๋์ ๋น์จ\"], marker=\"o\"); ax[0, 1].axhline(0.8, ls=\"--\", color=\"k\"); ax[0, 1].set_title(\"2 Pareto cumulative share\")\n",
"mc.sum(axis=1).plot(ax=ax[0, 2]); ax[0, 2].set_title(\"3 Monthly failures\")\n",
"ev[\"codes\"].str.len().value_counts().sort_index().plot.bar(ax=ax[0, 3]); ax[0, 3].set_title(\"4 DTCs per maintenance case\")\n",
"log[\"code\"].value_counts().head(10).plot.bar(ax=ax[1, 0]); ax[1, 0].set_title(\"5 Top DTC codes\")\n",
"ev[\"part\"].map(PSYS).value_counts().plot.bar(ax=ax[1, 1]); ax[1, 1].set_title(\"6 Failures by system\")\n",
"ax[1, 2].scatter(fleet[\"age\"], vc, s=8); ax[1, 2].set_title(\"7 Vehicle age vs failures\")\n",
"ax[1, 3].scatter(fleet[\"pm_gap\"], vc, s=8); ax[1, 3].set_title(\"8 PM gap vs failures\")\n",
"plt.tight_layout(); plt.show()\n",
"\n",
"print(f\"\\nEDA ์ฐจํธ 8์ข
์์ฑ(๋ชฉํ 8์ข
์ด์: ๋ฌ์ฑ), ์ ์ํ ํผ์ฒ {nsig}๊ฐ (๋ชฉํ 5๊ฐ ์ด์: {'๋ฌ์ฑ' if nsig >= 5 else '๋ฏธ๋ฌ'})\")\n",
"print(\"ํต์ฌ ๋ฐ๊ฒฌ: 1) ์์ ๋ถํ์ ๊ณ ์ฅ ์ง์ค 2) ๊ณต์ ยท์ฆ์ ์ฝ๋๊ฐ ์ฒซ ๊ธฐ๋ก์ด๋ฉด Baseline์ด ์์ฃผ ํ๋ฆผ 3) ๊ณ ๋ นยท๊ฐํน ์งํยท์๋ฐฉ์ ๋น ์ง์ฐ ์ฐจ๋์ ๊ณ ์ฅ ์ฆ๊ฐ\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
03 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ ๋น ๊ฑด 10681๊ฑด, DTC ๋ก๊ทธ 22079ํ, ์ฐจ๋ 100๋, 18๊ฐ์\n",
"Baseline ๋ถํ ์ ํ๋ 68.7%, ๊ณ ์ฅ ๊ณํต ์ ํ๋ 96.8% (๊ฒ์ฆ 3890๊ฑด)\n",
" ๋ชจํธํ ์ฝ๋(๊ณต์ ยท์ฆ์) ์ฒซ ๊ธฐ๋ก ๊ฑด ์ ํ๋ 59.7% vs ๊ณ ์ ์ฝ๋ 80.6%\n",
"\n",
"[ํ๋ ํ ] ์ด ๊ฐ๋ 171,649์๊ฐ, ์ ์ฒด ๊ณ ์ฅ๋ฅ 62.2๊ฑด/1000h\n",
" ๋ถํ ๊ฑด์ ๊ณ ์ฅ๋ฅ ๋์ ๋น์จ ๋ฑ๊ธ\n",
"P01 ์ฐ๋ฃ๋ถ์ฌ๊ธฐ 2800 16.312 26% A\n",
"P02 ํฐ๋ณด์ฐจ์ 1985 11.564 45% A\n",
"P03 ์ถ์ ์ง 1983 11.553 63% A\n",
"P04 ๋ฐ์ ๊ธฐ 1244 7.247 75% A\n",
"P05 ๋ณ์๊ธฐ ์ ์ด๋ชจ๋ 665 3.874 81% B\n",
"P06 ๋๊ฐ์ ํํ 477 2.779 86% B\n",
"P07 ์ ์ ํํ 390 2.272 89% B\n",
"์์ 20% ํ๋ชฉ(4์ข
: ์ฐ๋ฃ๋ถ์ฌ๊ธฐ, ํฐ๋ณด์ฐจ์ , ์ถ์ ์ง, ๋ฐ์ ๊ธฐ)์ด ์ ์ฒด ๊ณ ์ฅ์ 75%, A๋ฑ๊ธ 4์ข
\n",
"\n",
"[์๊ด๋ถ์] ํผ์ฒ | r | p-value | ์ ์(p<0.05)\n",
" ์ฐจ๋ น +0.41 0.0000 ์ ์\n",
" ์๋ฐฉ์ ๋น ๊ฒฝ๊ณผ์๊ฐ +0.57 0.0000 ์ ์\n",
" ์งํ ๊ฐํน๋ +0.40 0.0000 ์ ์\n",
" ์ด์ฉ ๋ถํ +0.35 0.0004 ์ ์\n",
" ์ด ๊ฐ๋์๊ฐ +0.32 0.0011 ์ ์\n",
" ์ต๋ ์ง์ +0.07 0.5174 -\n",
" ์ํ๊ท ๊ธฐ์จ-์ถ์ ์ง ๊ณ ์ฅ -0.95 0.0000 ์ ์\n",
"\n",
"EDA ์ฐจํธ 8์ข
์์ฑ(๋ชฉํ 8์ข
์ด์: ๋ฌ์ฑ), ์ ์ํ ํผ์ฒ 6๊ฐ (๋ชฉํ 5๊ฐ ์ด์: ๋ฌ์ฑ)\n",
"ํต์ฌ ๋ฐ๊ฒฌ: 1) ์์ ๋ถํ์ ๊ณ ์ฅ ์ง์ค 2) ๊ณต์ ยท์ฆ์ ์ฝ๋๊ฐ ์ฒซ ๊ธฐ๋ก์ด๋ฉด Baseline์ด ์์ฃผ ํ๋ฆผ 3) ๊ณ ๋ นยท๊ฐํน ์งํยท์๋ฐฉ์ ๋น ์ง์ฐ ์ฐจ๋์ ๊ณ ์ฅ ์ฆ๊ฐ\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
04. ์ฐ๊ด ๊ท์น ๋ง์ด๋(Apriori/FP-Growth) ๊ธฐ๋ฐ ๋ค์ค DTC ๋์ ๋ฐ์ ์ ๊ทผ๋ณธ ์์ธ ์์ธก ๋ชจ๋ธ ๊ณ ๋ํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ฐ๊ด ๊ท์น ๋ง์ด๋ ๊ธฐ๋ฐ ๋ค์ค DTC ๋์ ๋ฐ์ ์ ๊ทผ๋ณธ ์์ธ ์์ธก ๋ชจ๋ธ์ ๊ณ ๋ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ฐ๊ด ๊ท์น ๋ง์ด๋(Apriori)์ผ๋ก ๋ค์ค DTC ๋์ ๋ฐ์ ์ ๊ทผ๋ณธ ์์ธ ๋ถํ์ ์์ธกํ๋ ๋ชจ๋ธ์ ๊ณ ๋ํํ๊ณ , ์๊ฐ ๊ณ ์ฅ ๊ฑด์ ๊ธฐ์ค ๋ชจ๋ธ์ ํ์ ํผ์ฒ๋ฅผ ์ค๊ณํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๋์ ๋ฐ์ DTC ์ธํธ์ ๊ทผ๋ณธ ์์ธ ๋ถํ์ ํญ๋ชฉ์ผ๋ก ๋ณด๊ณ ์ต์ ์ง์ง๋ ๋ฏธ๋ง์ ๊ฐ์ง์น๊ธฐํ๋ฉฐ ๋น๋ฐ ํญ๋ชฉ์งํฉ๊ณผ ์ฐ๊ด ๊ท์น(์ ๋ขฐ๋ยทํฅ์๋)์ ๊ตฌ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/features/mnt03_feature_engineering.py ๋ฐ ๊ธฐ์ค ๋ชจ๋ธ` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ตฐ์ ์ค๋ฌด์์ ๋ฉ๋ ๊ฐ๋ฅํ ํผ์ฒ ์ค๊ณ์ ๋
ผ๋ฆฌ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ํ์ ํผ์ฒ 10๊ฐ ์ด์ ์์ฑ, ๊ธฐ์ค ๋ชจ๋ธ MAPE 25% ์ดํ ๋ฌ์ฑ\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ํผ์ฒ ํ์ดํ๋ผ์ธ ๋ฐ ๋ฒ ์ด์ค๋ผ์ธ ๋ชจ๋ธ ์ฑ๋ฅ ์งํ ๋์ถ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ๊ต์ฐจ ๊ฒ์ฆ(Time-series Cross Validation)์ ํตํ ๊ณผ์ ํฉ ๋ฐฉ์ง\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"Apriori ์ฐ๊ด ๊ท์น ๋ง์ด๋์ผ๋ก ์ฌ๋ฌ DTC๊ฐ ๋์์ ๋ํ๋ฌ์ ๋์ ๊ทผ๋ณธ ์์ธ ๋ถํ์ ์์ธกํ๋ ๋ชจ๋ธ์ ๋ง๋ค์ด ๋จ์ผ ์ฝ๋ Baseline๊ณผ ๋น๊ตํ๊ณ , ๋ถํ๋ณ ์๊ฐ ๊ณ ์ฅ ๊ฑด์ ์์ธก์ฉ ํ์ ํผ์ฒ 10๊ฐ ์ด์์ผ๋ก ๊ธฐ์ค ๋ชจ๋ธ์ ํ์ตํด ์๊ณ์ด ๊ต์ฐจ๊ฒ์ฆ MAPE์ ํผ์ฒ ์ค์๋๋ฅผ ํ์ธํ๋ค.\"\"\"\n",
"import itertools\n",
"from collections import Counter\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.linear_model import Ridge\n",
"from sklearn.preprocessing import StandardScaler\n",
"from sklearn.pipeline import make_pipeline\n",
"from sklearn.inspection import permutation_importance\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ฐจ๋ 100๋์ 18๊ฐ์ ๊ณ ์ฅ ์ง๋จ ์ฝ๋(DTC)ยท์ ๋น ์ด๋ ฅ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์ฅ๋นยท์ ๋น ์ค์ ์์)\n",
"# ๋ถํ 16์ข
(1000๊ฐ๋์๊ฐ๋น ๊ณ ์ฅ๋ฅ ์ฐจ๋ฑ), DTC 28์ข
(๋ถํ ๊ณ ์ ์ฝ๋, ์ง ๋ถํ ๊ณต์ ์ฝ๋, ๊ณํต ์ฆ์ ์ฝ๋)\n",
"# ๊ณ ์ฅ 1๊ฑด(์ ๋น ์ด๋ ฅ)๋ง๋ค DTC๊ฐ 1~5๊ฐ ๋์ ๋ฐ์ํ๊ณ , ์ฐ์ ๊ณ ์ฅ ์ฝ๋์ ์ผ์ ์ก์ ์ฝ๋๊ฐ ์์ธ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"PARTS = [ # (ID, ์ด๋ฆ, ๊ณํต, 1000๊ฐ๋์๊ฐ๋น ๊ณ ์ฅ๋ฅ , ๊ฐ์ ๋จ๊ฐ์ง์, ์๋ฆฌ์๊ฐ h)\n",
" (\"P01\", \"์ฐ๋ฃ๋ถ์ฌ๊ธฐ\", \"engine\", 12.0, 3.0, 2.0), (\"P02\", \"ํฐ๋ณด์ฐจ์ \", \"engine\", 9.0, 8.0, 5.0),\n",
" (\"P03\", \"์ถ์ ์ง\", \"electric\", 6.5, 4.0, 1.5), (\"P04\", \"๋ฐ์ ๊ธฐ\", \"electric\", 4.0, 9.0, 3.0),\n",
" (\"P05\", \"๋ณ์๊ธฐ ์ ์ด๋ชจ๋\", \"drive\", 3.0, 12.0, 6.0), (\"P06\", \"๋๊ฐ์ ํํ\", \"engine\", 2.2, 3.5, 3.0),\n",
" (\"P07\", \"์ ์ ํํ\", \"hydraulic\", 1.6, 10.0, 4.0), (\"P08\", \"์๋ ์ผ์\", \"drive\", 1.2, 2.0, 1.5),\n",
" (\"P09\", \"ECU ์ปค๋ฅํฐ\", \"electric\", 0.9, 2.5, 1.5), (\"P10\", \"์ฐ๋ฃ ํํฐ\", \"engine\", 0.7, 0.8, 0.7),\n",
" (\"P11\", \"์ ๋ ๋ฐธ๋ธ\", \"brake\", 0.5, 6.0, 3.0), (\"P12\", \"์กฐํฅ ์ก์ถ์์ดํฐ\", \"drive\", 0.4, 9.0, 5.0),\n",
" (\"P13\", \"๊ณต๊ธฐ ํํฐ\", \"engine\", 0.3, 0.5, 0.5), (\"P14\", \"ํต์ ์ธํฐํ์ด์ค ๋ชจ๋\", \"electric\", 0.25, 3.0, 1.5),\n",
" (\"P15\", \"์คํ์ ์๋ ฅ ์ผ์\", \"engine\", 0.2, 1.0, 1.0), (\"P16\", \"ํฌ ๊ตฌ๋ ๋ชจํฐ\", \"cooling\", 0.15, 2.0, 2.0)]\n",
"PID = [p[0] for p in PARTS]\n",
"PNAME = {p[0]: p[1] for p in PARTS}; PSYS = {p[0]: p[2] for p in PARTS}; RATE = {p[0]: p[3] for p in PARTS}\n",
"COST = {p[0]: p[4] for p in PARTS}; REPH = {p[0]: p[5] for p in PARTS}\n",
"PAIRS = [(\"P01\", \"P02\"), (\"P03\", \"P04\"), (\"P05\", \"P08\"), (\"P06\", \"P10\"), (\"P09\", \"P14\"), (\"P13\", \"P15\")]\n",
"SHARED = {p: f\"S{k + 1:02d}\" for k, pr in enumerate(PAIRS) for p in pr} # ์ง ๋ถํ์ด ํจ๊ป ์ฐ๋ ๋ชจํธํ ์ฝ๋\n",
"SPEC = {p: f\"C{p[1:]}1\" for p in PID} # ๋ถํ ๊ณ ์ ์ฝ๋\n",
"SYS = [\"engine\", \"electric\", \"drive\", \"hydraulic\", \"brake\", \"cooling\"]\n",
"SYMP = {s: f\"G{i + 1:02d}\" for i, s in enumerate(SYS)} # ๊ณํต ์ฆ์ ์ฝ๋\n",
"CODES = sorted(set(SPEC.values()) | set(SHARED.values()) | set(SYMP.values()))\n",
"CASCADE = {\"P01\": [(\"P10\", 0.20)], \"P02\": [(\"P06\", 0.20)], \"P03\": [(\"P04\", 0.25)], \"P04\": [(\"P09\", 0.25)],\n",
" \"P05\": [(\"P08\", 0.30)], \"P06\": [(\"P16\", 0.30)], \"P09\": [(\"P14\", 0.20)], \"P12\": [(\"P07\", 0.20)]}\n",
"CRIT_SYS = {\"engine\", \"drive\", \"brake\"} # ๊ธฐ๋ยท์ ๋ ๋ถ๋ฅ ์ํ ๊ณํต(๊ฒฝ๋ณด ๋์)\n",
"MONTHS = 18\n",
"\n",
"\n",
"def make_fleet(n=100, seed=0):\n",
" rng = np.random.default_rng(seed)\n",
" f = pd.DataFrame({\"veh\": [f\"V{i:03d}\" for i in range(n)], \"age\": rng.integers(1, 26, n).astype(float),\n",
" \"terrain\": rng.uniform(0, 1, n), \"load\": rng.uniform(0, 1, n), \"pm_gap\": rng.uniform(20, 200, n),\n",
" \"humid\": rng.uniform(0, 1, n), \"base_h\": rng.uniform(55, 125, n)})\n",
" f[\"mult\"] = np.exp(0.045 * (f[\"age\"] - 13) + 0.9 * (f[\"terrain\"] - .5) + 0.8 * (f[\"load\"] - .5)\n",
" + 0.006 * (f[\"pm_gap\"] - 110) + 0.7 * (f[\"humid\"] - .5))\n",
" return f\n",
"\n",
"\n",
"def make_data(seed=1, months=MONTHS):\n",
" \"\"\"๋ฐํ: fleet(์ฐจ๋ ์์ฑ), ev(์ ๋น ์ด๋ ฅ 1๊ฑด=1ํ: ๋ฐ์ DTC ๋ชฉ๋ก, ์ค์ ๊ณ ์ฅ ๋ถํ), log(DTC ๋ฐ์ ๋ก๊ทธ, ํ=์ฝ๋ 1๊ฑด)\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" fleet = make_fleet()\n",
" V = len(fleet)\n",
" temp = 12 + 14 * np.sin(2 * np.pi * (np.arange(months) - 3) / 12) # ์ํ๊ท ๊ธฐ์จ(๊ฐ์)\n",
" hours = fleet[\"base_h\"].values[:, None] * (1 + 0.25 * np.sin(2 * np.pi * np.arange(months) / 12 + 1)) * rng.lognormal(0, .12, (V, months))\n",
" fleet[\"hours\"] = hours.sum(axis=1) # ์ฐจ๋๋ณ 18๊ฐ์ ์ด ๊ฐ๋์๊ฐ\n",
" fleet.attrs[\"month_h\"] = hours.sum(axis=0) # ์๋ณ ์ ์ฒด ๊ฐ๋์๊ฐ(๊ณ ์ฅ๋ฅ ๋ถ๋ชจ)\n",
" evs = []\n",
" for p in PID:\n",
" s = np.ones(months)\n",
" if p in (\"P03\", \"P04\"): s = np.exp(-0.06 * (temp - 12)) # ํ๋ญ๊ธฐ ์ ๊ธฐ๊ณํต ์ฆ๊ฐ\n",
" if p in (\"P06\", \"P16\"): s = np.exp(0.05 * (temp - 12)) # ํน์๊ธฐ ๋๊ฐ๊ณํต ์ฆ๊ฐ\n",
" cnt = rng.poisson(RATE[p] * hours / 1000 * fleet[\"mult\"].values[:, None] * s[None, :])\n",
" for v, m in zip(*np.nonzero(cnt)):\n",
" for _ in range(cnt[v, m]):\n",
" evs.append((fleet[\"veh\"][v], int(m), m * 720 + rng.uniform(0, 720), p, float(temp[m] + rng.normal(0, 3))))\n",
" ev = pd.DataFrame(evs, columns=[\"veh\", \"month\", \"t\", \"part\", \"temp\"]).sort_values(\"t\").reset_index(drop=True)\n",
" ev[\"ev_id\"] = [f\"W{i:05d}\" for i in range(len(ev))]\n",
" rows, codes = [], []\n",
" for e in ev.itertuples():\n",
" c, r = [], rng.random(3)\n",
" if r[0] < 0.6: c.append(SPEC[e.part])\n",
" if e.part in SHARED and r[1] < 0.6: c.append(SHARED[e.part])\n",
" if r[2] < 0.5: c.append(SYMP[PSYS[e.part]])\n",
" if not c: c.append(SHARED.get(e.part, SPEC[e.part]))\n",
" for q, pr in CASCADE.get(e.part, []):\n",
" if rng.random() < pr: c.append(SPEC[q])\n",
" if rng.random() < 0.12: c.append(SPEC[PID[rng.integers(0, 16)]])\n",
" c = list(dict.fromkeys(c))\n",
" rng.shuffle(c) # ๋์ ๋ฐ์ ์ฝ๋์ ๊ธฐ๋ก ์์๋ ์์\n",
" codes.append(c)\n",
" for k, cd in enumerate(c):\n",
" rows.append((e.ev_id, e.veh, e.t + k * 0.1, cd))\n",
" ev[\"codes\"] = codes\n",
" log = pd.DataFrame(rows, columns=[\"ev_id\", \"veh\", \"ts\", \"code\"])\n",
" return fleet, ev, log\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# ๊ณตํต ๋ถ์ ์กฐ๊ฐ: ์ฝ๋ ๋ฉํฐํซ ํ๋ ฌ, ์ฐจ๋ ๋งฅ๋ฝ ํผ์ฒ, ํ์ต/๊ฒ์ฆ ๋ถํ (์ 12๊ฐ์ ํ์ต, ๋ค 6๊ฐ์ ๊ฒ์ฆ)\n",
"# ---------------------------------------------------------------------------\n",
"CIDX = {c: i for i, c in enumerate(CODES)}\n",
"SPLIT_M = 12\n",
"\n",
"\n",
"def multi_hot(ev):\n",
" X = np.zeros((len(ev), len(CODES)))\n",
" for r, cs in enumerate(ev[\"codes\"]):\n",
" for c in cs:\n",
" X[r, CIDX[c]] = 1\n",
" return X\n",
"\n",
"\n",
"def context(ev, fleet):\n",
" f = fleet.set_index(\"veh\")\n",
" return np.column_stack([ev[\"veh\"].map(f[\"age\"]) / 25, ev[\"veh\"].map(f[\"pm_gap\"]) / 200, ev[\"veh\"].map(f[\"terrain\"]),\n",
" ev[\"veh\"].map(f[\"load\"]), (ev[\"temp\"] - 12) / 15])\n",
"\n",
"\n",
"def feats(ev, fleet):\n",
" X = multi_hot(ev)\n",
" n = X.sum(axis=1, keepdims=True) / 3\n",
" return np.hstack([X, n, context(ev, fleet)])\n",
"\n",
"\n",
"def monthly_counts(ev):\n",
" return ev.groupby([\"month\", \"part\"]).size().unstack(fill_value=0).reindex(columns=PID, fill_value=0)\n",
"\n",
"\n",
"def build_monthly(ev, fleet, top_n=8):\n",
" \"\"\"๋ถํ๋ณ ์๊ฐ ๊ณ ์ฅ ๊ฑด์ ์์ธก์ฉ ํ: ์ง์ฐ๊ฐ(lag), ์ด๋ํ๊ท , ์ถ์ธ, ๊ธฐ๋ ๊ฑด์(๊ฐ๋์๊ฐ x ์ค๊ณ ๊ณ ์ฅ๋ฅ ), ๊ธฐ์จ ์ํธ์์ฉ, ๊ณ์ ์ฑ๋ถ\"\"\"\n",
" mc = ev.groupby([\"month\", \"part\"]).size().unstack(fill_value=0)\n",
" top = ev[\"part\"].value_counts().index[:top_n]\n",
" mh, tm = fleet.attrs[\"month_h\"], ev.groupby(\"month\")[\"temp\"].mean().values\n",
" rows = []\n",
" for p in top:\n",
" c = mc[p].values.astype(float)\n",
" for m in range(3, MONTHS):\n",
" L = np.log1p(c)\n",
" rows.append({\"part\": p, \"month\": m, \"y\": c[m], \"lag1\": L[m - 1], \"lag2\": L[m - 2], \"lag3\": L[m - 3], \"roll3\": L[m - 3:m].mean(),\n",
" \"trend\": L[m - 1] - L[m - 2], \"roll3_std\": L[m - 3:m].std(), \"log_expected\": np.log(mh[m] / 1000 * RATE[p]),\n",
" \"rate_lag1\": c[m - 1] / (mh[m - 1] / 1000) / RATE[p], \"temp\": tm[m] / 10, \"cold_elec\": max(0, 12 - tm[m]) / 10 * (p in (\"P03\", \"P04\")),\n",
" \"heat_cool\": max(0, tm[m] - 12) / 10 * (p in (\"P06\", \"P16\")), \"sin\": np.sin(2 * np.pi * m / 12), \"cos\": np.cos(2 * np.pi * m / 12)})\n",
" D = pd.DataFrame(rows)\n",
" return D, [c for c in D.columns if c not in (\"part\", \"month\", \"y\")], top\n",
"\n",
"fleet, ev, log = make_data()\n",
"ev[\"first\"] = ev[\"codes\"].str[0]\n",
"train, test = ev[ev[\"month\"] < 12], ev[ev[\"month\"] >= 12]\n",
"\n",
"\n",
"# 1) Apriori: ํญ๋ชฉ = DTC ์ฝ๋ + ๊ทผ๋ณธ ์์ธ ํญ๋ชฉ(ROOT:๋ถํ). ์์ค๋ณ๋ก ํ๋ณด๋ฅผ ๋ง๋ค๊ณ ์ต์ ์ง์ง๋ ๋ฏธ๋ง์ ๊ฐ์ง์น๊ธฐํ๋ค.\n",
"def apriori(trans, min_sup, max_len=3):\n",
" n = len(trans)\n",
" cnt = Counter(i for t in trans for i in t)\n",
" freq = {frozenset([i]): c / n for i, c in cnt.items() if c / n >= min_sup}\n",
" allf, cur = dict(freq), list(freq)\n",
" for k in range(2, max_len + 1):\n",
" cands = {a | b for a, b in itertools.combinations(cur, 2) if len(a | b) == k}\n",
" cands = {c for c in cands if all(frozenset(s) in allf for s in itertools.combinations(c, k - 1))} # ๋ถ๋ถ์งํฉ ๊ฐ์ง์น๊ธฐ\n",
" cc = Counter(c for t in trans for c in cands if c <= t)\n",
" cur = [c for c, v in cc.items() if v / n >= min_sup]\n",
" allf.update({c: cc[c] / n for c in cur})\n",
" return allf\n",
"\n",
"\n",
"trans = [frozenset(cs) | {\"ROOT:\" + p} for cs, p in zip(train[\"codes\"], train[\"part\"])]\n",
"F = apriori(trans, min_sup=0.004)\n",
"rules = []\n",
"for s, sup in F.items():\n",
" roots = [i for i in s if i.startswith(\"ROOT:\")]\n",
" if len(roots) == 1 and len(s) >= 2:\n",
" ante = s - set(roots)\n",
" conf = sup / F[ante]\n",
" lift = conf / F[frozenset(roots)]\n",
" rules.append({\"ante\": ante, \"part\": roots[0][5:], \"sup\": sup, \"conf\": conf, \"lift\": lift})\n",
"R = pd.DataFrame(rules)\n",
"R = R[(R[\"conf\"] >= 0.5) & (R[\"lift\"] > 1.5)].sort_values(\"conf\", ascending=False).reset_index(drop=True)\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ํ์ต ์ ๋น ๊ฑด {len(train)}๊ฑด: ๋น๋ฐ ํญ๋ชฉ์งํฉ {len(F)}๊ฐ -> ์ฐ๊ด ๊ท์น {len(R)}๊ฐ (์ ๋ขฐ๋ 0.5 ์ด์, ํฅ์๋ 1.5 ์ด๊ณผ)\")\n",
"for r in R.head(4).itertuples():\n",
" print(f\" {{{', '.join(sorted(r.ante))}}} -> {PNAME[r.part]} ์ง์ง๋ {r.sup:.3f} ์ ๋ขฐ๋ {r.conf:.2f} ํฅ์๋ {r.lift:.1f}\")\n",
"\n",
"mp = train.groupby(\"first\")[\"part\"].agg(lambda s: s.value_counts().index[0])\n",
"rl = sorted(zip(R[\"ante\"], R[\"part\"], R[\"conf\"]), key=lambda x: (-x[2], -len(x[0])))\n",
"\n",
"\n",
"def rule_predict(codes, fallback):\n",
" for a, p, c in rl:\n",
" if a <= set(codes):\n",
" return p, c\n",
" return fallback, 0.0\n",
"\n",
"\n",
"pr = [rule_predict(cs, mp.get(f, \"P01\")) for cs, f in zip(test[\"codes\"], test[\"first\"])]\n",
"pred_rule = np.array([p for p, _ in pr]); pred_base = test[\"first\"].map(mp).values\n",
"a_b, a_r = (pred_base == test[\"part\"].values).mean(), (pred_rule == test[\"part\"].values).mean()\n",
"multi = test[\"codes\"].str.len().values >= 2\n",
"print(f\"\\n๊ทผ๋ณธ ์์ธ ์ ํ๋: ๋จ์ผ ์ฝ๋ Baseline {a_b:.1%} -> ์ฐ๊ด ๊ท์น {a_r:.1%} (์ฝ๋ 2๊ฐ ์ด์ ๋์ ๋ฐ์ ๊ฑด {(pred_base[multi] == test['part'].values[multi]).mean():.1%} -> {(pred_rule[multi] == test['part'].values[multi]).mean():.1%})\")\n",
"\n",
"# 2) ํ์ ํผ์ฒ + ์๊ฐ ๊ณ ์ฅ ๊ฑด์ ๊ธฐ์ค ๋ชจ๋ธ(์์ 8๊ฐ ๋ถํ, ๊ฑด์๋ ๋ก๊ทธ ๋ณํํด ํ์ต)\n",
"D, FE, top = build_monthly(ev, fleet)\n",
"print(f\"\\n์๊ฐ ๊ณ ์ฅ ๊ฑด์ ์์ธก: ํ์ ํผ์ฒ {len(FE)}๊ฐ (๋ชฉํ 10๊ฐ ์ด์: {'๋ฌ์ฑ' if len(FE) >= 10 else '๋ฏธ๋ฌ'}), ๋์ {len(top)}๊ฐ ๋ถํ x {MONTHS - 3}๊ฐ์\")\n",
"\n",
"\n",
"def model():\n",
" return make_pipeline(StandardScaler(), Ridge(alpha=3.0))\n",
"\n",
"\n",
"def mape(a, p):\n",
" return float(np.mean(np.abs(a - p) / a))\n",
"\n",
"\n",
"# 3) ์๊ณ์ด ๊ต์ฐจ๊ฒ์ฆ: ํ์ฅ ์๋(๊ณผ๊ฑฐ๋ก๋ง ํ์ต, ๋ค 2๊ฐ์ ๊ฒ์ฆ)\n",
"folds, mapes = [(range(3, s), range(s, s + 2)) for s in (11, 13, 15)], []\n",
"for tr, te in folds:\n",
" a, b = D[D[\"month\"].isin(tr)], D[D[\"month\"].isin(te)]\n",
" mapes.append(mape(b[\"y\"].values, np.exp(model().fit(a[FE], np.log(a[\"y\"])).predict(b[FE]))))\n",
"cv = float(np.mean(mapes))\n",
"print(\"์๊ณ์ด ๊ต์ฐจ๊ฒ์ฆ ํด๋๋ณ MAPE: \" + \", \".join(f\"{m:.1%}\" for m in mapes) + f\" -> ํ๊ท {cv:.1%} (๋ชฉํ 25% ์ดํ: {'๋ฌ์ฑ' if cv <= 0.25 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# 4) ํผ์ฒ ์ค์๋(์์ด ์ค์๋)๋ก ์ฐจ์ ์ถ์\n",
"a, b = D[D[\"month\"] < 13], D[D[\"month\"] >= 13]\n",
"m = model().fit(a[FE], np.log(a[\"y\"]))\n",
"imp = permutation_importance(m, b[FE], np.log(b[\"y\"]), n_repeats=20, random_state=0, scoring=\"neg_mean_absolute_error\").importances_mean\n",
"order = np.argsort(-imp)\n",
"keep = [FE[i] for i in order[:8]]\n",
"m5 = model().fit(a[keep], np.log(a[\"y\"]))\n",
"full, red = mape(b[\"y\"].values, np.exp(m.predict(b[FE]))), mape(b[\"y\"].values, np.exp(m5.predict(b[keep])))\n",
"print(\"ํผ์ฒ ์ค์๋ ์์ 5: \" + \", \".join(f\"{FE[i]}({imp[i]:.2f})\" for i in order[:5]))\n",
"print(f\"์ฐจ์ ์ถ์ {len(FE)}๊ฐ -> 8๊ฐ: ๊ฒ์ฆ MAPE {full:.1%} -> {red:.1%}\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(10, 3.5))\n",
"ax[0].bar([\"baseline\", \"rules\"], [a_b, a_r]); ax[0].set_title(\"Root-cause accuracy\")\n",
"ax[1].barh([FE[i] for i in order[:8]][::-1], imp[order[:8]][::-1]); ax[1].set_title(\"Permutation importance (log MAE)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
04 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ํ์ต ์ ๋น ๊ฑด 6791๊ฑด: ๋น๋ฐ ํญ๋ชฉ์งํฉ 188๊ฐ -> ์ฐ๊ด ๊ท์น 57๊ฐ (์ ๋ขฐ๋ 0.5 ์ด์, ํฅ์๋ 1.5 ์ด๊ณผ)\n",
" {G04} -> ์ ์ ํํ ์ง์ง๋ 0.020 ์ ๋ขฐ๋ 1.00 ํฅ์๋ 29.1\n",
" {G05} -> ์ ๋ ๋ฐธ๋ธ ์ง์ง๋ 0.007 ์ ๋ขฐ๋ 1.00 ํฅ์๋ 87.1\n",
" {C051, G03} -> ๋ณ์๊ธฐ ์ ์ด๋ชจ๋ ์ง์ง๋ 0.021 ์ ๋ขฐ๋ 1.00 ํฅ์๋ 15.5\n",
" {C071, G04} -> ์ ์ ํํ ์ง์ง๋ 0.013 ์ ๋ขฐ๋ 1.00 ํฅ์๋ 29.1\n",
"\n",
"๊ทผ๋ณธ ์์ธ ์ ํ๋: ๋จ์ผ ์ฝ๋ Baseline 68.7% -> ์ฐ๊ด ๊ท์น 78.3% (์ฝ๋ 2๊ฐ ์ด์ ๋์ ๋ฐ์ ๊ฑด 67.1% -> 81.1%)\n",
"\n",
"์๊ฐ ๊ณ ์ฅ ๊ฑด์ ์์ธก: ํ์ ํผ์ฒ 13๊ฐ (๋ชฉํ 10๊ฐ ์ด์: ๋ฌ์ฑ), ๋์ 8๊ฐ ๋ถํ x 15๊ฐ์\n",
"์๊ณ์ด ๊ต์ฐจ๊ฒ์ฆ ํด๋๋ณ MAPE: 28.9%, 17.3%, 17.8% -> ํ๊ท 21.3% (๋ชฉํ 25% ์ดํ: ๋ฌ์ฑ)\n",
"ํผ์ฒ ์ค์๋ ์์ 5: lag1(0.21), log_expected(0.18), lag2(0.14), cold_elec(0.09), rate_lag1(0.07)\n",
"์ฐจ์ ์ถ์ 13๊ฐ -> 8๊ฐ: ๊ฒ์ฆ MAPE 18.6% -> 21.6%\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
05. DTC ๊ธฐ๋ฐ ์ ๋น ์กฐ์น ๊ฐ์ด๋๋ผ์ธ ๋ฐ ์ถ์ฒ ์ ๋ขฐ๋ ์ ์ ์ฐ์ถ ์์ง ๊ฐ๋ฐ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"DTC ๊ธฐ๋ฐ ์ ๋น ์กฐ์น ๊ฐ์ด๋๋ผ์ธ ๋ฐ ์ถ์ฒ ์ ๋ขฐ๋ ์ ์ ์ฐ์ถ ์์ง์ ๊ฐ๋ฐํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **DTC ์
๋ ฅ์ผ๋ก ์ ๋น ์กฐ์น ๊ฐ์ด๋๋ผ์ธ๊ณผ ์ถ์ฒ ์ ๋ขฐ๋ ์ ์๋ฅผ ์ฐ์ถํ๋ ์์ง์ ๊ฐ๋ฐํ๊ณ ๊ธฐ์ค ๋ชจ๋ธ ๋๋น ๊ฐ์ ์ ํ์ธํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ฝ๋ ์ ํ๋ณ ๊ฐ์ค์น๋ฅผ ๋ ๊ณ ์ฅ ๋ถํ ํ๋ฅ ๋ชจ๋ธ๋ก ์์ 3๊ฐ ํ๋ณด์ ์ ๋ขฐ๋๋ฅผ ๋ด๊ณ , ํ๋ณด๋ณ ์ ๋น ์กฐ์น ์ ์ฐจ๋ฅผ ์ง๋จ ์นด๋๋ก ์ถ๋ ฅ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/mnt03_advanced_model.py` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๋น์ ํ/๋ถ๊ท์น ์์์ ๋ํ ๋ชจ๋ธ์ ์ ์๋ ฅ ๋ฐ ์์ธก ์์ ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๊ธฐ์ค ๋ชจ๋ธ ๋๋น ์ค์ฐจ์จ(MAE/RMSE) 20% ์ด์ ๊ฐ์ , R2 Score 0.85 ์ด์\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ๋ชจ๋ธ ํ์ดํผํ๋ผ๋ฏธํฐ ํ๋ ์๋ฃ ๋ฐ ์ต์ ๋ชจ๋ธ ๊ฐ์ค์น ์ ์ฅ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: Hold-out ํ
์คํธ ๋ฐ์ดํฐ์
๋์ ์ต์ข
์ฑ๋ฅ ํ๊ฐ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"DTC ์กฐํฉ๊ณผ ์ฐจ๋ ๋งฅ๋ฝ์ ์
๋ ฅ๋ฐ์ ํ๋ณด ๊ณ ์ฅ ๋ถํ 3๊ฐ์ ์ถ์ฒ ์ ๋ขฐ๋ ์ ์, ์ ๋น ์กฐ์น ๊ฐ์ด๋๋ผ์ธ์ ์ฐ์ถํ๋ ์ถ์ฒ ์์ง์ ๊ฐ๋ฐํ๊ณ , ์ ๋ขฐ๋๊ฐ ์ค์ ์ ํ๋์ ๋ง๋์ง(๋ณด์ ) ํ์ธํ๋ค. ๋ถํ๋ณ ์๊ฐ ๊ณ ์ฅ ๊ฑด์ ์์ธก์ ์ ์๊ฐ ๊ธฐ์ค ๋ชจ๋ธ ๋๋น ๋ฆฟ์งยท๊ทธ๋๋์ธํธ ๋ถ์คํ
์์๋ธ์ MAE/RMSE ๊ฐ์ ์จ๊ณผ R2๋ก ํ๊ฐํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.linear_model import LogisticRegression, Ridge\n",
"from sklearn.ensemble import GradientBoostingRegressor\n",
"from sklearn.preprocessing import StandardScaler\n",
"from sklearn.pipeline import make_pipeline\n",
"from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ฐจ๋ 100๋์ 18๊ฐ์ ๊ณ ์ฅ ์ง๋จ ์ฝ๋(DTC)ยท์ ๋น ์ด๋ ฅ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์ฅ๋นยท์ ๋น ์ค์ ์์)\n",
"# ๋ถํ 16์ข
(1000๊ฐ๋์๊ฐ๋น ๊ณ ์ฅ๋ฅ ์ฐจ๋ฑ), DTC 28์ข
(๋ถํ ๊ณ ์ ์ฝ๋, ์ง ๋ถํ ๊ณต์ ์ฝ๋, ๊ณํต ์ฆ์ ์ฝ๋)\n",
"# ๊ณ ์ฅ 1๊ฑด(์ ๋น ์ด๋ ฅ)๋ง๋ค DTC๊ฐ 1~5๊ฐ ๋์ ๋ฐ์ํ๊ณ , ์ฐ์ ๊ณ ์ฅ ์ฝ๋์ ์ผ์ ์ก์ ์ฝ๋๊ฐ ์์ธ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"PARTS = [ # (ID, ์ด๋ฆ, ๊ณํต, 1000๊ฐ๋์๊ฐ๋น ๊ณ ์ฅ๋ฅ , ๊ฐ์ ๋จ๊ฐ์ง์, ์๋ฆฌ์๊ฐ h)\n",
" (\"P01\", \"์ฐ๋ฃ๋ถ์ฌ๊ธฐ\", \"engine\", 12.0, 3.0, 2.0), (\"P02\", \"ํฐ๋ณด์ฐจ์ \", \"engine\", 9.0, 8.0, 5.0),\n",
" (\"P03\", \"์ถ์ ์ง\", \"electric\", 6.5, 4.0, 1.5), (\"P04\", \"๋ฐ์ ๊ธฐ\", \"electric\", 4.0, 9.0, 3.0),\n",
" (\"P05\", \"๋ณ์๊ธฐ ์ ์ด๋ชจ๋\", \"drive\", 3.0, 12.0, 6.0), (\"P06\", \"๋๊ฐ์ ํํ\", \"engine\", 2.2, 3.5, 3.0),\n",
" (\"P07\", \"์ ์ ํํ\", \"hydraulic\", 1.6, 10.0, 4.0), (\"P08\", \"์๋ ์ผ์\", \"drive\", 1.2, 2.0, 1.5),\n",
" (\"P09\", \"ECU ์ปค๋ฅํฐ\", \"electric\", 0.9, 2.5, 1.5), (\"P10\", \"์ฐ๋ฃ ํํฐ\", \"engine\", 0.7, 0.8, 0.7),\n",
" (\"P11\", \"์ ๋ ๋ฐธ๋ธ\", \"brake\", 0.5, 6.0, 3.0), (\"P12\", \"์กฐํฅ ์ก์ถ์์ดํฐ\", \"drive\", 0.4, 9.0, 5.0),\n",
" (\"P13\", \"๊ณต๊ธฐ ํํฐ\", \"engine\", 0.3, 0.5, 0.5), (\"P14\", \"ํต์ ์ธํฐํ์ด์ค ๋ชจ๋\", \"electric\", 0.25, 3.0, 1.5),\n",
" (\"P15\", \"์คํ์ ์๋ ฅ ์ผ์\", \"engine\", 0.2, 1.0, 1.0), (\"P16\", \"ํฌ ๊ตฌ๋ ๋ชจํฐ\", \"cooling\", 0.15, 2.0, 2.0)]\n",
"PID = [p[0] for p in PARTS]\n",
"PNAME = {p[0]: p[1] for p in PARTS}; PSYS = {p[0]: p[2] for p in PARTS}; RATE = {p[0]: p[3] for p in PARTS}\n",
"COST = {p[0]: p[4] for p in PARTS}; REPH = {p[0]: p[5] for p in PARTS}\n",
"PAIRS = [(\"P01\", \"P02\"), (\"P03\", \"P04\"), (\"P05\", \"P08\"), (\"P06\", \"P10\"), (\"P09\", \"P14\"), (\"P13\", \"P15\")]\n",
"SHARED = {p: f\"S{k + 1:02d}\" for k, pr in enumerate(PAIRS) for p in pr} # ์ง ๋ถํ์ด ํจ๊ป ์ฐ๋ ๋ชจํธํ ์ฝ๋\n",
"SPEC = {p: f\"C{p[1:]}1\" for p in PID} # ๋ถํ ๊ณ ์ ์ฝ๋\n",
"SYS = [\"engine\", \"electric\", \"drive\", \"hydraulic\", \"brake\", \"cooling\"]\n",
"SYMP = {s: f\"G{i + 1:02d}\" for i, s in enumerate(SYS)} # ๊ณํต ์ฆ์ ์ฝ๋\n",
"CODES = sorted(set(SPEC.values()) | set(SHARED.values()) | set(SYMP.values()))\n",
"CASCADE = {\"P01\": [(\"P10\", 0.20)], \"P02\": [(\"P06\", 0.20)], \"P03\": [(\"P04\", 0.25)], \"P04\": [(\"P09\", 0.25)],\n",
" \"P05\": [(\"P08\", 0.30)], \"P06\": [(\"P16\", 0.30)], \"P09\": [(\"P14\", 0.20)], \"P12\": [(\"P07\", 0.20)]}\n",
"CRIT_SYS = {\"engine\", \"drive\", \"brake\"} # ๊ธฐ๋ยท์ ๋ ๋ถ๋ฅ ์ํ ๊ณํต(๊ฒฝ๋ณด ๋์)\n",
"MONTHS = 18\n",
"\n",
"\n",
"def make_fleet(n=100, seed=0):\n",
" rng = np.random.default_rng(seed)\n",
" f = pd.DataFrame({\"veh\": [f\"V{i:03d}\" for i in range(n)], \"age\": rng.integers(1, 26, n).astype(float),\n",
" \"terrain\": rng.uniform(0, 1, n), \"load\": rng.uniform(0, 1, n), \"pm_gap\": rng.uniform(20, 200, n),\n",
" \"humid\": rng.uniform(0, 1, n), \"base_h\": rng.uniform(55, 125, n)})\n",
" f[\"mult\"] = np.exp(0.045 * (f[\"age\"] - 13) + 0.9 * (f[\"terrain\"] - .5) + 0.8 * (f[\"load\"] - .5)\n",
" + 0.006 * (f[\"pm_gap\"] - 110) + 0.7 * (f[\"humid\"] - .5))\n",
" return f\n",
"\n",
"\n",
"def make_data(seed=1, months=MONTHS):\n",
" \"\"\"๋ฐํ: fleet(์ฐจ๋ ์์ฑ), ev(์ ๋น ์ด๋ ฅ 1๊ฑด=1ํ: ๋ฐ์ DTC ๋ชฉ๋ก, ์ค์ ๊ณ ์ฅ ๋ถํ), log(DTC ๋ฐ์ ๋ก๊ทธ, ํ=์ฝ๋ 1๊ฑด)\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" fleet = make_fleet()\n",
" V = len(fleet)\n",
" temp = 12 + 14 * np.sin(2 * np.pi * (np.arange(months) - 3) / 12) # ์ํ๊ท ๊ธฐ์จ(๊ฐ์)\n",
" hours = fleet[\"base_h\"].values[:, None] * (1 + 0.25 * np.sin(2 * np.pi * np.arange(months) / 12 + 1)) * rng.lognormal(0, .12, (V, months))\n",
" fleet[\"hours\"] = hours.sum(axis=1) # ์ฐจ๋๋ณ 18๊ฐ์ ์ด ๊ฐ๋์๊ฐ\n",
" fleet.attrs[\"month_h\"] = hours.sum(axis=0) # ์๋ณ ์ ์ฒด ๊ฐ๋์๊ฐ(๊ณ ์ฅ๋ฅ ๋ถ๋ชจ)\n",
" evs = []\n",
" for p in PID:\n",
" s = np.ones(months)\n",
" if p in (\"P03\", \"P04\"): s = np.exp(-0.06 * (temp - 12)) # ํ๋ญ๊ธฐ ์ ๊ธฐ๊ณํต ์ฆ๊ฐ\n",
" if p in (\"P06\", \"P16\"): s = np.exp(0.05 * (temp - 12)) # ํน์๊ธฐ ๋๊ฐ๊ณํต ์ฆ๊ฐ\n",
" cnt = rng.poisson(RATE[p] * hours / 1000 * fleet[\"mult\"].values[:, None] * s[None, :])\n",
" for v, m in zip(*np.nonzero(cnt)):\n",
" for _ in range(cnt[v, m]):\n",
" evs.append((fleet[\"veh\"][v], int(m), m * 720 + rng.uniform(0, 720), p, float(temp[m] + rng.normal(0, 3))))\n",
" ev = pd.DataFrame(evs, columns=[\"veh\", \"month\", \"t\", \"part\", \"temp\"]).sort_values(\"t\").reset_index(drop=True)\n",
" ev[\"ev_id\"] = [f\"W{i:05d}\" for i in range(len(ev))]\n",
" rows, codes = [], []\n",
" for e in ev.itertuples():\n",
" c, r = [], rng.random(3)\n",
" if r[0] < 0.6: c.append(SPEC[e.part])\n",
" if e.part in SHARED and r[1] < 0.6: c.append(SHARED[e.part])\n",
" if r[2] < 0.5: c.append(SYMP[PSYS[e.part]])\n",
" if not c: c.append(SHARED.get(e.part, SPEC[e.part]))\n",
" for q, pr in CASCADE.get(e.part, []):\n",
" if rng.random() < pr: c.append(SPEC[q])\n",
" if rng.random() < 0.12: c.append(SPEC[PID[rng.integers(0, 16)]])\n",
" c = list(dict.fromkeys(c))\n",
" rng.shuffle(c) # ๋์ ๋ฐ์ ์ฝ๋์ ๊ธฐ๋ก ์์๋ ์์\n",
" codes.append(c)\n",
" for k, cd in enumerate(c):\n",
" rows.append((e.ev_id, e.veh, e.t + k * 0.1, cd))\n",
" ev[\"codes\"] = codes\n",
" log = pd.DataFrame(rows, columns=[\"ev_id\", \"veh\", \"ts\", \"code\"])\n",
" return fleet, ev, log\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# ๊ณตํต ๋ถ์ ์กฐ๊ฐ: ์ฝ๋ ๋ฉํฐํซ ํ๋ ฌ, ์ฐจ๋ ๋งฅ๋ฝ ํผ์ฒ, ํ์ต/๊ฒ์ฆ ๋ถํ (์ 12๊ฐ์ ํ์ต, ๋ค 6๊ฐ์ ๊ฒ์ฆ)\n",
"# ---------------------------------------------------------------------------\n",
"CIDX = {c: i for i, c in enumerate(CODES)}\n",
"SPLIT_M = 12\n",
"\n",
"\n",
"def multi_hot(ev):\n",
" X = np.zeros((len(ev), len(CODES)))\n",
" for r, cs in enumerate(ev[\"codes\"]):\n",
" for c in cs:\n",
" X[r, CIDX[c]] = 1\n",
" return X\n",
"\n",
"\n",
"def context(ev, fleet):\n",
" f = fleet.set_index(\"veh\")\n",
" return np.column_stack([ev[\"veh\"].map(f[\"age\"]) / 25, ev[\"veh\"].map(f[\"pm_gap\"]) / 200, ev[\"veh\"].map(f[\"terrain\"]),\n",
" ev[\"veh\"].map(f[\"load\"]), (ev[\"temp\"] - 12) / 15])\n",
"\n",
"\n",
"def feats(ev, fleet):\n",
" X = multi_hot(ev)\n",
" n = X.sum(axis=1, keepdims=True) / 3\n",
" return np.hstack([X, n, context(ev, fleet)])\n",
"\n",
"\n",
"def monthly_counts(ev):\n",
" return ev.groupby([\"month\", \"part\"]).size().unstack(fill_value=0).reindex(columns=PID, fill_value=0)\n",
"\n",
"\n",
"def build_monthly(ev, fleet, top_n=8):\n",
" \"\"\"๋ถํ๋ณ ์๊ฐ ๊ณ ์ฅ ๊ฑด์ ์์ธก์ฉ ํ: ์ง์ฐ๊ฐ(lag), ์ด๋ํ๊ท , ์ถ์ธ, ๊ธฐ๋ ๊ฑด์(๊ฐ๋์๊ฐ x ์ค๊ณ ๊ณ ์ฅ๋ฅ ), ๊ธฐ์จ ์ํธ์์ฉ, ๊ณ์ ์ฑ๋ถ\"\"\"\n",
" mc = ev.groupby([\"month\", \"part\"]).size().unstack(fill_value=0)\n",
" top = ev[\"part\"].value_counts().index[:top_n]\n",
" mh, tm = fleet.attrs[\"month_h\"], ev.groupby(\"month\")[\"temp\"].mean().values\n",
" rows = []\n",
" for p in top:\n",
" c = mc[p].values.astype(float)\n",
" for m in range(3, MONTHS):\n",
" L = np.log1p(c)\n",
" rows.append({\"part\": p, \"month\": m, \"y\": c[m], \"lag1\": L[m - 1], \"lag2\": L[m - 2], \"lag3\": L[m - 3], \"roll3\": L[m - 3:m].mean(),\n",
" \"trend\": L[m - 1] - L[m - 2], \"roll3_std\": L[m - 3:m].std(), \"log_expected\": np.log(mh[m] / 1000 * RATE[p]),\n",
" \"rate_lag1\": c[m - 1] / (mh[m - 1] / 1000) / RATE[p], \"temp\": tm[m] / 10, \"cold_elec\": max(0, 12 - tm[m]) / 10 * (p in (\"P03\", \"P04\")),\n",
" \"heat_cool\": max(0, tm[m] - 12) / 10 * (p in (\"P06\", \"P16\")), \"sin\": np.sin(2 * np.pi * m / 12), \"cos\": np.cos(2 * np.pi * m / 12)})\n",
" D = pd.DataFrame(rows)\n",
" return D, [c for c in D.columns if c not in (\"part\", \"month\", \"y\")], top\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ถํ๋ณ ์ ๋น ์กฐ์น ๊ฐ์ด๋๋ผ์ธ(์์ฝ 3๋จ๊ณ). ์ค์ ์ ๋น ๊ต๋ฒ์ด ์๋๋ผ ๊ต์ก์ฉ์ผ๋ก ๋ง๋ ์์ ๋ฌธ๊ตฌ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"GUIDE = {\n",
" \"P01\": \"์ฐ๋ฃ ์๋ ฅ ์ธก์ -> ๋ถ์ฌ๊ธฐ ๋์คยท๋งํ ํ์ธ -> ๋ถ๋ ์ ๋ถ์ฌ๊ธฐ ๊ต์ฒด\", \"P02\": \"ํก๊ธฐ ์๋ ฅยท์ด๋ฌผ ์ ๊ฒ -> ์ถ ์ ๊ฒฉ ํ์ธ -> ๋ถ๋ ์ ํฐ๋ณด์ฐจ์ ๊ต์ฒด\",\n",
" \"P03\": \"๋จ์ ์ ์ยท๋ถ์ ์ ๊ฒ -> ์ถฉ์ ๊ณํต ํ์ธ -> ๋ถ๋ ์ ์ถ์ ์ง ๊ต์ฒด\", \"P04\": \"๋ฐ์ ์ ์ ์ธก์ -> ๋ฒจํธยท๋ฐฐ์ ์ ๊ฒ -> ๋ถ๋ ์ ๋ฐ์ ๊ธฐ ๊ต์ฒด\",\n",
" \"P05\": \"์ ์ด๋ชจ๋ ์ํํธ์จ์ดยท์ปค๋ฅํฐ ์ ๊ฒ -> ๋ณ์ ์ด๋ ฅ ํ์ธ -> ๋ถ๋ ์ ๋ชจ๋ ๊ต์ฒด\", \"P06\": \"๋๊ฐ์๋ยท๋์ ์ ๊ฒ -> ํํ ์๋์ ํ์ธ -> ๋ถ๋ ์ ํํ ๊ต์ฒด\",\n",
" \"P07\": \"์ ์ ์ค์ผ๋ยท์๋ ฅ ์ ๊ฒ -> ํํ ๋์ค ํ์ธ -> ๋ถ๋ ์ ํํ ๊ต์ฒด\", \"P08\": \"์ผ์ ๋ฐฐ์ ยท๊ฐ๊ทน ์ ๊ฒ -> ์ ํธ ํํ ํ์ธ -> ๋ถ๋ ์ ์ผ์ ๊ต์ฒด\",\n",
" \"P09\": \"์ปค๋ฅํฐ ์ ์ดยท๋ถ์ ์ ๊ฒ -> ํ ์ํ ํ์ธ -> ๋ถ๋ ์ ์ปค๋ฅํฐ ๊ต์ฒด\", \"P10\": \"ํํฐ ์ค์ผ๋ ์ ๊ฒ -> ์ฐ๋ฃ ํ๋ฆ ํ์ธ -> ๋ถ๋ ์ ํํฐ ๊ต์ฒด\",\n",
" \"P11\": \"์ ๋ ์๋ ฅ ์ ๊ฒ -> ๋ฐธ๋ธ ์๋ ํ์ธ -> ๋ถ๋ ์ ๋ฐธ๋ธ ๊ต์ฒด(์์ ํ์ธ ํ์)\", \"P12\": \"์กฐํฅ ๋ฐ์ ์ ๊ฒ -> ์ก์ถ์์ดํฐ ์๋ ํ์ธ -> ๋ถ๋ ์ ๊ต์ฒด(์์ ํ์ธ ํ์)\",\n",
" \"P13\": \"ํํฐ ์ฐจ์ ์ ๊ฒ -> ์ค์ผ ํ์ธ -> ๋ถ๋ ์ ํํฐ ๊ต์ฒด\", \"P14\": \"ํต์ ๋งํฌ ์ํ ์ ๊ฒ -> ๋ชจ๋ ์ ์ ํ์ธ -> ๋ถ๋ ์ ๋ชจ๋ ๊ต์ฒด\",\n",
" \"P15\": \"์๋ ฅ ์ผ์ ์ ํธ ์ ๊ฒ -> ์คํ์ ๋ ํ์ธ -> ๋ถ๋ ์ ์ผ์ ๊ต์ฒด\", \"P16\": \"ํฌ ๋ชจํฐ ์ ๋ฅ ์ ๊ฒ -> ํ์ ํ์ธ -> ๋ถ๋ ์ ๋ชจํฐ ๊ต์ฒด\"}\n",
"\n",
"fleet, ev, log = make_data()\n",
"train, test = ev[ev[\"month\"] < SPLIT_M], ev[ev[\"month\"] >= SPLIT_M]\n",
"\n",
"# 1) ์ถ์ฒ ์์ง: DTC ๋ฉํฐํซ + ๋งฅ๋ฝ -> ๋ถํ๋ณ ํ๋ฅ (= ์ถ์ฒ ์ ๋ขฐ๋ ์ ์)\n",
"Xtr, Xte = feats(train, fleet), feats(test, fleet)\n",
"clf = LogisticRegression(C=3, max_iter=500).fit(Xtr, train[\"part\"])\n",
"P = clf.predict_proba(Xte)\n",
"cls = clf.classes_\n",
"top1 = cls[P.argmax(axis=1)]\n",
"conf = P.max(axis=1)\n",
"y = test[\"part\"].values\n",
"top3 = np.mean([t in cls[np.argsort(-p)[:3]] for t, p in zip(y, P)])\n",
"mp = train.assign(first=train[\"codes\"].str[0]).groupby(\"first\")[\"part\"].agg(lambda s: s.value_counts().index[0])\n",
"base_acc = (test[\"codes\"].str[0].map(mp).values == y).mean()\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ถ์ฒ ์์ง ๊ฒ์ฆ {len(test)}๊ฑด: top-1 ์ ํ๋ {(top1 == y).mean():.1%} (๋จ์ผ ์ฝ๋ Baseline {base_acc:.1%}), top-3 ํฌํจ๋ฅ {top3:.1%}\")\n",
"\n",
"# 2) ์ ๋ขฐ๋ ์ ์ ๋ณด์ : ์ ๋ขฐ๋ ๊ตฌ๊ฐ๋ณ ์ค์ ์ ํ๋\n",
"bins = [0, 0.5, 0.7, 0.9, 1.01]\n",
"lab = [\"~0.5\", \"0.5~0.7\", \"0.7~0.9\", \"0.9~\"]\n",
"cb = pd.cut(conf, bins, labels=lab, right=False)\n",
"cal = pd.DataFrame({\"conf\": conf, \"ok\": top1 == y, \"b\": cb}).groupby(\"b\", observed=True).agg(๊ฑด์=(\"ok\", \"size\"), ํ๊ท ์ ๋ขฐ๋=(\"conf\", \"mean\"), ์ค์ ์ ํ๋=(\"ok\", \"mean\"))\n",
"print(\"\\n[์ ๋ขฐ๋ ๋ณด์ ] ๊ตฌ๊ฐ | ๊ฑด์ | ํ๊ท ์ ๋ขฐ๋ | ์ค์ ์ ํ๋\")\n",
"for k, r in cal.iterrows():\n",
" print(f\" {k:8s} {int(r['๊ฑด์']):5d} {r['ํ๊ท ์ ๋ขฐ๋']:.2f} {r['์ค์ ์ ํ๋']:.2f}\")\n",
"ece = float((cal[\"๊ฑด์\"] * (cal[\"ํ๊ท ์ ๋ขฐ๋\"] - cal[\"์ค์ ์ ํ๋\"]).abs()).sum() / cal[\"๊ฑด์\"].sum())\n",
"print(f\"๊ธฐ๋ ๋ณด์ ์ค์ฐจ(ECE) {ece:.3f}\")\n",
"\n",
"# 3) ์ ๋น ์กฐ์น ๊ฐ์ด๋๋ผ์ธ ์นด๋(์์ ํ๋ณด 3๊ฐ์ ์ ๋ขฐ๋)\n",
"print(\"\\n[์ง๋จ ์นด๋ ์์] ์ ๋น ์กฐ์น ๊ฐ์ด๋๋ผ์ธ\")\n",
"for i in (int(np.argmin(np.abs(conf - 0.95))), int(np.argmin(np.abs(conf - 0.55)))):\n",
" order = np.argsort(-P[i])[:3]\n",
" print(f\" ์
๋ ฅ DTC {test['codes'].iloc[i]} -> \" + \", \".join(f\"{PNAME[cls[j]]} {P[i, j]:.0%}\" for j in order))\n",
" print(f\" ์กฐ์น: {GUIDE[cls[order[0]]]}\" + (\"\" if conf[i] >= 0.7 else \" (์ ๋ขฐ๋ ๋ฎ์: ์๋ จ ์ ๋น๊ด ํ์ธ ๊ถ๊ณ )\"))\n",
"\n",
"# 4) ์๊ฐ ๊ณ ์ฅ ๊ฑด์ ์์ธก: ๊ธฐ์ค(์ ์๊ฐ) vs ๊ณ ๋ํ(๋ฆฟ์ง + GBR ์์๋ธ, ์๊ท๋ชจ ๊ฒฉ์ ํ์)\n",
"D, FE, top = build_monthly(ev, fleet)\n",
"a, b = D[D[\"month\"] < 13], D[D[\"month\"] >= 13]\n",
"ya, yb = np.log(a[\"y\"]), b[\"y\"].values\n",
"naive = np.expm1(b[\"lag1\"].values)\n",
"def ens(tr, te, al, dep):\n",
" g = GradientBoostingRegressor(n_estimators=150, max_depth=dep, learning_rate=0.05, random_state=0).fit(tr[FE], np.log(tr[\"y\"]))\n",
" r = make_pipeline(StandardScaler(), Ridge(alpha=al)).fit(tr[FE], np.log(tr[\"y\"]))\n",
" return np.exp(0.5 * g.predict(te[FE]) + 0.5 * r.predict(te[FE]))\n",
"\n",
"\n",
"inner_tr, inner_va = a[a[\"month\"] < 11], a[a[\"month\"] >= 11] # ํ์ดํผํ๋ผ๋ฏธํฐ๋ ํ์ต ๊ตฌ๊ฐ ์์์๋ง ์ ํ(๊ฒ์ฆ ๊ตฌ๊ฐ ๋ฏธ์ฌ์ฉ)\n",
"score = {(al, dep): mean_absolute_error(inner_va[\"y\"], ens(inner_tr, inner_va, al, dep)) for al in (1, 3, 10) for dep in (2, 3)}\n",
"al, dep = min(score, key=score.get)\n",
"adv = ens(a, b, al, dep)\n",
"met = lambda p: (mean_absolute_error(yb, p), mean_squared_error(yb, p) ** 0.5, r2_score(yb, p))\n",
"m0, m1 = met(naive), met(adv)\n",
"print(f\"\\n[์๊ฐ ๊ณ ์ฅ ๊ฑด์ ์์ธก] ๊ฒ์ฆ {len(b)}๊ฑด(์์ {len(top)}๊ฐ ๋ถํ x 5๊ฐ์), ์ ํ ํ์ดํผํ๋ผ๋ฏธํฐ ridge alpha={al}, GBR depth={dep}\")\n",
"print(f\" ๊ธฐ์ค(์ ์๊ฐ) MAE {m0[0]:.2f} RMSE {m0[1]:.2f} R2 {m0[2]:.3f}\")\n",
"print(f\" ๊ณ ๋ํ(์์๋ธ) MAE {m1[0]:.2f} RMSE {m1[1]:.2f} R2 {m1[2]:.3f}\")\n",
"imp_mae, imp_rmse = 1 - m1[0] / m0[0], 1 - m1[1] / m0[1]\n",
"print(f\"๊ฐ์ ์จ MAE {imp_mae:.1%}, RMSE {imp_rmse:.1%} (๋ชฉํ 20% ์ด์: {'๋ฌ์ฑ' if min(imp_mae, imp_rmse) >= 0.2 else '๋ฏธ๋ฌ'}), R2 {m1[2]:.3f} (๋ชฉํ 0.85 ์ด์: {'๋ฌ์ฑ' if m1[2] >= 0.85 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(10, 3.5))\n",
"ax[0].plot([0, 1], [0, 1], \"k--\"); ax[0].plot(cal[\"ํ๊ท ์ ๋ขฐ๋\"], cal[\"์ค์ ์ ํ๋\"], marker=\"o\"); ax[0].set_title(\"Confidence calibration\")\n",
"ax[1].scatter(yb, adv, s=12, label=\"ensemble\"); ax[1].scatter(yb, naive, s=12, label=\"naive\"); ax[1].plot([0, yb.max()], [0, yb.max()], \"k--\")\n",
"ax[1].set_title(\"Monthly failure count: actual vs predicted\"); ax[1].legend()\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
05 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ถ์ฒ ์์ง ๊ฒ์ฆ 3890๊ฑด: top-1 ์ ํ๋ 83.9% (๋จ์ผ ์ฝ๋ Baseline 68.7%), top-3 ํฌํจ๋ฅ 99.5%\n",
"\n",
"[์ ๋ขฐ๋ ๋ณด์ ] ๊ตฌ๊ฐ | ๊ฑด์ | ํ๊ท ์ ๋ขฐ๋ | ์ค์ ์ ํ๋\n",
" ~0.5 155 0.46 0.48\n",
" 0.5~0.7 1123 0.60 0.61\n",
" 0.7~0.9 387 0.78 0.74\n",
" 0.9~ 2225 0.99 1.00\n",
"๊ธฐ๋ ๋ณด์ ์ค์ฐจ(ECE) 0.014\n",
"\n",
"[์ง๋จ ์นด๋ ์์] ์ ๋น ์กฐ์น ๊ฐ์ด๋๋ผ์ธ\n",
" ์
๋ ฅ DTC ['G01', 'C101', 'S01'] -> ์ฐ๋ฃ๋ถ์ฌ๊ธฐ 95%, ํฐ๋ณด์ฐจ์ 5%, ์ฐ๋ฃ ํํฐ 0%\n",
" ์กฐ์น: ์ฐ๋ฃ ์๋ ฅ ์ธก์ -> ๋ถ์ฌ๊ธฐ ๋์คยท๋งํ ํ์ธ -> ๋ถ๋ ์ ๋ถ์ฌ๊ธฐ ๊ต์ฒด\n",
" ์
๋ ฅ DTC ['G01'] -> ์ฐ๋ฃ๋ถ์ฌ๊ธฐ 55%, ํฐ๋ณด์ฐจ์ 34%, ์ฐ๋ฃ ํํฐ 5%\n",
" ์กฐ์น: ์ฐ๋ฃ ์๋ ฅ ์ธก์ -> ๋ถ์ฌ๊ธฐ ๋์คยท๋งํ ํ์ธ -> ๋ถ๋ ์ ๋ถ์ฌ๊ธฐ ๊ต์ฒด (์ ๋ขฐ๋ ๋ฎ์: ์๋ จ ์ ๋น๊ด ํ์ธ ๊ถ๊ณ )\n",
"\n",
"[์๊ฐ ๊ณ ์ฅ ๊ฑด์ ์์ธก] ๊ฒ์ฆ 40๊ฑด(์์ 8๊ฐ ๋ถํ x 5๊ฐ์), ์ ํ ํ์ดํผํ๋ผ๋ฏธํฐ ridge alpha=10, GBR depth=3\n",
" ๊ธฐ์ค(์ ์๊ฐ) MAE 15.50 RMSE 21.96 R2 0.860\n",
" ๊ณ ๋ํ(์์๋ธ) MAE 10.62 RMSE 14.47 R2 0.939\n",
"๊ฐ์ ์จ MAE 31.5%, RMSE 34.1% (๋ชฉํ 20% ์ด์: ๋ฌ์ฑ), R2 0.939 (๋ชฉํ 0.85 ์ด์: ๋ฌ์ฑ)\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
06. ์ ๋น๋ณ์ฉ ๋ชจ๋ฐ์ผ/์น ๊ณ ์ฅ ์ฝ๋ ์ง๋จ ๋ฐ ์ ๋น ์กฐ์น ์ ์ฐจ ๋์๋ณด๋ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ ๋น๋ณ์ฉ ๋ชจ๋ฐ์ผ/์น ๊ณ ์ฅ ์ฝ๋ ์ง๋จ ๋์๋ณด๋๋ฅผ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ ๋น๋ณ์ฉ ๊ณ ์ฅ ์ฝ๋ ์ง๋จ ํ๋ฉด(์ง๋จ ์นด๋์ ์น ๋์๋ณด๋ ๊ตฌ์ฑ)์ ๊ตฌํํ๊ณ , ์์ธก๋ง๋ค ๊ธฐ์ฌ๋ ์์ 3๊ฐ ์ธ์์ ์์ฐ์ด ์์ฝ์ ์๋ ์ฐ์ถํ๋ ์ค๋ช
๊ฐ๋ฅ์ฑ(XAI) ๋ชจ๋์ ๋ง๋ ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๋ชจ๋ธ์ ์
๋ ฅ ์ธ์๋ณ ๊ธฐ์ฌ๋๋ฅผ ๊ณ์ฐํด ๊ฑด๋ง๋ค ์์ 3๊ฐ ๊ทผ๊ฑฐ๋ฅผ ๋ฝ๊ณ , ์ธ์๋ฅผ ํ๊ท ๊ฐ์ผ๋ก ๋๋๋ ธ์ ๋ ์์ธก ํ๋ฅ ์ด ๋ฎ์์ง๋์ง๋ก ๊ทผ๊ฑฐ ์ ํจ์ฑ์ ์ ๊ฒ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/explainability/mnt03_xai_explainer.py ๋ฐ XAI ๋ฆฌํฌํธ` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ตฐ์ ์ฅ๊ต/๊ตฐ๋ฌด์์ด ์ ๋ขฐํ๊ณ ์กฐ์นํ ์ ์๋ ์ค๋ช
์ ๋ช
ํ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์ฃผ์ ์์ธก ๊ฑด๋น ๊ธฐ์ฌ๋ ์์ 3๊ฐ ์ธ์ ์๋ ์ฐ์ถ๋ฅ 100%\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: SHAP summary plot ๋ฐ waterfall plot ์์ฑ ํ์ดํ๋ผ์ธ ์์ฑ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ์ค๋ฌด์ ๋์ ์ค๋ช
๊ทผ๊ฑฐ ์ ์ฉ์ฑ ํ๊ฐ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์ ๋น๋ณ์ด ๋ชจ๋ฐ์ผ/์น ํ๋ฉด์์ DTC๋ฅผ ์
๋ ฅํ๋ฉด ํ๋ณด ๊ณ ์ฅ ๋ถํยท์ ๋ขฐ๋ยท์ ๋น ์กฐ์น ์ ์ฐจ์ ํ๋จ ๊ทผ๊ฑฐ(๊ธฐ์ฌ๋ ์์ 3๊ฐ ์ธ์)๋ฅผ ๋ณด์ฌ ์ฃผ๋ ์ง๋จ ๋์๋ณด๋๋ฅผ ๊ตฌํํ๋ค. ๋ชจ๋ ์์ธก ๊ฑด์ ์์ 3๊ฐ ์ธ์๋ฅผ ์๋ ์ฐ์ถํ๊ณ ๊ทผ๊ฑฐ ์ ๊ฑฐ ์ ํ๋ฅ ์ด ์ค์ ๋ก ๋จ์ด์ง๋์ง ์ ๊ฒํ๋ฉฐ, ์์ฐ์ด ์์ฝ๊ณผ ๊ฒฝ๋ณด ํํฉ ํ๋ฉด์ ๋ง๋ ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.linear_model import LogisticRegression\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ฐจ๋ 100๋์ 18๊ฐ์ ๊ณ ์ฅ ์ง๋จ ์ฝ๋(DTC)ยท์ ๋น ์ด๋ ฅ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์ฅ๋นยท์ ๋น ์ค์ ์์)\n",
"# ๋ถํ 16์ข
(1000๊ฐ๋์๊ฐ๋น ๊ณ ์ฅ๋ฅ ์ฐจ๋ฑ), DTC 28์ข
(๋ถํ ๊ณ ์ ์ฝ๋, ์ง ๋ถํ ๊ณต์ ์ฝ๋, ๊ณํต ์ฆ์ ์ฝ๋)\n",
"# ๊ณ ์ฅ 1๊ฑด(์ ๋น ์ด๋ ฅ)๋ง๋ค DTC๊ฐ 1~5๊ฐ ๋์ ๋ฐ์ํ๊ณ , ์ฐ์ ๊ณ ์ฅ ์ฝ๋์ ์ผ์ ์ก์ ์ฝ๋๊ฐ ์์ธ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"PARTS = [ # (ID, ์ด๋ฆ, ๊ณํต, 1000๊ฐ๋์๊ฐ๋น ๊ณ ์ฅ๋ฅ , ๊ฐ์ ๋จ๊ฐ์ง์, ์๋ฆฌ์๊ฐ h)\n",
" (\"P01\", \"์ฐ๋ฃ๋ถ์ฌ๊ธฐ\", \"engine\", 12.0, 3.0, 2.0), (\"P02\", \"ํฐ๋ณด์ฐจ์ \", \"engine\", 9.0, 8.0, 5.0),\n",
" (\"P03\", \"์ถ์ ์ง\", \"electric\", 6.5, 4.0, 1.5), (\"P04\", \"๋ฐ์ ๊ธฐ\", \"electric\", 4.0, 9.0, 3.0),\n",
" (\"P05\", \"๋ณ์๊ธฐ ์ ์ด๋ชจ๋\", \"drive\", 3.0, 12.0, 6.0), (\"P06\", \"๋๊ฐ์ ํํ\", \"engine\", 2.2, 3.5, 3.0),\n",
" (\"P07\", \"์ ์ ํํ\", \"hydraulic\", 1.6, 10.0, 4.0), (\"P08\", \"์๋ ์ผ์\", \"drive\", 1.2, 2.0, 1.5),\n",
" (\"P09\", \"ECU ์ปค๋ฅํฐ\", \"electric\", 0.9, 2.5, 1.5), (\"P10\", \"์ฐ๋ฃ ํํฐ\", \"engine\", 0.7, 0.8, 0.7),\n",
" (\"P11\", \"์ ๋ ๋ฐธ๋ธ\", \"brake\", 0.5, 6.0, 3.0), (\"P12\", \"์กฐํฅ ์ก์ถ์์ดํฐ\", \"drive\", 0.4, 9.0, 5.0),\n",
" (\"P13\", \"๊ณต๊ธฐ ํํฐ\", \"engine\", 0.3, 0.5, 0.5), (\"P14\", \"ํต์ ์ธํฐํ์ด์ค ๋ชจ๋\", \"electric\", 0.25, 3.0, 1.5),\n",
" (\"P15\", \"์คํ์ ์๋ ฅ ์ผ์\", \"engine\", 0.2, 1.0, 1.0), (\"P16\", \"ํฌ ๊ตฌ๋ ๋ชจํฐ\", \"cooling\", 0.15, 2.0, 2.0)]\n",
"PID = [p[0] for p in PARTS]\n",
"PNAME = {p[0]: p[1] for p in PARTS}; PSYS = {p[0]: p[2] for p in PARTS}; RATE = {p[0]: p[3] for p in PARTS}\n",
"COST = {p[0]: p[4] for p in PARTS}; REPH = {p[0]: p[5] for p in PARTS}\n",
"PAIRS = [(\"P01\", \"P02\"), (\"P03\", \"P04\"), (\"P05\", \"P08\"), (\"P06\", \"P10\"), (\"P09\", \"P14\"), (\"P13\", \"P15\")]\n",
"SHARED = {p: f\"S{k + 1:02d}\" for k, pr in enumerate(PAIRS) for p in pr} # ์ง ๋ถํ์ด ํจ๊ป ์ฐ๋ ๋ชจํธํ ์ฝ๋\n",
"SPEC = {p: f\"C{p[1:]}1\" for p in PID} # ๋ถํ ๊ณ ์ ์ฝ๋\n",
"SYS = [\"engine\", \"electric\", \"drive\", \"hydraulic\", \"brake\", \"cooling\"]\n",
"SYMP = {s: f\"G{i + 1:02d}\" for i, s in enumerate(SYS)} # ๊ณํต ์ฆ์ ์ฝ๋\n",
"CODES = sorted(set(SPEC.values()) | set(SHARED.values()) | set(SYMP.values()))\n",
"CASCADE = {\"P01\": [(\"P10\", 0.20)], \"P02\": [(\"P06\", 0.20)], \"P03\": [(\"P04\", 0.25)], \"P04\": [(\"P09\", 0.25)],\n",
" \"P05\": [(\"P08\", 0.30)], \"P06\": [(\"P16\", 0.30)], \"P09\": [(\"P14\", 0.20)], \"P12\": [(\"P07\", 0.20)]}\n",
"CRIT_SYS = {\"engine\", \"drive\", \"brake\"} # ๊ธฐ๋ยท์ ๋ ๋ถ๋ฅ ์ํ ๊ณํต(๊ฒฝ๋ณด ๋์)\n",
"MONTHS = 18\n",
"\n",
"\n",
"def make_fleet(n=100, seed=0):\n",
" rng = np.random.default_rng(seed)\n",
" f = pd.DataFrame({\"veh\": [f\"V{i:03d}\" for i in range(n)], \"age\": rng.integers(1, 26, n).astype(float),\n",
" \"terrain\": rng.uniform(0, 1, n), \"load\": rng.uniform(0, 1, n), \"pm_gap\": rng.uniform(20, 200, n),\n",
" \"humid\": rng.uniform(0, 1, n), \"base_h\": rng.uniform(55, 125, n)})\n",
" f[\"mult\"] = np.exp(0.045 * (f[\"age\"] - 13) + 0.9 * (f[\"terrain\"] - .5) + 0.8 * (f[\"load\"] - .5)\n",
" + 0.006 * (f[\"pm_gap\"] - 110) + 0.7 * (f[\"humid\"] - .5))\n",
" return f\n",
"\n",
"\n",
"def make_data(seed=1, months=MONTHS):\n",
" \"\"\"๋ฐํ: fleet(์ฐจ๋ ์์ฑ), ev(์ ๋น ์ด๋ ฅ 1๊ฑด=1ํ: ๋ฐ์ DTC ๋ชฉ๋ก, ์ค์ ๊ณ ์ฅ ๋ถํ), log(DTC ๋ฐ์ ๋ก๊ทธ, ํ=์ฝ๋ 1๊ฑด)\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" fleet = make_fleet()\n",
" V = len(fleet)\n",
" temp = 12 + 14 * np.sin(2 * np.pi * (np.arange(months) - 3) / 12) # ์ํ๊ท ๊ธฐ์จ(๊ฐ์)\n",
" hours = fleet[\"base_h\"].values[:, None] * (1 + 0.25 * np.sin(2 * np.pi * np.arange(months) / 12 + 1)) * rng.lognormal(0, .12, (V, months))\n",
" fleet[\"hours\"] = hours.sum(axis=1) # ์ฐจ๋๋ณ 18๊ฐ์ ์ด ๊ฐ๋์๊ฐ\n",
" fleet.attrs[\"month_h\"] = hours.sum(axis=0) # ์๋ณ ์ ์ฒด ๊ฐ๋์๊ฐ(๊ณ ์ฅ๋ฅ ๋ถ๋ชจ)\n",
" evs = []\n",
" for p in PID:\n",
" s = np.ones(months)\n",
" if p in (\"P03\", \"P04\"): s = np.exp(-0.06 * (temp - 12)) # ํ๋ญ๊ธฐ ์ ๊ธฐ๊ณํต ์ฆ๊ฐ\n",
" if p in (\"P06\", \"P16\"): s = np.exp(0.05 * (temp - 12)) # ํน์๊ธฐ ๋๊ฐ๊ณํต ์ฆ๊ฐ\n",
" cnt = rng.poisson(RATE[p] * hours / 1000 * fleet[\"mult\"].values[:, None] * s[None, :])\n",
" for v, m in zip(*np.nonzero(cnt)):\n",
" for _ in range(cnt[v, m]):\n",
" evs.append((fleet[\"veh\"][v], int(m), m * 720 + rng.uniform(0, 720), p, float(temp[m] + rng.normal(0, 3))))\n",
" ev = pd.DataFrame(evs, columns=[\"veh\", \"month\", \"t\", \"part\", \"temp\"]).sort_values(\"t\").reset_index(drop=True)\n",
" ev[\"ev_id\"] = [f\"W{i:05d}\" for i in range(len(ev))]\n",
" rows, codes = [], []\n",
" for e in ev.itertuples():\n",
" c, r = [], rng.random(3)\n",
" if r[0] < 0.6: c.append(SPEC[e.part])\n",
" if e.part in SHARED and r[1] < 0.6: c.append(SHARED[e.part])\n",
" if r[2] < 0.5: c.append(SYMP[PSYS[e.part]])\n",
" if not c: c.append(SHARED.get(e.part, SPEC[e.part]))\n",
" for q, pr in CASCADE.get(e.part, []):\n",
" if rng.random() < pr: c.append(SPEC[q])\n",
" if rng.random() < 0.12: c.append(SPEC[PID[rng.integers(0, 16)]])\n",
" c = list(dict.fromkeys(c))\n",
" rng.shuffle(c) # ๋์ ๋ฐ์ ์ฝ๋์ ๊ธฐ๋ก ์์๋ ์์\n",
" codes.append(c)\n",
" for k, cd in enumerate(c):\n",
" rows.append((e.ev_id, e.veh, e.t + k * 0.1, cd))\n",
" ev[\"codes\"] = codes\n",
" log = pd.DataFrame(rows, columns=[\"ev_id\", \"veh\", \"ts\", \"code\"])\n",
" return fleet, ev, log\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# ๊ณตํต ๋ถ์ ์กฐ๊ฐ: ์ฝ๋ ๋ฉํฐํซ ํ๋ ฌ, ์ฐจ๋ ๋งฅ๋ฝ ํผ์ฒ, ํ์ต/๊ฒ์ฆ ๋ถํ (์ 12๊ฐ์ ํ์ต, ๋ค 6๊ฐ์ ๊ฒ์ฆ)\n",
"# ---------------------------------------------------------------------------\n",
"CIDX = {c: i for i, c in enumerate(CODES)}\n",
"SPLIT_M = 12\n",
"\n",
"\n",
"def multi_hot(ev):\n",
" X = np.zeros((len(ev), len(CODES)))\n",
" for r, cs in enumerate(ev[\"codes\"]):\n",
" for c in cs:\n",
" X[r, CIDX[c]] = 1\n",
" return X\n",
"\n",
"\n",
"def context(ev, fleet):\n",
" f = fleet.set_index(\"veh\")\n",
" return np.column_stack([ev[\"veh\"].map(f[\"age\"]) / 25, ev[\"veh\"].map(f[\"pm_gap\"]) / 200, ev[\"veh\"].map(f[\"terrain\"]),\n",
" ev[\"veh\"].map(f[\"load\"]), (ev[\"temp\"] - 12) / 15])\n",
"\n",
"\n",
"def feats(ev, fleet):\n",
" X = multi_hot(ev)\n",
" n = X.sum(axis=1, keepdims=True) / 3\n",
" return np.hstack([X, n, context(ev, fleet)])\n",
"\n",
"\n",
"def monthly_counts(ev):\n",
" return ev.groupby([\"month\", \"part\"]).size().unstack(fill_value=0).reindex(columns=PID, fill_value=0)\n",
"\n",
"\n",
"def build_monthly(ev, fleet, top_n=8):\n",
" \"\"\"๋ถํ๋ณ ์๊ฐ ๊ณ ์ฅ ๊ฑด์ ์์ธก์ฉ ํ: ์ง์ฐ๊ฐ(lag), ์ด๋ํ๊ท , ์ถ์ธ, ๊ธฐ๋ ๊ฑด์(๊ฐ๋์๊ฐ x ์ค๊ณ ๊ณ ์ฅ๋ฅ ), ๊ธฐ์จ ์ํธ์์ฉ, ๊ณ์ ์ฑ๋ถ\"\"\"\n",
" mc = ev.groupby([\"month\", \"part\"]).size().unstack(fill_value=0)\n",
" top = ev[\"part\"].value_counts().index[:top_n]\n",
" mh, tm = fleet.attrs[\"month_h\"], ev.groupby(\"month\")[\"temp\"].mean().values\n",
" rows = []\n",
" for p in top:\n",
" c = mc[p].values.astype(float)\n",
" for m in range(3, MONTHS):\n",
" L = np.log1p(c)\n",
" rows.append({\"part\": p, \"month\": m, \"y\": c[m], \"lag1\": L[m - 1], \"lag2\": L[m - 2], \"lag3\": L[m - 3], \"roll3\": L[m - 3:m].mean(),\n",
" \"trend\": L[m - 1] - L[m - 2], \"roll3_std\": L[m - 3:m].std(), \"log_expected\": np.log(mh[m] / 1000 * RATE[p]),\n",
" \"rate_lag1\": c[m - 1] / (mh[m - 1] / 1000) / RATE[p], \"temp\": tm[m] / 10, \"cold_elec\": max(0, 12 - tm[m]) / 10 * (p in (\"P03\", \"P04\")),\n",
" \"heat_cool\": max(0, tm[m] - 12) / 10 * (p in (\"P06\", \"P16\")), \"sin\": np.sin(2 * np.pi * m / 12), \"cos\": np.cos(2 * np.pi * m / 12)})\n",
" D = pd.DataFrame(rows)\n",
" return D, [c for c in D.columns if c not in (\"part\", \"month\", \"y\")], top\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ถํ๋ณ ์ ๋น ์กฐ์น ๊ฐ์ด๋๋ผ์ธ(์์ฝ 3๋จ๊ณ). ์ค์ ์ ๋น ๊ต๋ฒ์ด ์๋๋ผ ๊ต์ก์ฉ์ผ๋ก ๋ง๋ ์์ ๋ฌธ๊ตฌ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"GUIDE = {\n",
" \"P01\": \"์ฐ๋ฃ ์๋ ฅ ์ธก์ -> ๋ถ์ฌ๊ธฐ ๋์คยท๋งํ ํ์ธ -> ๋ถ๋ ์ ๋ถ์ฌ๊ธฐ ๊ต์ฒด\", \"P02\": \"ํก๊ธฐ ์๋ ฅยท์ด๋ฌผ ์ ๊ฒ -> ์ถ ์ ๊ฒฉ ํ์ธ -> ๋ถ๋ ์ ํฐ๋ณด์ฐจ์ ๊ต์ฒด\",\n",
" \"P03\": \"๋จ์ ์ ์ยท๋ถ์ ์ ๊ฒ -> ์ถฉ์ ๊ณํต ํ์ธ -> ๋ถ๋ ์ ์ถ์ ์ง ๊ต์ฒด\", \"P04\": \"๋ฐ์ ์ ์ ์ธก์ -> ๋ฒจํธยท๋ฐฐ์ ์ ๊ฒ -> ๋ถ๋ ์ ๋ฐ์ ๊ธฐ ๊ต์ฒด\",\n",
" \"P05\": \"์ ์ด๋ชจ๋ ์ํํธ์จ์ดยท์ปค๋ฅํฐ ์ ๊ฒ -> ๋ณ์ ์ด๋ ฅ ํ์ธ -> ๋ถ๋ ์ ๋ชจ๋ ๊ต์ฒด\", \"P06\": \"๋๊ฐ์๋ยท๋์ ์ ๊ฒ -> ํํ ์๋์ ํ์ธ -> ๋ถ๋ ์ ํํ ๊ต์ฒด\",\n",
" \"P07\": \"์ ์ ์ค์ผ๋ยท์๋ ฅ ์ ๊ฒ -> ํํ ๋์ค ํ์ธ -> ๋ถ๋ ์ ํํ ๊ต์ฒด\", \"P08\": \"์ผ์ ๋ฐฐ์ ยท๊ฐ๊ทน ์ ๊ฒ -> ์ ํธ ํํ ํ์ธ -> ๋ถ๋ ์ ์ผ์ ๊ต์ฒด\",\n",
" \"P09\": \"์ปค๋ฅํฐ ์ ์ดยท๋ถ์ ์ ๊ฒ -> ํ ์ํ ํ์ธ -> ๋ถ๋ ์ ์ปค๋ฅํฐ ๊ต์ฒด\", \"P10\": \"ํํฐ ์ค์ผ๋ ์ ๊ฒ -> ์ฐ๋ฃ ํ๋ฆ ํ์ธ -> ๋ถ๋ ์ ํํฐ ๊ต์ฒด\",\n",
" \"P11\": \"์ ๋ ์๋ ฅ ์ ๊ฒ -> ๋ฐธ๋ธ ์๋ ํ์ธ -> ๋ถ๋ ์ ๋ฐธ๋ธ ๊ต์ฒด(์์ ํ์ธ ํ์)\", \"P12\": \"์กฐํฅ ๋ฐ์ ์ ๊ฒ -> ์ก์ถ์์ดํฐ ์๋ ํ์ธ -> ๋ถ๋ ์ ๊ต์ฒด(์์ ํ์ธ ํ์)\",\n",
" \"P13\": \"ํํฐ ์ฐจ์ ์ ๊ฒ -> ์ค์ผ ํ์ธ -> ๋ถ๋ ์ ํํฐ ๊ต์ฒด\", \"P14\": \"ํต์ ๋งํฌ ์ํ ์ ๊ฒ -> ๋ชจ๋ ์ ์ ํ์ธ -> ๋ถ๋ ์ ๋ชจ๋ ๊ต์ฒด\",\n",
" \"P15\": \"์๋ ฅ ์ผ์ ์ ํธ ์ ๊ฒ -> ์คํ์ ๋ ํ์ธ -> ๋ถ๋ ์ ์ผ์ ๊ต์ฒด\", \"P16\": \"ํฌ ๋ชจํฐ ์ ๋ฅ ์ ๊ฒ -> ํ์ ํ์ธ -> ๋ถ๋ ์ ๋ชจํฐ ๊ต์ฒด\"}\n",
"\n",
"SYSKR = {\"engine\": \"์์ง\", \"electric\": \"์ ๊ธฐ\", \"drive\": \"๊ตฌ๋\", \"hydraulic\": \"์ ์\", \"brake\": \"์ ๋\", \"cooling\": \"๋๊ฐ\"}\n",
"SHNAME = {SHARED[p[0]]: f\"{PNAME[p[0]]}ยท{PNAME[p[1]]} ๊ณต์ \" for p in PAIRS}\n",
"\n",
"\n",
"def code_name(c):\n",
" if c.startswith(\"S\"):\n",
" return f\"๊ณต์ ์ฝ๋ {c}({SHNAME[c]})\"\n",
" if c.startswith(\"G\"):\n",
" return f\"{SYSKR[[s for s, v in SYMP.items() if v == c][0]]} ๊ณํต ์ฆ์ ์ฝ๋ {c}\"\n",
" return f\"{PNAME['P' + c[1:3]]} ๊ณ ์ ์ฝ๋ {c}\"\n",
"\n",
"\n",
"FNAME = [code_name(c) for c in CODES] + [\"๋์ ๋ฐ์ ์ฝ๋ ์\", \"์ฐจ๋ น\", \"์๋ฐฉ์ ๋น ๊ฒฝ๊ณผ์๊ฐ\", \"์งํ ๊ฐํน๋\", \"์ด์ฉ ๋ถํ\", \"๊ธฐ์จ\"]\n",
"NC = len(CODES)\n",
"\n",
"fleet, ev, log = make_data()\n",
"train, test = ev[ev[\"month\"] < SPLIT_M], ev[ev[\"month\"] >= SPLIT_M].reset_index(drop=True)\n",
"Xtr, Xte = feats(train, fleet), feats(test, fleet)\n",
"clf = LogisticRegression(C=3, max_iter=500).fit(Xtr, train[\"part\"])\n",
"P = clf.predict_proba(Xte)\n",
"cls, mu = clf.classes_, Xtr.mean(axis=0)\n",
"pred = P.argmax(axis=1)\n",
"\n",
"\n",
"def explain(i, k=3):\n",
" \"\"\"์์ธก ๋ถํ์ ๋ํ ์
๋ ฅ ์ธ์๋ณ ๊ธฐ์ฌ๋ = ๊ณ์ x (์
๋ ฅ๊ฐ - ํ์ต ํ๊ท ). ๊ฐ์ด ํฐ ์ธ์ k๊ฐ๋ฅผ ๋ฐํ.\"\"\"\n",
" contrib = clf.coef_[pred[i]] * (Xte[i] - mu)\n",
" contrib[:NC][Xte[i][:NC] == 0] = -np.inf # ์
๋ ฅ๋์ง ์์ ์ฝ๋๋ ๊ทผ๊ฑฐ์์ ์ ์ธ\n",
" idx = np.argsort(-contrib)[:k]\n",
" return [(FNAME[j], contrib[j]) for j in idx], idx\n",
"\n",
"\n",
"def summary(i):\n",
" f, _ = explain(i)\n",
" return (f\"{PNAME[cls[pred[i]]]} ๊ณ ์ฅ ๊ฐ๋ฅ์ฑ {P[i, pred[i]]:.0%}. ์ฃผ์ ๊ทผ๊ฑฐ: \" + \", \".join(n for n, _ in f) + \".\")\n",
"\n",
"\n",
"# 1) ์ ์์ธก ๊ฑด ๊ธฐ์ฌ๋ ์๋ ์ฐ์ถ + ๊ทผ๊ฑฐ ์ ํจ์ฑ ์ ๊ฒ\n",
"ok3, drop = 0, 0\n",
"for i in range(len(test)):\n",
" f, idx = explain(i)\n",
" ok3 += len(f) == 3\n",
" x2 = Xte[i].copy(); x2[idx[0]] = mu[idx[0]] # ๊ฐ์ฅ ํฐ ๊ทผ๊ฑฐ๋ฅผ ํ๊ท ๊ฐ์ผ๋ก ๋๋๋ ค ๋ณธ๋ค\n",
" drop += clf.predict_proba(x2[None])[0, pred[i]] < P[i, pred[i]]\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ์์ธก {len(test)}๊ฑด ์ค ๊ธฐ์ฌ๋ ์์ 3๊ฐ ์ธ์ ์ฐ์ถ {ok3}๊ฑด (์ฐ์ถ๋ฅ {ok3 / len(test):.0%}, ๋ชฉํ 100%: {'๋ฌ์ฑ' if ok3 == len(test) else '๋ฏธ๋ฌ'})\")\n",
"print(f\"๊ทผ๊ฑฐ ์ ํจ์ฑ: ์์ ์ธ์๋ฅผ ํ๊ท ๊ฐ์ผ๋ก ๋๋๋ฆฌ๋ฉด ์์ธก ํ๋ฅ ์ด ๋ฎ์์ง๋ ๋น์จ {drop / len(test):.1%}\")\n",
"\n",
"# 2) ๋ชจ๋ฐ์ผ ์ง๋จ ์นด๋ 2๊ฑด: ์ ๋ขฐ๋ ๋์ / ๋ฎ์\n",
"print(\"\\n[๋ชจ๋ฐ์ผ ์ง๋จ ์นด๋]\")\n",
"for tgt in (0.95, 0.55):\n",
" i = int(np.argmin(np.abs(P.max(axis=1) - tgt)))\n",
" f, _ = explain(i)\n",
" print(f\" ์
๋ ฅ DTC {test['codes'][i]} (์ค์ ๊ณ ์ฅ {PNAME[test['part'][i]]})\")\n",
" print(f\" ์ง๋จ {PNAME[cls[pred[i]]]} ์ ๋ขฐ๋ {P[i, pred[i]]:.0%} | ๊ทผ๊ฑฐ: \" + \" / \".join(f\"{n}({v:+.1f})\" for n, v in f))\n",
" print(f\" ์กฐ์น ์ ์ฐจ: {GUIDE[cls[pred[i]]]}\")\n",
" print(f\" ์์ฝ: {summary(i)}\")\n",
"\n",
"# 3) ์น ๋์๋ณด๋: ์ต๊ทผ ๋ฌ ํํฉ๊ณผ ๊ฒฝ๋ณด\n",
"last = test[test[\"month\"] == MONTHS - 1]\n",
"li = last.index.values\n",
"alert = [(PSYS[cls[pred[i]]] in CRIT_SYS) and P[i, pred[i]] >= 0.7 for i in li]\n",
"print(f\"\\n[์น ๋์๋ณด๋] ์ต๊ทผ ๋ฌ ์ ๋น ๊ฑด {len(last)}๊ฑด, ๊ธฐ๋ยท์ ๋ ๊ณํต ๊ฒฝ๋ณด {sum(alert)}๊ฑด, ์ ๋ขฐ๋ ๋ฎ์(0.7 ๋ฏธ๋ง) {int((P[li].max(axis=1) < 0.7).sum())}๊ฑด\")\n",
"cnt = pd.Series([cls[pred[i]] for i in li]).value_counts().head(3)\n",
"print(\" ๋ค๋น๋ ์ง๋จ ๋ถํ: \" + \", \".join(f\"{PNAME[p]} {n}๊ฑด\" for p, n in cnt.items()))\n",
"\n",
"fig, ax = plt.subplots(2, 2, figsize=(11, 7))\n",
"cnt5 = pd.Series([cls[pred[i]] for i in li]).value_counts().head(8)\n",
"ax[0, 0].bar(cnt5.index, cnt5.values); ax[0, 0].set_title(\"Diagnosed parts (last month)\")\n",
"mon = ev.groupby(\"month\").size(); ax[0, 1].plot(mon.index, mon.values, marker=\"o\"); ax[0, 1].set_title(\"Monthly maintenance cases\")\n",
"j = int(np.argmin(np.abs(P.max(axis=1) - 0.55))); f, _ = explain(j)\n",
"ax[1, 0].barh([f\"factor {k + 1}\" for k in range(3)][::-1], [v for _, v in f][::-1]); ax[1, 0].set_title(\"Top-3 contributions (low-confidence case)\")\n",
"ax[1, 1].hist(P.max(axis=1), bins=20); ax[1, 1].axvline(0.7, ls=\"--\", color=\"k\"); ax[1, 1].set_title(\"Confidence distribution (line = 0.7)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
06 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ์์ธก 3890๊ฑด ์ค ๊ธฐ์ฌ๋ ์์ 3๊ฐ ์ธ์ ์ฐ์ถ 3890๊ฑด (์ฐ์ถ๋ฅ 100%, ๋ชฉํ 100%: ๋ฌ์ฑ)\n",
"๊ทผ๊ฑฐ ์ ํจ์ฑ: ์์ ์ธ์๋ฅผ ํ๊ท ๊ฐ์ผ๋ก ๋๋๋ฆฌ๋ฉด ์์ธก ํ๋ฅ ์ด ๋ฎ์์ง๋ ๋น์จ 92.5%\n",
"\n",
"[๋ชจ๋ฐ์ผ ์ง๋จ ์นด๋]\n",
" ์
๋ ฅ DTC ['G01', 'C101', 'S01'] (์ค์ ๊ณ ์ฅ ์ฐ๋ฃ๋ถ์ฌ๊ธฐ)\n",
" ์ง๋จ ์ฐ๋ฃ๋ถ์ฌ๊ธฐ ์ ๋ขฐ๋ 95% | ๊ทผ๊ฑฐ: ๊ณต์ ์ฝ๋ S01(์ฐ๋ฃ๋ถ์ฌ๊ธฐยทํฐ๋ณด์ฐจ์ ๊ณต์ )(+4.3) / ์์ง ๊ณํต ์ฆ์ ์ฝ๋ G01(+3.0) / ์ฐ๋ฃ ํํฐ ๊ณ ์ ์ฝ๋ C101(+1.6)\n",
" ์กฐ์น ์ ์ฐจ: ์ฐ๋ฃ ์๋ ฅ ์ธก์ -> ๋ถ์ฌ๊ธฐ ๋์คยท๋งํ ํ์ธ -> ๋ถ๋ ์ ๋ถ์ฌ๊ธฐ ๊ต์ฒด\n",
" ์์ฝ: ์ฐ๋ฃ๋ถ์ฌ๊ธฐ ๊ณ ์ฅ ๊ฐ๋ฅ์ฑ 95%. ์ฃผ์ ๊ทผ๊ฑฐ: ๊ณต์ ์ฝ๋ S01(์ฐ๋ฃ๋ถ์ฌ๊ธฐยทํฐ๋ณด์ฐจ์ ๊ณต์ ), ์์ง ๊ณํต ์ฆ์ ์ฝ๋ G01, ์ฐ๋ฃ ํํฐ ๊ณ ์ ์ฝ๋ C101.\n",
" ์
๋ ฅ DTC ['G01'] (์ค์ ๊ณ ์ฅ ํฐ๋ณด์ฐจ์ )\n",
" ์ง๋จ ์ฐ๋ฃ๋ถ์ฌ๊ธฐ ์ ๋ขฐ๋ 55% | ๊ทผ๊ฑฐ: ์์ง ๊ณํต ์ฆ์ ์ฝ๋ G01(+3.0) / ๊ธฐ์จ(+0.1) / ์ด์ฉ ๋ถํ(+0.1)\n",
" ์กฐ์น ์ ์ฐจ: ์ฐ๋ฃ ์๋ ฅ ์ธก์ -> ๋ถ์ฌ๊ธฐ ๋์คยท๋งํ ํ์ธ -> ๋ถ๋ ์ ๋ถ์ฌ๊ธฐ ๊ต์ฒด\n",
" ์์ฝ: ์ฐ๋ฃ๋ถ์ฌ๊ธฐ ๊ณ ์ฅ ๊ฐ๋ฅ์ฑ 55%. ์ฃผ์ ๊ทผ๊ฑฐ: ์์ง ๊ณํต ์ฆ์ ์ฝ๋ G01, ๊ธฐ์จ, ์ด์ฉ ๋ถํ.\n",
"\n",
"[์น ๋์๋ณด๋] ์ต๊ทผ ๋ฌ ์ ๋น ๊ฑด 457๊ฑด, ๊ธฐ๋ยท์ ๋ ๊ณํต ๊ฒฝ๋ณด 238๊ฑด, ์ ๋ขฐ๋ ๋ฎ์(0.7 ๋ฏธ๋ง) 148๊ฑด\n",
" ๋ค๋น๋ ์ง๋จ ๋ถํ: ์ฐ๋ฃ๋ถ์ฌ๊ธฐ 189๊ฑด, ํฐ๋ณด์ฐจ์ 72๊ฑด, ๋๊ฐ์ ํํ 40๊ฑด\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
07. ๋ณตํฉ ๊ณ ์ฅ ์์ฌ ์ ์๋ จ ์ ๋น๊ด 2์ฐจ ์ ๋ฐ ์ง๋จ(HITL) ์ํฌํ๋ก์ฐ ์ค๊ณ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋ณตํฉ ๊ณ ์ฅ ์์ฌ ์ ์๋ จ ์ ๋น๊ด 2์ฐจ ์ ๋ฐ ์ง๋จ(HITL) ์ํฌํ๋ก์ฐ๋ฅผ ์ค๊ณํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋ณตํฉ ๊ณ ์ฅ์ด ์์ฌ๋ ๋ ์๋ จ ์ ๋น๊ด 2์ฐจ ์ ๋ฐ ์ง๋จ(HITL) ์ํฌํ๋ก์ฐ๋ฅผ ์๋ฎฌ๋ ์ด์
์ผ๋ก ์ค๊ณํ๊ณ ์ค์ง๋จ ๋น์ฉ ์ ๊ฐ ํจ๊ณผ๋ฅผ ๊ฒ์ฆํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ ๋ขฐ๋๊ฐ ๊ธฐ์ค ๋ฏธ๋ง์ด๊ฑฐ๋ 3๊ฐ ๊ณํต ์ด์ DTC๊ฐ ๊ฒน์น ๊ฑด์ ์ ๋น๊ด ๋๊ธฐ์ด์ ์ฌ๋ฆฌ๊ณ , ์์ง ์์ ์๋ ํ์ ํ์ง ์์ ์ฑ ์ ๋น๊ด์ ํ์ ยท์ ์ ยท๋ฌด์๋ต(์์ง ์ ์ ์ง, ์ฌํ์ธ ํ์)์ ์ฌ์ ์ ํจ๊ป ํด์ ์ฒด์ธ ๊ฐ์ฌ ๋ก๊ทธ๋ก ๋จ๊ธด๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 5๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/optimization/mnt03_simulator.py` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ํ์ค์ ๊ตฐ์ ์ ์ฝ์กฐ๊ฑด ๋ฐ์๋ ๋ฐ ์๋๋ฆฌ์ค๋ณ ๋์ ์ ์ ์ ์ ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์ต์ ํ ์ ์ฉ ์ ๋น์ฉ 15% ์ ๊ฐ ๋๋ ์ฌ๊ณ ํ์ ์จ 20% ํฅ์\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์ ํ/์ ์ ๊ณํ๋ฒ(LP/MIP) ์ต์ ํ ํด ๋์ถ ๋ฐ ์๋ฎฌ๋ ์ด์
์๋ฃ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ๊ณผ๊ฑฐ ์ค์ ๋ฐ์ดํฐ ๋๋น ์ต์ ํ ๊ฒฐ๊ณผ ๋น๊ต ๋ฐฑํ
์คํ
\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"๋ณตํฉ ๊ณ ์ฅ์ด ์์ฌ๋๋ ์ ๋น ๊ฑด(์ ๋ขฐ๋ ๋ฎ์, ์๋ก ๋ค๋ฅธ ๊ณํต์ DTC ๋์ ๋ฐ์)์ ์๋ จ ์ ๋น๊ด 2์ฐจ ์ ๋ฐ ์ง๋จ ๋๊ธฐ์ด์ ์ฌ๋ ค ์น์ธยท์ ์ ยท๋ฌด์๋ต(์์ง ์ ์ ์งยท์ฌํ์ธ ํ์)์ ์ฌ์ ์ ํจ๊ป ํด์ ์ฒด์ธ ๊ฐ์ฌ ๋ก๊ทธ์ ๋จ๊ธฐ๋ HITL ์ํฌํ๋ก์ฐ ์๋ฎฌ๋ ์ดํฐ๋ฅผ ๊ตฌํํ๋ค. ์๋ ํ์ 0๊ฑด, ์ค์ง๋จ ๋น์ฉ ์ ๊ฐ๋ฅ , ์ ๋ขฐ๋ ๊ธฐ์คยท์ ๋น๊ด ์์ฉ๋ยท๊ณ ์ฅ ๊ธ์ฆ์ ๋ํ ๋ฏผ๊ฐ๋๋ฅผ ํ์ธํ๋ค.\"\"\"\n",
"import hashlib\n",
"import json\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.linear_model import LogisticRegression\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ฐจ๋ 100๋์ 18๊ฐ์ ๊ณ ์ฅ ์ง๋จ ์ฝ๋(DTC)ยท์ ๋น ์ด๋ ฅ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์ฅ๋นยท์ ๋น ์ค์ ์์)\n",
"# ๋ถํ 16์ข
(1000๊ฐ๋์๊ฐ๋น ๊ณ ์ฅ๋ฅ ์ฐจ๋ฑ), DTC 28์ข
(๋ถํ ๊ณ ์ ์ฝ๋, ์ง ๋ถํ ๊ณต์ ์ฝ๋, ๊ณํต ์ฆ์ ์ฝ๋)\n",
"# ๊ณ ์ฅ 1๊ฑด(์ ๋น ์ด๋ ฅ)๋ง๋ค DTC๊ฐ 1~5๊ฐ ๋์ ๋ฐ์ํ๊ณ , ์ฐ์ ๊ณ ์ฅ ์ฝ๋์ ์ผ์ ์ก์ ์ฝ๋๊ฐ ์์ธ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"PARTS = [ # (ID, ์ด๋ฆ, ๊ณํต, 1000๊ฐ๋์๊ฐ๋น ๊ณ ์ฅ๋ฅ , ๊ฐ์ ๋จ๊ฐ์ง์, ์๋ฆฌ์๊ฐ h)\n",
" (\"P01\", \"์ฐ๋ฃ๋ถ์ฌ๊ธฐ\", \"engine\", 12.0, 3.0, 2.0), (\"P02\", \"ํฐ๋ณด์ฐจ์ \", \"engine\", 9.0, 8.0, 5.0),\n",
" (\"P03\", \"์ถ์ ์ง\", \"electric\", 6.5, 4.0, 1.5), (\"P04\", \"๋ฐ์ ๊ธฐ\", \"electric\", 4.0, 9.0, 3.0),\n",
" (\"P05\", \"๋ณ์๊ธฐ ์ ์ด๋ชจ๋\", \"drive\", 3.0, 12.0, 6.0), (\"P06\", \"๋๊ฐ์ ํํ\", \"engine\", 2.2, 3.5, 3.0),\n",
" (\"P07\", \"์ ์ ํํ\", \"hydraulic\", 1.6, 10.0, 4.0), (\"P08\", \"์๋ ์ผ์\", \"drive\", 1.2, 2.0, 1.5),\n",
" (\"P09\", \"ECU ์ปค๋ฅํฐ\", \"electric\", 0.9, 2.5, 1.5), (\"P10\", \"์ฐ๋ฃ ํํฐ\", \"engine\", 0.7, 0.8, 0.7),\n",
" (\"P11\", \"์ ๋ ๋ฐธ๋ธ\", \"brake\", 0.5, 6.0, 3.0), (\"P12\", \"์กฐํฅ ์ก์ถ์์ดํฐ\", \"drive\", 0.4, 9.0, 5.0),\n",
" (\"P13\", \"๊ณต๊ธฐ ํํฐ\", \"engine\", 0.3, 0.5, 0.5), (\"P14\", \"ํต์ ์ธํฐํ์ด์ค ๋ชจ๋\", \"electric\", 0.25, 3.0, 1.5),\n",
" (\"P15\", \"์คํ์ ์๋ ฅ ์ผ์\", \"engine\", 0.2, 1.0, 1.0), (\"P16\", \"ํฌ ๊ตฌ๋ ๋ชจํฐ\", \"cooling\", 0.15, 2.0, 2.0)]\n",
"PID = [p[0] for p in PARTS]\n",
"PNAME = {p[0]: p[1] for p in PARTS}; PSYS = {p[0]: p[2] for p in PARTS}; RATE = {p[0]: p[3] for p in PARTS}\n",
"COST = {p[0]: p[4] for p in PARTS}; REPH = {p[0]: p[5] for p in PARTS}\n",
"PAIRS = [(\"P01\", \"P02\"), (\"P03\", \"P04\"), (\"P05\", \"P08\"), (\"P06\", \"P10\"), (\"P09\", \"P14\"), (\"P13\", \"P15\")]\n",
"SHARED = {p: f\"S{k + 1:02d}\" for k, pr in enumerate(PAIRS) for p in pr} # ์ง ๋ถํ์ด ํจ๊ป ์ฐ๋ ๋ชจํธํ ์ฝ๋\n",
"SPEC = {p: f\"C{p[1:]}1\" for p in PID} # ๋ถํ ๊ณ ์ ์ฝ๋\n",
"SYS = [\"engine\", \"electric\", \"drive\", \"hydraulic\", \"brake\", \"cooling\"]\n",
"SYMP = {s: f\"G{i + 1:02d}\" for i, s in enumerate(SYS)} # ๊ณํต ์ฆ์ ์ฝ๋\n",
"CODES = sorted(set(SPEC.values()) | set(SHARED.values()) | set(SYMP.values()))\n",
"CASCADE = {\"P01\": [(\"P10\", 0.20)], \"P02\": [(\"P06\", 0.20)], \"P03\": [(\"P04\", 0.25)], \"P04\": [(\"P09\", 0.25)],\n",
" \"P05\": [(\"P08\", 0.30)], \"P06\": [(\"P16\", 0.30)], \"P09\": [(\"P14\", 0.20)], \"P12\": [(\"P07\", 0.20)]}\n",
"CRIT_SYS = {\"engine\", \"drive\", \"brake\"} # ๊ธฐ๋ยท์ ๋ ๋ถ๋ฅ ์ํ ๊ณํต(๊ฒฝ๋ณด ๋์)\n",
"MONTHS = 18\n",
"\n",
"\n",
"def make_fleet(n=100, seed=0):\n",
" rng = np.random.default_rng(seed)\n",
" f = pd.DataFrame({\"veh\": [f\"V{i:03d}\" for i in range(n)], \"age\": rng.integers(1, 26, n).astype(float),\n",
" \"terrain\": rng.uniform(0, 1, n), \"load\": rng.uniform(0, 1, n), \"pm_gap\": rng.uniform(20, 200, n),\n",
" \"humid\": rng.uniform(0, 1, n), \"base_h\": rng.uniform(55, 125, n)})\n",
" f[\"mult\"] = np.exp(0.045 * (f[\"age\"] - 13) + 0.9 * (f[\"terrain\"] - .5) + 0.8 * (f[\"load\"] - .5)\n",
" + 0.006 * (f[\"pm_gap\"] - 110) + 0.7 * (f[\"humid\"] - .5))\n",
" return f\n",
"\n",
"\n",
"def make_data(seed=1, months=MONTHS):\n",
" \"\"\"๋ฐํ: fleet(์ฐจ๋ ์์ฑ), ev(์ ๋น ์ด๋ ฅ 1๊ฑด=1ํ: ๋ฐ์ DTC ๋ชฉ๋ก, ์ค์ ๊ณ ์ฅ ๋ถํ), log(DTC ๋ฐ์ ๋ก๊ทธ, ํ=์ฝ๋ 1๊ฑด)\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" fleet = make_fleet()\n",
" V = len(fleet)\n",
" temp = 12 + 14 * np.sin(2 * np.pi * (np.arange(months) - 3) / 12) # ์ํ๊ท ๊ธฐ์จ(๊ฐ์)\n",
" hours = fleet[\"base_h\"].values[:, None] * (1 + 0.25 * np.sin(2 * np.pi * np.arange(months) / 12 + 1)) * rng.lognormal(0, .12, (V, months))\n",
" fleet[\"hours\"] = hours.sum(axis=1) # ์ฐจ๋๋ณ 18๊ฐ์ ์ด ๊ฐ๋์๊ฐ\n",
" fleet.attrs[\"month_h\"] = hours.sum(axis=0) # ์๋ณ ์ ์ฒด ๊ฐ๋์๊ฐ(๊ณ ์ฅ๋ฅ ๋ถ๋ชจ)\n",
" evs = []\n",
" for p in PID:\n",
" s = np.ones(months)\n",
" if p in (\"P03\", \"P04\"): s = np.exp(-0.06 * (temp - 12)) # ํ๋ญ๊ธฐ ์ ๊ธฐ๊ณํต ์ฆ๊ฐ\n",
" if p in (\"P06\", \"P16\"): s = np.exp(0.05 * (temp - 12)) # ํน์๊ธฐ ๋๊ฐ๊ณํต ์ฆ๊ฐ\n",
" cnt = rng.poisson(RATE[p] * hours / 1000 * fleet[\"mult\"].values[:, None] * s[None, :])\n",
" for v, m in zip(*np.nonzero(cnt)):\n",
" for _ in range(cnt[v, m]):\n",
" evs.append((fleet[\"veh\"][v], int(m), m * 720 + rng.uniform(0, 720), p, float(temp[m] + rng.normal(0, 3))))\n",
" ev = pd.DataFrame(evs, columns=[\"veh\", \"month\", \"t\", \"part\", \"temp\"]).sort_values(\"t\").reset_index(drop=True)\n",
" ev[\"ev_id\"] = [f\"W{i:05d}\" for i in range(len(ev))]\n",
" rows, codes = [], []\n",
" for e in ev.itertuples():\n",
" c, r = [], rng.random(3)\n",
" if r[0] < 0.6: c.append(SPEC[e.part])\n",
" if e.part in SHARED and r[1] < 0.6: c.append(SHARED[e.part])\n",
" if r[2] < 0.5: c.append(SYMP[PSYS[e.part]])\n",
" if not c: c.append(SHARED.get(e.part, SPEC[e.part]))\n",
" for q, pr in CASCADE.get(e.part, []):\n",
" if rng.random() < pr: c.append(SPEC[q])\n",
" if rng.random() < 0.12: c.append(SPEC[PID[rng.integers(0, 16)]])\n",
" c = list(dict.fromkeys(c))\n",
" rng.shuffle(c) # ๋์ ๋ฐ์ ์ฝ๋์ ๊ธฐ๋ก ์์๋ ์์\n",
" codes.append(c)\n",
" for k, cd in enumerate(c):\n",
" rows.append((e.ev_id, e.veh, e.t + k * 0.1, cd))\n",
" ev[\"codes\"] = codes\n",
" log = pd.DataFrame(rows, columns=[\"ev_id\", \"veh\", \"ts\", \"code\"])\n",
" return fleet, ev, log\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# ๊ณตํต ๋ถ์ ์กฐ๊ฐ: ์ฝ๋ ๋ฉํฐํซ ํ๋ ฌ, ์ฐจ๋ ๋งฅ๋ฝ ํผ์ฒ, ํ์ต/๊ฒ์ฆ ๋ถํ (์ 12๊ฐ์ ํ์ต, ๋ค 6๊ฐ์ ๊ฒ์ฆ)\n",
"# ---------------------------------------------------------------------------\n",
"CIDX = {c: i for i, c in enumerate(CODES)}\n",
"SPLIT_M = 12\n",
"\n",
"\n",
"def multi_hot(ev):\n",
" X = np.zeros((len(ev), len(CODES)))\n",
" for r, cs in enumerate(ev[\"codes\"]):\n",
" for c in cs:\n",
" X[r, CIDX[c]] = 1\n",
" return X\n",
"\n",
"\n",
"def context(ev, fleet):\n",
" f = fleet.set_index(\"veh\")\n",
" return np.column_stack([ev[\"veh\"].map(f[\"age\"]) / 25, ev[\"veh\"].map(f[\"pm_gap\"]) / 200, ev[\"veh\"].map(f[\"terrain\"]),\n",
" ev[\"veh\"].map(f[\"load\"]), (ev[\"temp\"] - 12) / 15])\n",
"\n",
"\n",
"def feats(ev, fleet):\n",
" X = multi_hot(ev)\n",
" n = X.sum(axis=1, keepdims=True) / 3\n",
" return np.hstack([X, n, context(ev, fleet)])\n",
"\n",
"\n",
"def monthly_counts(ev):\n",
" return ev.groupby([\"month\", \"part\"]).size().unstack(fill_value=0).reindex(columns=PID, fill_value=0)\n",
"\n",
"\n",
"def build_monthly(ev, fleet, top_n=8):\n",
" \"\"\"๋ถํ๋ณ ์๊ฐ ๊ณ ์ฅ ๊ฑด์ ์์ธก์ฉ ํ: ์ง์ฐ๊ฐ(lag), ์ด๋ํ๊ท , ์ถ์ธ, ๊ธฐ๋ ๊ฑด์(๊ฐ๋์๊ฐ x ์ค๊ณ ๊ณ ์ฅ๋ฅ ), ๊ธฐ์จ ์ํธ์์ฉ, ๊ณ์ ์ฑ๋ถ\"\"\"\n",
" mc = ev.groupby([\"month\", \"part\"]).size().unstack(fill_value=0)\n",
" top = ev[\"part\"].value_counts().index[:top_n]\n",
" mh, tm = fleet.attrs[\"month_h\"], ev.groupby(\"month\")[\"temp\"].mean().values\n",
" rows = []\n",
" for p in top:\n",
" c = mc[p].values.astype(float)\n",
" for m in range(3, MONTHS):\n",
" L = np.log1p(c)\n",
" rows.append({\"part\": p, \"month\": m, \"y\": c[m], \"lag1\": L[m - 1], \"lag2\": L[m - 2], \"lag3\": L[m - 3], \"roll3\": L[m - 3:m].mean(),\n",
" \"trend\": L[m - 1] - L[m - 2], \"roll3_std\": L[m - 3:m].std(), \"log_expected\": np.log(mh[m] / 1000 * RATE[p]),\n",
" \"rate_lag1\": c[m - 1] / (mh[m - 1] / 1000) / RATE[p], \"temp\": tm[m] / 10, \"cold_elec\": max(0, 12 - tm[m]) / 10 * (p in (\"P03\", \"P04\")),\n",
" \"heat_cool\": max(0, tm[m] - 12) / 10 * (p in (\"P06\", \"P16\")), \"sin\": np.sin(2 * np.pi * m / 12), \"cos\": np.cos(2 * np.pi * m / 12)})\n",
" D = pd.DataFrame(rows)\n",
" return D, [c for c in D.columns if c not in (\"part\", \"month\", \"y\")], top\n",
"\n",
"fleet, ev, log = make_data()\n",
"train, test = ev[ev[\"month\"] < SPLIT_M], ev[ev[\"month\"] >= SPLIT_M].reset_index(drop=True)\n",
"clf = LogisticRegression(C=3, max_iter=500).fit(feats(train, fleet), train[\"part\"])\n",
"P = clf.predict_proba(feats(test, fleet))\n",
"cls = clf.classes_\n",
"top1 = cls[P.argmax(axis=1)]\n",
"conf = P.max(axis=1)\n",
"CSYS = {c: {PSYS[p] for p in PID if c in (SPEC[p], SHARED.get(p)) or c == SYMP[PSYS[p]]} for c in CODES}\n",
"nsys = test[\"codes\"].map(lambda cs: len(set().union(*[CSYS[c] for c in cs])))\n",
"multi_sys = ((test[\"codes\"].str.len() >= 3) & (nsys >= 3)).values # 3๊ฐ ์ด์ ๊ณํต์ ๊ฑธ์น DTC๊ฐ ๋์์ ๋ฐ์\n",
"THR, CAP = 0.8, 300 # ์ด์์ : ์ ๋ขฐ๋ 0.8 ๋ฏธ๋ง ๋๋ ๋ค๊ณํต ๋์ ๋ฐ์์ด๋ฉด ์ ๋ฐ ์ง๋จ, ์ ๋น๊ด ์ ์์ฉ๋ 300๊ฑด(์๋ ๋ฏผ๊ฐ๋ ๋ถ์์ผ๋ก ์ ์ )\n",
"\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋น์ฉ์ง์: ์ค์ง๋จ ์ ํ๊ต์ฒด ๋ถํ๋น + ํ๊ต์ฒด ๊ณต์ + ์ฌ์
๊ณ ํจ๋ํฐ 3.0 + ์ฌ๋ฐ๋ฅธ ์๋ฆฌ, ์ ๋ฐ ์ง๋จ 1๊ฑด 1.0\n",
"REVIEW_COST, REVISIT, SENIOR_ACC, NO_RESP = 1.0, 3.0, 0.93, 0.08\n",
"\n",
"\n",
"def case_cost(pred, true):\n",
" if pred == true:\n",
" return REPH[true]\n",
" return COST[pred] + REPH[pred] + REVISIT + REPH[true]\n",
"\n",
"\n",
"class Audit:\n",
" def __init__(self):\n",
" self.log = []\n",
"\n",
" def add(self, **body):\n",
" prev = self.log[-1][\"hash\"] if self.log else \"GENESIS\"\n",
" body = {\"seq\": len(self.log), **body}\n",
" self.log.append({**body, \"prev\": prev, \"hash\": hashlib.sha256((prev + json.dumps(body, sort_keys=True, ensure_ascii=False)).encode()).hexdigest()})\n",
"\n",
" def verify(self):\n",
" prev = \"GENESIS\"\n",
" for e in self.log:\n",
" b = {k: v for k, v in e.items() if k not in (\"prev\", \"hash\")}\n",
" if e[\"prev\"] != prev or hashlib.sha256((prev + json.dumps(b, sort_keys=True, ensure_ascii=False)).encode()).hexdigest() != e[\"hash\"]:\n",
" return e[\"seq\"]\n",
" prev = e[\"hash\"]\n",
" return None\n",
"\n",
"\n",
"def run(thr=THR, cap=CAP, surge=1.0, seed=0, audit=None, mode=\"hitl\"):\n",
" \"\"\"mode: auto(์์ง top-1 ์ ๋ถ ํ์ : ๋น๊ต์ฉ, ์ค์ ์ด์ฉ ๊ธ์ง) / hitl(๋ณตํฉ ์์ฌ ๊ฑด์ ์ ๋น๊ด ๊ฒฐ์ ์ ๊น์ง ํ์ ํ์ง ์์)\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" idx_all = np.arange(len(test))\n",
" if surge != 1.0:\n",
" idx_all = np.concatenate([idx_all, rng.choice(idx_all, int(len(idx_all) * (surge - 1)), replace=True)])\n",
" cost, wrong, queued, decided, pending_auto, silent = 0.0, 0, 0, 0, 0, 0\n",
" for m in range(SPLIT_M, MONTHS):\n",
" mi = idx_all[test[\"month\"].values[idx_all] == m]\n",
" flag = [i for i in mi if conf[i] < thr or multi_sys[i]]\n",
" flag.sort(key=lambda i: (PSYS[top1[i]] not in CRIT_SYS, conf[i])) # ์์ ๊ณํต ์ฐ์ , ์ ๋ขฐ๋ ๋ฎ์ ์\n",
" queue = set(flag[:cap]) if mode == \"hitl\" else set()\n",
" for i in mi:\n",
" true, final = test[\"part\"][i], top1[i]\n",
" if i in queue:\n",
" queued += 1\n",
" audit.add(event=\"PROPOSE\", case=int(i), engine=str(top1[i]), conf=round(float(conf[i]), 3), status=\"PENDING\")\n",
" cost += REVIEW_COST\n",
" r = rng.random()\n",
" if r < NO_RESP:\n",
" dec, why = \"๋ฌด์๋ต\", \"์๋ต ์ง์ฐ -> ์์ง ์ ์ ์ง, ์ฌํ์ธ ํ์ ํ์\"; silent += 1\n",
" elif r < NO_RESP + (1 - NO_RESP) * SENIOR_ACC:\n",
" dec, final, why = (\"์น์ธ\" if top1[i] == true else \"์ ์ \"), true, \"์ถ๊ฐ ๊ณ์ธกยท๋ถํ ๋ถ๋ฆฌ ์ ๊ฒ์ผ๋ก ํ์ธ\"\n",
" else:\n",
" second = cls[np.argsort(-P[i])[1]]\n",
" dec, final, why = \"์ ์ \", second, \"2์์ ํ๋ณด๋ก ์ ์ (๊ทผ๊ฑฐ ๋ถ์กฑ)\"\n",
" audit.add(event=\"DECIDE\", case=int(i), decision=dec, final=str(final), reason=why, by=\"์๋ จ ์ ๋น๊ด(๋ชจ์)\")\n",
" decided += 1\n",
" cost += case_cost(final, true); wrong += final != true\n",
" n = len(idx_all)\n",
" return {\"cost\": cost, \"wrong\": wrong / n, \"queued\": queued, \"decided\": decided, \"silent\": silent, \"n\": n, \"auto\": pending_auto}\n",
"\n",
"\n",
"aud = Audit()\n",
"A, H = run(mode=\"auto\", audit=Audit()), run(audit=aud)\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฒ์ฆ ์ ๋น ๊ฑด {A['n']}๊ฑด, ๋ณตํฉ ๊ณ ์ฅ ์์ฌ ๊ฑด {int(((conf < THR) | multi_sys).sum())}๊ฑด (์ ๋ขฐ๋ {THR} ๋ฏธ๋ง ๋๋ 3๊ฐ ๊ณํต ์ด์ DTC)\")\n",
"print(f\"์์ง ๋จ๋
์๋ ํ์ (๋น๊ต์ฉ): ๋น์ฉ์ง์ {A['cost']:.0f}, ์ค์ง๋จ์จ {A['wrong']:.1%}\")\n",
"print(f\"HITL(์ ๋น๊ด ๋๊ธฐ์ด {H['queued']}๊ฑด): ๋น์ฉ์ง์ {H['cost']:.0f}, ์ค์ง๋จ์จ {H['wrong']:.1%}\")\n",
"cut = 1 - H[\"cost\"] / A[\"cost\"]\n",
"print(f\"์ค์ง๋จ ๋น์ฉ ์ ๊ฐ๋ฅ {cut:.1%} (๋ชฉํ 15% ์ด์: {'๋ฌ์ฑ' if cut >= 0.15 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# ์น์ธ ์ฒด๊ณ ์ ๊ฒ\n",
"print(f\"\\n์๋ ํ์ 0๊ฑด ํ์ธ: ๋๊ธฐ์ด ๊ฑด์ ๋ชจ๋ ์ ๋น๊ด ๊ฒฐ์ ๊ธฐ๋ก ํ ํ์ (์๋ ํ์ {H['auto']}๊ฑด)\")\n",
"print(f\"๊ฒฐ์ ๋ก๊น
๋ฅ {H['decided'] / max(H['queued'], 1):.0%} ({H['decided']}/{H['queued']}), ๋ฌด์๋ต {H['silent']}๊ฑด์ ์์ง ์ ์ ์ง + ์ฌํ์ธ ํ์\")\n",
"print(f\"๊ฐ์ฌ ๋ก๊ทธ ๋ฌด๊ฒฐ์ฑ: {'์ ์' if aud.verify() is None else '์ด์'}\", end=\", \")\n",
"aud.log[7][\"reason\"] = \"์ฌํ ์์ \"\n",
"print(f\"์ฌํ ๋ณ์กฐ ์ seq {aud.verify()}์์ ํ์ง\")\n",
"print(\"๊ฒฐ์ ์ฌ์ ์:\", \"; \".join(sorted({e[\"reason\"] for e in aud.log if e[\"event\"] == \"DECIDE\"})[:3]))\n",
"\n",
"# ๋ฏผ๊ฐ๋ ๋ถ์\n",
"print(\"\\n[๋ฏผ๊ฐ๋ 1] ์ ๋ขฐ๋ ๊ธฐ์ค(๋ฏธ๋ง์ด๋ฉด ์ ๋ฐ ์ง๋จ) -> ์ ๊ฐ๋ฅ , ์ ๋น๊ด ๋๊ธฐ์ด(6๊ฐ์)\")\n",
"rows = []\n",
"for thr in (0.5, 0.6, 0.7, 0.8, 0.9):\n",
" r = run(thr=thr, cap=10 ** 6, audit=Audit()); rows.append((thr, 1 - r[\"cost\"] / A[\"cost\"], r[\"queued\"]))\n",
" print(f\" {thr:.1f}: ์ ๊ฐ {rows[-1][1]:.1%}, ๋๊ธฐ์ด {r['queued']}๊ฑด\")\n",
"print(\"[๋ฏผ๊ฐ๋ 2] ์ ๋น๊ด ์ ์์ฉ๋ -> ์ ๊ฐ๋ฅ (์ ์ ์ ํ ๊ฐ์ : ๊ณ ์ฅ 1.5๋ฐฐ ๊ธ์ฆ)\")\n",
"for cap in (50, 100, 200, 300):\n",
" r1, r2 = run(cap=cap, audit=Audit()), run(cap=cap, surge=1.5, audit=Audit())\n",
" b2 = run(mode=\"auto\", surge=1.5, audit=Audit())\n",
" print(f\" ์ {cap}๊ฑด: ํ์ {1 - r1['cost'] / A['cost']:.1%}, ๊ธ์ฆ {1 - r2['cost'] / b2['cost']:.1%}\")\n",
"\n",
"plt.figure(figsize=(6, 3.5))\n",
"plt.plot([r[0] for r in rows], [r[1] * 100 for r in rows], marker=\"o\"); plt.xlabel(\"confidence threshold\"); plt.ylabel(\"cost saving (%)\")\n",
"plt.title(\"HITL sensitivity: confidence threshold\"); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
07 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฒ์ฆ ์ ๋น ๊ฑด 3890๊ฑด, ๋ณตํฉ ๊ณ ์ฅ ์์ฌ ๊ฑด 1555๊ฑด (์ ๋ขฐ๋ 0.8 ๋ฏธ๋ง ๋๋ 3๊ฐ ๊ณํต ์ด์ DTC)\n",
"์์ง ๋จ๋
์๋ ํ์ (๋น๊ต์ฉ): ๋น์ฉ์ง์ 17546, ์ค์ง๋จ์จ 16.1%\n",
"HITL(์ ๋น๊ด ๋๊ธฐ์ด 1501๊ฑด): ๋น์ฉ์ง์ 14809, ์ค์ง๋จ์จ 4.2%\n",
"์ค์ง๋จ ๋น์ฉ ์ ๊ฐ๋ฅ 15.6% (๋ชฉํ 15% ์ด์: ๋ฌ์ฑ)\n",
"\n",
"์๋ ํ์ 0๊ฑด ํ์ธ: ๋๊ธฐ์ด ๊ฑด์ ๋ชจ๋ ์ ๋น๊ด ๊ฒฐ์ ๊ธฐ๋ก ํ ํ์ (์๋ ํ์ 0๊ฑด)\n",
"๊ฒฐ์ ๋ก๊น
๋ฅ 100% (1501/1501), ๋ฌด์๋ต 120๊ฑด์ ์์ง ์ ์ ์ง + ์ฌํ์ธ ํ์\n",
"๊ฐ์ฌ ๋ก๊ทธ ๋ฌด๊ฒฐ์ฑ: ์ ์, ์ฌํ ๋ณ์กฐ ์ seq 7์์ ํ์ง\n",
"๊ฒฐ์ ์ฌ์ ์: 2์์ ํ๋ณด๋ก ์ ์ (๊ทผ๊ฑฐ ๋ถ์กฑ); ์ฌํ ์์ ; ์๋ต ์ง์ฐ -> ์์ง ์ ์ ์ง, ์ฌํ์ธ ํ์ ํ์\n",
"\n",
"[๋ฏผ๊ฐ๋ 1] ์ ๋ขฐ๋ ๊ธฐ์ค(๋ฏธ๋ง์ด๋ฉด ์ ๋ฐ ์ง๋จ) -> ์ ๊ฐ๋ฅ , ์ ๋น๊ด ๋๊ธฐ์ด(6๊ฐ์)\n",
" 0.5: ์ ๊ฐ 1.9%, ๋๊ธฐ์ด 162๊ฑด\n",
" 0.6: ์ ๊ฐ 9.4%, ๋๊ธฐ์ด 784๊ฑด\n",
" 0.7: ์ ๊ฐ 14.6%, ๋๊ธฐ์ด 1285๊ฑด\n",
" 0.8: ์ ๊ฐ 16.9%, ๋๊ธฐ์ด 1555๊ฑด\n",
" 0.9: ์ ๊ฐ 17.7%, ๋๊ธฐ์ด 1671๊ฑด\n",
"[๋ฏผ๊ฐ๋ 2] ์ ๋น๊ด ์ ์์ฉ๋ -> ์ ๊ฐ๋ฅ (์ ์ ์ ํ ๊ฐ์ : ๊ณ ์ฅ 1.5๋ฐฐ ๊ธ์ฆ)\n",
" ์ 50๊ฑด: ํ์ 2.8%, ๊ธ์ฆ 2.8%\n",
" ์ 100๊ฑด: ํ์ 7.0%, ๊ธ์ฆ 4.4%\n",
" ์ 200๊ฑด: ํ์ 13.8%, ๊ธ์ฆ 9.3%\n",
" ์ 300๊ฑด: ํ์ 15.6%, ๊ธ์ฆ 12.7%\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
08. 1,000๊ฑด ๋์ ๋ค๋ฐ ๊ณ ์ฅ ์ฝ๋ ์ฃผ์
์ ์ฐ๊ด ๊ณ ์ฅ ์ถ๋ก ๋ถํ ํ
์คํธ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"1,000๊ฑด ๋์ ๊ณ ์ฅ ์ฝ๋ ์ฃผ์
์ ์ฐ๊ด ๊ณ ์ฅ ์ถ๋ก ๋ถํ ํ
์คํธ๋ฅผ ์ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **1,000๊ฑด์ DTC ์ง๋จ ์์ฒญ์ ๋์์ ์ฃผ์
ํด ์ฐ๊ด ๊ณ ์ฅ ์ถ๋ก ์ ์ฒ๋ฆฌ ์๊ฐ๊ณผ ๊ฒฝ๋ณด ์ ํ๋๋ฅผ ํ์ธํ๋ ๋ถํ ํ
์คํธ๋ฅผ ์ํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ฐ์ ์ง๋จ ์์ฒญ 1,000๊ฑด์ ํ๊ธฐ ์ค๋ฅ, ๋น ์์ฒญ, ๋ฏธ์ ์ ์ฝ๋, ๋ฏธ๋ฑ๋ก ์ฐจ๋, ์ค๋ณต ์ฝ๋ ๊ฐ์ ์ค๋ฅ๋ฅผ ์์ด ์ผ๊ด ์ฒ๋ฆฌํ๊ณ , ์ ์ ์ฒ๋ฆฌยท๊ฒฉ๋ฆฌ ๊ฑด์์ ์ ์ค ์ฌ๋ถ๋ฅผ ์ผ๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/dashboard/mnt03_app.py (Streamlit/Dash ๋ฑ)` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์์ฌ๊ฒฐ์ ์ง์์ ์ต์ ํ๋ ์๊ฐ์ ์๊ณ ๋ฐ ์ฌ์ฉ์ ๊ฒฝํ(UX)\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๋์๋ณด๋ ๋ก๋ฉ ์๊ฐ 2์ด ์ด๋ด, ๊ฒฝ๋ณด ๋ฐ๋ น ์ ํ๋ 95% ์ด์\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์น ๊ธฐ๋ฐ ๋ถ์ ๋์๋ณด๋ ๊ตฌ๋ ๋ฐ ์ค์๊ฐ ํํฐ๋ง ๊ธฐ๋ฅ ๊ฒ์ฆ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ์ค์ ๊ตฐ์ ๋ฐ์ดํฐ ์ฐ๋ ํ๋ฉด ํ
์คํธ ๋ฐ UI ๋์ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"1,000๊ฑด์ DTC ์ง๋จ ์์ฒญ์ ๋์์ ์ฃผ์
ํด ์ฐ๊ด ๊ณ ์ฅ ์ถ๋ก (๋ถํ ์ง๋จ + ๊ธฐ๋ยท์ ๋ ๊ณํต ๊ฒฝ๋ณด)์ ์ฒ๋ฆฌ ์๊ฐ๊ณผ ๊ฒฝ๋ณด ์ ํ๋๋ฅผ ์ธก์ ํ๋ ๋ถํ ํ
์คํธ๋ฅผ ์ํํ๋ค. ํ์์ด ๊นจ์ง ์ฝ๋ยท๋น ์์ฒญยท๋ฏธ๋ฑ๋ก ์ฐจ๋์ ์์ ์ค๋ฅ ์ฃผ์
์์ฒญ์ด ๊ฒฉ๋ฆฌ ์ฒ๋ฆฌ๋๋์ง, ์์ฒญ ์๋ฅผ 100~5,000๊ฑด์ผ๋ก ๋๋ ธ์ ๋ ์๊ฐ์ด ์ด๋ป๊ฒ ๋ณํ๋์ง ํ์ธํ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from concurrent.futures import ThreadPoolExecutor\n",
"from sklearn.linear_model import LogisticRegression\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ฐจ๋ 100๋์ 18๊ฐ์ ๊ณ ์ฅ ์ง๋จ ์ฝ๋(DTC)ยท์ ๋น ์ด๋ ฅ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์ฅ๋นยท์ ๋น ์ค์ ์์)\n",
"# ๋ถํ 16์ข
(1000๊ฐ๋์๊ฐ๋น ๊ณ ์ฅ๋ฅ ์ฐจ๋ฑ), DTC 28์ข
(๋ถํ ๊ณ ์ ์ฝ๋, ์ง ๋ถํ ๊ณต์ ์ฝ๋, ๊ณํต ์ฆ์ ์ฝ๋)\n",
"# ๊ณ ์ฅ 1๊ฑด(์ ๋น ์ด๋ ฅ)๋ง๋ค DTC๊ฐ 1~5๊ฐ ๋์ ๋ฐ์ํ๊ณ , ์ฐ์ ๊ณ ์ฅ ์ฝ๋์ ์ผ์ ์ก์ ์ฝ๋๊ฐ ์์ธ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"PARTS = [ # (ID, ์ด๋ฆ, ๊ณํต, 1000๊ฐ๋์๊ฐ๋น ๊ณ ์ฅ๋ฅ , ๊ฐ์ ๋จ๊ฐ์ง์, ์๋ฆฌ์๊ฐ h)\n",
" (\"P01\", \"์ฐ๋ฃ๋ถ์ฌ๊ธฐ\", \"engine\", 12.0, 3.0, 2.0), (\"P02\", \"ํฐ๋ณด์ฐจ์ \", \"engine\", 9.0, 8.0, 5.0),\n",
" (\"P03\", \"์ถ์ ์ง\", \"electric\", 6.5, 4.0, 1.5), (\"P04\", \"๋ฐ์ ๊ธฐ\", \"electric\", 4.0, 9.0, 3.0),\n",
" (\"P05\", \"๋ณ์๊ธฐ ์ ์ด๋ชจ๋\", \"drive\", 3.0, 12.0, 6.0), (\"P06\", \"๋๊ฐ์ ํํ\", \"engine\", 2.2, 3.5, 3.0),\n",
" (\"P07\", \"์ ์ ํํ\", \"hydraulic\", 1.6, 10.0, 4.0), (\"P08\", \"์๋ ์ผ์\", \"drive\", 1.2, 2.0, 1.5),\n",
" (\"P09\", \"ECU ์ปค๋ฅํฐ\", \"electric\", 0.9, 2.5, 1.5), (\"P10\", \"์ฐ๋ฃ ํํฐ\", \"engine\", 0.7, 0.8, 0.7),\n",
" (\"P11\", \"์ ๋ ๋ฐธ๋ธ\", \"brake\", 0.5, 6.0, 3.0), (\"P12\", \"์กฐํฅ ์ก์ถ์์ดํฐ\", \"drive\", 0.4, 9.0, 5.0),\n",
" (\"P13\", \"๊ณต๊ธฐ ํํฐ\", \"engine\", 0.3, 0.5, 0.5), (\"P14\", \"ํต์ ์ธํฐํ์ด์ค ๋ชจ๋\", \"electric\", 0.25, 3.0, 1.5),\n",
" (\"P15\", \"์คํ์ ์๋ ฅ ์ผ์\", \"engine\", 0.2, 1.0, 1.0), (\"P16\", \"ํฌ ๊ตฌ๋ ๋ชจํฐ\", \"cooling\", 0.15, 2.0, 2.0)]\n",
"PID = [p[0] for p in PARTS]\n",
"PNAME = {p[0]: p[1] for p in PARTS}; PSYS = {p[0]: p[2] for p in PARTS}; RATE = {p[0]: p[3] for p in PARTS}\n",
"COST = {p[0]: p[4] for p in PARTS}; REPH = {p[0]: p[5] for p in PARTS}\n",
"PAIRS = [(\"P01\", \"P02\"), (\"P03\", \"P04\"), (\"P05\", \"P08\"), (\"P06\", \"P10\"), (\"P09\", \"P14\"), (\"P13\", \"P15\")]\n",
"SHARED = {p: f\"S{k + 1:02d}\" for k, pr in enumerate(PAIRS) for p in pr} # ์ง ๋ถํ์ด ํจ๊ป ์ฐ๋ ๋ชจํธํ ์ฝ๋\n",
"SPEC = {p: f\"C{p[1:]}1\" for p in PID} # ๋ถํ ๊ณ ์ ์ฝ๋\n",
"SYS = [\"engine\", \"electric\", \"drive\", \"hydraulic\", \"brake\", \"cooling\"]\n",
"SYMP = {s: f\"G{i + 1:02d}\" for i, s in enumerate(SYS)} # ๊ณํต ์ฆ์ ์ฝ๋\n",
"CODES = sorted(set(SPEC.values()) | set(SHARED.values()) | set(SYMP.values()))\n",
"CASCADE = {\"P01\": [(\"P10\", 0.20)], \"P02\": [(\"P06\", 0.20)], \"P03\": [(\"P04\", 0.25)], \"P04\": [(\"P09\", 0.25)],\n",
" \"P05\": [(\"P08\", 0.30)], \"P06\": [(\"P16\", 0.30)], \"P09\": [(\"P14\", 0.20)], \"P12\": [(\"P07\", 0.20)]}\n",
"CRIT_SYS = {\"engine\", \"drive\", \"brake\"} # ๊ธฐ๋ยท์ ๋ ๋ถ๋ฅ ์ํ ๊ณํต(๊ฒฝ๋ณด ๋์)\n",
"MONTHS = 18\n",
"\n",
"\n",
"def make_fleet(n=100, seed=0):\n",
" rng = np.random.default_rng(seed)\n",
" f = pd.DataFrame({\"veh\": [f\"V{i:03d}\" for i in range(n)], \"age\": rng.integers(1, 26, n).astype(float),\n",
" \"terrain\": rng.uniform(0, 1, n), \"load\": rng.uniform(0, 1, n), \"pm_gap\": rng.uniform(20, 200, n),\n",
" \"humid\": rng.uniform(0, 1, n), \"base_h\": rng.uniform(55, 125, n)})\n",
" f[\"mult\"] = np.exp(0.045 * (f[\"age\"] - 13) + 0.9 * (f[\"terrain\"] - .5) + 0.8 * (f[\"load\"] - .5)\n",
" + 0.006 * (f[\"pm_gap\"] - 110) + 0.7 * (f[\"humid\"] - .5))\n",
" return f\n",
"\n",
"\n",
"def make_data(seed=1, months=MONTHS):\n",
" \"\"\"๋ฐํ: fleet(์ฐจ๋ ์์ฑ), ev(์ ๋น ์ด๋ ฅ 1๊ฑด=1ํ: ๋ฐ์ DTC ๋ชฉ๋ก, ์ค์ ๊ณ ์ฅ ๋ถํ), log(DTC ๋ฐ์ ๋ก๊ทธ, ํ=์ฝ๋ 1๊ฑด)\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" fleet = make_fleet()\n",
" V = len(fleet)\n",
" temp = 12 + 14 * np.sin(2 * np.pi * (np.arange(months) - 3) / 12) # ์ํ๊ท ๊ธฐ์จ(๊ฐ์)\n",
" hours = fleet[\"base_h\"].values[:, None] * (1 + 0.25 * np.sin(2 * np.pi * np.arange(months) / 12 + 1)) * rng.lognormal(0, .12, (V, months))\n",
" fleet[\"hours\"] = hours.sum(axis=1) # ์ฐจ๋๋ณ 18๊ฐ์ ์ด ๊ฐ๋์๊ฐ\n",
" fleet.attrs[\"month_h\"] = hours.sum(axis=0) # ์๋ณ ์ ์ฒด ๊ฐ๋์๊ฐ(๊ณ ์ฅ๋ฅ ๋ถ๋ชจ)\n",
" evs = []\n",
" for p in PID:\n",
" s = np.ones(months)\n",
" if p in (\"P03\", \"P04\"): s = np.exp(-0.06 * (temp - 12)) # ํ๋ญ๊ธฐ ์ ๊ธฐ๊ณํต ์ฆ๊ฐ\n",
" if p in (\"P06\", \"P16\"): s = np.exp(0.05 * (temp - 12)) # ํน์๊ธฐ ๋๊ฐ๊ณํต ์ฆ๊ฐ\n",
" cnt = rng.poisson(RATE[p] * hours / 1000 * fleet[\"mult\"].values[:, None] * s[None, :])\n",
" for v, m in zip(*np.nonzero(cnt)):\n",
" for _ in range(cnt[v, m]):\n",
" evs.append((fleet[\"veh\"][v], int(m), m * 720 + rng.uniform(0, 720), p, float(temp[m] + rng.normal(0, 3))))\n",
" ev = pd.DataFrame(evs, columns=[\"veh\", \"month\", \"t\", \"part\", \"temp\"]).sort_values(\"t\").reset_index(drop=True)\n",
" ev[\"ev_id\"] = [f\"W{i:05d}\" for i in range(len(ev))]\n",
" rows, codes = [], []\n",
" for e in ev.itertuples():\n",
" c, r = [], rng.random(3)\n",
" if r[0] < 0.6: c.append(SPEC[e.part])\n",
" if e.part in SHARED and r[1] < 0.6: c.append(SHARED[e.part])\n",
" if r[2] < 0.5: c.append(SYMP[PSYS[e.part]])\n",
" if not c: c.append(SHARED.get(e.part, SPEC[e.part]))\n",
" for q, pr in CASCADE.get(e.part, []):\n",
" if rng.random() < pr: c.append(SPEC[q])\n",
" if rng.random() < 0.12: c.append(SPEC[PID[rng.integers(0, 16)]])\n",
" c = list(dict.fromkeys(c))\n",
" rng.shuffle(c) # ๋์ ๋ฐ์ ์ฝ๋์ ๊ธฐ๋ก ์์๋ ์์\n",
" codes.append(c)\n",
" for k, cd in enumerate(c):\n",
" rows.append((e.ev_id, e.veh, e.t + k * 0.1, cd))\n",
" ev[\"codes\"] = codes\n",
" log = pd.DataFrame(rows, columns=[\"ev_id\", \"veh\", \"ts\", \"code\"])\n",
" return fleet, ev, log\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# ๊ณตํต ๋ถ์ ์กฐ๊ฐ: ์ฝ๋ ๋ฉํฐํซ ํ๋ ฌ, ์ฐจ๋ ๋งฅ๋ฝ ํผ์ฒ, ํ์ต/๊ฒ์ฆ ๋ถํ (์ 12๊ฐ์ ํ์ต, ๋ค 6๊ฐ์ ๊ฒ์ฆ)\n",
"# ---------------------------------------------------------------------------\n",
"CIDX = {c: i for i, c in enumerate(CODES)}\n",
"SPLIT_M = 12\n",
"\n",
"\n",
"def multi_hot(ev):\n",
" X = np.zeros((len(ev), len(CODES)))\n",
" for r, cs in enumerate(ev[\"codes\"]):\n",
" for c in cs:\n",
" X[r, CIDX[c]] = 1\n",
" return X\n",
"\n",
"\n",
"def context(ev, fleet):\n",
" f = fleet.set_index(\"veh\")\n",
" return np.column_stack([ev[\"veh\"].map(f[\"age\"]) / 25, ev[\"veh\"].map(f[\"pm_gap\"]) / 200, ev[\"veh\"].map(f[\"terrain\"]),\n",
" ev[\"veh\"].map(f[\"load\"]), (ev[\"temp\"] - 12) / 15])\n",
"\n",
"\n",
"def feats(ev, fleet):\n",
" X = multi_hot(ev)\n",
" n = X.sum(axis=1, keepdims=True) / 3\n",
" return np.hstack([X, n, context(ev, fleet)])\n",
"\n",
"\n",
"def monthly_counts(ev):\n",
" return ev.groupby([\"month\", \"part\"]).size().unstack(fill_value=0).reindex(columns=PID, fill_value=0)\n",
"\n",
"\n",
"def build_monthly(ev, fleet, top_n=8):\n",
" \"\"\"๋ถํ๋ณ ์๊ฐ ๊ณ ์ฅ ๊ฑด์ ์์ธก์ฉ ํ: ์ง์ฐ๊ฐ(lag), ์ด๋ํ๊ท , ์ถ์ธ, ๊ธฐ๋ ๊ฑด์(๊ฐ๋์๊ฐ x ์ค๊ณ ๊ณ ์ฅ๋ฅ ), ๊ธฐ์จ ์ํธ์์ฉ, ๊ณ์ ์ฑ๋ถ\"\"\"\n",
" mc = ev.groupby([\"month\", \"part\"]).size().unstack(fill_value=0)\n",
" top = ev[\"part\"].value_counts().index[:top_n]\n",
" mh, tm = fleet.attrs[\"month_h\"], ev.groupby(\"month\")[\"temp\"].mean().values\n",
" rows = []\n",
" for p in top:\n",
" c = mc[p].values.astype(float)\n",
" for m in range(3, MONTHS):\n",
" L = np.log1p(c)\n",
" rows.append({\"part\": p, \"month\": m, \"y\": c[m], \"lag1\": L[m - 1], \"lag2\": L[m - 2], \"lag3\": L[m - 3], \"roll3\": L[m - 3:m].mean(),\n",
" \"trend\": L[m - 1] - L[m - 2], \"roll3_std\": L[m - 3:m].std(), \"log_expected\": np.log(mh[m] / 1000 * RATE[p]),\n",
" \"rate_lag1\": c[m - 1] / (mh[m - 1] / 1000) / RATE[p], \"temp\": tm[m] / 10, \"cold_elec\": max(0, 12 - tm[m]) / 10 * (p in (\"P03\", \"P04\")),\n",
" \"heat_cool\": max(0, tm[m] - 12) / 10 * (p in (\"P06\", \"P16\")), \"sin\": np.sin(2 * np.pi * m / 12), \"cos\": np.cos(2 * np.pi * m / 12)})\n",
" D = pd.DataFrame(rows)\n",
" return D, [c for c in D.columns if c not in (\"part\", \"month\", \"y\")], top\n",
"\n",
"fleet, ev, log = make_data()\n",
"train, test = ev[ev[\"month\"] < SPLIT_M], ev[ev[\"month\"] >= SPLIT_M].reset_index(drop=True)\n",
"clf = LogisticRegression(C=3, max_iter=500).fit(feats(train, fleet), train[\"part\"])\n",
"cls, FL = clf.classes_, fleet.set_index(\"veh\")\n",
"VALID = set(CODES)\n",
"\n",
"\n",
"def make_requests(n, seed=0, bad_rate=0.05):\n",
" \"\"\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฒ์ฆ ๊ธฐ๊ฐ ์ ๋น ๊ฑด์์ n๊ฑด์ ๋ฝ์ DTC ์์ฒญ์ผ๋ก ๋ง๋ ๋ค. bad_rate ๋น์จ๋งํผ ์ค๋ฅ๋ฅผ ์ฃผ์
ํ๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" rows = test.iloc[rng.choice(len(test), n, replace=n > len(test))]\n",
" reqs, kinds = [], []\n",
" for e in rows.itertuples():\n",
" r, kind = {\"veh\": e.veh, \"codes\": list(e.codes), \"temp\": e.temp, \"true\": e.part}, \"์ ์\"\n",
" u = rng.random()\n",
" if u < bad_rate * 0.3: r[\"codes\"] = [c.lower() + \" \" for c in r[\"codes\"]]; kind = \"ํ๊ธฐ ์ค๋ฅ\"\n",
" elif u < bad_rate * 0.5: r[\"codes\"] = [\"ZZ9\"]; kind = \"๋ฏธ์ ์ ์ฝ๋\"\n",
" elif u < bad_rate * 0.7: r[\"codes\"] = []; kind = \"๋น ์์ฒญ\"\n",
" elif u < bad_rate * 0.85: r[\"veh\"] = \"V999\"; kind = \"๋ฏธ๋ฑ๋ก ์ฐจ๋\"\n",
" elif u < bad_rate: r[\"codes\"] = r[\"codes\"] * 3; kind = \"์ค๋ณต ์ฝ๋\"\n",
" reqs.append(r); kinds.append(kind)\n",
" return reqs, kinds\n",
"\n",
"\n",
"def normalize(r):\n",
" \"\"\"ํ๊ธฐ ์ ๋ฆฌ(๊ณต๋ฐฑยท๋์๋ฌธ์) ํ ๊ฒ์ฆ. ๋ฌธ์ ๊ฐ ์์ผ๋ฉด ์์ธ ๋์ ์ฌ์ ๋ฅผ ๋๋ ค์ค๋ค.\"\"\"\n",
" codes = sorted({c.strip().upper() for c in r[\"codes\"]})\n",
" if r[\"veh\"] not in FL.index: return None, \"๋ฏธ๋ฑ๋ก ์ฐจ๋\"\n",
" if not codes or any(c not in VALID for c in codes): return None, \"๋น ์์ฒญยท๋ฏธ์ ์ ์ฝ๋\"\n",
" return codes, \"\"\n",
"\n",
"\n",
"def build_x(batch):\n",
" X = np.zeros((len(batch), len(CODES) + 6))\n",
" for i, (r, cs) in enumerate(batch):\n",
" for c in cs: X[i, CIDX[c]] = 1\n",
" v = FL.loc[r[\"veh\"]]\n",
" X[i, len(CODES):] = [len(cs) / 3, v[\"age\"] / 25, v[\"pm_gap\"] / 200, v[\"terrain\"], v[\"load\"], (r[\"temp\"] - 12) / 15]\n",
" return X\n",
"\n",
"\n",
"def serve_batch(reqs):\n",
" ok, rej = [], []\n",
" for k, r in enumerate(reqs):\n",
" cs, why = normalize(r)\n",
" (ok if cs else rej).append((k, r, cs) if cs else (k, why))\n",
" X = build_x([(r, cs) for _, r, cs in ok])\n",
" P = clf.predict_proba(X) if len(ok) else np.zeros((0, len(cls)))\n",
" top = cls[P.argmax(axis=1)] if len(ok) else []\n",
" out = [{\"k\": k, \"part\": p, \"conf\": float(P[i].max()), \"alert\": PSYS[p] in CRIT_SYS} for i, ((k, _, _), p) in enumerate(zip(ok, top))]\n",
" return out, rej\n",
"\n",
"\n",
"def serve_one(r):\n",
" cs, why = normalize(r)\n",
" if not cs: return None\n",
" p = cls[clf.predict_proba(build_x([(r, cs)]))[0].argmax()]\n",
" return {\"part\": p, \"alert\": PSYS[p] in CRIT_SYS}\n",
"\n",
"\n",
"# 1) 1,000๊ฑด ๋์ ์ฃผ์
\n",
"reqs, kinds = make_requests(1000)\n",
"t0 = time.perf_counter(); out, rej = serve_batch(reqs)\n",
"agg = pd.DataFrame(out).groupby(\"part\").size().sort_values(ascending=False).head(5) # ๋์๋ณด๋ ์ง๊ณ๊น์ง ํฌํจ\n",
"alerts = sum(o[\"alert\"] for o in out); dt = time.perf_counter() - t0\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] 1,000๊ฑด ๋์ ์ฃผ์
: ์ ์ ์ฒ๋ฆฌ {len(out)}๊ฑด, ๊ฒฉ๋ฆฌ(์ค๋ฅ) {len(rej)}๊ฑด, ์ฒ๋ฆฌ ์๊ฐ {dt:.2f}์ด (๋ชฉํ 2์ด ์ด๋ด: {'๋ฌ์ฑ' if dt <= 2 else '๋ฏธ๋ฌ'})\")\n",
"print(f\" ์ค๋ฅ ์ฃผ์
{sum(k != '์ ์' for k in kinds)}๊ฑด ์ ํ: \" + \", \".join(f\"{k} {v}\" for k, v in pd.Series(kinds).value_counts().items() if k != \"์ ์\"))\n",
"print(\" ๊ฒฉ๋ฆฌ ์ฌ์ : \" + \", \".join(f\"{k} {v}๊ฑด\" for k, v in pd.Series([w for _, w in rej]).value_counts().items()))\n",
"\n",
"# 2) ๊ฒฝ๋ณด ์ ํ๋(๊ธฐ๋ยท์ ๋ ๊ณํต ๊ณ ์ฅ ๊ฒฝ๋ณด)\n",
"truth = np.array([PSYS[reqs[o[\"k\"]][\"true\"]] in CRIT_SYS for o in out]); pa = np.array([o[\"alert\"] for o in out])\n",
"acc = (truth == pa).mean(); rec = (pa & truth).sum() / truth.sum(); prec = (pa & truth).sum() / pa.sum()\n",
"part_acc = np.mean([o[\"part\"] == reqs[o[\"k\"]][\"true\"] for o in out])\n",
"print(f\"\\n๊ฒฝ๋ณด ์ ํ๋ {acc:.1%} (๋ชฉํ 95% ์ด์: {'๋ฌ์ฑ' if acc >= 0.95 else '๋ฏธ๋ฌ'}), ์ฌํ์จ {rec:.1%}, ์ ๋ฐ๋ {prec:.1%}, ๊ฒฝ๋ณด {alerts}๊ฑด\")\n",
"print(f\"์ฐธ๊ณ : ๋ถํ ๋จ์ ์ง๋จ ์ ํ๋ {part_acc:.1%}, ์์ ๋ถํ \" + \", \".join(f\"{PNAME[p]} {n}\" for p, n in agg.items()))\n",
"\n",
"# 3) ๋จ๊ฑด ํธ์ถ ๋ฐฉ์๊ณผ ๋น๊ต(8๊ฐ ์์
์ ๋์ ํธ์ถ)\n",
"t0 = time.perf_counter()\n",
"with ThreadPoolExecutor(8) as ex:\n",
" res = list(ex.map(serve_one, reqs))\n",
"dt1 = time.perf_counter() - t0\n",
"print(f\"\\n๋จ๊ฑด ํธ์ถ 8์ค๋ ๋ {dt1:.2f}์ด vs ์ผ๊ด ์ฒ๋ฆฌ {dt:.2f}์ด (์ผ๊ด์ด {dt1 / dt:.1f}๋ฐฐ ๋น ๋ฆ), ๋จ๊ฑด ๋ฐฉ์ ์์ธ 0๊ฑด: {all(r is None or 'part' in r for r in res)}\")\n",
"\n",
"# 4) ์์ฒญ ์ ํ์ฅ ์ํ\n",
"print(\"[์์ฒญ ์๋ณ ์ผ๊ด ์ฒ๋ฆฌ ์๊ฐ]\")\n",
"ns, ts = [100, 500, 1000, 2000, 5000], []\n",
"for n in ns:\n",
" rq, _ = make_requests(n, seed=n)\n",
" t0 = time.perf_counter(); serve_batch(rq); ts.append(time.perf_counter() - t0)\n",
" print(f\" {n:5d}๊ฑด: {ts[-1]:.2f}์ด ({n / ts[-1]:.0f}๊ฑด/์ด)\")\n",
"\n",
"plt.figure(figsize=(6, 3.5)); plt.plot(ns, ts, marker=\"o\"); plt.axhline(2, ls=\"--\", color=\"k\")\n",
"plt.xlabel(\"requests\"); plt.ylabel(\"seconds\"); plt.title(\"Batch inference time (line = 2 s)\"); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
08 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] 1,000๊ฑด ๋์ ์ฃผ์
: ์ ์ ์ฒ๋ฆฌ 965๊ฑด, ๊ฒฉ๋ฆฌ(์ค๋ฅ) 35๊ฑด, ์ฒ๋ฆฌ ์๊ฐ 0.11์ด (๋ชฉํ 2์ด ์ด๋ด: ๋ฌ์ฑ)\n",
" ์ค๋ฅ ์ฃผ์
65๊ฑด ์ ํ: ํ๊ธฐ ์ค๋ฅ 23, ๋น ์์ฒญ 15, ๋ฏธ์ ์ ์ฝ๋ 11, ๋ฏธ๋ฑ๋ก ์ฐจ๋ 9, ์ค๋ณต ์ฝ๋ 7\n",
" ๊ฒฉ๋ฆฌ ์ฌ์ : ๋น ์์ฒญยท๋ฏธ์ ์ ์ฝ๋ 26๊ฑด, ๋ฏธ๋ฑ๋ก ์ฐจ๋ 9๊ฑด\n",
"\n",
"๊ฒฝ๋ณด ์ ํ๋ 99.8% (๋ชฉํ 95% ์ด์: ๋ฌ์ฑ), ์ฌํ์จ 100.0%, ์ ๋ฐ๋ 99.6%, ๊ฒฝ๋ณด 567๊ฑด\n",
"์ฐธ๊ณ : ๋ถํ ๋จ์ ์ง๋จ ์ ํ๋ 85.1%, ์์ ๋ถํ ์ฐ๋ฃ๋ถ์ฌ๊ธฐ 303, ์ถ์ ์ง 218, ํฐ๋ณด์ฐจ์ 126, ๋ฐ์ ๊ธฐ 119, ๋ณ์๊ธฐ ์ ์ด๋ชจ๋ 53\n",
"\n",
"๋จ๊ฑด ํธ์ถ 8์ค๋ ๋ 0.33์ด vs ์ผ๊ด ์ฒ๋ฆฌ 0.11์ด (์ผ๊ด์ด 3.1๋ฐฐ ๋น ๋ฆ), ๋จ๊ฑด ๋ฐฉ์ ์์ธ 0๊ฑด: True\n",
"[์์ฒญ ์๋ณ ์ผ๊ด ์ฒ๋ฆฌ ์๊ฐ]\n",
" 100๊ฑด: 0.01์ด (15586๊ฑด/์ด)\n",
" 500๊ฑด: 0.03์ด (18195๊ฑด/์ด)\n",
" 1000๊ฑด: 0.06์ด (16478๊ฑด/์ด)\n",
" 2000๊ฑด: 0.30์ด (6703๊ฑด/์ด)\n",
" 5000๊ฑด: 0.33์ด (15293๊ฑด/์ด)\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
09. AAR ๊ธฐ๋ฐ ์ฐ๊ด ๊ณ ์ฅ ์ค์ง๋จ ์ฌ๋ก ๋ถ์ ๋ฐ ๊ณ ์ฅ ๋ฃฐ๋ฒ ์ด์ค ์ฌํ๋\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"AAR ๊ธฐ๋ฐ ์ฐ๊ด ๊ณ ์ฅ ์ค์ง๋จ ์ฌ๋ก๋ฅผ ๋ถ์ํ๊ณ ๋ฃฐ๋ฒ ์ด์ค๋ฅผ ์ฌํ๋ํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **AAR ๊ธฐ๊ฐ์ ์ฐ๊ด ๊ณ ์ฅ ์ค์ง๋จ ์ฌ๋ก๋ฅผ ์์ธ๋ณ๋ก ๋ถ์ํ๊ณ ๊ณ ์ฅ ๋ฃฐ๋ฒ ์ด์ค(์ฝ๋ ์ ํ ๊ฐ์ค์นยท์์ธ ๊ท์น)๋ฅผ ์ฌํ๋ํด ํจ๊ณผ๋ฅผ ์งํ๋ก ์ฐ์ถํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **AAR ๊ธฐ๊ฐ ์ค์ง๋จ ๊ฑด์ ๋ชจํธ ์ฝ๋ ์
๋ ฅ, ์ฐ์ ๊ณ ์ฅ ์ฝ๋ ์ค์ธ, ๋ณต์ ๊ณ ์ ์ฝ๋ ์ถฉ๋ ๋ฑ์ผ๋ก ๋ถ๋ฅํ๊ณ ๋ํ ์ฌ๋ก ์ ๋ฆฌํ๊ณ ๊ท๊ฒฉํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `reports/mnt03_roi_analysis.md` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ถ์ ์ฐ์์ ๊ฐ๊ด์ฑ ๋ฐ ๊ตฐ ์ฌ์ /๊ตฐ์ ๊ธฐํ ๋ถ์ ์ค๋๋ ฅ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๋น์ฉ ์ ๊ฐ์ก, ์๊ฐ ๋จ์ถ๋ฅ ๋ฑ 3๋ ์ ๋ ํจ๊ณผ ์งํ ์ฐ์ถ\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ROI ๋ถ์ ๋ณด๊ณ ์ ์์ฑ ๋ฐ ์ฃผ์ ์งํ ๋ฏผ๊ฐ๋ ๋ถ์ ์๋ก\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ํ๊ณ/๊ธฐํ ๊ธฐ์ค ๋ชจ๋ธ๊ณผ์ ๋น๊ต ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"AAR(์ฌํ๊ฒํ ) ๋์ ๊ธฐ๊ฐ์ ์ฐ๊ด ๊ณ ์ฅ ์ค์ง๋จ ์ฌ๋ก๋ฅผ ์์ธ๋ณ(๋ชจํธ ์ฝ๋๋ง ์
๋ ฅ, ์ฐ์ ๊ณ ์ฅ ์ฝ๋ ์ค์ธ, ์ก์ ์ฝ๋ ์ค์ธ, ๊ธฐํ)๋ก ๋ถ๋ฅํ๊ณ , ๊ณ ์ฅ ๋ฃฐ๋ฒ ์ด์ค์ ์ฝ๋ ์ ํ๋ณ ๊ฐ์ค์น์ ์ฌ์ ํ๋ฅ ๋ณด์ ์ AAR ๊ธฐ๊ฐ ์๋ฃ๋ก ์ฌํ๋ํ ๋ค ๋ณ๋ ๊ฒ์ฆ ๊ธฐ๊ฐ์์ ํจ๊ณผ๋ฅผ ํ์ธํ๋ค. ๋น์ฉ ์ ๊ฐ์ก, ์ฌ์์
์๊ฐ ๋จ์ถ๋ฅ , ํ๊ต์ฒด ๋ถํ ๊ฐ์์จ์ 3๋ ํจ๊ณผ ์งํ์ ๋จ๊ณ๋ณ ๋์
๋ก๋๋งต์ ์ฐ์ถํ๋ค.\"\"\"\n",
"import itertools\n",
"from collections import Counter\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ฐจ๋ 100๋์ 18๊ฐ์ ๊ณ ์ฅ ์ง๋จ ์ฝ๋(DTC)ยท์ ๋น ์ด๋ ฅ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์ฅ๋นยท์ ๋น ์ค์ ์์)\n",
"# ๋ถํ 16์ข
(1000๊ฐ๋์๊ฐ๋น ๊ณ ์ฅ๋ฅ ์ฐจ๋ฑ), DTC 28์ข
(๋ถํ ๊ณ ์ ์ฝ๋, ์ง ๋ถํ ๊ณต์ ์ฝ๋, ๊ณํต ์ฆ์ ์ฝ๋)\n",
"# ๊ณ ์ฅ 1๊ฑด(์ ๋น ์ด๋ ฅ)๋ง๋ค DTC๊ฐ 1~5๊ฐ ๋์ ๋ฐ์ํ๊ณ , ์ฐ์ ๊ณ ์ฅ ์ฝ๋์ ์ผ์ ์ก์ ์ฝ๋๊ฐ ์์ธ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"PARTS = [ # (ID, ์ด๋ฆ, ๊ณํต, 1000๊ฐ๋์๊ฐ๋น ๊ณ ์ฅ๋ฅ , ๊ฐ์ ๋จ๊ฐ์ง์, ์๋ฆฌ์๊ฐ h)\n",
" (\"P01\", \"์ฐ๋ฃ๋ถ์ฌ๊ธฐ\", \"engine\", 12.0, 3.0, 2.0), (\"P02\", \"ํฐ๋ณด์ฐจ์ \", \"engine\", 9.0, 8.0, 5.0),\n",
" (\"P03\", \"์ถ์ ์ง\", \"electric\", 6.5, 4.0, 1.5), (\"P04\", \"๋ฐ์ ๊ธฐ\", \"electric\", 4.0, 9.0, 3.0),\n",
" (\"P05\", \"๋ณ์๊ธฐ ์ ์ด๋ชจ๋\", \"drive\", 3.0, 12.0, 6.0), (\"P06\", \"๋๊ฐ์ ํํ\", \"engine\", 2.2, 3.5, 3.0),\n",
" (\"P07\", \"์ ์ ํํ\", \"hydraulic\", 1.6, 10.0, 4.0), (\"P08\", \"์๋ ์ผ์\", \"drive\", 1.2, 2.0, 1.5),\n",
" (\"P09\", \"ECU ์ปค๋ฅํฐ\", \"electric\", 0.9, 2.5, 1.5), (\"P10\", \"์ฐ๋ฃ ํํฐ\", \"engine\", 0.7, 0.8, 0.7),\n",
" (\"P11\", \"์ ๋ ๋ฐธ๋ธ\", \"brake\", 0.5, 6.0, 3.0), (\"P12\", \"์กฐํฅ ์ก์ถ์์ดํฐ\", \"drive\", 0.4, 9.0, 5.0),\n",
" (\"P13\", \"๊ณต๊ธฐ ํํฐ\", \"engine\", 0.3, 0.5, 0.5), (\"P14\", \"ํต์ ์ธํฐํ์ด์ค ๋ชจ๋\", \"electric\", 0.25, 3.0, 1.5),\n",
" (\"P15\", \"์คํ์ ์๋ ฅ ์ผ์\", \"engine\", 0.2, 1.0, 1.0), (\"P16\", \"ํฌ ๊ตฌ๋ ๋ชจํฐ\", \"cooling\", 0.15, 2.0, 2.0)]\n",
"PID = [p[0] for p in PARTS]\n",
"PNAME = {p[0]: p[1] for p in PARTS}; PSYS = {p[0]: p[2] for p in PARTS}; RATE = {p[0]: p[3] for p in PARTS}\n",
"COST = {p[0]: p[4] for p in PARTS}; REPH = {p[0]: p[5] for p in PARTS}\n",
"PAIRS = [(\"P01\", \"P02\"), (\"P03\", \"P04\"), (\"P05\", \"P08\"), (\"P06\", \"P10\"), (\"P09\", \"P14\"), (\"P13\", \"P15\")]\n",
"SHARED = {p: f\"S{k + 1:02d}\" for k, pr in enumerate(PAIRS) for p in pr} # ์ง ๋ถํ์ด ํจ๊ป ์ฐ๋ ๋ชจํธํ ์ฝ๋\n",
"SPEC = {p: f\"C{p[1:]}1\" for p in PID} # ๋ถํ ๊ณ ์ ์ฝ๋\n",
"SYS = [\"engine\", \"electric\", \"drive\", \"hydraulic\", \"brake\", \"cooling\"]\n",
"SYMP = {s: f\"G{i + 1:02d}\" for i, s in enumerate(SYS)} # ๊ณํต ์ฆ์ ์ฝ๋\n",
"CODES = sorted(set(SPEC.values()) | set(SHARED.values()) | set(SYMP.values()))\n",
"CASCADE = {\"P01\": [(\"P10\", 0.20)], \"P02\": [(\"P06\", 0.20)], \"P03\": [(\"P04\", 0.25)], \"P04\": [(\"P09\", 0.25)],\n",
" \"P05\": [(\"P08\", 0.30)], \"P06\": [(\"P16\", 0.30)], \"P09\": [(\"P14\", 0.20)], \"P12\": [(\"P07\", 0.20)]}\n",
"CRIT_SYS = {\"engine\", \"drive\", \"brake\"} # ๊ธฐ๋ยท์ ๋ ๋ถ๋ฅ ์ํ ๊ณํต(๊ฒฝ๋ณด ๋์)\n",
"MONTHS = 18\n",
"\n",
"\n",
"def make_fleet(n=100, seed=0):\n",
" rng = np.random.default_rng(seed)\n",
" f = pd.DataFrame({\"veh\": [f\"V{i:03d}\" for i in range(n)], \"age\": rng.integers(1, 26, n).astype(float),\n",
" \"terrain\": rng.uniform(0, 1, n), \"load\": rng.uniform(0, 1, n), \"pm_gap\": rng.uniform(20, 200, n),\n",
" \"humid\": rng.uniform(0, 1, n), \"base_h\": rng.uniform(55, 125, n)})\n",
" f[\"mult\"] = np.exp(0.045 * (f[\"age\"] - 13) + 0.9 * (f[\"terrain\"] - .5) + 0.8 * (f[\"load\"] - .5)\n",
" + 0.006 * (f[\"pm_gap\"] - 110) + 0.7 * (f[\"humid\"] - .5))\n",
" return f\n",
"\n",
"\n",
"def make_data(seed=1, months=MONTHS):\n",
" \"\"\"๋ฐํ: fleet(์ฐจ๋ ์์ฑ), ev(์ ๋น ์ด๋ ฅ 1๊ฑด=1ํ: ๋ฐ์ DTC ๋ชฉ๋ก, ์ค์ ๊ณ ์ฅ ๋ถํ), log(DTC ๋ฐ์ ๋ก๊ทธ, ํ=์ฝ๋ 1๊ฑด)\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" fleet = make_fleet()\n",
" V = len(fleet)\n",
" temp = 12 + 14 * np.sin(2 * np.pi * (np.arange(months) - 3) / 12) # ์ํ๊ท ๊ธฐ์จ(๊ฐ์)\n",
" hours = fleet[\"base_h\"].values[:, None] * (1 + 0.25 * np.sin(2 * np.pi * np.arange(months) / 12 + 1)) * rng.lognormal(0, .12, (V, months))\n",
" fleet[\"hours\"] = hours.sum(axis=1) # ์ฐจ๋๋ณ 18๊ฐ์ ์ด ๊ฐ๋์๊ฐ\n",
" fleet.attrs[\"month_h\"] = hours.sum(axis=0) # ์๋ณ ์ ์ฒด ๊ฐ๋์๊ฐ(๊ณ ์ฅ๋ฅ ๋ถ๋ชจ)\n",
" evs = []\n",
" for p in PID:\n",
" s = np.ones(months)\n",
" if p in (\"P03\", \"P04\"): s = np.exp(-0.06 * (temp - 12)) # ํ๋ญ๊ธฐ ์ ๊ธฐ๊ณํต ์ฆ๊ฐ\n",
" if p in (\"P06\", \"P16\"): s = np.exp(0.05 * (temp - 12)) # ํน์๊ธฐ ๋๊ฐ๊ณํต ์ฆ๊ฐ\n",
" cnt = rng.poisson(RATE[p] * hours / 1000 * fleet[\"mult\"].values[:, None] * s[None, :])\n",
" for v, m in zip(*np.nonzero(cnt)):\n",
" for _ in range(cnt[v, m]):\n",
" evs.append((fleet[\"veh\"][v], int(m), m * 720 + rng.uniform(0, 720), p, float(temp[m] + rng.normal(0, 3))))\n",
" ev = pd.DataFrame(evs, columns=[\"veh\", \"month\", \"t\", \"part\", \"temp\"]).sort_values(\"t\").reset_index(drop=True)\n",
" ev[\"ev_id\"] = [f\"W{i:05d}\" for i in range(len(ev))]\n",
" rows, codes = [], []\n",
" for e in ev.itertuples():\n",
" c, r = [], rng.random(3)\n",
" if r[0] < 0.6: c.append(SPEC[e.part])\n",
" if e.part in SHARED and r[1] < 0.6: c.append(SHARED[e.part])\n",
" if r[2] < 0.5: c.append(SYMP[PSYS[e.part]])\n",
" if not c: c.append(SHARED.get(e.part, SPEC[e.part]))\n",
" for q, pr in CASCADE.get(e.part, []):\n",
" if rng.random() < pr: c.append(SPEC[q])\n",
" if rng.random() < 0.12: c.append(SPEC[PID[rng.integers(0, 16)]])\n",
" c = list(dict.fromkeys(c))\n",
" rng.shuffle(c) # ๋์ ๋ฐ์ ์ฝ๋์ ๊ธฐ๋ก ์์๋ ์์\n",
" codes.append(c)\n",
" for k, cd in enumerate(c):\n",
" rows.append((e.ev_id, e.veh, e.t + k * 0.1, cd))\n",
" ev[\"codes\"] = codes\n",
" log = pd.DataFrame(rows, columns=[\"ev_id\", \"veh\", \"ts\", \"code\"])\n",
" return fleet, ev, log\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# ๊ณตํต ๋ถ์ ์กฐ๊ฐ: ์ฝ๋ ๋ฉํฐํซ ํ๋ ฌ, ์ฐจ๋ ๋งฅ๋ฝ ํผ์ฒ, ํ์ต/๊ฒ์ฆ ๋ถํ (์ 12๊ฐ์ ํ์ต, ๋ค 6๊ฐ์ ๊ฒ์ฆ)\n",
"# ---------------------------------------------------------------------------\n",
"CIDX = {c: i for i, c in enumerate(CODES)}\n",
"SPLIT_M = 12\n",
"\n",
"\n",
"def multi_hot(ev):\n",
" X = np.zeros((len(ev), len(CODES)))\n",
" for r, cs in enumerate(ev[\"codes\"]):\n",
" for c in cs:\n",
" X[r, CIDX[c]] = 1\n",
" return X\n",
"\n",
"\n",
"def context(ev, fleet):\n",
" f = fleet.set_index(\"veh\")\n",
" return np.column_stack([ev[\"veh\"].map(f[\"age\"]) / 25, ev[\"veh\"].map(f[\"pm_gap\"]) / 200, ev[\"veh\"].map(f[\"terrain\"]),\n",
" ev[\"veh\"].map(f[\"load\"]), (ev[\"temp\"] - 12) / 15])\n",
"\n",
"\n",
"def feats(ev, fleet):\n",
" X = multi_hot(ev)\n",
" n = X.sum(axis=1, keepdims=True) / 3\n",
" return np.hstack([X, n, context(ev, fleet)])\n",
"\n",
"\n",
"def monthly_counts(ev):\n",
" return ev.groupby([\"month\", \"part\"]).size().unstack(fill_value=0).reindex(columns=PID, fill_value=0)\n",
"\n",
"\n",
"def build_monthly(ev, fleet, top_n=8):\n",
" \"\"\"๋ถํ๋ณ ์๊ฐ ๊ณ ์ฅ ๊ฑด์ ์์ธก์ฉ ํ: ์ง์ฐ๊ฐ(lag), ์ด๋ํ๊ท , ์ถ์ธ, ๊ธฐ๋ ๊ฑด์(๊ฐ๋์๊ฐ x ์ค๊ณ ๊ณ ์ฅ๋ฅ ), ๊ธฐ์จ ์ํธ์์ฉ, ๊ณ์ ์ฑ๋ถ\"\"\"\n",
" mc = ev.groupby([\"month\", \"part\"]).size().unstack(fill_value=0)\n",
" top = ev[\"part\"].value_counts().index[:top_n]\n",
" mh, tm = fleet.attrs[\"month_h\"], ev.groupby(\"month\")[\"temp\"].mean().values\n",
" rows = []\n",
" for p in top:\n",
" c = mc[p].values.astype(float)\n",
" for m in range(3, MONTHS):\n",
" L = np.log1p(c)\n",
" rows.append({\"part\": p, \"month\": m, \"y\": c[m], \"lag1\": L[m - 1], \"lag2\": L[m - 2], \"lag3\": L[m - 3], \"roll3\": L[m - 3:m].mean(),\n",
" \"trend\": L[m - 1] - L[m - 2], \"roll3_std\": L[m - 3:m].std(), \"log_expected\": np.log(mh[m] / 1000 * RATE[p]),\n",
" \"rate_lag1\": c[m - 1] / (mh[m - 1] / 1000) / RATE[p], \"temp\": tm[m] / 10, \"cold_elec\": max(0, 12 - tm[m]) / 10 * (p in (\"P03\", \"P04\")),\n",
" \"heat_cool\": max(0, tm[m] - 12) / 10 * (p in (\"P06\", \"P16\")), \"sin\": np.sin(2 * np.pi * m / 12), \"cos\": np.cos(2 * np.pi * m / 12)})\n",
" D = pd.DataFrame(rows)\n",
" return D, [c for c in D.columns if c not in (\"part\", \"month\", \"y\")], top\n",
"\n",
"fleet, ev, log = make_data()\n",
"train, aar, val = ev[ev[\"month\"] < 12], ev[(ev[\"month\"] >= 12) & (ev[\"month\"] < 15)].reset_index(drop=True), ev[ev[\"month\"] >= 15].reset_index(drop=True)\n",
"\n",
"# 1) ๋ฃฐ๋ฒ ์ด์ค: ์ ์(๋ถํ) = sum[์ฝ๋ ์ ํ ๊ฐ์ค์น x P(๋ถํ | ์ฝ๋)] + ์ฌ์ ํ๋ฅ ๋ณด์ . ํ์ต ๊ธฐ๊ฐ ์กฐ๊ฑด๋ถํ๋ฅ ํ๋ฅผ ์ด๋ค.\n",
"Xtr = multi_hot(train)\n",
"Y = pd.get_dummies(train[\"part\"]).reindex(columns=PID, fill_value=0).values.astype(float)\n",
"COND = (Xtr.T @ Y) / np.maximum(Xtr.sum(axis=0)[:, None], 1) # P(๋ถํ | ์ฝ๋)\n",
"PRIOR = np.log(Y.mean(axis=0) + 1e-9)\n",
"CTYPE = np.array([0 if c in SPEC.values() else 1 if c in SHARED.values() else 2 for c in CODES]) # 0 ๊ณ ์ / 1 ๊ณต์ / 2 ๊ณํต ์ฆ์\n",
"V1 = {\"w\": (1.0, 1.0, 1.0), \"beta\": 0.0} # ๊ธฐ์กด ๋ฃฐ๋ฒ ์ด์ค(๋ชจ๋ ์ฝ๋ ๋์ผ ๊ฐ์ค, ์ฌ์ ํ๋ฅ ๋ฏธ๋ฐ์)\n",
"\n",
"\n",
"def predict(ev_, p):\n",
" W = np.array(p[\"w\"])[CTYPE][:, None] * COND\n",
" S = multi_hot(ev_) @ W + p[\"beta\"] * PRIOR[None, :]\n",
" base = np.array(PID)[S.argmax(axis=1)]\n",
" ov = p.get(\"override\", {}) # AAR์์ ๋ง๋ ์์ธ ๊ท์น(DTC ์กฐํฉ -> ๋ถํ)์ด ์ฐ์ \n",
" return np.array([ov.get(frozenset(c), b) for c, b in zip(ev_[\"codes\"], base)])\n",
"\n",
"\n",
"def cost(pred, true):\n",
" return sum(REPH[t] if p == t else COST[p] + REPH[p] + 3.0 + REPH[t] for p, t in zip(pred, true))\n",
"\n",
"\n",
"def err_stats(pred, true):\n",
" wrong = pred != true.values\n",
" return int(wrong.sum()), sum(REPH[p] + 3.0 + REPH[t] for p, t in zip(pred[wrong], true[wrong])), int(wrong.sum())\n",
"\n",
"\n",
"# 2) AAR: ์ค์ง๋จ ์์ธ ๋ถ๋ฅ\n",
"pa = predict(aar, V1)\n",
"casc = {(p, q) for p, lst in CASCADE.items() for q, _ in lst}\n",
"cause = []\n",
"for cs, t, q in zip(aar[\"codes\"], aar[\"part\"], pa):\n",
" if t == q: continue\n",
" sp = [c for c in cs if c in SPEC.values()]\n",
" if not sp: cause.append(\"๋ชจํธ ์ฝ๋(๊ณต์ ยท์ฆ์)๋ง ์
๋ ฅ\")\n",
" elif len(sp) >= 2 and ((t, q) in casc or (q, t) in casc): cause.append(\"์ฐ์ ๊ณ ์ฅ ์ฝ๋ ์ค์ธ\")\n",
" elif len(sp) >= 2: cause.append(\"๋ณต์ ๊ณ ์ ์ฝ๋ ์ถฉ๋(์ก์ ํฌํจ)\")\n",
" else: cause.append(\"๊ธฐํ(๊ณ ์ ์ฝ๋ 1๊ฐ, ํ๋ฅ ํธํฅ)\")\n",
"C = pd.Series(cause).value_counts()\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] AAR ๊ธฐ๊ฐ(12~14์) ์ ๋น {len(aar)}๊ฑด: ๊ธฐ์กด ๋ฃฐ๋ฒ ์ด์ค ์ค์ง๋จ {len(cause)}๊ฑด ({len(cause) / len(aar):.1%})\")\n",
"for k, v in C.items():\n",
" print(f\" {k:24s} {v:4d}๊ฑด ({v / len(cause):.0%})\")\n",
"ex = [(cs, PNAME[t], PNAME[q]) for cs, t, q in zip(aar[\"codes\"], aar[\"part\"], pa) if t != q and sum(c in SPEC.values() for c in cs) >= 2][:1]\n",
"print(\" ์ฌ๋ก:\", f\"์
๋ ฅ DTC {ex[0][0]} -> ์ค์ง๋จ {ex[0][2]}, ์ค์ {ex[0][1]}\" if ex else \"-\")\n",
"\n",
"# 3) ์ฌํ๋: (๊ฐ) ์ฝ๋ ์ ํ ๊ฐ์ค์นยท์ฌ์ ํ๋ฅ ๊ฒฉ์ ํ์, (๋) AAR ์ค์ง๋จ ์กฐํฉ์์ ์์ธ ๊ท์น ์ถ์ถ. ๋ชจ๋ AAR ๊ธฐ๊ฐ ์๋ฃ๋ง ์ฐ๊ณ ๊ฒ์ฆ ๊ธฐ๊ฐ์ ๊ฑด๋๋ฆฌ์ง ์๋๋ค.\n",
"best = None\n",
"for w in itertools.product((0.5, 1.0, 1.5, 2.0), repeat=3):\n",
" for beta in (0.0, 0.2, 0.5, 1.0):\n",
" p = {\"w\": w, \"beta\": beta}\n",
" c = cost(predict(aar, p), aar[\"part\"])\n",
" if best is None or c < best[0]: best = (c, p)\n",
"V2 = dict(best[1])\n",
"pa2 = predict(aar, V2)\n",
"bad_sets = {frozenset(c) for c, t, q in zip(aar[\"codes\"], aar[\"part\"], pa2) if t != q}\n",
"grp = {}\n",
"for c, t in zip(aar[\"codes\"], aar[\"part\"]):\n",
" grp.setdefault(frozenset(c), Counter())[t] += 1\n",
"V2[\"override\"] = {k: v.most_common(1)[0][0] for k, v in grp.items()\n",
" if k in bad_sets and sum(v.values()) >= 3 and v.most_common(1)[0][1] / sum(v.values()) >= 0.6}\n",
"print(f\"\\n์ฌํ๋ ๊ฒฐ๊ณผ: ์ฝ๋ ์ ํ ๊ฐ์ค์น(๊ณ ์ ยท๊ณต์ ยท์ฆ์) {V1['w']} -> {V2['w']}, ์ฌ์ ํ๋ฅ ๋ณด์ {V1['beta']} -> {V2['beta']}, AAR ์์ธ ๊ท์น {len(V2['override'])}๊ฐ ์ถ๊ฐ\")\n",
"rows = []\n",
"for nm, d in ((\"AAR ๊ธฐ๊ฐ(ํ๋)\", aar), (\"๊ฒ์ฆ ๊ธฐ๊ฐ(15~17์)\", val)):\n",
" p1, p2 = predict(d, V1), predict(d, V2)\n",
" rows.append((nm, (p1 == d[\"part\"].values).mean(), (p2 == d[\"part\"].values).mean(), cost(p1, d[\"part\"]), cost(p2, d[\"part\"])))\n",
" print(f\" {nm:14s} ์ ํ๋ {rows[-1][1]:.1%} -> {rows[-1][2]:.1%}, ๋น์ฉ์ง์ {rows[-1][3]:.0f} -> {rows[-1][4]:.0f}\")\n",
"\n",
"# 4) ROI 3๋ ์งํ(๊ฒ์ฆ ๊ธฐ๊ฐ 3๊ฐ์, ๊ฐ์ ๋จ๊ฐ ๊ธฐ์ค ์๋๊ฐ)\n",
"p1, p2 = predict(val, V1), predict(val, V2)\n",
"n1, h1, _ = err_stats(p1, val[\"part\"]); n2, h2, _ = err_stats(p2, val[\"part\"])\n",
"saved = cost(p1, val[\"part\"]) - cost(p2, val[\"part\"])\n",
"wasted1 = sum(COST[p] for p, t in zip(p1, val[\"part\"]) if p != t); wasted2 = sum(COST[p] for p, t in zip(p2, val[\"part\"]) if p != t)\n",
"print(\"\\n[ROI 3๋ ์ ๋ ํจ๊ณผ ์งํ] (๊ฒ์ฆ 3๊ฐ์, ๊ฐ์ ๋จ๊ฐ)\")\n",
"print(f\" 1) ๋น์ฉ ์ ๊ฐ: ๋น์ฉ์ง์ {saved:.0f} ์ ๊ฐ ({saved / cost(p1, val['part']):.1%}), ์ฐํ์ฐ ์ฝ {saved * 4:.0f}\")\n",
"print(f\" 2) ์๊ฐ ๋จ์ถ: ์ค์ง๋จ ์ฌ์์
{h1:.0f}h -> {h2:.0f}h ({1 - h2 / h1:.1%} ๋จ์ถ)\")\n",
"print(f\" 3) ํ๊ต์ฒด ๋ถํ ๋น์ฉ: {wasted1:.0f} -> {wasted2:.0f} ({1 - wasted2 / wasted1:.1%} ๊ฐ์), ์ค์ง๋จ {n1}๊ฑด -> {n2}๊ฑด\")\n",
"print(\"[๋์
๋ก๋๋งต] ๋จ๊ธฐ(1~3์): ๊ฐ์ค์น ๊ฐฑ์ + ์ ๋น๊ด ํ์ธ ๋ณํ / ์ค๊ธฐ(3~12์): AAR ๋ถ๊ธฐ ๋ฐ๋ณตยท์ ๊ท ์ฝ๋ ๋ฐ์ / ์ฅ๊ธฐ(1๋
~): ๊ตฐ์ ์ ๋ณด์ฒด๊ณ ์ด๋ ฅ ์ฐ๋\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(10, 3.5))\n",
"ax[0].barh([f\"cause {i + 1}\" for i in range(len(C))][::-1], C.values[::-1]); ax[0].set_title(\"Misdiagnosis causes (AAR period)\")\n",
"ax[1].bar([\"before\", \"after\"], [rows[1][1], rows[1][2]]); ax[1].set_ylim(0.5, 1); ax[1].set_title(\"Validation accuracy\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
09 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] AAR ๊ธฐ๊ฐ(12~14์) ์ ๋น 2336๊ฑด: ๊ธฐ์กด ๋ฃฐ๋ฒ ์ด์ค ์ค์ง๋จ 522๊ฑด (22.3%)\n",
" ๋ชจํธ ์ฝ๋(๊ณต์ ยท์ฆ์)๋ง ์
๋ ฅ 243๊ฑด (47%)\n",
" ๊ธฐํ(๊ณ ์ ์ฝ๋ 1๊ฐ, ํ๋ฅ ํธํฅ) 237๊ฑด (45%)\n",
" ์ฐ์ ๊ณ ์ฅ ์ฝ๋ ์ค์ธ 26๊ฑด (5%)\n",
" ๋ณต์ ๊ณ ์ ์ฝ๋ ์ถฉ๋(์ก์ ํฌํจ) 16๊ฑด (3%)\n",
" ์ฌ๋ก: ์
๋ ฅ DTC ['S04', 'G01', 'C081', 'C101'] -> ์ค์ง๋จ ์ฐ๋ฃ๋ถ์ฌ๊ธฐ, ์ค์ ์ฐ๋ฃ ํํฐ\n",
"\n",
"์ฌํ๋ ๊ฒฐ๊ณผ: ์ฝ๋ ์ ํ ๊ฐ์ค์น(๊ณ ์ ยท๊ณต์ ยท์ฆ์) (1.0, 1.0, 1.0) -> (2.0, 1.0, 0.5), ์ฌ์ ํ๋ฅ ๋ณด์ 0.0 -> 0.0, AAR ์์ธ ๊ท์น 31๊ฐ ์ถ๊ฐ\n",
" AAR ๊ธฐ๊ฐ(ํ๋) ์ ํ๋ 77.7% -> 82.9%, ๋น์ฉ์ง์ 11938 -> 10805\n",
" ๊ฒ์ฆ ๊ธฐ๊ฐ(15~17์) ์ ํ๋ 78.1% -> 80.1%, ๋น์ฉ์ง์ 8449 -> 7832\n",
"\n",
"[ROI 3๋ ์ ๋ ํจ๊ณผ ์งํ] (๊ฒ์ฆ 3๊ฐ์, ๊ฐ์ ๋จ๊ฐ)\n",
" 1) ๋น์ฉ ์ ๊ฐ: ๋น์ฉ์ง์ 616 ์ ๊ฐ (7.3%), ์ฐํ์ฐ ์ฝ 2466\n",
" 2) ์๊ฐ ๋จ์ถ: ์ค์ง๋จ ์ฌ์์
3038h -> 2713h (10.7% ๋จ์ถ)\n",
" 3) ํ๊ต์ฒด ๋ถํ ๋น์ฉ: 1778 -> 1441 (18.9% ๊ฐ์), ์ค์ง๋จ 341๊ฑด -> 309๊ฑด\n",
"[๋์
๋ก๋๋งต] ๋จ๊ธฐ(1~3์): ๊ฐ์ค์น ๊ฐฑ์ + ์ ๋น๊ด ํ์ธ ๋ณํ / ์ค๊ธฐ(3~12์): AAR ๋ถ๊ธฐ ๋ฐ๋ณตยท์ ๊ท ์ฝ๋ ๋ฐ์ / ์ฅ๊ธฐ(1๋
~): ๊ตฐ์ ์ ๋ณด์ฒด๊ณ ์ด๋ ฅ ์ฐ๋\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
10. ๋ฌด๊ธฐ์ฒด๊ณ ๊ณ ์ฅ ์ฝ๋(DTC) ๊ธฐ๋ฐ ์ฐ๊ด ๊ณ ์ฅ ์์ธก ๋ฐ ์ ๋น ๊ฐ์ด๋ ๋๊ตฌ ์์ฑ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋ฌด๊ธฐ์ฒด๊ณ ๊ณ ์ฅ ์ฝ๋(DTC) ๊ธฐ๋ฐ ์ฐ๊ด ๊ณ ์ฅ ์์ธก ๋ฐ ์ ๋น ๊ฐ์ด๋ ๋๊ตฌ๋ฅผ ์์ฑํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **DTC ์
๋ ฅ๋ถํฐ ์ฐ๊ด ๊ณ ์ฅ ์์ธก, ์ ๋น ๊ฐ์ด๋, ์น์ธ ๋์ ์ ๋ณ, ์ฌํ์ต๊น์ง ํ ํ์ผ๋ก ํตํฉํ๊ณ ์์ฒด ์ ๊ฒ๊ณผ ๋ธ๋ฆฌํ ์์ฝ์ ์ถ๋ ฅํ๋ ๋๊ตฌ๋ฅผ ์์ฑํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์๋ณ ์ ๋น ์ค์ ์ผ๋ก ์ง๋จ ๋ชจ๋ธ์ ํ์ตํ๊ณ , ์ ๊ท ์ค์ ์ผ๋ก ๋์ ์ ๋ชจ๋ธ์ ๋ง๋ค์ด ์ฑํผ์ธ๊ณผ ๊ฒ์ฆ ์ค์ฐจ๋ฅผ ๋น๊ตํ ๋ค ๋น์ทํ๊ฑฐ๋ ๋์ ๋๋ง ๊ต์ฒด ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `docs/mnt03_operations_guide.md ๋ฐ pipeline/mnt03_retrain.py` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ง์ ๊ฐ๋ฅํ ์ด์ ๊ฐ๋ฅ์ฑ ๋ฐ ๊ตญ๋ฐฉ ๊ตฐ์ ์ ๋ณด์ฒด๊ณ์์ ์ฐ๋ ์ฉ์ด์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์๋ ์ฌํ์ต ํ์ดํ๋ผ์ธ ์ ์ ์คํ, ์ต์ข
๋ฌธ์ 100% ์๋น\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์ต์ข
์ธ๋๋ฌผ ๊ฒ์ ๋ฐ ํ๋ก์ ํธ ์๋ฃ ๋ฐํ์๋ฃ ์์ฑ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: End-to-End ์ ์ฒด ํ์ดํ๋ผ์ธ ์๋ํ ์คํ ๋ฐ ์ต์ข
๊ฒ์\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"DTC ๋ก๊ทธ ๊ฒ์ฆยท์ ์ , ์ง๋จ ๋ชจ๋ธ ์๋ ์ฌํ์ต(์ฑํผ์ธยท๋์ ์ ๋น๊ต ํ ๊ต์ฒด), ๊ณ ์ฅ ์ฝ๋ ๊ธฐ๋ฐ ์ฐ๊ด ๊ณ ์ฅ ์ง๋จ๊ณผ ์ ๋ขฐ๋ ์ฐ์ถ, ์ ๋ขฐ๋ ๋ฎ์ ๊ฑด์ ์ ๋น๊ด ์น์ธ ๋๊ธฐ ๋ถ๋ฆฌ, ๋ค๋น๋ ๊ณ ์ฅํ๋ชฉ ์๊ฐ ์์ ์์ธก๊น์ง ์ ๋น ๊ฐ์ด๋ ๋๊ตฌ๋ฅผ ํ ํ์ผ๋ก ํตํฉ ์คํํ๊ณ , ์์ฒด ์ ๊ฒยท์ด์ ๋ฌธ์ ์๋น ์ ๊ฒยท์ ๋น ๋ด๋น ์ฅ๊ต ๋ธ๋ฆฌํ ์์ฝ์ ์ถ๋ ฅํ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.linear_model import LogisticRegression, Ridge\n",
"from sklearn.preprocessing import StandardScaler\n",
"from sklearn.pipeline import make_pipeline\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ฐจ๋ 100๋์ 18๊ฐ์ ๊ณ ์ฅ ์ง๋จ ์ฝ๋(DTC)ยท์ ๋น ์ด๋ ฅ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์ฅ๋นยท์ ๋น ์ค์ ์์)\n",
"# ๋ถํ 16์ข
(1000๊ฐ๋์๊ฐ๋น ๊ณ ์ฅ๋ฅ ์ฐจ๋ฑ), DTC 28์ข
(๋ถํ ๊ณ ์ ์ฝ๋, ์ง ๋ถํ ๊ณต์ ์ฝ๋, ๊ณํต ์ฆ์ ์ฝ๋)\n",
"# ๊ณ ์ฅ 1๊ฑด(์ ๋น ์ด๋ ฅ)๋ง๋ค DTC๊ฐ 1~5๊ฐ ๋์ ๋ฐ์ํ๊ณ , ์ฐ์ ๊ณ ์ฅ ์ฝ๋์ ์ผ์ ์ก์ ์ฝ๋๊ฐ ์์ธ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"PARTS = [ # (ID, ์ด๋ฆ, ๊ณํต, 1000๊ฐ๋์๊ฐ๋น ๊ณ ์ฅ๋ฅ , ๊ฐ์ ๋จ๊ฐ์ง์, ์๋ฆฌ์๊ฐ h)\n",
" (\"P01\", \"์ฐ๋ฃ๋ถ์ฌ๊ธฐ\", \"engine\", 12.0, 3.0, 2.0), (\"P02\", \"ํฐ๋ณด์ฐจ์ \", \"engine\", 9.0, 8.0, 5.0),\n",
" (\"P03\", \"์ถ์ ์ง\", \"electric\", 6.5, 4.0, 1.5), (\"P04\", \"๋ฐ์ ๊ธฐ\", \"electric\", 4.0, 9.0, 3.0),\n",
" (\"P05\", \"๋ณ์๊ธฐ ์ ์ด๋ชจ๋\", \"drive\", 3.0, 12.0, 6.0), (\"P06\", \"๋๊ฐ์ ํํ\", \"engine\", 2.2, 3.5, 3.0),\n",
" (\"P07\", \"์ ์ ํํ\", \"hydraulic\", 1.6, 10.0, 4.0), (\"P08\", \"์๋ ์ผ์\", \"drive\", 1.2, 2.0, 1.5),\n",
" (\"P09\", \"ECU ์ปค๋ฅํฐ\", \"electric\", 0.9, 2.5, 1.5), (\"P10\", \"์ฐ๋ฃ ํํฐ\", \"engine\", 0.7, 0.8, 0.7),\n",
" (\"P11\", \"์ ๋ ๋ฐธ๋ธ\", \"brake\", 0.5, 6.0, 3.0), (\"P12\", \"์กฐํฅ ์ก์ถ์์ดํฐ\", \"drive\", 0.4, 9.0, 5.0),\n",
" (\"P13\", \"๊ณต๊ธฐ ํํฐ\", \"engine\", 0.3, 0.5, 0.5), (\"P14\", \"ํต์ ์ธํฐํ์ด์ค ๋ชจ๋\", \"electric\", 0.25, 3.0, 1.5),\n",
" (\"P15\", \"์คํ์ ์๋ ฅ ์ผ์\", \"engine\", 0.2, 1.0, 1.0), (\"P16\", \"ํฌ ๊ตฌ๋ ๋ชจํฐ\", \"cooling\", 0.15, 2.0, 2.0)]\n",
"PID = [p[0] for p in PARTS]\n",
"PNAME = {p[0]: p[1] for p in PARTS}; PSYS = {p[0]: p[2] for p in PARTS}; RATE = {p[0]: p[3] for p in PARTS}\n",
"COST = {p[0]: p[4] for p in PARTS}; REPH = {p[0]: p[5] for p in PARTS}\n",
"PAIRS = [(\"P01\", \"P02\"), (\"P03\", \"P04\"), (\"P05\", \"P08\"), (\"P06\", \"P10\"), (\"P09\", \"P14\"), (\"P13\", \"P15\")]\n",
"SHARED = {p: f\"S{k + 1:02d}\" for k, pr in enumerate(PAIRS) for p in pr} # ์ง ๋ถํ์ด ํจ๊ป ์ฐ๋ ๋ชจํธํ ์ฝ๋\n",
"SPEC = {p: f\"C{p[1:]}1\" for p in PID} # ๋ถํ ๊ณ ์ ์ฝ๋\n",
"SYS = [\"engine\", \"electric\", \"drive\", \"hydraulic\", \"brake\", \"cooling\"]\n",
"SYMP = {s: f\"G{i + 1:02d}\" for i, s in enumerate(SYS)} # ๊ณํต ์ฆ์ ์ฝ๋\n",
"CODES = sorted(set(SPEC.values()) | set(SHARED.values()) | set(SYMP.values()))\n",
"CASCADE = {\"P01\": [(\"P10\", 0.20)], \"P02\": [(\"P06\", 0.20)], \"P03\": [(\"P04\", 0.25)], \"P04\": [(\"P09\", 0.25)],\n",
" \"P05\": [(\"P08\", 0.30)], \"P06\": [(\"P16\", 0.30)], \"P09\": [(\"P14\", 0.20)], \"P12\": [(\"P07\", 0.20)]}\n",
"CRIT_SYS = {\"engine\", \"drive\", \"brake\"} # ๊ธฐ๋ยท์ ๋ ๋ถ๋ฅ ์ํ ๊ณํต(๊ฒฝ๋ณด ๋์)\n",
"MONTHS = 18\n",
"\n",
"\n",
"def make_fleet(n=100, seed=0):\n",
" rng = np.random.default_rng(seed)\n",
" f = pd.DataFrame({\"veh\": [f\"V{i:03d}\" for i in range(n)], \"age\": rng.integers(1, 26, n).astype(float),\n",
" \"terrain\": rng.uniform(0, 1, n), \"load\": rng.uniform(0, 1, n), \"pm_gap\": rng.uniform(20, 200, n),\n",
" \"humid\": rng.uniform(0, 1, n), \"base_h\": rng.uniform(55, 125, n)})\n",
" f[\"mult\"] = np.exp(0.045 * (f[\"age\"] - 13) + 0.9 * (f[\"terrain\"] - .5) + 0.8 * (f[\"load\"] - .5)\n",
" + 0.006 * (f[\"pm_gap\"] - 110) + 0.7 * (f[\"humid\"] - .5))\n",
" return f\n",
"\n",
"\n",
"def make_data(seed=1, months=MONTHS):\n",
" \"\"\"๋ฐํ: fleet(์ฐจ๋ ์์ฑ), ev(์ ๋น ์ด๋ ฅ 1๊ฑด=1ํ: ๋ฐ์ DTC ๋ชฉ๋ก, ์ค์ ๊ณ ์ฅ ๋ถํ), log(DTC ๋ฐ์ ๋ก๊ทธ, ํ=์ฝ๋ 1๊ฑด)\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" fleet = make_fleet()\n",
" V = len(fleet)\n",
" temp = 12 + 14 * np.sin(2 * np.pi * (np.arange(months) - 3) / 12) # ์ํ๊ท ๊ธฐ์จ(๊ฐ์)\n",
" hours = fleet[\"base_h\"].values[:, None] * (1 + 0.25 * np.sin(2 * np.pi * np.arange(months) / 12 + 1)) * rng.lognormal(0, .12, (V, months))\n",
" fleet[\"hours\"] = hours.sum(axis=1) # ์ฐจ๋๋ณ 18๊ฐ์ ์ด ๊ฐ๋์๊ฐ\n",
" fleet.attrs[\"month_h\"] = hours.sum(axis=0) # ์๋ณ ์ ์ฒด ๊ฐ๋์๊ฐ(๊ณ ์ฅ๋ฅ ๋ถ๋ชจ)\n",
" evs = []\n",
" for p in PID:\n",
" s = np.ones(months)\n",
" if p in (\"P03\", \"P04\"): s = np.exp(-0.06 * (temp - 12)) # ํ๋ญ๊ธฐ ์ ๊ธฐ๊ณํต ์ฆ๊ฐ\n",
" if p in (\"P06\", \"P16\"): s = np.exp(0.05 * (temp - 12)) # ํน์๊ธฐ ๋๊ฐ๊ณํต ์ฆ๊ฐ\n",
" cnt = rng.poisson(RATE[p] * hours / 1000 * fleet[\"mult\"].values[:, None] * s[None, :])\n",
" for v, m in zip(*np.nonzero(cnt)):\n",
" for _ in range(cnt[v, m]):\n",
" evs.append((fleet[\"veh\"][v], int(m), m * 720 + rng.uniform(0, 720), p, float(temp[m] + rng.normal(0, 3))))\n",
" ev = pd.DataFrame(evs, columns=[\"veh\", \"month\", \"t\", \"part\", \"temp\"]).sort_values(\"t\").reset_index(drop=True)\n",
" ev[\"ev_id\"] = [f\"W{i:05d}\" for i in range(len(ev))]\n",
" rows, codes = [], []\n",
" for e in ev.itertuples():\n",
" c, r = [], rng.random(3)\n",
" if r[0] < 0.6: c.append(SPEC[e.part])\n",
" if e.part in SHARED and r[1] < 0.6: c.append(SHARED[e.part])\n",
" if r[2] < 0.5: c.append(SYMP[PSYS[e.part]])\n",
" if not c: c.append(SHARED.get(e.part, SPEC[e.part]))\n",
" for q, pr in CASCADE.get(e.part, []):\n",
" if rng.random() < pr: c.append(SPEC[q])\n",
" if rng.random() < 0.12: c.append(SPEC[PID[rng.integers(0, 16)]])\n",
" c = list(dict.fromkeys(c))\n",
" rng.shuffle(c) # ๋์ ๋ฐ์ ์ฝ๋์ ๊ธฐ๋ก ์์๋ ์์\n",
" codes.append(c)\n",
" for k, cd in enumerate(c):\n",
" rows.append((e.ev_id, e.veh, e.t + k * 0.1, cd))\n",
" ev[\"codes\"] = codes\n",
" log = pd.DataFrame(rows, columns=[\"ev_id\", \"veh\", \"ts\", \"code\"])\n",
" return fleet, ev, log\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# ๊ณตํต ๋ถ์ ์กฐ๊ฐ: ์ฝ๋ ๋ฉํฐํซ ํ๋ ฌ, ์ฐจ๋ ๋งฅ๋ฝ ํผ์ฒ, ํ์ต/๊ฒ์ฆ ๋ถํ (์ 12๊ฐ์ ํ์ต, ๋ค 6๊ฐ์ ๊ฒ์ฆ)\n",
"# ---------------------------------------------------------------------------\n",
"CIDX = {c: i for i, c in enumerate(CODES)}\n",
"SPLIT_M = 12\n",
"\n",
"\n",
"def multi_hot(ev):\n",
" X = np.zeros((len(ev), len(CODES)))\n",
" for r, cs in enumerate(ev[\"codes\"]):\n",
" for c in cs:\n",
" X[r, CIDX[c]] = 1\n",
" return X\n",
"\n",
"\n",
"def context(ev, fleet):\n",
" f = fleet.set_index(\"veh\")\n",
" return np.column_stack([ev[\"veh\"].map(f[\"age\"]) / 25, ev[\"veh\"].map(f[\"pm_gap\"]) / 200, ev[\"veh\"].map(f[\"terrain\"]),\n",
" ev[\"veh\"].map(f[\"load\"]), (ev[\"temp\"] - 12) / 15])\n",
"\n",
"\n",
"def feats(ev, fleet):\n",
" X = multi_hot(ev)\n",
" n = X.sum(axis=1, keepdims=True) / 3\n",
" return np.hstack([X, n, context(ev, fleet)])\n",
"\n",
"\n",
"def monthly_counts(ev):\n",
" return ev.groupby([\"month\", \"part\"]).size().unstack(fill_value=0).reindex(columns=PID, fill_value=0)\n",
"\n",
"\n",
"def build_monthly(ev, fleet, top_n=8):\n",
" \"\"\"๋ถํ๋ณ ์๊ฐ ๊ณ ์ฅ ๊ฑด์ ์์ธก์ฉ ํ: ์ง์ฐ๊ฐ(lag), ์ด๋ํ๊ท , ์ถ์ธ, ๊ธฐ๋ ๊ฑด์(๊ฐ๋์๊ฐ x ์ค๊ณ ๊ณ ์ฅ๋ฅ ), ๊ธฐ์จ ์ํธ์์ฉ, ๊ณ์ ์ฑ๋ถ\"\"\"\n",
" mc = ev.groupby([\"month\", \"part\"]).size().unstack(fill_value=0)\n",
" top = ev[\"part\"].value_counts().index[:top_n]\n",
" mh, tm = fleet.attrs[\"month_h\"], ev.groupby(\"month\")[\"temp\"].mean().values\n",
" rows = []\n",
" for p in top:\n",
" c = mc[p].values.astype(float)\n",
" for m in range(3, MONTHS):\n",
" L = np.log1p(c)\n",
" rows.append({\"part\": p, \"month\": m, \"y\": c[m], \"lag1\": L[m - 1], \"lag2\": L[m - 2], \"lag3\": L[m - 3], \"roll3\": L[m - 3:m].mean(),\n",
" \"trend\": L[m - 1] - L[m - 2], \"roll3_std\": L[m - 3:m].std(), \"log_expected\": np.log(mh[m] / 1000 * RATE[p]),\n",
" \"rate_lag1\": c[m - 1] / (mh[m - 1] / 1000) / RATE[p], \"temp\": tm[m] / 10, \"cold_elec\": max(0, 12 - tm[m]) / 10 * (p in (\"P03\", \"P04\")),\n",
" \"heat_cool\": max(0, tm[m] - 12) / 10 * (p in (\"P06\", \"P16\")), \"sin\": np.sin(2 * np.pi * m / 12), \"cos\": np.cos(2 * np.pi * m / 12)})\n",
" D = pd.DataFrame(rows)\n",
" return D, [c for c in D.columns if c not in (\"part\", \"month\", \"y\")], top\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ถํ๋ณ ์ ๋น ์กฐ์น ๊ฐ์ด๋๋ผ์ธ(์์ฝ 3๋จ๊ณ). ์ค์ ์ ๋น ๊ต๋ฒ์ด ์๋๋ผ ๊ต์ก์ฉ์ผ๋ก ๋ง๋ ์์ ๋ฌธ๊ตฌ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"GUIDE = {\n",
" \"P01\": \"์ฐ๋ฃ ์๋ ฅ ์ธก์ -> ๋ถ์ฌ๊ธฐ ๋์คยท๋งํ ํ์ธ -> ๋ถ๋ ์ ๋ถ์ฌ๊ธฐ ๊ต์ฒด\", \"P02\": \"ํก๊ธฐ ์๋ ฅยท์ด๋ฌผ ์ ๊ฒ -> ์ถ ์ ๊ฒฉ ํ์ธ -> ๋ถ๋ ์ ํฐ๋ณด์ฐจ์ ๊ต์ฒด\",\n",
" \"P03\": \"๋จ์ ์ ์ยท๋ถ์ ์ ๊ฒ -> ์ถฉ์ ๊ณํต ํ์ธ -> ๋ถ๋ ์ ์ถ์ ์ง ๊ต์ฒด\", \"P04\": \"๋ฐ์ ์ ์ ์ธก์ -> ๋ฒจํธยท๋ฐฐ์ ์ ๊ฒ -> ๋ถ๋ ์ ๋ฐ์ ๊ธฐ ๊ต์ฒด\",\n",
" \"P05\": \"์ ์ด๋ชจ๋ ์ํํธ์จ์ดยท์ปค๋ฅํฐ ์ ๊ฒ -> ๋ณ์ ์ด๋ ฅ ํ์ธ -> ๋ถ๋ ์ ๋ชจ๋ ๊ต์ฒด\", \"P06\": \"๋๊ฐ์๋ยท๋์ ์ ๊ฒ -> ํํ ์๋์ ํ์ธ -> ๋ถ๋ ์ ํํ ๊ต์ฒด\",\n",
" \"P07\": \"์ ์ ์ค์ผ๋ยท์๋ ฅ ์ ๊ฒ -> ํํ ๋์ค ํ์ธ -> ๋ถ๋ ์ ํํ ๊ต์ฒด\", \"P08\": \"์ผ์ ๋ฐฐ์ ยท๊ฐ๊ทน ์ ๊ฒ -> ์ ํธ ํํ ํ์ธ -> ๋ถ๋ ์ ์ผ์ ๊ต์ฒด\",\n",
" \"P09\": \"์ปค๋ฅํฐ ์ ์ดยท๋ถ์ ์ ๊ฒ -> ํ ์ํ ํ์ธ -> ๋ถ๋ ์ ์ปค๋ฅํฐ ๊ต์ฒด\", \"P10\": \"ํํฐ ์ค์ผ๋ ์ ๊ฒ -> ์ฐ๋ฃ ํ๋ฆ ํ์ธ -> ๋ถ๋ ์ ํํฐ ๊ต์ฒด\",\n",
" \"P11\": \"์ ๋ ์๋ ฅ ์ ๊ฒ -> ๋ฐธ๋ธ ์๋ ํ์ธ -> ๋ถ๋ ์ ๋ฐธ๋ธ ๊ต์ฒด(์์ ํ์ธ ํ์)\", \"P12\": \"์กฐํฅ ๋ฐ์ ์ ๊ฒ -> ์ก์ถ์์ดํฐ ์๋ ํ์ธ -> ๋ถ๋ ์ ๊ต์ฒด(์์ ํ์ธ ํ์)\",\n",
" \"P13\": \"ํํฐ ์ฐจ์ ์ ๊ฒ -> ์ค์ผ ํ์ธ -> ๋ถ๋ ์ ํํฐ ๊ต์ฒด\", \"P14\": \"ํต์ ๋งํฌ ์ํ ์ ๊ฒ -> ๋ชจ๋ ์ ์ ํ์ธ -> ๋ถ๋ ์ ๋ชจ๋ ๊ต์ฒด\",\n",
" \"P15\": \"์๋ ฅ ์ผ์ ์ ํธ ์ ๊ฒ -> ์คํ์ ๋ ํ์ธ -> ๋ถ๋ ์ ์ผ์ ๊ต์ฒด\", \"P16\": \"ํฌ ๋ชจํฐ ์ ๋ฅ ์ ๊ฒ -> ํ์ ํ์ธ -> ๋ถ๋ ์ ๋ชจํฐ ๊ต์ฒด\"}\n",
"\n",
"CONFIG = {\"conf_review\": 0.8, \"retrain_tolerance\": 0.01, \"mape_target\": 0.25}\n",
"OPS_GUIDE = { # docs/mnt03_operations_guide.md ์์ฝ\n",
" \"1. ๋ชฉ์ ๊ณผ ๋ฒ์\": \"DTC ๊ธฐ๋ฐ ๊ณ ์ฅ ๋ถํ ์ง๋จ ๋ณด์กฐ์ ๋ค๋น๋ ๊ณ ์ฅํ๋ชฉ ์๋ณ. ์ต์ข
์ ๋น ํ๋จ์ ์๋ จ ์ ๋น๊ด.\",\n",
" \"2. ์
๋ ฅ ๋ฐ์ดํฐ\": \"DTC ๋ก๊ทธ(์ฐจ๋ ID, ์๊ฐ, ์ฝ๋), ์ ๋น ์ด๋ ฅ(์ ๋น ๊ฑด ID, ๊ณ ์ฅ ๋ถํ), ์ฐจ๋ ์์ฑ. ์ค์ ๋ถ๋ ์๋ฃ ์ธ๋ถ AI ์
๋ ฅ ๊ธ์ง.\",\n",
" \"3. ์ผ์ผ ์ด์ฉ ์ ์ฐจ\": \"๋ก๊ทธ ๊ฒ์ฆ -> ์ ๋น ๊ฑด ๋ฌถ์ -> ์ง๋จยท์ ๋ขฐ๋ ์ฐ์ถ -> ๋ฎ์ ์ ๋ขฐ๋ ๊ฑด ์น์ธ ๋๊ธฐ -> ์กฐ์น ํ ์ ๋น ์ด๋ ฅ ์
๋ ฅ.\",\n",
" \"4. ์น์ธ(HITL) ๊ธฐ์ค\": \"์ ๋ขฐ๋ 0.8 ๋ฏธ๋งยท๋ค๊ณํต ๋์ ๋ฐ์์ ์๋ จ ์ ๋น๊ด ๊ฒฐ์ ์ ํ์ ๊ธ์ง, ๋ฌด์๋ต ์ ์ฌํ์ธ ํ์, ๊ฒฐ์ ์ฌ์ ๊ธฐ๋ก.\",\n",
" \"5. ์ฌํ์ต ์ ์ฐจ\": \"์ 1ํ ์ ๊ท ์ ๋น ์ด๋ ฅ์ผ๋ก ๋์ ์ ํ์ต, ๊ฒ์ฆ ์ ํ๋๊ฐ ๋๋น ์ง์ง ์์ ๋๋ง ๊ต์ฒด.\",\n",
" \"6. ์ฅ์ ๋์\": \"๋ชจ๋ธ ์ค๋ฅ ์ ๋จ์ผ ์ฝ๋ ๋งคํํ๋ก ๋์ฒด, ์์ ๋ก๊ทธ๋ ์๋ ํ์ธ ๋ชฉ๋ก์ผ๋ก ๋ถ๋ฆฌ.\",\n",
" \"7. ์ฐ๋ ์ธํฐํ์ด์ค\": \"๊ตญ๋ฐฉ ๊ตฐ์ ์ ๋ณด์ฒด๊ณ์ DTCยท์ ๋น ์ด๋ ฅ ํ์ค ํ๋(์ฅ๋น์ฝ๋ยท๋ถ๋ฅ๊ตฐยท์๊ฐ)๋ฅผ CSV/API๋ก ๊ตํํ๋ ์์ ์ฐจ๊ธฐ ๊ณผ์ ๋ก ๋๋ค.\",\n",
"}\n",
"\n",
"fleet, ev, log = make_data()\n",
"OLD, NEW, OPS = ev[ev[\"month\"] < 12], ev[(ev[\"month\"] >= 12) & (ev[\"month\"] < 15)], ev[ev[\"month\"] >= 15].reset_index(drop=True)\n",
"t_start = time.perf_counter()\n",
"\n",
"\n",
"def fit(d):\n",
" return LogisticRegression(C=3, max_iter=500).fit(feats(d, fleet), d[\"part\"])\n",
"\n",
"\n",
"def retrain(champion, old, new):\n",
" \"\"\"๋์ ์๋ฅผ (๊ธฐ์กด + ์ ๊ท ์ 2/3 ๊ธฐ๊ฐ)์ผ๋ก ํ์ตํ๊ณ , ์ ๊ท ๋ค 1/3 ๊ธฐ๊ฐ์์ ์ฑํผ์ธ๊ณผ ์ ํ๋๋ฅผ ๋น๊ตํด ๋น์ทํ๊ฑฐ๋ ๋ซ๋ค๋ฉด ๊ต์ฒด.\"\"\"\n",
" cut = new[\"month\"].min() + 2\n",
" hold = new[new[\"month\"] >= cut]\n",
" challenger = fit(pd.concat([old, new[new[\"month\"] < cut]], ignore_index=True))\n",
" a_old = (champion.predict(feats(hold, fleet)) == hold[\"part\"].values).mean()\n",
" a_new = (challenger.predict(feats(hold, fleet)) == hold[\"part\"].values).mean()\n",
" swap = a_new >= a_old - CONFIG[\"retrain_tolerance\"]\n",
" return (challenger if swap else champion), {\"์ฑํผ์ธ\": round(float(a_old), 3), \"๋์ ์\": round(float(a_new), 3), \"๊ต์ฒด\": bool(swap)}\n",
"\n",
"\n",
"def validate(df):\n",
" ok = df[\"codes\"].map(lambda cs: len(cs) > 0 and all(c in CODES for c in cs)) & df[\"veh\"].isin(fleet[\"veh\"])\n",
" return df[ok].copy(), df[~ok].copy()\n",
"\n",
"\n",
"def predict_safe(model, d):\n",
" \"\"\"๋ชจ๋ธ ์ค๋ฅ ์ ๋จ์ผ ์ฝ๋ -> ์ต๋ค ๋ถํ ๋งคํํ๋ก ๋์ฒดํ๋ค.\"\"\"\n",
" try:\n",
" P = model.predict_proba(feats(d, fleet))\n",
" return model.classes_[P.argmax(axis=1)], P.max(axis=1), \"๋ชจ๋ธ\"\n",
" except Exception:\n",
" mp = OLD.assign(first=OLD[\"codes\"].str[0]).groupby(\"first\")[\"part\"].agg(lambda s: s.value_counts().index[0])\n",
" return d[\"codes\"].str[0].map(mp).fillna(\"P01\").values, np.full(len(d), 0.5), \"๋งคํํ ๋์ฒด\"\n",
"\n",
"\n",
"# ---------------- 1. ํตํฉ ์คํ ----------------\n",
"champion = fit(OLD)\n",
"model, rt = retrain(champion, OLD, NEW)\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ฌํ์ต: {rt} -> {'๋์ ์๋ก ๊ต์ฒด' if rt['๊ต์ฒด'] else '์ฑํผ์ธ ์ ์ง'}\")\n",
"ops_all = OPS.copy()\n",
"ops_all.at[3, \"codes\"] = [\"ZZ9\"]; ops_all.at[40, \"codes\"] = [] # ์์ ๋ก๊ทธ 2๊ฑด ์ฃผ์
\n",
"ops, manual = validate(ops_all)\n",
"ops = ops.reset_index(drop=True)\n",
"pred, conf, src = predict_safe(model, ops)\n",
"queue = ops[conf < CONFIG[\"conf_review\"]]\n",
"alerts = [(i, p) for i, p in enumerate(pred) if PSYS[p] in CRIT_SYS and conf[i] >= CONFIG[\"conf_review\"]]\n",
"acc = (pred == ops[\"part\"].values).mean()\n",
"hi = conf >= CONFIG[\"conf_review\"]\n",
"print(f\"์ด์ฉ ์ ๋น ๊ฑด {len(ops_all)}๊ฑด: ์ง๋จ {len(ops)}๊ฑด, ์๋ ํ์ธ {len(manual)}๊ฑด, ์ถ์ฒ '{src}', ์ ํ๋ {acc:.1%} (์ ๋ขฐ๋ 0.8 ์ด์ {hi.sum()}๊ฑด์ {(pred[hi] == ops['part'].values[hi]).mean():.1%})\")\n",
"\n",
"# ๋ค๋น๋ ํ๋ชฉ ์๊ฐ ์์ ์์ธก(๋ง์ง๋ง ๋ฌ): ๊ณผ๊ฑฐ ์๋ก ํ์ตํ ๋ฆฟ์ง๋ก ๋ค์ ๋ฌ ๊ฑด์ ์์ธก\n",
"D, FE, top = build_monthly(ev, fleet)\n",
"a, b = D[D[\"month\"] < MONTHS - 1], D[D[\"month\"] == MONTHS - 1]\n",
"fc = np.exp(make_pipeline(StandardScaler(), Ridge(alpha=3.0)).fit(a[FE], np.log(a[\"y\"])).predict(b[FE]))\n",
"fmape = float(np.mean(np.abs(b[\"y\"].values - fc) / b[\"y\"].values))\n",
"print(\"๋ค๋น๋ ํ๋ชฉ ๋ค์ ๋ฌ ์์ ์์ธก: \" + \", \".join(f\"{PNAME[p]} {f:.0f}๊ฑด(์ค์ {y:.0f})\" for p, f, y in zip(b[\"part\"].values[:3], fc[:3], b[\"y\"].values[:3])) + f\" / 8๊ฐ ํ๋ชฉ MAPE {fmape:.1%}\")\n",
"sec = time.perf_counter() - t_start\n",
"\n",
"# ---------------- 2. ์์ฒด ์ ๊ฒ ----------------\n",
"_, _, bad_src = predict_safe(None, ops.head(3))\n",
"checks = {\n",
" \"๋ก๊ทธ ์ ์ค 0๊ฑด\": len(ops) + len(manual) == len(ops_all),\n",
" \"์์ ๋ก๊ทธ ์๋ ํ์ธ ๋ถ๋ฆฌ\": len(manual) == 2,\n",
" \"์ฌํ์ต ํ์ดํ๋ผ์ธ ์คํ\": \"๊ต์ฒด\" in rt,\n",
" \"์ ๋ขฐ๋ 0~1 ๋ฒ์\": bool(((conf >= 0) & (conf <= 1)).all()),\n",
" \"๋ชจ๋ธ ์ฅ์ ์ ๋งคํํ ๋์ฒด\": bad_src == \"๋งคํํ ๋์ฒด\",\n",
" \"๋ชจ๋ ์ง๋จ์ ์กฐ์น ๊ฐ์ด๋ ์ฐ๊ฒฐ\": all(p in GUIDE for p in pred),\n",
" \"์ ์ ๋ขฐ ๊ฑด ์๋ ํ์ ๊ธ์ง(๋๊ธฐ)\": len(queue) == int((conf < CONFIG[\"conf_review\"]).sum()),\n",
" f\"์์ ์์ธก MAPE {CONFIG['mape_target']:.0%} ์ดํ\": fmape <= CONFIG[\"mape_target\"],\n",
"}\n",
"print(f\"\\n์์ฒด ์ ๊ฒ {sum(checks.values())}/{len(checks)} ํต๊ณผ, ์๋ ์ฌํ์ต ํ์ดํ๋ผ์ธ {'์ ์ ์คํ' if checks['์ฌํ์ต ํ์ดํ๋ผ์ธ ์คํ'] else '์คํจ'}, ์ ์ฒด ์คํ {sec:.1f}์ด\")\n",
"for k, v in checks.items():\n",
" if not v:\n",
" print(\" ์คํจ:\", k)\n",
"doc = sum(bool(v.strip()) for v in OPS_GUIDE.values()) / len(OPS_GUIDE)\n",
"print(f\"์ด์ ๋ฌธ์ {len(OPS_GUIDE)}๊ฐ ์ฅ ์์ฑ๋ฅ {doc:.0%} (๋ชฉํ 100%: {'๋ฌ์ฑ' if doc == 1 else '๋ฏธ๋ฌ'}) - \" + \", \".join(k.split(\". \")[1] for k in OPS_GUIDE))\n",
"\n",
"# ---------------- 3. ํ๋ฉด๊ณผ ๋ธ๋ฆฌํ ----------------\n",
"fig, ax = plt.subplots(1, 2, figsize=(11, 4))\n",
"cnt = pd.Series(pred).value_counts().head(8)\n",
"ax[0].bar(cnt.index, cnt.values); ax[0].set_title(\"Diagnosed parts (operating period)\")\n",
"ax[1].hist(conf, bins=20); ax[1].axvline(CONFIG[\"conf_review\"], ls=\"--\", color=\"k\"); ax[1].set_title(\"Confidence (line = review threshold)\")\n",
"plt.tight_layout(); plt.show()\n",
"\n",
"fr = (ev[\"part\"].value_counts() / fleet[\"hours\"].sum() * 1000)\n",
"cum = ev[\"part\"].value_counts(normalize=True).cumsum()\n",
"a_cnt = int((cum <= 0.8).sum()) + 1\n",
"print(\"\\n[์ ๋น ๋ด๋น ์ฅ๊ต ๋ธ๋ฆฌํ ์์ฝ] [๊ต์ก์ฉ๊ฐ์์๋ฃ]\")\n",
"print(f\"1. 18๊ฐ์ ์ ๋น {len(ev)}๊ฑด: ๊ณ ์ฅ๋ฅ ์์ \" + \", \".join(f\"{PNAME[p]} {v:.1f}๊ฑด/1000h\" for p, v in fr.sort_values(ascending=False).head(3).items()))\n",
"print(f\"2. ์์ {a_cnt}๊ฐ ํ๋ชฉ์ด ์ ์ฒด ๊ณ ์ฅ์ 80% ์ด์: ์๋น ์๋ฆฌ๋ถ์ ์ฐ์ ํ๋ณด ๋์\")\n",
"print(f\"3. ์ด์ฉ ๊ธฐ๊ฐ ์ง๋จ {len(ops)}๊ฑด ์ค ์ ๋ขฐ๋ 0.8 ์ด์ {int(hi.sum())}๊ฑด(์ ํ๋ {(pred[hi] == ops['part'].values[hi]).mean():.1%}), ์ ๋น๊ด ์น์ธ ๋๊ธฐ {len(queue)}๊ฑด(์๋ ํ์ 0๊ฑด)\")\n",
"print(f\"4. ๊ธฐ๋ยท์ ๋ ๊ณํต ๊ฒฝ๋ณด {len(alerts)}๊ฑด, ์์ ๋ก๊ทธ {len(manual)}๊ฑด ์๋ ํ์ธ\")\n",
"print(f\"5. ์ง๋จ ๋ชจ๋ธ: {'์ ๊ท ์ ๋น ์ด๋ ฅ์ผ๋ก ๊ต์ฒด' if rt['๊ต์ฒด'] else '๊ธฐ์กด ์ ์ง'}, ์์ ์์ธก MAPE {fmape:.1%}\")\n",
"print(\"6. ๊ฑด์: ๋ค๋น๋ ํ๋ชฉ ์๋ฐฉ์ ๋น ์ฃผ๊ธฐ ์ฌ๊ฒํ , ๊ตญ๋ฐฉ ๊ตฐ์ ์ ๋ณด์ฒด๊ณ์ DTCยท์ ๋น ์ด๋ ฅ ํ์ค ํ๋ ์ฐ๋ ์ธํฐํ์ด์ค ์ ์\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
10 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ฌํ์ต: {'์ฑํผ์ธ': 0.845, '๋์ ์': 0.847, '๊ต์ฒด': True} -> ๋์ ์๋ก ๊ต์ฒด\n",
"์ด์ฉ ์ ๋น ๊ฑด 1554๊ฑด: ์ง๋จ 1552๊ฑด, ์๋ ํ์ธ 2๊ฑด, ์ถ์ฒ '๋ชจ๋ธ', ์ ํ๋ 83.2% (์ ๋ขฐ๋ 0.8 ์ด์ 931๊ฑด์ 99.2%)\n",
"๋ค๋น๋ ํ๋ชฉ ๋ค์ ๋ฌ ์์ ์์ธก: ์ฐ๋ฃ๋ถ์ฌ๊ธฐ 133๊ฑด(์ค์ 147), ํฐ๋ณด์ฐจ์ 97๊ฑด(์ค์ 109), ์ถ์ ์ง 44๊ฑด(์ค์ 35) / 8๊ฐ ํ๋ชฉ MAPE 22.7%\n",
"\n",
"์์ฒด ์ ๊ฒ 8/8 ํต๊ณผ, ์๋ ์ฌํ์ต ํ์ดํ๋ผ์ธ ์ ์ ์คํ, ์ ์ฒด ์คํ 18.1์ด\n",
"์ด์ ๋ฌธ์ 7๊ฐ ์ฅ ์์ฑ๋ฅ 100% (๋ชฉํ 100%: ๋ฌ์ฑ) - ๋ชฉ์ ๊ณผ ๋ฒ์, ์
๋ ฅ ๋ฐ์ดํฐ, ์ผ์ผ ์ด์ฉ ์ ์ฐจ, ์น์ธ(HITL) ๊ธฐ์ค, ์ฌํ์ต ์ ์ฐจ, ์ฅ์ ๋์, ์ฐ๋ ์ธํฐํ์ด์ค\n",
"\n",
"[์ ๋น ๋ด๋น ์ฅ๊ต ๋ธ๋ฆฌํ ์์ฝ] [๊ต์ก์ฉ๊ฐ์์๋ฃ]\n",
"1. 18๊ฐ์ ์ ๋น 10681๊ฑด: ๊ณ ์ฅ๋ฅ ์์ ์ฐ๋ฃ๋ถ์ฌ๊ธฐ 16.3๊ฑด/1000h, ํฐ๋ณด์ฐจ์ 11.6๊ฑด/1000h, ์ถ์ ์ง 11.6๊ฑด/1000h\n",
"2. ์์ 5๊ฐ ํ๋ชฉ์ด ์ ์ฒด ๊ณ ์ฅ์ 80% ์ด์: ์๋น ์๋ฆฌ๋ถ์ ์ฐ์ ํ๋ณด ๋์\n",
"3. ์ด์ฉ ๊ธฐ๊ฐ ์ง๋จ 1552๊ฑด ์ค ์ ๋ขฐ๋ 0.8 ์ด์ 931๊ฑด(์ ํ๋ 99.2%), ์ ๋น๊ด ์น์ธ ๋๊ธฐ 621๊ฑด(์๋ ํ์ 0๊ฑด)\n",
"4. ๊ธฐ๋ยท์ ๋ ๊ณํต ๊ฒฝ๋ณด 710๊ฑด, ์์ ๋ก๊ทธ 2๊ฑด ์๋ ํ์ธ\n",
"5. ์ง๋จ ๋ชจ๋ธ: ์ ๊ท ์ ๋น ์ด๋ ฅ์ผ๋ก ๊ต์ฒด, ์์ ์์ธก MAPE 22.7%\n",
"6. ๊ฑด์: ๋ค๋น๋ ํ๋ชฉ ์๋ฐฉ์ ๋น ์ฃผ๊ธฐ ์ฌ๊ฒํ , ๊ตญ๋ฐฉ ๊ตฐ์ ์ ๋ณด์ฒด๊ณ์ DTCยท์ ๋น ์ด๋ ฅ ํ์ค ํ๋ ์ฐ๋ ์ธํฐํ์ด์ค ์ ์\n",
"```\n",
" \n"
]
}
],
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}