{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# ๐ฏ [SUP02] ๊ตฐ์ํ ์ํจ์ฑ ํ๋ชฉ ์ฌ๊ณ ํต์ ยท์์ฌ์ผ ๊ด๋ฆฌ\n",
"### Google Colab ์ค์ต ํ๊ฒฝ (๋ณด๊ธยท์ฌ์ฐยท์ํจ๊ด๋ฆฌ / Level 2)\n",
"\n",
"[](https://colab.research.google.com/github/edunlab/milicontent2/blob/main/uploads/SUP02/SUP02_solution.ipynb)\n",
"\n",
"---\n",
"\n",
"## ๐ 1. ํ๋ก์ ํธ ๊ฐ์ ๋ฐ ๋ฐฐ๊ฒฝ\n",
"\n",
"| ํญ๋ชฉ | ๋ด์ฉ |\n",
"| :--- | :--- |\n",
"| **๊ณผ์ ๋ถ๋ฅ** | ๋ณด๊ธยท์ฌ์ฐยท์ํจ๊ด๋ฆฌ (Level 2) |\n",
"| **ํ์ต ์์ ์๊ฐ** | 10๊ฐ ๋ฏธ์
(5์ผ / ์ด 28.5์๊ฐ) |\n",
"| **ํ๊ฒฝ ์ฌ์** | Python 3.10+, numpy, pandas, matplotlib, scipy, scikit-learn |\n",
"\n",
"### ๐ ํ๋ก์ ํธ ๋ชฉํ\n",
"๊ตฐ์ํ ์ค์๋ ์ ๋น ์ฃผ๊ธฐ์ ์ฌ์ฉ ๊ฐ๋ฅ ๊ธฐํ์ด ์ ํด์ ธ ์์ด ์ํจ๋ฅผ ๋๊ธฐ๊ฑฐ๋ ์ ๋น ์์ ์ ๋์น๋ฉด ์์ค์ด ์๊ธฐ๋ ํ๋ชฉ์ด ๋ง๋ค. ์ด ๊ณผ์ ๋ ์ํจ์ฑ ํ๋ชฉ์ ์์ฌ์ผ ๊ด๋ฆฌ ๋ฌธ์ ๋ฅผ ์๊ฐ ํ๋๊ฐ ์๋ ์ฅ๋นยท๋ถํ์ ์์ฌ ์๋ช
(RUL) ๊ด๋ฆฌ์ ์ํจ์ฑ ๋ถํ ํคํธ์ ์ ์
์ ์ถ(FEFO) ์ฌ๊ณ ํต์ ๋ก ์ฐ๊ฒฐํด ๋ค๋ฃฌ๋ค. ๊ฐ์ ์ฅ๋น ๋ณด์ ํํฉ์ ์ด์ฉ ์๊ฐ, ํ๊ฒฝ, ์ ๋น ์ด๋ ฅ์ผ๋ก ์์กด ๋ถ์ ๊ธฐ๋ฐ ์๋ช
๋ชจ๋ธ์ ๋ง๋ค๊ณ , ์ ๋น์ฐฝ ๋ฅ๋ ฅยท์์ฐยทํคํธ ์ ํจ๊ธฐ๊ฐ ์ ์ฝ ์๋ 12๊ฐ์ ๊ณํ์ ๋น ์ผ์ ์ ์ถ์ฒํ๋ค. ์ผ์ ํ์ ๊ณผ ๋ํ ๊ฑด์๋ ๊ตฐ์์งํ๊ด ์น์ธ ์ ์ฐจ๋ก ๋๊ณ , ๋๊ท๋ชจ ๋ถํ ์ํ, AAR ์ฌ๋ณด์ , ์๋ ์ฌํ์ต ํ์ดํ๋ผ์ธ๊น์ง ์์ฑํด ์์ค์ ์ค์ด๋ ์ฌ๊ณ ยท์ ๋น ํต์ ๋๊ตฌ๋ฅผ ๋ง๋ ๋ค.\n",
"\n",
"> **โ ๏ธ [๊ต์ก์ฉ ๊ฐ์ ๋ฐ์ดํฐ์
์์น]**\n",
"> - ๋ณธ ๋
ธํธ๋ถ์ ๋ชจ๋ ๋ฐ์ดํฐ(์ ์, ๋ก๊ทธ, ์๋๋ฆฌ์ค ๋ฑ)๋ ๋ณด์ ๊ท์ ์ ์ค์ํ์ฌ **์ฝ๋ ๋ด ๋์ ๊ธฐ๋ฐ ๊ฐ์ ๋ฐ์ดํฐ๋ก ์์ฑ**๋ฉ๋๋ค.\n",
"> - ์ค์ ๋ถ๋ ์์น, ์์ ์ธ๊ฐ, ์ค์ ์ ์ฅ ์ ๋ณด, ๊ตฐ์ฌ ๊ธฐ๋ฐ ๋ฐ์ดํฐ๋ ์ผ์ฒด ํฌํจ๋์ง ์์ต๋๋ค.\n",
"\n",
"---\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# ============================================================================\n",
"# [Google Colab ํ๊ฒฝ ์ค์ ] ํ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค์น ๋ฐ ์ํฌํธ\n",
"# ============================================================================\n",
"!pip install -q numpy pandas matplotlib scipy scikit-learn\n",
"\n",
"import os\n",
"import sys\n",
"import time\n",
"import math\n",
"import json\n",
"import re\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"import warnings\n",
"warnings.filterwarnings('ignore')\n",
"\n",
"# ๊ทธ๋ํ ์๋ฌธ ํฐํธ ๊ธฐ๋ณธ ์ค์ (ํ๊ฒฝ ๋
๋ฆฝ์ฑ ๋ณด์ฅ)\n",
"plt.rcParams['font.sans-serif'] = 'DejaVu Sans'\n",
"plt.rcParams['axes.unicode_minus'] = False\n",
"\n",
"print(f\"์คํ ํ๊ฒฝ ์ค๋น ์๋ฃ (Python {sys.version.split()[0]})\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
01. ์ฅ๋น ์ด์ฉ ์๊ฐ, ๊ธฐ๋ ๊ฑฐ๋ฆฌ, ์ ๋น ์ด๋ ฅ ๋ฐ ์ฐฝ์ ๋น ์ฃผ๊ธฐ ๋ฐ์ดํฐ์
์คํค๋ง ์ค๊ณ\n",
"**์์์๊ฐ**: ์ฝ 1.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ฅ๋น ์ด์ฉ ์๊ฐ, ๊ธฐ๋ ๊ฑฐ๋ฆฌ, ์ ๋น ์ด๋ ฅ ๋ฐ ์ฐฝ์ ๋น ์ฃผ๊ธฐ ๋ฐ์ดํฐ์
์ ์ค๊ณํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๊ฐ์ ์ฅ๋น ๋ณด์ ํํฉ์ ์ด์ฉ ์๊ฐ, ๊ธฐ๋ ๊ฑฐ๋ฆฌ, ์ ๋น ์ด๋ ฅ, ์ฐฝ์ ๋น ์ฃผ๊ธฐ ๋ฐ์ดํฐ์
์คํค๋ง๋ฅผ ์ค๊ณํ๊ณ , ์ํจ์ฑ ๋ถํ ํคํธ ๋กํธ ์คํค๋ง๋ฅผ ํจ๊ป ์ ์ํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ฅ๋น ์๋ณ, ๋ถ๋, ์ด์ฉ ํ๊ฒฝ, ๋์ ์ด์ฉ์๊ฐ, ์ฐฝ์ ๋น ํ ์ด์ฉ์๊ฐ, ๊ธฐ๋๊ฑฐ๋ฆฌ, ์ผ ๊ฐ๋๋ฅ , ๊ณํยท๋น๊ณํ ์ ๋น ํ์, ์ ๊ฒ ๋ง๋ชจ ๋น์จ๊ณผ ๊ณ ์ฅ ๋ผ๋ฒจ์ ํ๋ยทํ์ฉ๊ฐ์ ์ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `docs/sup02_logistics_domain_spec.md ๋ฐ ๋ฐ์ดํฐ ๋์
๋๋ฆฌ` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ตฐ์ ์ค๋ฌด ์
๋ฌด ํ๋ก์ธ์ค์์ ์ผ์น์ฑ ๋ฐ ๋ถ์ ๋ชฉํ์ ๋ช
ํ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์
๋ฌด ํ๋ก์ธ์ค ๋งต ์์ฑ ์๋ฃ, ํ์ ํผ์ฒ(Feature) ๋ชฉ๋ก 20๊ฐ ์ด์ ์ ์\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ๋๋ฉ์ธ ๋ถ์์ ๋ฐ ๋ฐ์ดํฐ ์๊ตฌ๋ช
์ธ์ ์น์ธ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ๊ตฐ์ ๋ด๋น ์ค๋ฌด์ ์ธํฐ๋ทฐ ๋ฐ ๊ธฐ์กด ์ ์ฐ์ฒด๊ณ ๋ฐ์ดํฐ ๊ตฌ์กฐ ๋์กฐ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์ฅ๋น ์ด์ฉ ์๊ฐยท๊ธฐ๋ ๊ฑฐ๋ฆฌยท์ ๋น ์ด๋ ฅยท์ฐฝ์ ๋น ์ฃผ๊ธฐ์ ์ํจ์ฑ ๋ถํ ํคํธ ๋กํธ์ ๋ฐ์ดํฐ์
์คํค๋ง๋ฅผ ์ค๊ณํ๊ณ ๋ฐ์ดํฐ ์ฌ์ (ํผ์ฒ 20๊ฐ ์ด์)์ ์ค์ ์์ฑ ๋ฐ์ดํฐ์ ๋์กฐํ๋ฉฐ, ํ์ฉ๊ฐ ๊ฒ์ฆ๊ธฐ์ ์ค๋ฅ 6์ข
์ ์ฃผ์
ํด ๊ฒ์ถํ๊ณ ์ฅ๋น ์๋ช
๊ด๋ฆฌ ์
๋ฌด ํ๋ก์ธ์ค ๋งต์ ๋ง๋ ๋ค.\"\"\"\n",
"import copy\n",
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ฅ๋น ๋ณด์ ํํฉยท๊ณ ์ฅ ์ด๋ ฅ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์ฅ๋นยท์์น ์์)\n",
"# ๊ฐ์ ๋ถ๋ 6๊ฐ(U1~U6)์ ์ฅ๋น๊ฐ ์ด์ฉ ํ๊ฒฝ(normal/rough/salt/cold)์ ๋ฐ๋ผ ๋ค๋ฅธ ์๋๋ก ๋
ธํํํ๋ค. ์ฐฝ์ ๋น ํ ์ด์ฉ์๊ฐ(h_since)์ด\n",
"# ์ฅ๋น๋ง๋ค ์จ์ ์๋ช
(t_fail)์ ๋์ผ๋ฉด ๊ณ ์ฅ(event=1) ์ด๋ ฅ, ๋์ง ์์์ผ๋ฉด ํ์ฌ๊น์ง ์์กด(event=0, ๊ด์ธก ์ค๋์ ๋จ)์ด๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"ENVS = [\"normal\", \"rough\", \"salt\", \"cold\"]\n",
"ENV_W = {\"normal\": 1.00, \"rough\": 1.35, \"salt\": 1.25, \"cold\": 1.12} # ํ๊ฒฝ ๊ฐ์ค์น(์จ์ ์ ๋ต, ๊ฐ์ ๊ฐ): ํด์๋ก ์๋ช
์ด ์งง๋ค\n",
"ENV_KO = {\"normal\": \"์ผ๋ฐ\", \"rough\": \"ํ์ง\", \"salt\": \"์ผ๋ถ\", \"cold\": \"ํนํ\"}\n",
"UNITS = [\"U1\", \"U2\", \"U3\", \"U4\", \"U5\", \"U6\"]\n",
"UNIT_ENV = {\"U1\": [.7, .1, .1, .1], \"U2\": [.2, .6, .1, .1], \"U3\": [.2, .1, .6, .1], \"U4\": [.3, .1, .1, .5], \"U5\": [.5, .3, .1, .1], \"U6\": [.4, .1, .4, .1]}\n",
"ETYPES = [\"truck\", \"generator\", \"trailer\", \"radio\"]\n",
"CYCLE = 8000.0 # ๊ท์ ์ฐฝ์ ๋น ์ฃผ๊ธฐ(์ด์ฉ์๊ฐ h, ๊ฐ์ ๊ฐ)\n",
"KSHAPE = 12.0 # ์๋ช
๋ถํฌ ์์ด๋ธ ํ์(๋ง๋ชจํ ๊ณ ์ฅ, ํด์๋ก ์๋ช
์ด ๋ชจ์ฌ ์๋ค)\n",
"INSP_NOISE = 0.025 # ์ ๋น ์ ๊ฒ ๋ง๋ชจ ๋น์จ ์ธก์ ์ค์ฐจ(๊ฐ์ ๊ฐ)\n",
"\n",
"\n",
"def make_fleet(n=900, seed=0, env_w=None, shift=1.0):\n",
" \"\"\"์ฅ๋น n๋. env_w๋ก ์จ์ ํ๊ฒฝ ๊ฐ์ค์น๋ฅผ ๋ฐ๊ฟ ์ด์ ๋ถํฌ ๋ณํ๋ฅผ ๋ชจ์ํ ์ ์๋ค. shift>1์ด๋ฉด ๊ฐ๋๋ฅ ์ด ๋์์ง๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" w = env_w or ENV_W\n",
" unit = rng.choice(UNITS, n)\n",
" env = np.array([rng.choice(ENVS, p=UNIT_ENV[u]) for u in unit])\n",
" et = rng.choice(ETYPES, n, p=[.4, .2, .2, .2])\n",
" rate = rng.lognormal(np.log(3.0 * shift), 0.25, n) # ์ผ ํ๊ท ์ด์ฉ์๊ฐ(h/์ผ)\n",
" h_since = rng.uniform(300, 11500, n) # ๋ง์ง๋ง ์ฐฝ์ ๋น ์ดํ ์ด์ฉ์๊ฐ\n",
" n_ovh = rng.integers(0, 3, n)\n",
" hours = h_since + n_ovh * rng.uniform(5000, 8000, n) # ๋์ ์ด์ฉ์๊ฐ\n",
" km = hours * rng.normal(17, 3, n) * np.where(env == \"rough\", 0.8, 1.0) # ๋์ ๊ธฐ๋๊ฑฐ๋ฆฌ(km)\n",
" age_y = np.clip(hours / (rate * 365) * rng.uniform(0.9, 1.15, n), 0.5, 25)\n",
" unpl = rng.poisson(0.35 * h_since / 1000 * np.array([w[e] for e in env]) / 1.3) # ๋น๊ณํ ์ ๋น ๊ฑด์(ํ๊ฒฝ์ด ๊ฐํนํ ์๋ก ๋ง๋ค)\n",
" n_maint = rng.poisson(0.6 * h_since / 1000 + 1) # ๊ณํ ์ ๋น ๊ฑด์\n",
" scale = 9600 / np.array([w[e] for e in env]) * 0.985 ** unpl * np.exp(rng.normal(0, 0.04, n))\n",
" t_fail = scale * rng.weibull(KSHAPE, n) # ์ฐฝ์ ๋น ํ ๊ณ ์ฅ๊น์ง ์ด์ฉ์๊ฐ(์จ์ ์ ๋ต)\n",
" event = (t_fail <= h_since).astype(int)\n",
" dur = np.where(event == 1, t_fail, h_since)\n",
" df = pd.DataFrame({\"eq_id\": [f\"EQ{seed:02d}{i:04d}\" for i in range(n)], \"unit\": unit, \"env\": env, \"etype\": et, \"rate\": rate, \"hours\": hours, \"km\": km, \"age_y\": age_y, \"n_ovh\": n_ovh,\n",
" \"h_since\": h_since, \"unpl\": unpl, \"n_maint\": n_maint, \"t_fail\": t_fail, \"event\": event, \"dur\": dur})\n",
" df[\"insp\"] = np.clip(dur / t_fail + rng.normal(0, INSP_NOISE, n), 0, 1.3) # ๋ง์ง๋ง ์ ๋น ์ ๊ฒ์์ ์ธก์ ํ ๋ง๋ชจ ๋น์จ(์๋ชจ ์๋ช
/์ด ์๋ช
, ์ค์ฐจ ํฌํจ)\n",
" df[\"rul_h\"] = np.where(event == 0, t_fail - h_since, np.nan) # ์์กด ์ฅ๋น์ ๋จ์ ์ด์ฉ์๊ฐ(์ ๋ต)\n",
" df[\"rul_d\"] = df[\"rul_h\"] / df[\"rate\"] # ๋จ์ ์ผ์(์ ๋ต)\n",
" return df\n",
"\n",
"\n",
"def mape(y, p):\n",
" # [TODO : ํ์ ์ค์ต 1] ์ฅ๋น ์ด์ฉ ์๊ฐ, ๊ธฐ๋ ๊ฑฐ๋ฆฌ, ์ ๋น ์ด๋ ฅ ๋ฐ ์ฐฝ์ ๋น ์ฃผ๊ธฐ ๋ฐ์ดํฐ์
์คํค๋ง ์ค๊ณ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def mae(y, p):\n",
" # [TODO : ํ์ ์ค์ต 1] ์ฅ๋น ์ด์ฉ ์๊ฐ, ๊ธฐ๋ ๊ฑฐ๋ฆฌ, ์ ๋น ์ด๋ ฅ ๋ฐ ์ฐฝ์ ๋น ์ฃผ๊ธฐ ๋ฐ์ดํฐ์
์คํค๋ง ์ค๊ณ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def rmse(y, p):\n",
" return float(np.sqrt(np.mean((np.asarray(y, float) - np.asarray(p, float)) ** 2)))\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ๋
ธํํ ํ์ ํผ์ฒ์ ํ์ต ํ๋ณธ ๋ง๋ค๊ธฐ. ํ๊ฒฝ ๊ฐ์ค์น W0๋ ๋์
์์ ์ ์ ๋ฌธ๊ฐ ์ถ์ ๊ฐ์ด๋ฉฐ(์ค์ ๊ฐ์ค์น์ ๋ค๋ฆ) M9์์ ์ฌ๋ณด์ ํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"W0 = {\"normal\": 1.00, \"rough\": 1.15, \"salt\": 1.10, \"cold\": 1.05}\n",
"FEATS = [\"h_since\", \"hours\", \"km\", \"age_y\", \"rate\", \"n_maint\", \"unpl\", \"unpl_per_kh\", \"km_per_h\", \"n_ovh\", \"k_rough\", \"k_salt\", \"k_cold\", \"k_truck\", \"k_gen\", \"k_trailer\",\n",
" \"aging_idx\", \"lin_rul_d\", \"hs_frac\", \"insp\", \"insp_eta_h\"]\n",
"\n",
"\n",
"def add_features(df, w=None):\n",
" d = df.copy()\n",
" w = w or W0\n",
" d[\"unpl_per_kh\"] = d[\"unpl\"] / (d[\"h_since\"] / 1000 + 0.5)\n",
" d[\"km_per_h\"] = d[\"km\"] / d[\"hours\"]\n",
" for e in (\"rough\", \"salt\", \"cold\"):\n",
" d[\"k_\" + e] = (d[\"env\"] == e).astype(int)\n",
" d[\"k_truck\"], d[\"k_gen\"], d[\"k_trailer\"] = (d[\"etype\"] == \"truck\").astype(int), (d[\"etype\"] == \"generator\").astype(int), (d[\"etype\"] == \"trailer\").astype(int)\n",
" d[\"aging_idx\"] = d[\"h_since\"] * d[\"env\"].map(w) / CYCLE # ํ๊ฒฝ ๊ฐ์ค ๋
ธํ ์ง์(1์ด๋ฉด ์ฐฝ์ ๋น ์ฃผ๊ธฐ์ ๋๋ฌ)\n",
" d[\"lin_rul_d\"] = (CYCLE - d[\"h_since\"]) / d[\"rate\"] # ์ ํ ๊ฐ๊ฐ์๊ฐ ์์ฌ ์ผ์(Baseline)\n",
" d[\"hs_frac\"] = d[\"h_since\"] / CYCLE\n",
" d[\"insp_eta_h\"] = d[\"h_since\"] / np.maximum(d[\"insp\"], 0.05) # ์ ๊ฒ ๋ง๋ชจ ๋น์จ๋ก ํ์ฐํ ์ด ์๋ช
์ถ์ (h)\n",
" return d\n",
"\n",
"\n",
"def snapshots(df, k=4, seed=0):\n",
" \"\"\"๊ณ ์ฅ ์ด๋ ฅ ์ฅ๋น์ ๊ณผ๊ฑฐ ์์ ํ๋ณธ: ๊ณ ์ฅ ์ ์์ ์์ ์ ์ ๊ฒ๊ฐยท์ด์ฉ์๊ฐ๊ณผ ๊ทธ๋์ ๋จ์ ์ผ์. (์์กด ์ฅ๋น์ ๋ฏธ๋๋ ์ ์ ์์ผ๋ฏ๋ก ๊ณ ์ฅ ์ด๋ ฅ๋ง ์ด๋ค)\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" f = df[df[\"event\"] == 1]\n",
" out = []\n",
" for _ in range(k):\n",
" s = f.copy()\n",
" h = s[\"t_fail\"] * rng.uniform(0.2, 0.98, len(s))\n",
" ratio = h / s[\"h_since\"]\n",
" s[\"hours\"] = s[\"hours\"] - (s[\"h_since\"] - h)\n",
" s[\"km\"] = s[\"km\"] * ratio.clip(upper=1)\n",
" s[\"unpl\"] = np.floor(s[\"unpl\"] * ratio)\n",
" s[\"n_maint\"] = np.floor(s[\"n_maint\"] * ratio)\n",
" s[\"insp\"] = np.clip(h / s[\"t_fail\"] + rng.normal(0, INSP_NOISE, len(s)), 0, 1.3)\n",
" s[\"h_since\"] = h\n",
" s[\"rul_d\"] = (s[\"t_fail\"] - h) / s[\"rate\"]\n",
" s[\"event\"] = 0\n",
" out.append(s)\n",
" return pd.concat(out, ignore_index=True)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# 1) ๋ฐ์ดํฐ ์ฌ์ (ํ๋, ๊ตฌ๋ถ, ์๋ฃํ, ํ์ฉ ๋ฒ์/๊ฐ, ์ค๋ช
)\n",
"DICT = [\n",
" (\"eq_id\", \"์๋ณ\", \"str\", r\"^EQ\\d{6}$\", \"์ฅ๋น ์๋ณ์ฝ๋(๊ฐ์)\"), (\"unit\", \"์๋ณ\", \"str\", \"U1~U6\", \"๊ฐ์ ๋ถ๋ ์ฝ๋\"), (\"etype\", \"์๋ณ\", \"str\", \"truck|generator|trailer|radio\", \"์ฅ๋น ๊ตฌ๋ถ(๊ฐ์)\"),\n",
" (\"env\", \"ํ๊ฒฝ\", \"str\", \"normal|rough|salt|cold\", \"์ฃผ ์ด์ฉ ํ๊ฒฝ(์ผ๋ฐ/ํ์ง/์ผ๋ถ/ํนํ)\"), (\"rate\", \"์ด์ฉ\", \"float\", \"0.5~20\", \"์ผ ํ๊ท ์ด์ฉ์๊ฐ(h/์ผ)\"),\n",
" (\"hours\", \"์ด์ฉ\", \"float\", \"0~60000\", \"๋์ ์ด์ฉ์๊ฐ(h)\"), (\"km\", \"์ด์ฉ\", \"float\", \"0~\", \"๋์ ๊ธฐ๋๊ฑฐ๋ฆฌ(km)\"), (\"age_y\", \"์ด์ฉ\", \"float\", \"0.5~25\", \"์ฌ์ฉ ์ฐ์(๋
)\"),\n",
" (\"n_ovh\", \"์ ๋น\", \"int\", \"0~5\", \"์ฐฝ์ ๋น ์ด๋ ฅ ํ์\"), (\"h_since\", \"์ ๋น\", \"float\", \"0~hours\", \"๋ง์ง๋ง ์ฐฝ์ ๋น ์ดํ ์ด์ฉ์๊ฐ(h)\"), (\"unpl\", \"์ ๋น\", \"int\", \"0~\", \"๋น๊ณํ ์ ๋น ๊ฑด์(๊ธด๊ธ ํฌํจ)\"),\n",
" (\"n_maint\", \"์ ๋น\", \"int\", \"0~\", \"๊ณํ ์ ๋น ๊ฑด์\"), (\"insp\", \"์ ๋น\", \"float\", \"0~1.3\", \"์ ๋น ์ ๊ฒ ๋ง๋ชจ ๋น์จ(์๋ชจ ์๋ช
/์ด ์๋ช
)\"),\n",
" (\"t_fail\", \"๋ผ๋ฒจ\", \"float\", \"0~\", \"๊ณ ์ฅ๊น์ง ์ด์ฉ์๊ฐ - ๋ชจ์ ์ ๋ต(์ค์ ์ด์ฉ์๋ ์์)\"), (\"event\", \"๋ผ๋ฒจ\", \"int\", \"0|1\", \"๊ณ ์ฅ ์ฌ๋ถ(1=๊ณ ์ฅ ์ด๋ ฅ, 0=์์กด)\"),\n",
" (\"dur\", \"๋ผ๋ฒจ\", \"float\", \"0~\", \"๊ด์ธก ์ด์ฉ์๊ฐ(๊ณ ์ฅ ์์ ๋๋ ์ค๋์ ๋จ)\"), (\"rul_h\", \"๋ผ๋ฒจ\", \"float\", \"0~\", \"๋จ์ ์ด์ฉ์๊ฐ - ๋ชจ์ ์ ๋ต\"), (\"rul_d\", \"๋ผ๋ฒจ\", \"float\", \"0~\", \"๋จ์ ์ผ์ - ๋ชจ์ ์ ๋ต\"),\n",
" (\"unpl_per_kh\", \"ํ์\", \"float\", \"0~\", \"1000h๋น ๋น๊ณํ ์ ๋น์จ\"), (\"km_per_h\", \"ํ์\", \"float\", \"5~40\", \"์ด์ฉ์๊ฐ๋น ๊ธฐ๋๊ฑฐ๋ฆฌ\"), (\"k_rough\", \"ํ์\", \"int\", \"0|1\", \"ํ์ง ๋๋ฏธ\"),\n",
" (\"k_salt\", \"ํ์\", \"int\", \"0|1\", \"์ผ๋ถ ๋๋ฏธ\"), (\"k_cold\", \"ํ์\", \"int\", \"0|1\", \"ํนํ ๋๋ฏธ\"), (\"k_truck\", \"ํ์\", \"int\", \"0|1\", \"์ฐจ๋ ๋๋ฏธ\"), (\"k_gen\", \"ํ์\", \"int\", \"0|1\", \"๋ฐ์ ๊ธฐ ๋๋ฏธ\"),\n",
" (\"k_trailer\", \"ํ์\", \"int\", \"0|1\", \"ํธ๋ ์ผ๋ฌ ๋๋ฏธ\"), (\"aging_idx\", \"ํ์\", \"float\", \"0~2\", \"ํ๊ฒฝ ๊ฐ์ค ๋
ธํ ์ง์(1์ด๋ฉด ์ฃผ๊ธฐ ๋๋ฌ)\"), (\"lin_rul_d\", \"ํ์\", \"float\", \"-\", \"์ ํ ๊ฐ๊ฐ์๊ฐ ์์ฌ ์ผ์\"),\n",
" (\"hs_frac\", \"ํ์\", \"float\", \"0~2\", \"์ฐฝ์ ๋น ์ฃผ๊ธฐ ๋๋น ๊ฒฝ๊ณผ ๋น์จ\"), (\"insp_eta_h\", \"ํ์\", \"float\", \"0~\", \"์ ๊ฒ ๋ง๋ชจ๋ก ํ์ฐํ ์ด ์๋ช
์ถ์ (h)\")]\n",
"dd = pd.DataFrame(DICT, columns=[\"ํ๋\", \"๊ตฌ๋ถ\", \"ํ\", \"ํ์ฉ๊ฐ\", \"์ค๋ช
\"])\n",
"print(f\"[1] ๋ฐ์ดํฐ ์ฌ์ {len(dd)}๊ฐ ํ๋ \" + str(dd[\"๊ตฌ๋ถ\"].value_counts().to_dict()))\n",
"LOT_SCHEMA = [(\"lot_id\", \"str\", \"์ํจ์ฑ ๋ถํ ํคํธ ๋กํธ ๋ฒํธ\"), (\"qty\", \"int\", \"์ฌ๊ณ ์๋(๊ฐ)\"), (\"expiry_d\", \"int\", \"์ ํจ๊ธฐํ๊น์ง ๋จ์ ์ผ์\"), (\"margin\", \"int\", \"์ ๋น์ผ ๊ธฐ์ค ์ต์ ์ ํจ๊ธฐ๊ฐ ์ฌ์ 14์ผ(๊ท์น)\")]\n",
"print(\" ์ํจ์ฑ ๋ถํ ํคํธ ๋กํธ ์คํค๋ง:\", \", \".join(f\"{a}({b})\" for a, b, _ in LOT_SCHEMA), \"- ์ ๋น ์์ ์ผ์ ์ ํจ๊ธฐ๊ฐ์ด ๋ถ์กฑํ ํคํธ๋ ์ธ ์ ์๋ค\")\n",
"\n",
"# 2) ์ค์ ์์ฑ ๋ฐ์ดํฐ์ ๋์กฐ\n",
"df = make_fleet(300, seed=1)\n",
"feat = add_features(df)\n",
"miss = sorted(set(feat.columns) - set(dd[\"ํ๋\"]))\n",
"extra = sorted(set(dd[\"ํ๋\"]) - set(feat.columns))\n",
"fe = dd[~dd[\"๊ตฌ๋ถ\"].isin([\"๋ผ๋ฒจ\", \"์๋ณ\"])]\n",
"print(f\"[2] ๋ฐ์ดํฐ ์ฌ์ ๋์กฐ({len(feat)}ํ): ์ฌ์ ๋๋ฝ ์ปฌ๋ผ {miss or '์์'}, ๋ฐ์ดํฐ์ ์๋ ํ๋ {extra or '์์'}\")\n",
"print(f\" ์์ธก ํผ์ฒ ํ๋ณด(์๋ณยท๋ผ๋ฒจ ์ ์ธ) {len(fe)}๊ฐ -> ๋ชฉํ(20๊ฐ ์ด์) {'๋ฌ์ฑ' if len(fe) >= 20 else '๋ฏธ๋ฌ'} / ์ฐฝ์ ๋น ๊ท์ ์ฃผ๊ธฐ {CYCLE:.0f}h(๊ฐ์ )\")\n",
"\n",
"# 3) ํ์ฉ๊ฐ ๊ฒ์ฆ๊ธฐ์ ์ค๋ฅ ์ฃผ์
\n",
"RULES = {\"eq_id\": lambda s: s.astype(str).str.match(r\"^EQ\\d{6}$\"), \"unit\": lambda s: s.isin(UNITS), \"env\": lambda s: s.isin(ENVS), \"rate\": lambda s: s.between(0.5, 20),\n",
" \"hours\": lambda s: s.between(0, 60000), \"h_since\": lambda s: s >= 0, \"insp\": lambda s: s.between(0, 1.3), \"age_y\": lambda s: s.between(0.5, 25)}\n",
"\n",
"\n",
"def validate(d):\n",
" bad = {}\n",
" for c, f in RULES.items():\n",
" n = int((~f(d[c])).sum() + d[c].isna().sum())\n",
" if n: bad[c] = n\n",
" n = int((d[\"h_since\"] > d[\"hours\"] + 1e-6).sum())\n",
" if n: bad[\"h_since>hours(๋ชจ์)\"] = n\n",
" n = int(d[\"eq_id\"].duplicated().sum())\n",
" if n: bad[\"eq_id ์ค๋ณต\"] = n\n",
" n = int((d[\"km\"] / d[\"hours\"].clip(lower=1) > 60).sum())\n",
" if n: bad[\"km ๋จ์ ์์ฌ\"] = n\n",
" return bad\n",
"\n",
"\n",
"print(f\"\\n[3] ์ ์ ์ํ {len(df)}๊ฑด ๊ฒ์ฆ ์ค๋ฅ {sum(validate(df).values())}๊ฑด\")\n",
"inj = {\"ํ๊ฒฝ ํ๊ธฐ ์ค๋ฅ\": (\"env\", 0, \"ํ์ง\"), \"์ด์ฉ์๊ฐ ์์\": (\"hours\", 1, -120.0), \"๋ฏธ๋ฑ๋ก ๋ถ๋\": (\"unit\", 2, \"U9\"), \"์ ๊ฒ ๋ง๋ชจ ๋น์จ ๋ฒ์ ์ด๊ณผ\": (\"insp\", 3, 2.5),\n",
" \"์ฐฝ์ ๋น ํ ์๊ฐ์ด ๋์ ๋ณด๋ค ํผ\": (\"h_since\", 4, 99999.0), \"km ๋จ์ ์ค๋ฅ(m)\": (\"km\", 5, 1.2e7)}\n",
"det = 0\n",
"for name, (col, i, val) in inj.items():\n",
" d2 = copy.deepcopy(df)\n",
" d2.loc[i, col] = val\n",
" bad = validate(d2)\n",
" ok = bool(bad)\n",
" det += ok\n",
" print(f\" - {name}: {'๊ฒ์ถ' if ok else '๋ฏธ๊ฒ์ถ'} {bad}\")\n",
"print(f\" ์ฃผ์
์ค๋ฅ {len(inj)}์ข
์ค {det}์ข
๊ฒ์ถ\")\n",
"\n",
"# 4) ์ฅ๋น ์๋ช
๊ด๋ฆฌ ์
๋ฌด ํ๋ก์ธ์ค ๋งต\n",
"proc = [(\"์ ๋น ์์ยท๊ณ ์ฅ ์ด๋ ฅ ์ ์\", \"์ ๋น๊ณํ๊ด\", \"์ ๋น ์์ฒญ, ๊ณ ์ฅ ๋ณด๊ณ \", \"๊ณ ์ฅ ์ด๋ ฅ ํ\", 30), (\"์ด์ฉ์๊ฐยท๊ธฐ๋๊ฑฐ๋ฆฌ ์ง๊ณ\", \"์ด์ฉ ๋ด๋น\", \"์ผ์ผ ์ด์ฉ ๋ณด๊ณ \", \"hours, km, rate\", 25),\n",
" (\"์ ๋น ์ด๋ ฅยท์ ๊ฒ ์
๋ ฅ\", \"์ ๋น ๋ด๋น\", \"์ ๋นยท์ ๊ฒ ๊ธฐ๋ก\", \"n_maint, unpl, insp\", 25), (\"ํ๊ฒฝ ๊ฐ์ค์น ์ ์ฉ\", \"๋ถ์ ๋ด๋น\", \"์ด์ฉ ํ๊ฒฝ ๊ตฌ๋ถ\", \"aging_idx\", 15), (\"์์ฌ ์๋ช
์์ธก\", \"๋ถ์ ๋ด๋น\", \"ํผ์ฒ ํ
์ด๋ธ\", \"RUL(์ผ), ๊ณ ์ฅ ํ๋ฅ \", 40),\n",
" (\"์ ๋น์ฐฝ ๋ฅ๋ ฅยท์์ฐ ๋์กฐ\", \"์ ๋น๊ณํ๊ด\", \"์๋ณ ์ฌ๋กฏ, ์์ฐ, ํคํธ ์ฌ๊ณ \", \"์ ๋น ์ผ์ ํ๋ณด\", 35), (\"์ ๋น ์ผ์ ํ์ ยท๋ํ ๊ฑด์\", \"๊ตฐ์์งํ๊ด\", \"์ถ์ฒ์, ์ฌ์ \", \"์น์ธ ์ผ์ \", 30), (\"์ ๋น ์คํยท๊ฒฐ๊ณผ ์
๋ ฅ\", \"์ ๋น์ฐฝ\", \"์น์ธ ์ผ์ \", \"์ ๋น ์๋ฃ ๊ธฐ๋ก\", 20),\n",
" (\"์์ธก ์ค์ฐจ ํ๋ฅ(AAR)\", \"๋ถ์ ๋ด๋น\", \"์์ธก ๋ ์ค์ \", \"์ฌ๋ณด์ ๋ณด๊ณ \", 30)]\n",
"pm = pd.DataFrame(proc, columns=[\"๋จ๊ณ\", \"๋ด๋น\", \"์
๋ ฅ\", \"์ฐ์ถ\", \"ํํ(๋ถ)\"])\n",
"pm.index = range(1, len(pm) + 1)\n",
"print(f\"\\n[4] ์
๋ฌด ํ๋ก์ธ์ค ๋งต {len(pm)}๋จ๊ณ, ํํ ํฉ๊ณ {pm['ํํ(๋ถ)'].sum()}๋ถ (๊ฐ์ฅ ๊ธด ๋จ๊ณ: {pm.loc[pm['ํํ(๋ถ)'].idxmax(), '๋จ๊ณ']})\")\n",
"print(pm.to_string())\n",
"print(\"\\nํ๋ก์ธ์ค ๋งต ์์ฑ ์๋ฃ(9๋จ๊ณ), ํผ์ฒ ๋ชฉ๋ก 20๊ฐ ์ด์ -> ๋ชฉํ\", \"๋ฌ์ฑ\" if len(pm) >= 5 and len(fe) >= 20 else \"๋ฏธ๋ฌ\")\n",
"print(\"๋ชจ๋ ๋ถ๋ยท์ฅ๋นยท์์น๋ ๊ต์ก์ฉ ๊ฐ์ ์๋ฃ์ด๋ฉฐ ์ค์ ๋ถ๋ยท์ฅ๋น์ ๋ฌด๊ดํ๋ค.\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
01 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[1] ๋ฐ์ดํฐ ์ฌ์ 30๊ฐ ํ๋ {'ํ์': 12, '์ ๋น': 5, '๋ผ๋ฒจ': 5, '์ด์ฉ': 4, '์๋ณ': 3, 'ํ๊ฒฝ': 1}\n",
" ์ํจ์ฑ ๋ถํ ํคํธ ๋กํธ ์คํค๋ง: lot_id(str), qty(int), expiry_d(int), margin(int) - ์ ๋น ์์ ์ผ์ ์ ํจ๊ธฐ๊ฐ์ด ๋ถ์กฑํ ํคํธ๋ ์ธ ์ ์๋ค\n",
"[2] ๋ฐ์ดํฐ ์ฌ์ ๋์กฐ(300ํ): ์ฌ์ ๋๋ฝ ์ปฌ๋ผ ์์, ๋ฐ์ดํฐ์ ์๋ ํ๋ ์์\n",
" ์์ธก ํผ์ฒ ํ๋ณด(์๋ณยท๋ผ๋ฒจ ์ ์ธ) 22๊ฐ -> ๋ชฉํ(20๊ฐ ์ด์) ๋ฌ์ฑ / ์ฐฝ์ ๋น ๊ท์ ์ฃผ๊ธฐ 8000h(๊ฐ์ )\n",
"\n",
"[3] ์ ์ ์ํ 300๊ฑด ๊ฒ์ฆ ์ค๋ฅ 0๊ฑด\n",
" - ํ๊ฒฝ ํ๊ธฐ ์ค๋ฅ: ๊ฒ์ถ {'env': 1}\n",
" - ์ด์ฉ์๊ฐ ์์: ๊ฒ์ถ {'hours': 1, 'h_since>hours(๋ชจ์)': 1, 'km ๋จ์ ์์ฌ': 1}\n",
" - ๋ฏธ๋ฑ๋ก ๋ถ๋: ๊ฒ์ถ {'unit': 1}\n",
" - ์ ๊ฒ ๋ง๋ชจ ๋น์จ ๋ฒ์ ์ด๊ณผ: ๊ฒ์ถ {'insp': 1}\n",
" - ์ฐฝ์ ๋น ํ ์๊ฐ์ด ๋์ ๋ณด๋ค ํผ: ๊ฒ์ถ {'h_since>hours(๋ชจ์)': 1}\n",
" - km ๋จ์ ์ค๋ฅ(m): ๊ฒ์ถ {'km ๋จ์ ์์ฌ': 1}\n",
" ์ฃผ์
์ค๋ฅ 6์ข
์ค 6์ข
๊ฒ์ถ\n",
"\n",
"[4] ์
๋ฌด ํ๋ก์ธ์ค ๋งต 9๋จ๊ณ, ํํ ํฉ๊ณ 250๋ถ (๊ฐ์ฅ ๊ธด ๋จ๊ณ: ์์ฌ ์๋ช
์์ธก)\n",
" ๋จ๊ณ ๋ด๋น ์
๋ ฅ ์ฐ์ถ ํํ(๋ถ)\n",
"1 ์ ๋น ์์ยท๊ณ ์ฅ ์ด๋ ฅ ์ ์ ์ ๋น๊ณํ๊ด ์ ๋น ์์ฒญ, ๊ณ ์ฅ ๋ณด๊ณ ๊ณ ์ฅ ์ด๋ ฅ ํ 30\n",
"2 ์ด์ฉ์๊ฐยท๊ธฐ๋๊ฑฐ๋ฆฌ ์ง๊ณ ์ด์ฉ ๋ด๋น ์ผ์ผ ์ด์ฉ ๋ณด๊ณ hours, km, rate 25\n",
"3 ์ ๋น ์ด๋ ฅยท์ ๊ฒ ์
๋ ฅ ์ ๋น ๋ด๋น ์ ๋นยท์ ๊ฒ ๊ธฐ๋ก n_maint, unpl, insp 25\n",
"4 ํ๊ฒฝ ๊ฐ์ค์น ์ ์ฉ ๋ถ์ ๋ด๋น ์ด์ฉ ํ๊ฒฝ ๊ตฌ๋ถ aging_idx 15\n",
"5 ์์ฌ ์๋ช
์์ธก ๋ถ์ ๋ด๋น ํผ์ฒ ํ
์ด๋ธ RUL(์ผ), ๊ณ ์ฅ ํ๋ฅ 40\n",
"6 ์ ๋น์ฐฝ ๋ฅ๋ ฅยท์์ฐ ๋์กฐ ์ ๋น๊ณํ๊ด ์๋ณ ์ฌ๋กฏ, ์์ฐ, ํคํธ ์ฌ๊ณ ์ ๋น ์ผ์ ํ๋ณด 35\n",
"7 ์ ๋น ์ผ์ ํ์ ยท๋ํ ๊ฑด์ ๊ตฐ์์งํ๊ด ์ถ์ฒ์, ์ฌ์ ์น์ธ ์ผ์ 30\n",
"8 ์ ๋น ์คํยท๊ฒฐ๊ณผ ์
๋ ฅ ์ ๋น์ฐฝ ์น์ธ ์ผ์ ์ ๋น ์๋ฃ ๊ธฐ๋ก 20\n",
"9 ์์ธก ์ค์ฐจ ํ๋ฅ(AAR) ๋ถ์ ๋ด๋น ์์ธก ๋ ์ค์ ์ฌ๋ณด์ ๋ณด๊ณ 30\n",
"\n",
"ํ๋ก์ธ์ค ๋งต ์์ฑ ์๋ฃ(9๋จ๊ณ), ํผ์ฒ ๋ชฉ๋ก 20๊ฐ ์ด์ -> ๋ชฉํ ๋ฌ์ฑ\n",
"๋ชจ๋ ๋ถ๋ยท์ฅ๋นยท์์น๋ ๊ต์ก์ฉ ๊ฐ์ ์๋ฃ์ด๋ฉฐ ์ค์ ๋ถ๋ยท์ฅ๋น์ ๋ฌด๊ดํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
02. ์ด์ฉ ํ๊ฒฝ(ํ์ง/์ผ๋ถ/ํนํ) ๊ฐ์ค์น ๋ฐ์ ๋ฐ ์ฅ๋น ๋
ธํํ ์งํ ์ ์ฒ๋ฆฌ ๋ชจ๋ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ด์ฉ ํ๊ฒฝ(ํ์ง/์ผ๋ถ) ๊ฐ์ค์น๋ฅผ ๋ฐ์ํ ๋
ธํํ ์งํ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ด์ฉ ํ๊ฒฝ(ํ์งยท์ผ๋ถยทํนํ) ๊ฐ์ค์น๋ฅผ ๋ฐ์ํ ๋
ธํํ ์งํ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ค๋ฅ๋ฅผ ์ฃผ์
ํ ์ค์ผ ๋ฐ์ดํฐ์ ํ๊ฒฝ ํ๊ธฐ ํต์ผ, ์ค๋ณต ์ ๊ฑฐ, ๋ถํธยท๋จ์ ์ค๋ฅ ๋ณด์ , ๊ฒฐ์ธก ๋ณด์ ๋ฑ ์ ์ ๊ท์น์ ์์๋๋ก ์ ์ฉํ๊ณ ์ฒ๋ฆฌ ๊ฑด์๋ฅผ ๋ก๊ทธ๋ก ๋จ๊ธด๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/etl/sup02_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": [
"\"\"\"ํ๊ธฐยท๋จ์ยท๋ชจ์ยท๊ฒฐ์ธกยท์ค๋ณต ์ค๋ฅ๋ฅผ ์ฃผ์
ํ ๊ฐ์ ์ฅ๋น ์ด์ฉ ๊ธฐ๋ก์ ์ด์ฉ ํ๊ฒฝ ๊ฐ์ค์น๋ฅผ ๋ฐ์ํ ๋
ธํํ ์งํ ์ฐ์ถ์ ํฌํจํ ์ ์ ๊ท์น 11์ข
์ ์ ์ฉํ๊ณ , ๊ธด๊ธ ์ด์ฉยท๋ค์ ๋น๊ณํ ์ ๋น ๊ฐ์ ๊ตฐ์ ํน์ ์์ธ๋ ์ญ์ ํ์ง ์๊ณ ๋ณด์กดํ๋ฉฐ ์ ํฉ์ฑ ๊ฒ์ฆ ํต๊ณผ์จ๊ณผ ์ ์ ์ ํ ํต๊ณ๋ฅผ ํ์ธํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ฅ๋น ๋ณด์ ํํฉยท๊ณ ์ฅ ์ด๋ ฅ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์ฅ๋นยท์์น ์์)\n",
"# ๊ฐ์ ๋ถ๋ 6๊ฐ(U1~U6)์ ์ฅ๋น๊ฐ ์ด์ฉ ํ๊ฒฝ(normal/rough/salt/cold)์ ๋ฐ๋ผ ๋ค๋ฅธ ์๋๋ก ๋
ธํํํ๋ค. ์ฐฝ์ ๋น ํ ์ด์ฉ์๊ฐ(h_since)์ด\n",
"# ์ฅ๋น๋ง๋ค ์จ์ ์๋ช
(t_fail)์ ๋์ผ๋ฉด ๊ณ ์ฅ(event=1) ์ด๋ ฅ, ๋์ง ์์์ผ๋ฉด ํ์ฌ๊น์ง ์์กด(event=0, ๊ด์ธก ์ค๋์ ๋จ)์ด๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"ENVS = [\"normal\", \"rough\", \"salt\", \"cold\"]\n",
"ENV_W = {\"normal\": 1.00, \"rough\": 1.35, \"salt\": 1.25, \"cold\": 1.12} # ํ๊ฒฝ ๊ฐ์ค์น(์จ์ ์ ๋ต, ๊ฐ์ ๊ฐ): ํด์๋ก ์๋ช
์ด ์งง๋ค\n",
"ENV_KO = {\"normal\": \"์ผ๋ฐ\", \"rough\": \"ํ์ง\", \"salt\": \"์ผ๋ถ\", \"cold\": \"ํนํ\"}\n",
"UNITS = [\"U1\", \"U2\", \"U3\", \"U4\", \"U5\", \"U6\"]\n",
"UNIT_ENV = {\"U1\": [.7, .1, .1, .1], \"U2\": [.2, .6, .1, .1], \"U3\": [.2, .1, .6, .1], \"U4\": [.3, .1, .1, .5], \"U5\": [.5, .3, .1, .1], \"U6\": [.4, .1, .4, .1]}\n",
"ETYPES = [\"truck\", \"generator\", \"trailer\", \"radio\"]\n",
"CYCLE = 8000.0 # ๊ท์ ์ฐฝ์ ๋น ์ฃผ๊ธฐ(์ด์ฉ์๊ฐ h, ๊ฐ์ ๊ฐ)\n",
"KSHAPE = 12.0 # ์๋ช
๋ถํฌ ์์ด๋ธ ํ์(๋ง๋ชจํ ๊ณ ์ฅ, ํด์๋ก ์๋ช
์ด ๋ชจ์ฌ ์๋ค)\n",
"INSP_NOISE = 0.025 # ์ ๋น ์ ๊ฒ ๋ง๋ชจ ๋น์จ ์ธก์ ์ค์ฐจ(๊ฐ์ ๊ฐ)\n",
"\n",
"\n",
"def make_fleet(n=900, seed=0, env_w=None, shift=1.0):\n",
" \"\"\"์ฅ๋น n๋. env_w๋ก ์จ์ ํ๊ฒฝ ๊ฐ์ค์น๋ฅผ ๋ฐ๊ฟ ์ด์ ๋ถํฌ ๋ณํ๋ฅผ ๋ชจ์ํ ์ ์๋ค. shift>1์ด๋ฉด ๊ฐ๋๋ฅ ์ด ๋์์ง๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" w = env_w or ENV_W\n",
" unit = rng.choice(UNITS, n)\n",
" env = np.array([rng.choice(ENVS, p=UNIT_ENV[u]) for u in unit])\n",
" et = rng.choice(ETYPES, n, p=[.4, .2, .2, .2])\n",
" rate = rng.lognormal(np.log(3.0 * shift), 0.25, n) # ์ผ ํ๊ท ์ด์ฉ์๊ฐ(h/์ผ)\n",
" h_since = rng.uniform(300, 11500, n) # ๋ง์ง๋ง ์ฐฝ์ ๋น ์ดํ ์ด์ฉ์๊ฐ\n",
" n_ovh = rng.integers(0, 3, n)\n",
" hours = h_since + n_ovh * rng.uniform(5000, 8000, n) # ๋์ ์ด์ฉ์๊ฐ\n",
" km = hours * rng.normal(17, 3, n) * np.where(env == \"rough\", 0.8, 1.0) # ๋์ ๊ธฐ๋๊ฑฐ๋ฆฌ(km)\n",
" age_y = np.clip(hours / (rate * 365) * rng.uniform(0.9, 1.15, n), 0.5, 25)\n",
" unpl = rng.poisson(0.35 * h_since / 1000 * np.array([w[e] for e in env]) / 1.3) # ๋น๊ณํ ์ ๋น ๊ฑด์(ํ๊ฒฝ์ด ๊ฐํนํ ์๋ก ๋ง๋ค)\n",
" n_maint = rng.poisson(0.6 * h_since / 1000 + 1) # ๊ณํ ์ ๋น ๊ฑด์\n",
" scale = 9600 / np.array([w[e] for e in env]) * 0.985 ** unpl * np.exp(rng.normal(0, 0.04, n))\n",
" t_fail = scale * rng.weibull(KSHAPE, n) # ์ฐฝ์ ๋น ํ ๊ณ ์ฅ๊น์ง ์ด์ฉ์๊ฐ(์จ์ ์ ๋ต)\n",
" event = (t_fail <= h_since).astype(int)\n",
" dur = np.where(event == 1, t_fail, h_since)\n",
" df = pd.DataFrame({\"eq_id\": [f\"EQ{seed:02d}{i:04d}\" for i in range(n)], \"unit\": unit, \"env\": env, \"etype\": et, \"rate\": rate, \"hours\": hours, \"km\": km, \"age_y\": age_y, \"n_ovh\": n_ovh,\n",
" \"h_since\": h_since, \"unpl\": unpl, \"n_maint\": n_maint, \"t_fail\": t_fail, \"event\": event, \"dur\": dur})\n",
" df[\"insp\"] = np.clip(dur / t_fail + rng.normal(0, INSP_NOISE, n), 0, 1.3) # ๋ง์ง๋ง ์ ๋น ์ ๊ฒ์์ ์ธก์ ํ ๋ง๋ชจ ๋น์จ(์๋ชจ ์๋ช
/์ด ์๋ช
, ์ค์ฐจ ํฌํจ)\n",
" df[\"rul_h\"] = np.where(event == 0, t_fail - h_since, np.nan) # ์์กด ์ฅ๋น์ ๋จ์ ์ด์ฉ์๊ฐ(์ ๋ต)\n",
" df[\"rul_d\"] = df[\"rul_h\"] / df[\"rate\"] # ๋จ์ ์ผ์(์ ๋ต)\n",
" return df\n",
"\n",
"\n",
"def mape(y, p):\n",
" # [TODO : ํ์ ์ค์ต 2] ์ด์ฉ ํ๊ฒฝ(ํ์ง/์ผ๋ถ/ํนํ) ๊ฐ์ค์น ๋ฐ์ ๋ฐ ์ฅ๋น ๋
ธํํ ์งํ ์ ์ฒ๋ฆฌ ๋ชจ๋ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def mae(y, p):\n",
" # [TODO : ํ์ ์ค์ต 2] ์ด์ฉ ํ๊ฒฝ(ํ์ง/์ผ๋ถ/ํนํ) ๊ฐ์ค์น ๋ฐ์ ๋ฐ ์ฅ๋น ๋
ธํํ ์งํ ์ ์ฒ๋ฆฌ ๋ชจ๋ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def rmse(y, p):\n",
" return float(np.sqrt(np.mean((np.asarray(y, float) - np.asarray(p, float)) ** 2)))\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ๋
ธํํ ํ์ ํผ์ฒ์ ํ์ต ํ๋ณธ ๋ง๋ค๊ธฐ. ํ๊ฒฝ ๊ฐ์ค์น W0๋ ๋์
์์ ์ ์ ๋ฌธ๊ฐ ์ถ์ ๊ฐ์ด๋ฉฐ(์ค์ ๊ฐ์ค์น์ ๋ค๋ฆ) M9์์ ์ฌ๋ณด์ ํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"W0 = {\"normal\": 1.00, \"rough\": 1.15, \"salt\": 1.10, \"cold\": 1.05}\n",
"FEATS = [\"h_since\", \"hours\", \"km\", \"age_y\", \"rate\", \"n_maint\", \"unpl\", \"unpl_per_kh\", \"km_per_h\", \"n_ovh\", \"k_rough\", \"k_salt\", \"k_cold\", \"k_truck\", \"k_gen\", \"k_trailer\",\n",
" \"aging_idx\", \"lin_rul_d\", \"hs_frac\", \"insp\", \"insp_eta_h\"]\n",
"\n",
"\n",
"def add_features(df, w=None):\n",
" d = df.copy()\n",
" w = w or W0\n",
" d[\"unpl_per_kh\"] = d[\"unpl\"] / (d[\"h_since\"] / 1000 + 0.5)\n",
" d[\"km_per_h\"] = d[\"km\"] / d[\"hours\"]\n",
" for e in (\"rough\", \"salt\", \"cold\"):\n",
" d[\"k_\" + e] = (d[\"env\"] == e).astype(int)\n",
" d[\"k_truck\"], d[\"k_gen\"], d[\"k_trailer\"] = (d[\"etype\"] == \"truck\").astype(int), (d[\"etype\"] == \"generator\").astype(int), (d[\"etype\"] == \"trailer\").astype(int)\n",
" d[\"aging_idx\"] = d[\"h_since\"] * d[\"env\"].map(w) / CYCLE # ํ๊ฒฝ ๊ฐ์ค ๋
ธํ ์ง์(1์ด๋ฉด ์ฐฝ์ ๋น ์ฃผ๊ธฐ์ ๋๋ฌ)\n",
" d[\"lin_rul_d\"] = (CYCLE - d[\"h_since\"]) / d[\"rate\"] # ์ ํ ๊ฐ๊ฐ์๊ฐ ์์ฌ ์ผ์(Baseline)\n",
" d[\"hs_frac\"] = d[\"h_since\"] / CYCLE\n",
" d[\"insp_eta_h\"] = d[\"h_since\"] / np.maximum(d[\"insp\"], 0.05) # ์ ๊ฒ ๋ง๋ชจ ๋น์จ๋ก ํ์ฐํ ์ด ์๋ช
์ถ์ (h)\n",
" return d\n",
"\n",
"\n",
"def snapshots(df, k=4, seed=0):\n",
" \"\"\"๊ณ ์ฅ ์ด๋ ฅ ์ฅ๋น์ ๊ณผ๊ฑฐ ์์ ํ๋ณธ: ๊ณ ์ฅ ์ ์์ ์์ ์ ์ ๊ฒ๊ฐยท์ด์ฉ์๊ฐ๊ณผ ๊ทธ๋์ ๋จ์ ์ผ์. (์์กด ์ฅ๋น์ ๋ฏธ๋๋ ์ ์ ์์ผ๋ฏ๋ก ๊ณ ์ฅ ์ด๋ ฅ๋ง ์ด๋ค)\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" f = df[df[\"event\"] == 1]\n",
" out = []\n",
" for _ in range(k):\n",
" s = f.copy()\n",
" h = s[\"t_fail\"] * rng.uniform(0.2, 0.98, len(s))\n",
" ratio = h / s[\"h_since\"]\n",
" s[\"hours\"] = s[\"hours\"] - (s[\"h_since\"] - h)\n",
" s[\"km\"] = s[\"km\"] * ratio.clip(upper=1)\n",
" s[\"unpl\"] = np.floor(s[\"unpl\"] * ratio)\n",
" s[\"n_maint\"] = np.floor(s[\"n_maint\"] * ratio)\n",
" s[\"insp\"] = np.clip(h / s[\"t_fail\"] + rng.normal(0, INSP_NOISE, len(s)), 0, 1.3)\n",
" s[\"h_since\"] = h\n",
" s[\"rul_d\"] = (s[\"t_fail\"] - h) / s[\"rate\"]\n",
" s[\"event\"] = 0\n",
" out.append(s)\n",
" return pd.concat(out, ignore_index=True)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"truth = make_fleet(220, seed=5)\n",
"rng = np.random.default_rng(2)\n",
"raw = truth.drop(columns=[\"t_fail\", \"event\", \"dur\", \"rul_h\", \"rul_d\"]).copy()\n",
"raw[\"urgent_ops\"] = False\n",
"# ๊ธด๊ธ ์ด์ฉ(๊ฐ๋๋ฅ ์ด ๋น์ ์์ ์ผ๋ก ๋์ง๋ง ์ค์ ์ฌ์ค์ธ ๊ธฐ๋ก): ์ญ์ ํ๋ฉด ์ ๋๋ค\n",
"urg = rng.choice(len(raw), 8, replace=False)\n",
"raw.loc[urg, \"rate\"] = rng.uniform(13, 17, 8)\n",
"raw.loc[urg, \"urgent_ops\"] = True\n",
"# ์ค๋ฅ ์ฃผ์
\n",
"errs = {}\n",
"i = rng.choice(len(raw), 12, replace=False); raw.loc[i, \"env\"] = rng.choice([\"ํ์ง\", \"ROUGH\", \"Salt \", \"ํนํ\", \"์ผ๋ฐ\", \"normal \"], 12); errs[\"ํ๊ฒฝ ํ๊ธฐ\"] = 12\n",
"i = rng.choice(len(raw), 8, replace=False); raw.loc[i, \"unit\"] = rng.choice([\"u3\", \"U 2\", \"U9\", \"u5\"], 8); errs[\"๋ถ๋ ์ฝ๋\"] = 8\n",
"i = rng.choice(len(raw), 6, replace=False); raw.loc[i, \"hours\"] = -raw.loc[i, \"hours\"]; errs[\"์ด์ฉ์๊ฐ ์์\"] = 6\n",
"i = rng.choice(len(raw), 7, replace=False); raw.loc[i, \"h_since\"] = raw.loc[i, \"hours\"] * rng.uniform(1.3, 2.0, 7); errs[\"์ฐฝ์ ๋น ํ ์๊ฐ > ๋์ \"] = 7\n",
"i = rng.choice(len(raw), 6, replace=False); raw.loc[i, \"km\"] = raw.loc[i, \"km\"] * 1000; errs[\"km ๋จ์(m)\"] = 6\n",
"i = rng.choice(len(raw), 9, replace=False); raw.loc[i, \"rate\"] = np.nan; errs[\"๊ฐ๋๋ฅ ๊ฒฐ์ธก\"] = 9\n",
"i = rng.choice(len(raw), 9, replace=False); raw.loc[i, \"insp\"] = np.nan; errs[\"์ ๊ฒ ๋ง๋ชจ ๊ฒฐ์ธก\"] = 9\n",
"i = rng.choice(len(raw), 5, replace=False); raw.loc[i, \"insp\"] = rng.uniform(1.8, 3.0, 5); errs[\"์ ๊ฒ ๋ง๋ชจ ๋ฒ์ ์ด๊ณผ\"] = 5\n",
"raw = pd.concat([raw, raw.iloc[rng.choice(len(raw), 7, replace=False)]], ignore_index=True); errs[\"์ค๋ณต ํ\"] = 7\n",
"raw[\"eq_id\"] = raw[\"eq_id\"].where(rng.random(len(raw)) > 0.05, raw[\"eq_id\"].str.lower()); errs[\"ID ํ๊ธฐ\"] = int((raw[\"eq_id\"].str.islower()).sum())\n",
"print(f\"[0] ์ค์ผ ๋ฐ์ดํฐ {len(raw)}ํ(์๋ณธ {len(truth)}ํ), ์ฃผ์
์ค๋ฅ: \" + \", \".join(f\"{k} {v}\" for k, v in errs.items()) + f\", ๊ธด๊ธ ์ด์ฉ ์ ์ ๊ธฐ๋ก {len(urg)}ํ\")\n",
"\n",
"# ---------------- ์ ์ ๊ท์น ----------------\n",
"log = []\n",
"d = raw.copy()\n",
"\n",
"\n",
"def rule(no, desc, n):\n",
" log.append((no, desc, int(n)))\n",
"\n",
"\n",
"n0 = len(d)\n",
"d[\"eq_id\"] = d[\"eq_id\"].str.upper().str.strip()\n",
"rule(1, \"์ฅ๋น ID ๋๋ฌธ์ยท๊ณต๋ฐฑ ํต์ผ\", (raw[\"eq_id\"] != d[\"eq_id\"]).sum())\n",
"d = d.drop_duplicates(\"eq_id\").reset_index(drop=True)\n",
"rule(2, \"์ค๋ณต ์ฅ๋น ํ ์ ๊ฑฐ\", n0 - len(d))\n",
"d[\"unit\"] = d[\"unit\"].astype(str).str.upper().str.replace(\" \", \"\")\n",
"d[\"env\"] = d[\"env\"].astype(str).str.strip()\n",
"EMAP = {\"ํ์ง\": \"rough\", \"rough\": \"rough\", \"salt\": \"salt\", \"ํนํ\": \"cold\", \"cold\": \"cold\", \"์ผ๋ฐ\": \"normal\", \"normal\": \"normal\", \"์ผ๋ถ\": \"salt\"}\n",
"env_fix = d[\"env\"].str.lower().map(EMAP)\n",
"rule(3, \"์ด์ฉ ํ๊ฒฝ ํ๊ธฐ ํ์คํ(ํ๊ธยท๋์๋ฌธ์ยท๊ณต๋ฐฑ)\", (env_fix != d[\"env\"]).sum())\n",
"d[\"env\"] = env_fix.fillna(\"normal\")\n",
"iso = ~d[\"unit\"].isin(UNITS)\n",
"quarantine = d[iso].copy()\n",
"d = d[~iso].reset_index(drop=True)\n",
"rule(4, \"๋ฏธ๋ฑ๋ก ๋ถ๋ ์ฝ๋ ํ ๊ฒฉ๋ฆฌ(์ญ์ ํ์ง ์๊ณ ๋ณ๋ ๋ณด๊ด)\", iso.sum())\n",
"neg = d[\"hours\"] < 0\n",
"d.loc[neg, \"hours\"] = -d.loc[neg, \"hours\"]\n",
"rule(5, \"๋์ ์ด์ฉ์๊ฐ ๋ถํธ ์ค๋ฅ ๋ณด์ \", neg.sum())\n",
"bad_hs = d[\"h_since\"] > d[\"hours\"]\n",
"d.loc[bad_hs, \"h_since\"] = d.loc[bad_hs, \"hours\"]\n",
"rule(6, \"์ฐฝ์ ๋น ํ ์ด์ฉ์๊ฐ > ๋์ ์ด์ฉ์๊ฐ ๋ชจ์ ๋ณด์ \", bad_hs.sum())\n",
"unit_km = d[\"km\"] / d[\"hours\"].clip(lower=1) > 100\n",
"d.loc[unit_km, \"km\"] = d.loc[unit_km, \"km\"] / 1000\n",
"rule(7, \"๊ธฐ๋๊ฑฐ๋ฆฌ ๋จ์ ์ค๋ฅ(m -> km) ๋ณด์ \", unit_km.sum())\n",
"nan_rate = d[\"rate\"].isna()\n",
"d[\"rate\"] = d[\"rate\"].fillna(d.groupby([\"etype\"])[\"rate\"].transform(\"median\"))\n",
"rule(8, \"์ผ ํ๊ท ์ด์ฉ์๊ฐ ๊ฒฐ์ธก์ ์ฅ๋น ๊ตฌ๋ถ ์ค์๊ฐ์ผ๋ก ๋ณด์ \", nan_rate.sum())\n",
"out_i = d[\"insp\"] > 1.3\n",
"d.loc[out_i, \"insp\"] = np.nan\n",
"miss_i = d[\"insp\"].isna()\n",
"d[\"insp_flag\"] = np.where(miss_i, \"์ถ์ \", \"์ธก์ \")\n",
"d[\"insp\"] = d[\"insp\"].fillna((d[\"h_since\"] / CYCLE * d[\"env\"].map(W0)).clip(0, 1.3))\n",
"rule(9, \"์ ๊ฒ ๋ง๋ชจ ๋น์จ ๋ฒ์ ์ด๊ณผ ์ ๊ฑฐ ํ ๊ฒฐ์ธก์ ํ๊ฒฝ ๊ฐ์ค ๋
ธํ ์ง์๋ก ๋์ฒด(์ถ์ ํ์)\", miss_i.sum())\n",
"exc = (d[\"rate\"] > 12) | d[\"urgent_ops\"]\n",
"d[\"exception\"] = np.where(exc, \"๊ธด๊ธ ์ด์ฉ ๋ณด์กด\", \"\")\n",
"rule(10, \"๊ธด๊ธ ์ด์ฉ(๊ฐ๋๋ฅ 12h/์ผ ์ด๊ณผ) ์์ธ ํ์ ํ ์ด์์น ์ญ์ ์์ด ๋ณด์กด\", exc.sum())\n",
"d[\"aging_idx\"] = d[\"h_since\"] * d[\"env\"].map(W0) / CYCLE\n",
"d[\"eff_hours\"] = d[\"h_since\"] * d[\"env\"].map(W0)\n",
"rule(11, \"ํ๊ฒฝ ๊ฐ์ค์น ๋ฐ์ ๋
ธํํ ์งํ(aging_idx, eff_hours) ์ฐ์ถ\", len(d))\n",
"print(\"\\n[1] ์ ์ ๊ท์น ์ ์ฉ ๋ก๊ทธ\")\n",
"print(pd.DataFrame(log, columns=[\"๊ท์น\", \"๋ด์ฉ\", \"์ฒ๋ฆฌ ๊ฑด์\"]).to_string(index=False))\n",
"print(f\" ์ ์ ๊ท์น {len(log)}์ข
์ ์ฉ(๋ชฉํ 10์ข
์ด์: {'๋ฌ์ฑ' if len(log) >= 10 else '๋ฏธ๋ฌ'}), ๊ฒฉ๋ฆฌ {len(quarantine)}ํ์ ๋ด๋น์ ํ์ธ ๋๊ธฐ\")\n",
"\n",
"# ---------------- ์ ํฉ์ฑ ๊ฒ์ฆ ----------------\n",
"checks = {\"์ฅ๋น ID ์ ์ผ\": not d[\"eq_id\"].duplicated().any(), \"๋ถ๋ ์ฝ๋ ๋ฑ๋ก\": bool(d[\"unit\"].isin(UNITS).all()), \"ํ๊ฒฝ ๊ฐ 4์ข
\": bool(d[\"env\"].isin(ENVS).all()),\n",
" \"์ด์ฉ์๊ฐ 0 ์ด์\": bool((d[\"hours\"] >= 0).all()), \"์ฐฝ์ ๋น ํ <= ๋์ \": bool((d[\"h_since\"] <= d[\"hours\"] + 1e-6).all()), \"๊ธฐ๋ ์๋ 5~40km/h\": bool((d[\"km\"] / d[\"hours\"].clip(lower=1)).between(5, 40).all()),\n",
" \"์ ๊ฒ ๋ง๋ชจ 0~1.3\": bool(d[\"insp\"].between(0, 1.3).all()), \"ํ์ ์ปฌ๋ผ ๊ฒฐ์ธก 0\": int(d[[\"rate\", \"insp\", \"aging_idx\"]].isna().sum().sum()) == 0}\n",
"print(\"\\n[2] ์ ํฉ์ฑ ๊ฒ์ฆ\")\n",
"for k, v in checks.items():\n",
" print(f\" [{'PASS' if v else 'FAIL'}] {k}\")\n",
"print(f\" ํต๊ณผ์จ {sum(checks.values()) / len(checks):.0%} (๋ชฉํ 100%)\")\n",
"\n",
"# ---------------- ๋ณต์ ์ ํ๋ยท์์ธ ๋ณด์กดยท์ ํ ํต๊ณ ----------------\n",
"t = truth.set_index(\"eq_id\").loc[d[\"eq_id\"].isin(truth[\"eq_id\"]).pipe(lambda m: d.loc[m, \"eq_id\"])]\n",
"dd = d.set_index(\"eq_id\").loc[t.index]\n",
"print(f\"\\n[3] ๋ณต์ ์ ํ๋(์๋ณธ๊ณผ ๋น๊ต): ๋์ ์ด์ฉ์๊ฐ ์ค์ฐจ {np.abs(dd['hours'] - t['hours']).mean():.1f}h, ๊ธฐ๋๊ฑฐ๋ฆฌ ์ค์ฐจ {np.abs(dd['km'] - t['km']).mean():.1f}km, ๊ฐ๋๋ฅ ์ค์ฐจ {np.abs(dd['rate'] - t['rate'])[dd['exception'] == ''].mean():.3f}h/์ผ(๊ธด๊ธ ์ด์ฉ ์ ์ธ)\")\n",
"print(f\" ๊ธด๊ธ ์ด์ฉ ๊ธฐ๋ก {int((d['exception'] != '').sum())}ํ ๋ณด์กด(์ญ์ 0ํ), ์ ๊ฒ ๋ง๋ชจ ์ถ์ ๋์ฒด {int((d['insp_flag'] == '์ถ์ ').sum())}ํ์ ์ถ์ ํ์\")\n",
"rows = [(\"์ด์ฉ์๊ฐ ์ต์๊ฐ(h)\", raw[\"hours\"].min(), d[\"hours\"].min(), truth[\"hours\"].min()), (\"์ด์ฉ์๊ฐ ํ๊ท (h)\", raw[\"hours\"].mean(), d[\"hours\"].mean(), truth[\"hours\"].mean()),\n",
" (\"๊ธฐ๋๊ฑฐ๋ฆฌ ์ต๋๊ฐ(km)\", raw[\"km\"].max(), d[\"km\"].max(), truth[\"km\"].max()), (\"๊ฐ๋๋ฅ ์ ํจ ๊ฑด์\", raw[\"rate\"].notna().sum(), d[\"rate\"].notna().sum(), truth[\"rate\"].notna().sum()),\n",
" (\"๊ฐ๋๋ฅ ์ต๋๊ฐ(h/์ผ)\", raw[\"rate\"].max(), d[\"rate\"].max(), truth[\"rate\"].max()), (\"์ ๊ฒ ๋ง๋ชจ ์ต๋๊ฐ\", raw[\"insp\"].max(), d[\"insp\"].max(), truth[\"insp\"].max())]\n",
"print(\"[4] ์ ํ ๊ธฐ์ ํต๊ณ(์ค์ผ / ์ ์ ํ / ์๋ณธ)\")\n",
"print(pd.DataFrame(rows, columns=[\"ํญ๋ชฉ\", \"์ค์ผ\", \"์ ์ ํ\", \"์๋ณธ\"]).round(2).to_string(index=False))\n",
"w = d.groupby(\"env\")[\"aging_idx\"].mean().round(3)\n",
"print(\" ํ๊ฒฝ๋ณ ํ๊ท ๋
ธํ ์ง์(๊ฐ์ค์น W0 ์ ์ฉ):\", w.to_dict())\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
02 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[0] ์ค์ผ ๋ฐ์ดํฐ 227ํ(์๋ณธ 220ํ), ์ฃผ์
์ค๋ฅ: ํ๊ฒฝ ํ๊ธฐ 12, ๋ถ๋ ์ฝ๋ 8, ์ด์ฉ์๊ฐ ์์ 6, ์ฐฝ์ ๋น ํ ์๊ฐ > ๋์ 7, km ๋จ์(m) 6, ๊ฐ๋๋ฅ ๊ฒฐ์ธก 9, ์ ๊ฒ ๋ง๋ชจ ๊ฒฐ์ธก 9, ์ ๊ฒ ๋ง๋ชจ ๋ฒ์ ์ด๊ณผ 5, ์ค๋ณต ํ 7, ID ํ๊ธฐ 7, ๊ธด๊ธ ์ด์ฉ ์ ์ ๊ธฐ๋ก 8ํ\n",
"\n",
"[1] ์ ์ ๊ท์น ์ ์ฉ ๋ก๊ทธ\n",
" ๊ท์น ๋ด์ฉ ์ฒ๋ฆฌ ๊ฑด์\n",
" 1 ์ฅ๋น ID ๋๋ฌธ์ยท๊ณต๋ฐฑ ํต์ผ 7\n",
" 2 ์ค๋ณต ์ฅ๋น ํ ์ ๊ฑฐ 7\n",
" 3 ์ด์ฉ ํ๊ฒฝ ํ๊ธฐ ํ์คํ(ํ๊ธยท๋์๋ฌธ์ยท๊ณต๋ฐฑ) 10\n",
" 4 ๋ฏธ๋ฑ๋ก ๋ถ๋ ์ฝ๋ ํ ๊ฒฉ๋ฆฌ(์ญ์ ํ์ง ์๊ณ ๋ณ๋ ๋ณด๊ด) 1\n",
" 5 ๋์ ์ด์ฉ์๊ฐ ๋ถํธ ์ค๋ฅ ๋ณด์ 6\n",
" 6 ์ฐฝ์ ๋น ํ ์ด์ฉ์๊ฐ > ๋์ ์ด์ฉ์๊ฐ ๋ชจ์ ๋ณด์ 7\n",
" 7 ๊ธฐ๋๊ฑฐ๋ฆฌ ๋จ์ ์ค๋ฅ(m -> km) ๋ณด์ 6\n",
" 8 ์ผ ํ๊ท ์ด์ฉ์๊ฐ ๊ฒฐ์ธก์ ์ฅ๋น ๊ตฌ๋ถ ์ค์๊ฐ์ผ๋ก ๋ณด์ 9\n",
" 9 ์ ๊ฒ ๋ง๋ชจ ๋น์จ ๋ฒ์ ์ด๊ณผ ์ ๊ฑฐ ํ ๊ฒฐ์ธก์ ํ๊ฒฝ ๊ฐ์ค ๋
ธํ ์ง์๋ก ๋์ฒด(์ถ์ ํ์) 13\n",
" 10 ๊ธด๊ธ ์ด์ฉ(๊ฐ๋๋ฅ 12h/์ผ ์ด๊ณผ) ์์ธ ํ์ ํ ์ด์์น ์ญ์ ์์ด ๋ณด์กด 8\n",
" 11 ํ๊ฒฝ ๊ฐ์ค์น ๋ฐ์ ๋
ธํํ ์งํ(aging_idx, eff_hours) ์ฐ์ถ 219\n",
" ์ ์ ๊ท์น 11์ข
์ ์ฉ(๋ชฉํ 10์ข
์ด์: ๋ฌ์ฑ), ๊ฒฉ๋ฆฌ 1ํ์ ๋ด๋น์ ํ์ธ ๋๊ธฐ\n",
"\n",
"[2] ์ ํฉ์ฑ ๊ฒ์ฆ\n",
" [PASS] ์ฅ๋น ID ์ ์ผ\n",
" [PASS] ๋ถ๋ ์ฝ๋ ๋ฑ๋ก\n",
" [PASS] ํ๊ฒฝ ๊ฐ 4์ข
\n",
" [PASS] ์ด์ฉ์๊ฐ 0 ์ด์\n",
" [PASS] ์ฐฝ์ ๋น ํ <= ๋์ \n",
" [PASS] ๊ธฐ๋ ์๋ 5~40km/h\n",
" [PASS] ์ ๊ฒ ๋ง๋ชจ 0~1.3\n",
" [PASS] ํ์ ์ปฌ๋ผ ๊ฒฐ์ธก 0\n",
" ํต๊ณผ์จ 100% (๋ชฉํ 100%)\n",
"\n",
"[3] ๋ณต์ ์ ํ๋(์๋ณธ๊ณผ ๋น๊ต): ๋์ ์ด์ฉ์๊ฐ ์ค์ฐจ 0.0h, ๊ธฐ๋๊ฑฐ๋ฆฌ ์ค์ฐจ 0.0km, ๊ฐ๋๋ฅ ์ค์ฐจ 0.016h/์ผ(๊ธด๊ธ ์ด์ฉ ์ ์ธ)\n",
" ๊ธด๊ธ ์ด์ฉ ๊ธฐ๋ก 8ํ ๋ณด์กด(์ญ์ 0ํ), ์ ๊ฒ ๋ง๋ชจ ์ถ์ ๋์ฒด 13ํ์ ์ถ์ ํ์\n",
"[4] ์ ํ ๊ธฐ์ ํต๊ณ(์ค์ผ / ์ ์ ํ / ์๋ณธ)\n",
" ํญ๋ชฉ ์ค์ผ ์ ์ ํ ์๋ณธ\n",
" ์ด์ฉ์๊ฐ ์ต์๊ฐ(h) -25733.90 485.39 485.39\n",
" ์ด์ฉ์๊ฐ ํ๊ท (h) 11623.87 12523.40 12475.96\n",
"๊ธฐ๋๊ฑฐ๋ฆฌ ์ต๋๊ฐ(km) 476232660.71 490734.91 490734.91\n",
" ๊ฐ๋๋ฅ ์ ํจ ๊ฑด์ 218.00 219.00 220.00\n",
"๊ฐ๋๋ฅ ์ต๋๊ฐ(h/์ผ) 15.68 15.68 6.53\n",
" ์ ๊ฒ ๋ง๋ชจ ์ต๋๊ฐ 2.34 1.30 1.08\n",
" ํ๊ฒฝ๋ณ ํ๊ท ๋
ธํ ์ง์(๊ฐ์ค์น W0 ์ ์ฉ): {'cold': 0.8, 'normal': 0.848, 'rough': 0.846, 'salt': 0.83}\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
03. ์ ํ ๊ฐ๊ฐ์๊ฐ ๊ธฐ๋ฐ 1์ฐจ ์์ฌ ์๋ช
(RUL) ๊ณ์ฐ Baseline ์๊ณ ๋ฆฌ์ฆ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ ํ ๊ฐ๊ฐ์๊ฐ ๊ธฐ๋ฐ 1์ฐจ ์์ฌ ์๋ช
(RUL) ๊ณ์ฐ Baseline ์๊ณ ๋ฆฌ์ฆ์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ ํ ๊ฐ๊ฐ์๊ฐ ๊ธฐ๋ฐ 1์ฐจ ์์ฌ ์๋ช
(RUL) Baseline์ ๊ตฌํํ๊ณ EDA๋ก ์ค์ฐจ ์์ธ์ ์ฐพ๋๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ท์ ์ฃผ๊ธฐ์์ ์ฐฝ์ ๋น ํ ์ด์ฉ์๊ฐ์ ๋นผ๊ณ ์ผ ๊ฐ๋๋ฅ ๋ก ๋๋ ์์ฌ ์๋ช
์ฐ์ถํ๊ณ ๋ชจ๋ธ์ ์ ์ฉํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `notebooks/sup02_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": [
"\"\"\"์ ํ ๊ฐ๊ฐ์๊ฐ(๊ท์ ์ฐฝ์ ๋น ์ฃผ๊ธฐ - ๊ฒฝ๊ณผ ์ด์ฉ์๊ฐ) ๊ธฐ๋ฐ ์์ฌ ์๋ช
(RUL) Baseline์ ๊ตฌํํด ์ค์ฐจ์ ํ๊ฒฝ๋ณ ํธํฅยทํ๊ณ๋ฅผ ํ์ธํ๊ณ , EDA ์ฐจํธ 8์ข
ยท์๊ด๋ถ์(p-value)ยทํ๊ฒฝ๋ณ ์ฐจ์ด ๊ฒ์ ยท๋ถ๋๋ณ ์ํ ํ๋ ํ ๋ก ํต์ฌ ๋
ธํํ ์์ธ์ ์ฐพ๋๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy import stats\n",
"from sklearn.metrics import r2_score\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ฅ๋น ๋ณด์ ํํฉยท๊ณ ์ฅ ์ด๋ ฅ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์ฅ๋นยท์์น ์์)\n",
"# ๊ฐ์ ๋ถ๋ 6๊ฐ(U1~U6)์ ์ฅ๋น๊ฐ ์ด์ฉ ํ๊ฒฝ(normal/rough/salt/cold)์ ๋ฐ๋ผ ๋ค๋ฅธ ์๋๋ก ๋
ธํํํ๋ค. ์ฐฝ์ ๋น ํ ์ด์ฉ์๊ฐ(h_since)์ด\n",
"# ์ฅ๋น๋ง๋ค ์จ์ ์๋ช
(t_fail)์ ๋์ผ๋ฉด ๊ณ ์ฅ(event=1) ์ด๋ ฅ, ๋์ง ์์์ผ๋ฉด ํ์ฌ๊น์ง ์์กด(event=0, ๊ด์ธก ์ค๋์ ๋จ)์ด๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"ENVS = [\"normal\", \"rough\", \"salt\", \"cold\"]\n",
"ENV_W = {\"normal\": 1.00, \"rough\": 1.35, \"salt\": 1.25, \"cold\": 1.12} # ํ๊ฒฝ ๊ฐ์ค์น(์จ์ ์ ๋ต, ๊ฐ์ ๊ฐ): ํด์๋ก ์๋ช
์ด ์งง๋ค\n",
"ENV_KO = {\"normal\": \"์ผ๋ฐ\", \"rough\": \"ํ์ง\", \"salt\": \"์ผ๋ถ\", \"cold\": \"ํนํ\"}\n",
"UNITS = [\"U1\", \"U2\", \"U3\", \"U4\", \"U5\", \"U6\"]\n",
"UNIT_ENV = {\"U1\": [.7, .1, .1, .1], \"U2\": [.2, .6, .1, .1], \"U3\": [.2, .1, .6, .1], \"U4\": [.3, .1, .1, .5], \"U5\": [.5, .3, .1, .1], \"U6\": [.4, .1, .4, .1]}\n",
"ETYPES = [\"truck\", \"generator\", \"trailer\", \"radio\"]\n",
"CYCLE = 8000.0 # ๊ท์ ์ฐฝ์ ๋น ์ฃผ๊ธฐ(์ด์ฉ์๊ฐ h, ๊ฐ์ ๊ฐ)\n",
"KSHAPE = 12.0 # ์๋ช
๋ถํฌ ์์ด๋ธ ํ์(๋ง๋ชจํ ๊ณ ์ฅ, ํด์๋ก ์๋ช
์ด ๋ชจ์ฌ ์๋ค)\n",
"INSP_NOISE = 0.025 # ์ ๋น ์ ๊ฒ ๋ง๋ชจ ๋น์จ ์ธก์ ์ค์ฐจ(๊ฐ์ ๊ฐ)\n",
"\n",
"\n",
"def make_fleet(n=900, seed=0, env_w=None, shift=1.0):\n",
" \"\"\"์ฅ๋น n๋. env_w๋ก ์จ์ ํ๊ฒฝ ๊ฐ์ค์น๋ฅผ ๋ฐ๊ฟ ์ด์ ๋ถํฌ ๋ณํ๋ฅผ ๋ชจ์ํ ์ ์๋ค. shift>1์ด๋ฉด ๊ฐ๋๋ฅ ์ด ๋์์ง๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" w = env_w or ENV_W\n",
" unit = rng.choice(UNITS, n)\n",
" env = np.array([rng.choice(ENVS, p=UNIT_ENV[u]) for u in unit])\n",
" et = rng.choice(ETYPES, n, p=[.4, .2, .2, .2])\n",
" rate = rng.lognormal(np.log(3.0 * shift), 0.25, n) # ์ผ ํ๊ท ์ด์ฉ์๊ฐ(h/์ผ)\n",
" h_since = rng.uniform(300, 11500, n) # ๋ง์ง๋ง ์ฐฝ์ ๋น ์ดํ ์ด์ฉ์๊ฐ\n",
" n_ovh = rng.integers(0, 3, n)\n",
" hours = h_since + n_ovh * rng.uniform(5000, 8000, n) # ๋์ ์ด์ฉ์๊ฐ\n",
" km = hours * rng.normal(17, 3, n) * np.where(env == \"rough\", 0.8, 1.0) # ๋์ ๊ธฐ๋๊ฑฐ๋ฆฌ(km)\n",
" age_y = np.clip(hours / (rate * 365) * rng.uniform(0.9, 1.15, n), 0.5, 25)\n",
" unpl = rng.poisson(0.35 * h_since / 1000 * np.array([w[e] for e in env]) / 1.3) # ๋น๊ณํ ์ ๋น ๊ฑด์(ํ๊ฒฝ์ด ๊ฐํนํ ์๋ก ๋ง๋ค)\n",
" n_maint = rng.poisson(0.6 * h_since / 1000 + 1) # ๊ณํ ์ ๋น ๊ฑด์\n",
" scale = 9600 / np.array([w[e] for e in env]) * 0.985 ** unpl * np.exp(rng.normal(0, 0.04, n))\n",
" t_fail = scale * rng.weibull(KSHAPE, n) # ์ฐฝ์ ๋น ํ ๊ณ ์ฅ๊น์ง ์ด์ฉ์๊ฐ(์จ์ ์ ๋ต)\n",
" event = (t_fail <= h_since).astype(int)\n",
" dur = np.where(event == 1, t_fail, h_since)\n",
" df = pd.DataFrame({\"eq_id\": [f\"EQ{seed:02d}{i:04d}\" for i in range(n)], \"unit\": unit, \"env\": env, \"etype\": et, \"rate\": rate, \"hours\": hours, \"km\": km, \"age_y\": age_y, \"n_ovh\": n_ovh,\n",
" \"h_since\": h_since, \"unpl\": unpl, \"n_maint\": n_maint, \"t_fail\": t_fail, \"event\": event, \"dur\": dur})\n",
" df[\"insp\"] = np.clip(dur / t_fail + rng.normal(0, INSP_NOISE, n), 0, 1.3) # ๋ง์ง๋ง ์ ๋น ์ ๊ฒ์์ ์ธก์ ํ ๋ง๋ชจ ๋น์จ(์๋ชจ ์๋ช
/์ด ์๋ช
, ์ค์ฐจ ํฌํจ)\n",
" df[\"rul_h\"] = np.where(event == 0, t_fail - h_since, np.nan) # ์์กด ์ฅ๋น์ ๋จ์ ์ด์ฉ์๊ฐ(์ ๋ต)\n",
" df[\"rul_d\"] = df[\"rul_h\"] / df[\"rate\"] # ๋จ์ ์ผ์(์ ๋ต)\n",
" return df\n",
"\n",
"\n",
"def mape(y, p):\n",
" # [TODO : ํ์ ์ค์ต 3] ์ ํ ๊ฐ๊ฐ์๊ฐ ๊ธฐ๋ฐ 1์ฐจ ์์ฌ ์๋ช
(RUL) ๊ณ์ฐ Baseline ์๊ณ ๋ฆฌ์ฆ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def mae(y, p):\n",
" # [TODO : ํ์ ์ค์ต 3] ์ ํ ๊ฐ๊ฐ์๊ฐ ๊ธฐ๋ฐ 1์ฐจ ์์ฌ ์๋ช
(RUL) ๊ณ์ฐ Baseline ์๊ณ ๋ฆฌ์ฆ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def rmse(y, p):\n",
" return float(np.sqrt(np.mean((np.asarray(y, float) - np.asarray(p, float)) ** 2)))\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ๋
ธํํ ํ์ ํผ์ฒ์ ํ์ต ํ๋ณธ ๋ง๋ค๊ธฐ. ํ๊ฒฝ ๊ฐ์ค์น W0๋ ๋์
์์ ์ ์ ๋ฌธ๊ฐ ์ถ์ ๊ฐ์ด๋ฉฐ(์ค์ ๊ฐ์ค์น์ ๋ค๋ฆ) M9์์ ์ฌ๋ณด์ ํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"W0 = {\"normal\": 1.00, \"rough\": 1.15, \"salt\": 1.10, \"cold\": 1.05}\n",
"FEATS = [\"h_since\", \"hours\", \"km\", \"age_y\", \"rate\", \"n_maint\", \"unpl\", \"unpl_per_kh\", \"km_per_h\", \"n_ovh\", \"k_rough\", \"k_salt\", \"k_cold\", \"k_truck\", \"k_gen\", \"k_trailer\",\n",
" \"aging_idx\", \"lin_rul_d\", \"hs_frac\", \"insp\", \"insp_eta_h\"]\n",
"\n",
"\n",
"def add_features(df, w=None):\n",
" d = df.copy()\n",
" w = w or W0\n",
" d[\"unpl_per_kh\"] = d[\"unpl\"] / (d[\"h_since\"] / 1000 + 0.5)\n",
" d[\"km_per_h\"] = d[\"km\"] / d[\"hours\"]\n",
" for e in (\"rough\", \"salt\", \"cold\"):\n",
" d[\"k_\" + e] = (d[\"env\"] == e).astype(int)\n",
" d[\"k_truck\"], d[\"k_gen\"], d[\"k_trailer\"] = (d[\"etype\"] == \"truck\").astype(int), (d[\"etype\"] == \"generator\").astype(int), (d[\"etype\"] == \"trailer\").astype(int)\n",
" d[\"aging_idx\"] = d[\"h_since\"] * d[\"env\"].map(w) / CYCLE # ํ๊ฒฝ ๊ฐ์ค ๋
ธํ ์ง์(1์ด๋ฉด ์ฐฝ์ ๋น ์ฃผ๊ธฐ์ ๋๋ฌ)\n",
" d[\"lin_rul_d\"] = (CYCLE - d[\"h_since\"]) / d[\"rate\"] # ์ ํ ๊ฐ๊ฐ์๊ฐ ์์ฌ ์ผ์(Baseline)\n",
" d[\"hs_frac\"] = d[\"h_since\"] / CYCLE\n",
" d[\"insp_eta_h\"] = d[\"h_since\"] / np.maximum(d[\"insp\"], 0.05) # ์ ๊ฒ ๋ง๋ชจ ๋น์จ๋ก ํ์ฐํ ์ด ์๋ช
์ถ์ (h)\n",
" return d\n",
"\n",
"\n",
"def snapshots(df, k=4, seed=0):\n",
" \"\"\"๊ณ ์ฅ ์ด๋ ฅ ์ฅ๋น์ ๊ณผ๊ฑฐ ์์ ํ๋ณธ: ๊ณ ์ฅ ์ ์์ ์์ ์ ์ ๊ฒ๊ฐยท์ด์ฉ์๊ฐ๊ณผ ๊ทธ๋์ ๋จ์ ์ผ์. (์์กด ์ฅ๋น์ ๋ฏธ๋๋ ์ ์ ์์ผ๋ฏ๋ก ๊ณ ์ฅ ์ด๋ ฅ๋ง ์ด๋ค)\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" f = df[df[\"event\"] == 1]\n",
" out = []\n",
" for _ in range(k):\n",
" s = f.copy()\n",
" h = s[\"t_fail\"] * rng.uniform(0.2, 0.98, len(s))\n",
" ratio = h / s[\"h_since\"]\n",
" s[\"hours\"] = s[\"hours\"] - (s[\"h_since\"] - h)\n",
" s[\"km\"] = s[\"km\"] * ratio.clip(upper=1)\n",
" s[\"unpl\"] = np.floor(s[\"unpl\"] * ratio)\n",
" s[\"n_maint\"] = np.floor(s[\"n_maint\"] * ratio)\n",
" s[\"insp\"] = np.clip(h / s[\"t_fail\"] + rng.normal(0, INSP_NOISE, len(s)), 0, 1.3)\n",
" s[\"h_since\"] = h\n",
" s[\"rul_d\"] = (s[\"t_fail\"] - h) / s[\"rate\"]\n",
" s[\"event\"] = 0\n",
" out.append(s)\n",
" return pd.concat(out, ignore_index=True)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"df = make_fleet(900, seed=1)\n",
"d = add_features(df)\n",
"al = d[d.event == 0].copy()\n",
"al[\"err\"] = al[\"lin_rul_d\"] - al[\"rul_d\"] # ์์ = Baseline์ด ์๋ช
์ ๊ธธ๊ฒ ๋ณธ๋ค(์ํํ ๊ณผ๋ ์์ธก)\n",
"big = al[\"rul_d\"] >= 90\n",
"print(f\"[1] ๊ฐ์ ์ฅ๋น {len(d)}๋(๊ณ ์ฅ ์ด๋ ฅ {int(d.event.sum())}๋, ์์กด {len(al)}๋). Baseline RUL(์ผ) = (๊ท์ ์ฃผ๊ธฐ {CYCLE:.0f}h - ์ฐฝ์ ๋น ํ ์ด์ฉ์๊ฐ) / ์ผ ๊ฐ๋๋ฅ \")\n",
"print(f\" ์์กด ์ฅ๋น ํ๊ฐ: MAE {mae(al.rul_d, al.lin_rul_d):.0f}์ผ, RMSE {rmse(al.rul_d, al.lin_rul_d):.0f}์ผ, MAPE(์ฐธ RUL 90์ผ ์ด์) {mape(al.rul_d[big], al.lin_rul_d[big]):.1f}%, R2 {r2_score(al.rul_d, al.lin_rul_d):.3f}, ํ๊ท ํธํฅ {al.err.mean():+.0f}์ผ\")\n",
"over = (al[\"lin_rul_d\"] > al[\"rul_d\"] + 30)\n",
"print(f\" Baseline์ด ์ค์ ๋ณด๋ค 30์ผ ๋๊ฒ ๊ธธ๊ฒ ๋ณธ ์ฅ๋น {int(over.sum())}๋({over.mean():.0%}) - ๊ณ ์ฅ ์ ์ ์ ๋น ์์ ์ ๋์น ์ํ\")\n",
"fail_before = d[(d.event == 1) & (d.t_fail < CYCLE)]\n",
"print(f\" ๊ณ ์ฅ ์ด๋ ฅ {int(d.event.sum())}๋ ์ค ๊ท์ ์ฃผ๊ธฐ({CYCLE:.0f}h) ์ด์ ์ ๊ณ ์ฅ ๋ ์ฅ๋น {len(fail_before)}๋({len(fail_before) / d.event.sum():.0%}) -> ๊ณ ์ ์ฃผ๊ธฐ๋ ๊ฐํน ํ๊ฒฝ ์ฅ๋น๋ฅผ ์งํค์ง ๋ชปํ๋ค\")\n",
"\n",
"# ์กฐ๊ฑด๋ณ ์ค์ฐจ\n",
"conds = {\"์ผ๋ฐ ํ๊ฒฝ\": al.env == \"normal\", \"ํ์ง\": al.env == \"rough\", \"์ผ๋ถ\": al.env == \"salt\", \"ํนํ\": al.env == \"cold\", \"๋น๊ณํ ์ ๋น์จ ์์ 25%\": al.unpl_per_kh >= al.unpl_per_kh.quantile(.75),\n",
" \"์ผ ๊ฐ๋๋ฅ 4h ์ด์\": al.rate >= 4, \"์ ๊ฒ ๋ง๋ชจ 0.8 ์ด์(๋
ธํ)\": al.insp >= 0.8}\n",
"ct = pd.DataFrame([(k, int(m.sum()), mae(al.rul_d[m], al.lin_rul_d[m]), al.err[m].mean()) for k, m in conds.items()], columns=[\"์กฐ๊ฑด\", \"๋์\", \"MAE(์ผ)\", \"ํ๊ท ํธํฅ(์ผ)\"])\n",
"print(\"\\n[2] ์กฐ๊ฑด๋ณ Baseline ์ค์ฐจ(์์ = ๊ณผ๋ ์์ธก)\\n\" + ct.round(0).to_string(index=False))\n",
"print(\" -> ํ์งยท์ผ๋ถ ์ฅ๋น๋ ์๋ช
์ ๊ธธ๊ฒ(์ํ), ์ผ๋ฐ ํ๊ฒฝ ์ฅ๋น๋ ์งง๊ฒ(๊ณผ์ ์ ๋น) ๋ณด์ ํ๊ฒฝ๋ณ ํธํฅ ๋ฐฉํฅ์ด ๋ฐ๋๋ค. ํ๊ฒฝ ๊ฐ์ค์น ๋ฏธ๋ฐ์์ด ํ๊ณ๋ค.\")\n",
"\n",
"# ์๊ด๋ถ์\n",
"rows = []\n",
"for c in FEATS:\n",
" if al[c].nunique() < 2: continue\n",
" r, p = stats.spearmanr(al[c], al[\"err\"])\n",
" rows.append((c, r, p))\n",
"cr = pd.DataFrame(rows, columns=[\"ํผ์ฒ\", \"์คํผ์ด๋ง ์๊ด\", \"p-value\"]).assign(abs_r=lambda x: x[\"์คํผ์ด๋ง ์๊ด\"].abs()).sort_values(\"abs_r\", ascending=False)\n",
"sig = cr[(cr[\"abs_r\"] >= 0.2) & (cr[\"p-value\"] < 0.05)]\n",
"print(f\"\\n[3] Baseline ์ค์ฐจ(๋ถํธ ์์: ์์ = ๊ณผ๋ ์์ธก)์ ์ ์ํ ์๊ด(|r|>=0.2, p<0.05) ํผ์ฒ {len(sig)}๊ฐ (๋ชฉํ 5๊ฐ ์ด์: {'๋ฌ์ฑ' if len(sig) >= 5 else '๋ฏธ๋ฌ'})\")\n",
"print(sig.head(8)[[\"ํผ์ฒ\", \"์คํผ์ด๋ง ์๊ด\", \"p-value\"]].round(4).to_string(index=False))\n",
"print(\" (lin_rul_dยทh_sinceยทhs_frac๋ Baseline ์ฐ์๊ณผ ์ง์ ๊ฒน์น๋ฏ๋ก ํด์์์๋ ํ๊ฒฝ ๋๋ฏธยท์ ๊ฒ ๋ง๋ชจยท๊ธฐ๋ ๊ฐ๋ ๊ฐ์ ๋
๋ฆฝ ์์ธ์ ๋ณธ๋ค)\")\n",
"hi, lo = al[al.env.isin([\"rough\", \"salt\"])][\"err\"], al[al.env.isin([\"normal\"])][\"err\"]\n",
"u = stats.mannwhitneyu(hi, lo)\n",
"kw = stats.kruskal(*[g[\"insp_eta_h\"].values for _, g in al.groupby(\"env\")])\n",
"print(f\" ๊ฐ์ค๊ฒ์ : ํ์งยท์ผ๋ถ vs ์ผ๋ฐ ํ๊ฒฝ Baseline ํธํฅ ํ๊ท {hi.mean():+.0f}์ผ vs {lo.mean():+.0f}์ผ(Mann-Whitney p={u.pvalue:.1e}), ํ๊ฒฝ๋ณ ์ ๊ฒ ํ์ฐ ์ด ์๋ช
์ฐจ์ด Kruskal p={kw.pvalue:.1e}\")\n",
"\n",
"# ํ๋ ํ : ๋ถ๋๋ณ ๊ณ ์ํ(์ฐธ RUL 180์ผ ๋ฏธ๋ง) ์ฅ๋น\n",
"risk = al[al.rul_d < 180].groupby(\"unit\").size().sort_values(ascending=False)\n",
"cum = risk.cumsum() / risk.sum()\n",
"print(f\"\\n[4] ํ๋ ํ : ์ฐธ RUL 180์ผ ๋ฏธ๋ง ๊ณ ์ํ ์ฅ๋น {int(risk.sum())}๋, ์์ {int((cum <= 0.8).sum() + 1)}๊ฐ ๋ถ๋๊ฐ 80% ์ฐจ์ง -> \" + \", \".join(f\"{u} {n}๋\" for u, n in risk.items()))\n",
"print(\" ํต์ฌ ๋ฐ๊ฒฌ: ํ๊ฒฝ ๊ฐ์ค์น์ ์ ๋น ์ด๋ ฅยท์ ๊ฒ ๋ง๋ชจ๊ฐ Baseline์ด ๋์น๋ ์์ธ์ด๋ฉฐ, ์ด๋ M4 ํผ์ฒ ์ค๊ณ(ํ๊ฒฝ ๋๋ฏธ, ๋น๊ณํ ์ ๋น์จ, ์ ๊ฒ ๋ง๋ชจ)์ ๊ทผ๊ฑฐ๊ฐ ๋๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(2, 4, figsize=(18, 8))\n",
"ax[0, 0].boxplot([d.loc[d.env == e, \"insp_eta_h\"] for e in ENVS]); ax[0, 0].set_xticklabels([\"normal\", \"rough\", \"salt\", \"cold\"]); ax[0, 0].set_title(\"1 Estimated total life by environment (h)\")\n",
"ax[0, 1].scatter(d.h_since, d.dur, c=d.event, s=6, cmap=\"coolwarm\"); ax[0, 1].set_title(\"2 Hours since overhaul vs observed (red = failed)\")\n",
"ax[0, 2].hist(d.rate, 30); ax[0, 2].set_title(\"3 Daily usage rate (h/day)\")\n",
"ax[0, 3].scatter(d.h_since, d.unpl, s=6, c=d.env.map({e: i for i, e in enumerate(ENVS)})); ax[0, 3].set_title(\"4 Unplanned maintenance vs hours\")\n",
"ax[1, 0].bar(range(len(ct)), ct[\"ํ๊ท ํธํฅ(์ผ)\"]); ax[1, 0].set_xticks(range(len(ct))); ax[1, 0].set_xticklabels([f\"C{i + 1}\" for i in range(len(ct))]); ax[1, 0].set_title(\"5 Baseline bias by condition (days)\")\n",
"ax[1, 1].scatter(al.insp, al.hs_frac, s=6, c=al.env.map({e: i for i, e in enumerate(ENVS)})); ax[1, 1].set_title(\"6 Inspection wear vs hours fraction\")\n",
"ax[1, 2].scatter(al.rul_d, al.lin_rul_d, s=6); ax[1, 2].plot([0, 4000], [0, 4000], \"k--\"); ax[1, 2].set_title(\"7 True RUL vs Baseline RUL (days)\")\n",
"ax[1, 3].bar(risk.index, risk.values); ax[1, 3].set_title(\"8 High-risk equipment by unit (Pareto)\")\n",
"plt.tight_layout(); plt.show()\n",
"print(\"\\nEDA ์ฐจํธ 8์ข
์์ฑ ์๋ฃ(๋ชฉํ 8์ข
์ด์: ๋ฌ์ฑ)\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
03 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[1] ๊ฐ์ ์ฅ๋น 900๋(๊ณ ์ฅ ์ด๋ ฅ 288๋, ์์กด 612๋). Baseline RUL(์ผ) = (๊ท์ ์ฃผ๊ธฐ 8000h - ์ฐฝ์ ๋น ํ ์ด์ฉ์๊ฐ) / ์ผ ๊ฐ๋๋ฅ \n",
" ์์กด ์ฅ๋น ํ๊ฐ: MAE 366์ผ, RMSE 455์ผ, MAPE(์ฐธ RUL 90์ผ ์ด์) 51.8%, R2 0.740, ํ๊ท ํธํฅ -48์ผ\n",
" Baseline์ด ์ค์ ๋ณด๋ค 30์ผ ๋๊ฒ ๊ธธ๊ฒ ๋ณธ ์ฅ๋น 270๋(44%) - ๊ณ ์ฅ ์ ์ ์ ๋น ์์ ์ ๋์น ์ํ\n",
" ๊ณ ์ฅ ์ด๋ ฅ 288๋ ์ค ๊ท์ ์ฃผ๊ธฐ(8000h) ์ด์ ์ ๊ณ ์ฅ ๋ ์ฅ๋น 225๋(78%) -> ๊ณ ์ ์ฃผ๊ธฐ๋ ๊ฐํน ํ๊ฒฝ ์ฅ๋น๋ฅผ ์งํค์ง ๋ชปํ๋ค\n",
"\n",
"[2] ์กฐ๊ฑด๋ณ Baseline ์ค์ฐจ(์์ = ๊ณผ๋ ์์ธก)\n",
" ์กฐ๊ฑด ๋์ MAE(์ผ) ํ๊ท ํธํฅ(์ผ)\n",
" ์ผ๋ฐ ํ๊ฒฝ 261 431.0 -385.0\n",
" ํ์ง 102 434.0 426.0\n",
" ์ผ๋ถ 146 286.0 221.0\n",
" ํนํ 103 246.0 -45.0\n",
" ๋น๊ณํ ์ ๋น์จ ์์ 25% 153 380.0 51.0\n",
" ์ผ ๊ฐ๋๋ฅ 4h ์ด์ 71 236.0 -88.0\n",
"์ ๊ฒ ๋ง๋ชจ 0.8 ์ด์(๋
ธํ) 140 352.0 -19.0\n",
" -> ํ์งยท์ผ๋ถ ์ฅ๋น๋ ์๋ช
์ ๊ธธ๊ฒ(์ํ), ์ผ๋ฐ ํ๊ฒฝ ์ฅ๋น๋ ์งง๊ฒ(๊ณผ์ ์ ๋น) ๋ณด์ ํ๊ฒฝ๋ณ ํธํฅ ๋ฐฉํฅ์ด ๋ฐ๋๋ค. ํ๊ฒฝ ๊ฐ์ค์น ๋ฏธ๋ฐ์์ด ํ๊ณ๋ค.\n",
"\n",
"[3] Baseline ์ค์ฐจ(๋ถํธ ์์: ์์ = ๊ณผ๋ ์์ธก)์ ์ ์ํ ์๊ด(|r|>=0.2, p<0.05) ํผ์ฒ 7๊ฐ (๋ชฉํ 5๊ฐ ์ด์: ๋ฌ์ฑ)\n",
" ํผ์ฒ ์คํผ์ด๋ง ์๊ด p-value\n",
"insp_eta_h -0.8807 0.0\n",
" k_rough 0.4811 0.0\n",
" k_salt 0.3550 0.0\n",
" lin_rul_d 0.2540 0.0\n",
" h_since -0.2498 0.0\n",
" hs_frac -0.2498 0.0\n",
" km_per_h -0.2257 0.0\n",
" (lin_rul_dยทh_sinceยทhs_frac๋ Baseline ์ฐ์๊ณผ ์ง์ ๊ฒน์น๋ฏ๋ก ํด์์์๋ ํ๊ฒฝ ๋๋ฏธยท์ ๊ฒ ๋ง๋ชจยท๊ธฐ๋ ๊ฐ๋ ๊ฐ์ ๋
๋ฆฝ ์์ธ์ ๋ณธ๋ค)\n",
" ๊ฐ์ค๊ฒ์ : ํ์งยท์ผ๋ถ vs ์ผ๋ฐ ํ๊ฒฝ Baseline ํธํฅ ํ๊ท +305์ผ vs -385์ผ(Mann-Whitney p=1.9e-65), ํ๊ฒฝ๋ณ ์ ๊ฒ ํ์ฐ ์ด ์๋ช
์ฐจ์ด Kruskal p=1.1e-57\n",
"\n",
"[4] ํ๋ ํ : ์ฐธ RUL 180์ผ ๋ฏธ๋ง ๊ณ ์ํ ์ฅ๋น 46๋, ์์ 5๊ฐ ๋ถ๋๊ฐ 80% ์ฐจ์ง -> U5 12๋, U3 9๋, U4 8๋, U6 7๋, U2 5๋, U1 5๋\n",
" ํต์ฌ ๋ฐ๊ฒฌ: ํ๊ฒฝ ๊ฐ์ค์น์ ์ ๋น ์ด๋ ฅยท์ ๊ฒ ๋ง๋ชจ๊ฐ Baseline์ด ๋์น๋ ์์ธ์ด๋ฉฐ, ์ด๋ M4 ํผ์ฒ ์ค๊ณ(ํ๊ฒฝ ๋๋ฏธ, ๋น๊ณํ ์ ๋น์จ, ์ ๊ฒ ๋ง๋ชจ)์ ๊ทผ๊ฑฐ๊ฐ ๋๋ค.\n",
"\n",
"EDA ์ฐจํธ 8์ข
์์ฑ ์๋ฃ(๋ชฉํ 8์ข
์ด์: ๋ฌ์ฑ)\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
04. ์์กด ๋ถ์(Weibull/Cox) ๊ธฐ๋ฐ ์ฅ๋น/๋ถํ ๊ณ ์ฅ ํ๋ฅ ๊ณก์ ๋ฐ ๊ธฐ๋์๋ช
์์ธก ๋ชจ๋ธ ๊ณ ๋ํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์์กด ๋ถ์ ๊ธฐ๋ฐ ์ฅ๋น/๋ถํ ๊ณ ์ฅ ํ๋ฅ ๋ฐ ๊ธฐ๋์๋ช
์์ธก ๋ชจ๋ธ์ ๊ณ ๋ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์์ด๋ธ AFT์ Cox ๋น๋ก์ํ ๋ชจํ์ผ๋ก ๊ณ ์ฅ ํ๋ฅ ๊ณก์ ๊ณผ ๊ธฐ๋์๋ช
์ ์์ธกํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์์กด ๋ถ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฐ์ง ์๊ณ scipy๋ก ์์ด๋ธ AFT์ Cox ๋ชจํ์ ์ง์ ๊ตฌํํ๊ณ ๋์์ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/features/sup02_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": [
"\"\"\"์์กด ๋ถ์(์์ด๋ธ AFT ์ต๋์ฐ๋ยทCox ๋น๋ก์ํ์ scipy๋ก ์ง์ ๊ตฌํ)์ผ๋ก ํ๊ฒฝ๋ณ ๊ณ ์ฅ ํ๋ฅ ๊ณก์ ๊ณผ ๊ธฐ๋์๋ช
์ ์ถ์ ํ๊ณ , ํ๊ฒฝ ๊ฐ์ ๊ณ์๋ฅผ ์จ์ ์ ๋ต๊ณผ ๋น๊ตํ๋ฉฐ C-indexยท๊ธฐ๋์๋ช
MAPEยท1๋
๋ด ๊ณ ์ฅ ํ๋ฅ ๋ณด์ ์ผ๋ก ๋ชจ๋ธ์ ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ฅ๋น ๋ณด์ ํํฉยท๊ณ ์ฅ ์ด๋ ฅ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์ฅ๋นยท์์น ์์)\n",
"# ๊ฐ์ ๋ถ๋ 6๊ฐ(U1~U6)์ ์ฅ๋น๊ฐ ์ด์ฉ ํ๊ฒฝ(normal/rough/salt/cold)์ ๋ฐ๋ผ ๋ค๋ฅธ ์๋๋ก ๋
ธํํํ๋ค. ์ฐฝ์ ๋น ํ ์ด์ฉ์๊ฐ(h_since)์ด\n",
"# ์ฅ๋น๋ง๋ค ์จ์ ์๋ช
(t_fail)์ ๋์ผ๋ฉด ๊ณ ์ฅ(event=1) ์ด๋ ฅ, ๋์ง ์์์ผ๋ฉด ํ์ฌ๊น์ง ์์กด(event=0, ๊ด์ธก ์ค๋์ ๋จ)์ด๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"ENVS = [\"normal\", \"rough\", \"salt\", \"cold\"]\n",
"ENV_W = {\"normal\": 1.00, \"rough\": 1.35, \"salt\": 1.25, \"cold\": 1.12} # ํ๊ฒฝ ๊ฐ์ค์น(์จ์ ์ ๋ต, ๊ฐ์ ๊ฐ): ํด์๋ก ์๋ช
์ด ์งง๋ค\n",
"ENV_KO = {\"normal\": \"์ผ๋ฐ\", \"rough\": \"ํ์ง\", \"salt\": \"์ผ๋ถ\", \"cold\": \"ํนํ\"}\n",
"UNITS = [\"U1\", \"U2\", \"U3\", \"U4\", \"U5\", \"U6\"]\n",
"UNIT_ENV = {\"U1\": [.7, .1, .1, .1], \"U2\": [.2, .6, .1, .1], \"U3\": [.2, .1, .6, .1], \"U4\": [.3, .1, .1, .5], \"U5\": [.5, .3, .1, .1], \"U6\": [.4, .1, .4, .1]}\n",
"ETYPES = [\"truck\", \"generator\", \"trailer\", \"radio\"]\n",
"CYCLE = 8000.0 # ๊ท์ ์ฐฝ์ ๋น ์ฃผ๊ธฐ(์ด์ฉ์๊ฐ h, ๊ฐ์ ๊ฐ)\n",
"KSHAPE = 12.0 # ์๋ช
๋ถํฌ ์์ด๋ธ ํ์(๋ง๋ชจํ ๊ณ ์ฅ, ํด์๋ก ์๋ช
์ด ๋ชจ์ฌ ์๋ค)\n",
"INSP_NOISE = 0.025 # ์ ๋น ์ ๊ฒ ๋ง๋ชจ ๋น์จ ์ธก์ ์ค์ฐจ(๊ฐ์ ๊ฐ)\n",
"\n",
"\n",
"def make_fleet(n=900, seed=0, env_w=None, shift=1.0):\n",
" \"\"\"์ฅ๋น n๋. env_w๋ก ์จ์ ํ๊ฒฝ ๊ฐ์ค์น๋ฅผ ๋ฐ๊ฟ ์ด์ ๋ถํฌ ๋ณํ๋ฅผ ๋ชจ์ํ ์ ์๋ค. shift>1์ด๋ฉด ๊ฐ๋๋ฅ ์ด ๋์์ง๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" w = env_w or ENV_W\n",
" unit = rng.choice(UNITS, n)\n",
" env = np.array([rng.choice(ENVS, p=UNIT_ENV[u]) for u in unit])\n",
" et = rng.choice(ETYPES, n, p=[.4, .2, .2, .2])\n",
" rate = rng.lognormal(np.log(3.0 * shift), 0.25, n) # ์ผ ํ๊ท ์ด์ฉ์๊ฐ(h/์ผ)\n",
" h_since = rng.uniform(300, 11500, n) # ๋ง์ง๋ง ์ฐฝ์ ๋น ์ดํ ์ด์ฉ์๊ฐ\n",
" n_ovh = rng.integers(0, 3, n)\n",
" hours = h_since + n_ovh * rng.uniform(5000, 8000, n) # ๋์ ์ด์ฉ์๊ฐ\n",
" km = hours * rng.normal(17, 3, n) * np.where(env == \"rough\", 0.8, 1.0) # ๋์ ๊ธฐ๋๊ฑฐ๋ฆฌ(km)\n",
" age_y = np.clip(hours / (rate * 365) * rng.uniform(0.9, 1.15, n), 0.5, 25)\n",
" unpl = rng.poisson(0.35 * h_since / 1000 * np.array([w[e] for e in env]) / 1.3) # ๋น๊ณํ ์ ๋น ๊ฑด์(ํ๊ฒฝ์ด ๊ฐํนํ ์๋ก ๋ง๋ค)\n",
" n_maint = rng.poisson(0.6 * h_since / 1000 + 1) # ๊ณํ ์ ๋น ๊ฑด์\n",
" scale = 9600 / np.array([w[e] for e in env]) * 0.985 ** unpl * np.exp(rng.normal(0, 0.04, n))\n",
" t_fail = scale * rng.weibull(KSHAPE, n) # ์ฐฝ์ ๋น ํ ๊ณ ์ฅ๊น์ง ์ด์ฉ์๊ฐ(์จ์ ์ ๋ต)\n",
" event = (t_fail <= h_since).astype(int)\n",
" dur = np.where(event == 1, t_fail, h_since)\n",
" df = pd.DataFrame({\"eq_id\": [f\"EQ{seed:02d}{i:04d}\" for i in range(n)], \"unit\": unit, \"env\": env, \"etype\": et, \"rate\": rate, \"hours\": hours, \"km\": km, \"age_y\": age_y, \"n_ovh\": n_ovh,\n",
" \"h_since\": h_since, \"unpl\": unpl, \"n_maint\": n_maint, \"t_fail\": t_fail, \"event\": event, \"dur\": dur})\n",
" df[\"insp\"] = np.clip(dur / t_fail + rng.normal(0, INSP_NOISE, n), 0, 1.3) # ๋ง์ง๋ง ์ ๋น ์ ๊ฒ์์ ์ธก์ ํ ๋ง๋ชจ ๋น์จ(์๋ชจ ์๋ช
/์ด ์๋ช
, ์ค์ฐจ ํฌํจ)\n",
" df[\"rul_h\"] = np.where(event == 0, t_fail - h_since, np.nan) # ์์กด ์ฅ๋น์ ๋จ์ ์ด์ฉ์๊ฐ(์ ๋ต)\n",
" df[\"rul_d\"] = df[\"rul_h\"] / df[\"rate\"] # ๋จ์ ์ผ์(์ ๋ต)\n",
" return df\n",
"\n",
"\n",
"def mape(y, p):\n",
" # [TODO : ํ์ ์ค์ต 4] ์์กด ๋ถ์(Weibull/Cox) ๊ธฐ๋ฐ ์ฅ๋น/๋ถํ ๊ณ ์ฅ ํ๋ฅ ๊ณก์ ๋ฐ ๊ธฐ๋์๋ช
์์ธก ๋ชจ๋ธ ๊ณ ๋ํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def mae(y, p):\n",
" # [TODO : ํ์ ์ค์ต 4] ์์กด ๋ถ์(Weibull/Cox) ๊ธฐ๋ฐ ์ฅ๋น/๋ถํ ๊ณ ์ฅ ํ๋ฅ ๊ณก์ ๋ฐ ๊ธฐ๋์๋ช
์์ธก ๋ชจ๋ธ ๊ณ ๋ํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def rmse(y, p):\n",
" return float(np.sqrt(np.mean((np.asarray(y, float) - np.asarray(p, float)) ** 2)))\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ๋
ธํํ ํ์ ํผ์ฒ์ ํ์ต ํ๋ณธ ๋ง๋ค๊ธฐ. ํ๊ฒฝ ๊ฐ์ค์น W0๋ ๋์
์์ ์ ์ ๋ฌธ๊ฐ ์ถ์ ๊ฐ์ด๋ฉฐ(์ค์ ๊ฐ์ค์น์ ๋ค๋ฆ) M9์์ ์ฌ๋ณด์ ํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"W0 = {\"normal\": 1.00, \"rough\": 1.15, \"salt\": 1.10, \"cold\": 1.05}\n",
"FEATS = [\"h_since\", \"hours\", \"km\", \"age_y\", \"rate\", \"n_maint\", \"unpl\", \"unpl_per_kh\", \"km_per_h\", \"n_ovh\", \"k_rough\", \"k_salt\", \"k_cold\", \"k_truck\", \"k_gen\", \"k_trailer\",\n",
" \"aging_idx\", \"lin_rul_d\", \"hs_frac\", \"insp\", \"insp_eta_h\"]\n",
"\n",
"\n",
"def add_features(df, w=None):\n",
" d = df.copy()\n",
" w = w or W0\n",
" d[\"unpl_per_kh\"] = d[\"unpl\"] / (d[\"h_since\"] / 1000 + 0.5)\n",
" d[\"km_per_h\"] = d[\"km\"] / d[\"hours\"]\n",
" for e in (\"rough\", \"salt\", \"cold\"):\n",
" d[\"k_\" + e] = (d[\"env\"] == e).astype(int)\n",
" d[\"k_truck\"], d[\"k_gen\"], d[\"k_trailer\"] = (d[\"etype\"] == \"truck\").astype(int), (d[\"etype\"] == \"generator\").astype(int), (d[\"etype\"] == \"trailer\").astype(int)\n",
" d[\"aging_idx\"] = d[\"h_since\"] * d[\"env\"].map(w) / CYCLE # ํ๊ฒฝ ๊ฐ์ค ๋
ธํ ์ง์(1์ด๋ฉด ์ฐฝ์ ๋น ์ฃผ๊ธฐ์ ๋๋ฌ)\n",
" d[\"lin_rul_d\"] = (CYCLE - d[\"h_since\"]) / d[\"rate\"] # ์ ํ ๊ฐ๊ฐ์๊ฐ ์์ฌ ์ผ์(Baseline)\n",
" d[\"hs_frac\"] = d[\"h_since\"] / CYCLE\n",
" d[\"insp_eta_h\"] = d[\"h_since\"] / np.maximum(d[\"insp\"], 0.05) # ์ ๊ฒ ๋ง๋ชจ ๋น์จ๋ก ํ์ฐํ ์ด ์๋ช
์ถ์ (h)\n",
" return d\n",
"\n",
"\n",
"def snapshots(df, k=4, seed=0):\n",
" \"\"\"๊ณ ์ฅ ์ด๋ ฅ ์ฅ๋น์ ๊ณผ๊ฑฐ ์์ ํ๋ณธ: ๊ณ ์ฅ ์ ์์ ์์ ์ ์ ๊ฒ๊ฐยท์ด์ฉ์๊ฐ๊ณผ ๊ทธ๋์ ๋จ์ ์ผ์. (์์กด ์ฅ๋น์ ๋ฏธ๋๋ ์ ์ ์์ผ๋ฏ๋ก ๊ณ ์ฅ ์ด๋ ฅ๋ง ์ด๋ค)\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" f = df[df[\"event\"] == 1]\n",
" out = []\n",
" for _ in range(k):\n",
" s = f.copy()\n",
" h = s[\"t_fail\"] * rng.uniform(0.2, 0.98, len(s))\n",
" ratio = h / s[\"h_since\"]\n",
" s[\"hours\"] = s[\"hours\"] - (s[\"h_since\"] - h)\n",
" s[\"km\"] = s[\"km\"] * ratio.clip(upper=1)\n",
" s[\"unpl\"] = np.floor(s[\"unpl\"] * ratio)\n",
" s[\"n_maint\"] = np.floor(s[\"n_maint\"] * ratio)\n",
" s[\"insp\"] = np.clip(h / s[\"t_fail\"] + rng.normal(0, INSP_NOISE, len(s)), 0, 1.3)\n",
" s[\"h_since\"] = h\n",
" s[\"rul_d\"] = (s[\"t_fail\"] - h) / s[\"rate\"]\n",
" s[\"event\"] = 0\n",
" out.append(s)\n",
" return pd.concat(out, ignore_index=True)\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ์์กด ๋ถ์ ๋๊ตฌ(scipy๋ง ์ฌ์ฉ): ์์ด๋ธ AFT(๊ฐ์ ์๋ช
) ๋ชจํ๊ณผ Cox ๋น๋ก์ํ ๋ชจํ\n",
"# ---------------------------------------------------------------------------\n",
"from scipy.optimize import minimize\n",
"\n",
"\n",
"def aft_x(d):\n",
" return np.c_[np.ones(len(d)), d[\"env\"].eq(\"rough\"), d[\"env\"].eq(\"salt\"), d[\"env\"].eq(\"cold\"), d[\"unpl\"] / (d[\"h_since\"] / 1000 + 0.5)].astype(float)\n",
"\n",
"\n",
"AFT_COLS = [\"์ ํธ\", \"ํ์ง\", \"์ผ๋ถ\", \"ํนํ\", \"๋น๊ณํ ์ ๋น์จ\"]\n",
"\n",
"\n",
"def fit_aft(d):\n",
" \"\"\"์ฐฝ์ ๋น ํ ์ด์ฉ์๊ฐ dur(๊ณ ์ฅ์ด๋ฉด event=1, ์์กด์ด๋ฉด ์ค๋์ ๋จ)์ ๋ํ ์์ด๋ธ AFT ์ต๋์ฐ๋ ์ถ์ . ๋ฐํ: ๊ณ์(๋ง์ง๋ง์ log ํ์).\"\"\"\n",
" x, t, e = aft_x(d), d[\"dur\"].values, d[\"event\"].values\n",
"\n",
" def nll(p):\n",
" lam, k = np.exp(x @ p[:-1]), np.exp(p[-1])\n",
" z = (t / lam) ** k\n",
" return -(e * (np.log(k) - np.log(lam) + (k - 1) * (np.log(t) - np.log(lam))) - z).sum()\n",
"\n",
" r = minimize(nll, np.r_[np.log(8000), np.zeros(x.shape[1] - 1), np.log(5)], method=\"BFGS\")\n",
" return r.x\n",
"\n",
"\n",
"def aft_pred(p, d, q=0.5, h=None):\n",
" \"\"\"์ด๋ฏธ h(์ด์ฉ์๊ฐ)๊น์ง ์์กดํ๋ค๋ ์กฐ๊ฑด์์ ๋ถ์์ q ์์กด ์์ (์ด์ฉ์๊ฐ h ๊ธฐ์ค ์ด ์๋ช
)์ ๋๋ ค์ค๋ค. q=0.5๊ฐ ์กฐ๊ฑด๋ถ ์ค์๊ฐ.\"\"\"\n",
" lam, k = np.exp(aft_x(d) @ p[:-1]), np.exp(p[-1])\n",
" h = d[\"h_since\"].values if h is None else h\n",
" return lam * ((h / lam) ** k - np.log(q)) ** (1 / k)\n",
"\n",
"\n",
"def aft_fail_prob(p, d, h0, h1):\n",
" \"\"\"h0๊น์ง ์์กดํ ์ฅ๋น๊ฐ h0~h1 ์ฌ์ด์ ๊ณ ์ฅ ๋ ์กฐ๊ฑด๋ถ ํ๋ฅ .\"\"\"\n",
" lam, k = np.exp(aft_x(d) @ p[:-1]), np.exp(p[-1])\n",
" s0, s1 = np.exp(-(h0 / lam) ** k), np.exp(-(h1 / lam) ** k)\n",
" return 1 - s1 / s0\n",
"\n",
"\n",
"def fit_cox(d, iters=25):\n",
" \"\"\"Cox ๋น๋ก์ํ(Breslow ๊ทผ์ฌ) ๋ดํด-๋ฉ์จ. ๊ณต๋ณ๋: ํ์งยท์ผ๋ถยทํนํยท๋น๊ณํ ์ ๋น์จ. ๋ฐํ: ๊ณ์.\"\"\"\n",
" x = aft_x(d)[:, 1:]\n",
" o = np.argsort(-d[\"dur\"].values)\n",
" x, e = x[o], d[\"event\"].values[o]\n",
" b = np.zeros(x.shape[1])\n",
" for _ in range(iters):\n",
" r = np.exp(x @ b)\n",
" S0 = np.cumsum(r)\n",
" S1 = np.cumsum(r[:, None] * x, axis=0)\n",
" S2 = np.cumsum(r[:, None, None] * x[:, :, None] * x[:, None, :], axis=0)\n",
" m1 = S1 / S0[:, None]\n",
" g = ((x - m1) * e[:, None]).sum(axis=0)\n",
" H = -sum(e[i] * (S2[i] / S0[i] - np.outer(m1[i], m1[i])) for i in np.where(e == 1)[0])\n",
" step = np.linalg.solve(H - 1e-6 * np.eye(len(b)), g)\n",
" b = b - step\n",
" if np.abs(step).max() < 1e-7:\n",
" break\n",
" return b\n",
"\n",
"\n",
"def cindex(time, event, risk):\n",
" \"\"\"์ผ์น๋ C-index: ์ํ ์ ์๊ฐ ๋์ ์ชฝ์ด ๋จผ์ ๊ณ ์ฅ ๋ฌ๋๊ฐ(๊ด์ธก ๊ฐ๋ฅํ ์๋ง).\"\"\"\n",
" t, e, r = np.asarray(time), np.asarray(event), np.asarray(risk)\n",
" num = den = 0.0\n",
" for i in np.where(e == 1)[0]:\n",
" m = t > t[i]\n",
" den += m.sum()\n",
" num += (r[m] < r[i]).sum() + 0.5 * (r[m] == r[i]).sum()\n",
" return num / max(den, 1)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"tr = make_fleet(900, seed=1)\n",
"te = make_fleet(600, seed=2)\n",
"derived = [c for c in FEATS if c in (\"unpl_per_kh\", \"km_per_h\", \"k_rough\", \"k_salt\", \"k_cold\", \"k_truck\", \"k_gen\", \"k_trailer\", \"aging_idx\", \"lin_rul_d\", \"hs_frac\", \"insp_eta_h\")]\n",
"print(f\"[1] ํ์ ํผ์ฒ {len(derived)}๊ฐ(ํ๊ฒฝยท์ฅ๋น ๋๋ฏธ, ๋น๊ณํ ์ ๋น์จ, ํ๊ฒฝ ๊ฐ์ค ๋
ธํ ์ง์, ์ ๊ฒ ํ์ฐ ์๋ช
๋ฑ) / ํ์ต {len(tr)}๋(๊ณ ์ฅ {int(tr.event.sum())}๋, ์ค๋์ ๋จ {int((1 - tr.event).sum())}๋), ํ๊ฐ {len(te)}๋\")\n",
"\n",
"# ---------------- 1) ์์ด๋ธ AFTยทCox ์ถ์ ----------------\n",
"p = fit_aft(tr)\n",
"b = fit_cox(tr)\n",
"k = float(np.exp(p[-1]))\n",
"acc = pd.DataFrame({\"AFT ์๋ช
๋น exp(b)\": np.exp(p[1:5]), \"์จ์ ์ ๋ต ์๋ช
๋น\": [1 / ENV_W[\"rough\"], 1 / ENV_W[\"salt\"], 1 / ENV_W[\"cold\"], np.nan], \"Cox ์ํ๋น\": np.exp(b)}, index=AFT_COLS[1:])\n",
"print(f\"[2] ์์ด๋ธ AFT ํ์ k={k:.2f}(1๋ณด๋ค ํฌ๋ฉด ๋ง๋ชจํ ๊ณ ์ฅ), ๊ธฐ์ค ์๋ช
(์ผ๋ฐ ํ๊ฒฝ) {np.exp(p[0]):.0f}h\")\n",
"print(acc.round(3).to_string())\n",
"print(\" ํ๊ฒฝ ๊ฐ์(์๋ช
๋น < 1)์ด ๊ฐํน ํ๊ฒฝ์ผ์๋ก ํฌ๋ฉฐ, ์จ์ ์ ๋ต(1/๊ฐ์ค์น)๊ณผ ๋น์ทํ๊ฒ ๋ณต์๋๋ค. ๋น๊ณํ ์ ๋น์จ์ ์๋ช
์ ์ค์ด๋ ๋ฐฉํฅ์ด๋ค.\")\n",
"\n",
"# ---------------- 2) ๊ณ ์ฅ ํ๋ฅ ๊ณก์ ----------------\n",
"grid = np.array([5000, 6000, 7000, 8000, 9000, 10000])\n",
"typ = pd.DataFrame({\"env\": ENVS, \"unpl\": 1.0, \"h_since\": 1000.0})\n",
"typ[\"unpl\"] = 1.0 * (typ[\"h_since\"] / 1000 + 0.5)\n",
"lam = np.exp(aft_x(typ) @ p[:-1])\n",
"curves = pd.DataFrame({f\"{h}h\": 1 - np.exp(-(h / lam) ** k) for h in grid}, index=ENVS)\n",
"b10 = lam * (-np.log(0.9)) ** (1 / k)\n",
"print(\"\\n[3] ์ฐฝ์ ๋น ํ ์ด์ฉ์๊ฐ๋ณ ๋์ ๊ณ ์ฅ ํ๋ฅ (๋น๊ณํ ์ ๋น์จ 1.0/1000h ์ฅ๋น)\")\n",
"print(curves.apply(lambda c: c.map(\"{:.0%}\".format)).to_string())\n",
"print(\" B10 ์๋ช
(๊ณ ์ฅ 10% ์์ ): \" + \", \".join(f\"{e} {v:.0f}h\" for e, v in zip(ENVS, b10)) + f\" / ๊ท์ ์ฃผ๊ธฐ {CYCLE:.0f}h ์์ ๊ณ ์ฅ ํ๋ฅ ์ผ๋ฐ {curves.loc['normal', '8000h']:.0%}, ํ์ง {curves.loc['rough', '8000h']:.0%}\")\n",
"\n",
"# ---------------- 3) ํ๊ฐ: ๊ธฐ๋์๋ช
MAPE, C-index ----------------\n",
"zero = np.full(len(te), 1e-6)\n",
"life = aft_pred(p, te, 0.5, h=zero)\n",
"m1, m0 = mape(te.t_fail, life), mape(te.t_fail, np.full(len(te), CYCLE))\n",
"x = aft_x(te)[:, 1:]\n",
"ci_cox, ci_aft = cindex(te.dur, te.event, x @ b), cindex(te.dur, te.event, -life)\n",
"ci_base = cindex(te.dur, te.event, np.zeros(len(te)))\n",
"print(f\"\\n[4] ํ๊ฐ(์จ์ ์ ๋ต t_fail์ ๋ชจ์ ํ๊ฐ์๋ง ์ฌ์ฉ): ๊ธฐ๋์๋ช
(์ค์๊ฐ) MAPE {m1:.1f}% (๋ชฉํ 25% ์ดํ: {'๋ฌ์ฑ' if m1 <= 25 else '๋ฏธ๋ฌ'}), ๊ท์ ์ฃผ๊ธฐ {CYCLE:.0f}h๋ฅผ ๋ชจ๋์ ์ ์ฉํ๋ฉด {m0:.1f}%\")\n",
"print(f\" C-index: AFT {ci_aft:.3f}, Cox {ci_cox:.3f}, ํ๊ฒฝ ๋ฌด์(๋ฌด์ฐจ๋ณ) {ci_base:.3f} (0.5 = ๋ฌด์์)\")\n",
"by = pd.DataFrame({\"ํ๊ฒฝ\": te.env, \"์ ๋ต ํ๊ท \": te.t_fail, \"AFT\": life, \"๊ท์ ์ฃผ๊ธฐ\": CYCLE}).groupby(\"ํ๊ฒฝ\").mean().round(0)\n",
"print(\" ํ๊ฒฝ๋ณ ํ๊ท ์๋ช
(h):\\n\" + by.to_string())\n",
"\n",
"# ---------------- 4) 1๋
๋ด ๊ณ ์ฅ ํ๋ฅ ๋ณด์ (์์กด ์ฅ๋น) ----------------\n",
"a = te[te.event == 0].copy()\n",
"a[\"p1y\"] = aft_fail_prob(p, a, a[\"h_since\"].values, a[\"h_since\"].values + 365 * a[\"rate\"].values)\n",
"a[\"real\"] = (a[\"rul_d\"] < 365).astype(int)\n",
"a[\"bin\"] = pd.qcut(a[\"p1y\"], 5, labels=False, duplicates=\"drop\")\n",
"cal = a.groupby(\"bin\").agg(์์ธกํ๋ฅ =(\"p1y\", \"mean\"), ์ค์ ๋น์จ=(\"real\", \"mean\"), ๋์=(\"real\", \"size\"))\n",
"print(\"\\n[5] 1๋
๋ด ๊ณ ์ฅ ํ๋ฅ ๋ณด์ (์์กด ์ฅ๋น, ํ๋ฅ 5๋ถ์)\\n\" + cal.round(2).to_string())\n",
"brier = float(((a[\"p1y\"] - a[\"real\"]) ** 2).mean())\n",
"print(f\" Brier ์ ์ {brier:.3f}(๋ฌด์ ๋ณด ๊ธฐ์ค {float(a['real'].mean() * (1 - a['real'].mean())):.3f}). ์ ๊ฒ ๋ง๋ชจ๋ฅผ ์ฐ์ง ์๋ ์์กด ๋ชจํ์ ํ๋ฅ ์ ์์๋ ๋ง์ง๋ง ๊ฐ๋ณ ์ฅ๋น์ ์์ ์ ๊ฑฐ์น ๋ค -> M5์์ ์ ๊ฒ ๋ง๋ชจ ํผ์ฒ๋ก ๋ณด๊ฐํ๋ค.\")\n",
"\n",
"# ---------------- 5) ๋ณ์ ์ ๊ฑฐ ----------------\n",
"def ablate(cols):\n",
" keep = [0] + cols\n",
" sub_x = aft_x(tr)[:, keep]; t, e = tr[\"dur\"].values, tr[\"event\"].values\n",
" from scipy.optimize import minimize\n",
"\n",
" def nll(q):\n",
" lm, kk = np.exp(sub_x @ q[:-1]), np.exp(q[-1]); z = (t / lm) ** kk\n",
" return -(e * (np.log(kk) - np.log(lm) + (kk - 1) * (np.log(t) - np.log(lm))) - z).sum()\n",
"\n",
" q = minimize(nll, np.r_[np.log(8000), np.zeros(len(keep) - 1), np.log(5)], method=\"BFGS\").x\n",
" lm, kk = np.exp(aft_x(te)[:, keep] @ q[:-1]), np.exp(q[-1])\n",
" med = lm * np.log(2) ** (1 / kk)\n",
" return mape(te.t_fail, med), cindex(te.dur, te.event, -med)\n",
"\n",
"\n",
"rows = [(\"์ ์ฒด(ํ๊ฒฝ+๋น๊ณํ ์ ๋น์จ)\", *ablate([1, 2, 3, 4])), (\"ํ๊ฒฝ ์ ๊ฑฐ\", *ablate([4])), (\"๋น๊ณํ ์ ๋น์จ ์ ๊ฑฐ\", *ablate([1, 2, 3])), (\"๋ชจ๋ ์ ๊ฑฐ(์ ํธ๋ง)\", *ablate([]))]\n",
"print(\"\\n[6] ๋ณ์ ์ ๊ฑฐ ์คํ\\n\" + pd.DataFrame(rows, columns=[\"๋ชจํ\", \"๊ธฐ๋์๋ช
MAPE(%)\", \"C-index\"]).round(3).to_string(index=False))\n",
"\n",
"fig, ax = plt.subplots(1, 3, figsize=(15, 4.3))\n",
"hh = np.linspace(2000, 11000, 100)\n",
"for e, l in zip(ENVS, lam):\n",
" ax[0].plot(hh, 1 - np.exp(-(hh / l) ** k), label=e)\n",
"ax[0].axvline(CYCLE, c=\"k\", ls=\"--\"); ax[0].legend(); ax[0].set_title(\"Failure probability by environment (h since overhaul)\")\n",
"ax[1].plot(cal[\"์์ธกํ๋ฅ \"], cal[\"์ค์ ๋น์จ\"], \"o-\"); ax[1].plot([0, 1], [0, 1], \"k--\"); ax[1].set_title(\"1-year failure probability calibration\")\n",
"ax[2].bar([\"rough\", \"salt\", \"cold\", \"unplanned rate\"], np.exp(p[1:5])); ax[2].axhline(1, c=\"k\", ls=\"--\"); ax[2].set_title(\"AFT life ratio vs normal (exp(b))\"); ax[2].tick_params(axis=\"x\", rotation=20)\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
04 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[1] ํ์ ํผ์ฒ 12๊ฐ(ํ๊ฒฝยท์ฅ๋น ๋๋ฏธ, ๋น๊ณํ ์ ๋น์จ, ํ๊ฒฝ ๊ฐ์ค ๋
ธํ ์ง์, ์ ๊ฒ ํ์ฐ ์๋ช
๋ฑ) / ํ์ต 900๋(๊ณ ์ฅ 288๋, ์ค๋์ ๋จ 612๋), ํ๊ฐ 600๋\n",
"[2] ์์ด๋ธ AFT ํ์ k=10.83(1๋ณด๋ค ํฌ๋ฉด ๋ง๋ชจํ ๊ณ ์ฅ), ๊ธฐ์ค ์๋ช
(์ผ๋ฐ ํ๊ฒฝ) 9602h\n",
" AFT ์๋ช
๋น exp(b) ์จ์ ์ ๋ต ์๋ช
๋น Cox ์ํ๋น\n",
"ํ์ง 0.752 0.741 20.984\n",
"์ผ๋ถ 0.802 0.800 10.983\n",
"ํนํ 0.878 0.893 4.253\n",
"๋น๊ณํ ์ ๋น์จ 0.889 NaN 3.418\n",
" ํ๊ฒฝ ๊ฐ์(์๋ช
๋น < 1)์ด ๊ฐํน ํ๊ฒฝ์ผ์๋ก ํฌ๋ฉฐ, ์จ์ ์ ๋ต(1/๊ฐ์ค์น)๊ณผ ๋น์ทํ๊ฒ ๋ณต์๋๋ค. ๋น๊ณํ ์ ๋น์จ์ ์๋ช
์ ์ค์ด๋ ๋ฐฉํฅ์ด๋ค.\n",
"\n",
"[3] ์ฐฝ์ ๋น ํ ์ด์ฉ์๊ฐ๋ณ ๋์ ๊ณ ์ฅ ํ๋ฅ (๋น๊ณํ ์ ๋น์จ 1.0/1000h ์ฅ๋น)\n",
" 5000h 6000h 7000h 8000h 9000h 10000h\n",
"normal 0% 2% 11% 39% 83% 100%\n",
"rough 6% 38% 92% 100% 100% 100%\n",
"salt 3% 21% 72% 100% 100% 100%\n",
"cold 1% 9% 38% 87% 100% 100%\n",
" B10 ์๋ช
(๊ณ ์ฅ 10% ์์ ): normal 6936h, rough 5218h, salt 5560h, cold 6089h / ๊ท์ ์ฃผ๊ธฐ 8000h ์์ ๊ณ ์ฅ ํ๋ฅ ์ผ๋ฐ 39%, ํ์ง 100%\n",
"\n",
"[4] ํ๊ฐ(์จ์ ์ ๋ต t_fail์ ๋ชจ์ ํ๊ฐ์๋ง ์ฌ์ฉ): ๊ธฐ๋์๋ช
(์ค์๊ฐ) MAPE 10.2% (๋ชฉํ 25% ์ดํ: ๋ฌ์ฑ), ๊ท์ ์ฃผ๊ธฐ 8000h๋ฅผ ๋ชจ๋์ ์ ์ฉํ๋ฉด 15.2%\n",
" C-index: AFT 0.767, Cox 0.767, ํ๊ฒฝ ๋ฌด์(๋ฌด์ฐจ๋ณ) 0.500 (0.5 = ๋ฌด์์)\n",
" ํ๊ฒฝ๋ณ ํ๊ท ์๋ช
(h):\n",
" ์ ๋ต ํ๊ท AFT ๊ท์ ์ฃผ๊ธฐ\n",
"ํ๊ฒฝ \n",
"cold 7969.0 7890.0 8000.0\n",
"normal 8992.0 9046.0 8000.0\n",
"rough 6563.0 6700.0 8000.0\n",
"salt 7231.0 7167.0 8000.0\n",
"\n",
"[5] 1๋
๋ด ๊ณ ์ฅ ํ๋ฅ ๋ณด์ (์์กด ์ฅ๋น, ํ๋ฅ 5๋ถ์)\n",
" ์์ธกํ๋ฅ ์ค์ ๋น์จ ๋์\n",
"bin \n",
"0 0.00 0.00 78\n",
"1 0.00 0.00 78\n",
"2 0.01 0.00 78\n",
"3 0.09 0.05 78\n",
"4 0.64 0.59 78\n",
" Brier ์ ์ 0.045(๋ฌด์ ๋ณด ๊ธฐ์ค 0.112). ์ ๊ฒ ๋ง๋ชจ๋ฅผ ์ฐ์ง ์๋ ์์กด ๋ชจํ์ ํ๋ฅ ์ ์์๋ ๋ง์ง๋ง ๊ฐ๋ณ ์ฅ๋น์ ์์ ์ ๊ฑฐ์น ๋ค -> M5์์ ์ ๊ฒ ๋ง๋ชจ ํผ์ฒ๋ก ๋ณด๊ฐํ๋ค.\n",
"\n",
"[6] ๋ณ์ ์ ๊ฑฐ ์คํ\n",
" ๋ชจํ ๊ธฐ๋์๋ช
MAPE(%) C-index\n",
"์ ์ฒด(ํ๊ฒฝ+๋น๊ณํ ์ ๋น์จ) 10.152 0.767\n",
"... (์ดํ ์๋ต)\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
05. ์์ฐ ๋ฐ ์ ๋น์ฐฝ ๋ฅ๋ ฅ ์ฐ๊ณ ์ต์ ๊ณํ์ ๋น(Overhaul) ์ผ์ ์ถ์ฒ ์์ง ๊ฐ๋ฐ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ ๋น์ฐฝ ๋ฅ๋ ฅ ์ฐ๊ณ ์ต์ ๊ณํ์ ๋น(Overhaul) ์ผ์ ์ถ์ฒ ์์ง์ ๊ฐ๋ฐํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์์ฐ๊ณผ ์ ๋น์ฐฝ ๋ฅ๋ ฅ์ ์ฐ๊ณํ 12๊ฐ์ ๊ณํ์ ๋น ์ผ์ ์ถ์ฒ ์์ง์ ๊ฐ๋ฐํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์์ด๋ธ AFT์ ์ ๊ฒ ๋ง๋ชจ ํผ์ฒ๋ฅผ ์ฐ๋ GBR์ ์์๋ธํด ์์ฌ ์๋ช
์ ์์ธกํ๊ณ ์ ํ ๊ฐ๊ฐ์๊ฐ ๊ธฐ์ค๊ณผ ๋น๊ต ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/sup02_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": [
"\"\"\"์์ด๋ธ AFT์ ์ ๊ฒ ๋ง๋ชจ ํผ์ฒ๋ฅผ ์ฐ๋ ๊ทธ๋๋์ธํธ ๋ถ์คํ
์ ๊ฒฐํฉํ ์์ฌ ์๋ช
(RUL) ์์ธก ์์ง์ ๊ธฐ์ค ๋ชจ๋ธ ๋๋น MAEยทRMSEยทR2๋ก ๊ฒ์ฆํ๊ณ , ์ ๋น์ฐฝ ์ ๋ฅ๋ ฅยท์์ฐยท์ํจ์ฑ ๋ถํ ํคํธ ์ ํจ๊ธฐ๊ฐ ์ ์ฝ ์๋ ์ ์๊ณํ์ผ๋ก ์ต์ ๊ณํ์ ๋น(์ฐฝ์ ๋น) ์ผ์ ์ ์ถ์ฒํด ๊ท์ ์ฃผ๊ธฐ ๊ณ ์ ์ผ์ ๊ณผ ๋น์ฉ์ ๋น๊ตํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.ensemble import GradientBoostingRegressor\n",
"from sklearn.metrics import r2_score\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ฅ๋น ๋ณด์ ํํฉยท๊ณ ์ฅ ์ด๋ ฅ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์ฅ๋นยท์์น ์์)\n",
"# ๊ฐ์ ๋ถ๋ 6๊ฐ(U1~U6)์ ์ฅ๋น๊ฐ ์ด์ฉ ํ๊ฒฝ(normal/rough/salt/cold)์ ๋ฐ๋ผ ๋ค๋ฅธ ์๋๋ก ๋
ธํํํ๋ค. ์ฐฝ์ ๋น ํ ์ด์ฉ์๊ฐ(h_since)์ด\n",
"# ์ฅ๋น๋ง๋ค ์จ์ ์๋ช
(t_fail)์ ๋์ผ๋ฉด ๊ณ ์ฅ(event=1) ์ด๋ ฅ, ๋์ง ์์์ผ๋ฉด ํ์ฌ๊น์ง ์์กด(event=0, ๊ด์ธก ์ค๋์ ๋จ)์ด๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"ENVS = [\"normal\", \"rough\", \"salt\", \"cold\"]\n",
"ENV_W = {\"normal\": 1.00, \"rough\": 1.35, \"salt\": 1.25, \"cold\": 1.12} # ํ๊ฒฝ ๊ฐ์ค์น(์จ์ ์ ๋ต, ๊ฐ์ ๊ฐ): ํด์๋ก ์๋ช
์ด ์งง๋ค\n",
"ENV_KO = {\"normal\": \"์ผ๋ฐ\", \"rough\": \"ํ์ง\", \"salt\": \"์ผ๋ถ\", \"cold\": \"ํนํ\"}\n",
"UNITS = [\"U1\", \"U2\", \"U3\", \"U4\", \"U5\", \"U6\"]\n",
"UNIT_ENV = {\"U1\": [.7, .1, .1, .1], \"U2\": [.2, .6, .1, .1], \"U3\": [.2, .1, .6, .1], \"U4\": [.3, .1, .1, .5], \"U5\": [.5, .3, .1, .1], \"U6\": [.4, .1, .4, .1]}\n",
"ETYPES = [\"truck\", \"generator\", \"trailer\", \"radio\"]\n",
"CYCLE = 8000.0 # ๊ท์ ์ฐฝ์ ๋น ์ฃผ๊ธฐ(์ด์ฉ์๊ฐ h, ๊ฐ์ ๊ฐ)\n",
"KSHAPE = 12.0 # ์๋ช
๋ถํฌ ์์ด๋ธ ํ์(๋ง๋ชจํ ๊ณ ์ฅ, ํด์๋ก ์๋ช
์ด ๋ชจ์ฌ ์๋ค)\n",
"INSP_NOISE = 0.025 # ์ ๋น ์ ๊ฒ ๋ง๋ชจ ๋น์จ ์ธก์ ์ค์ฐจ(๊ฐ์ ๊ฐ)\n",
"\n",
"\n",
"def make_fleet(n=900, seed=0, env_w=None, shift=1.0):\n",
" \"\"\"์ฅ๋น n๋. env_w๋ก ์จ์ ํ๊ฒฝ ๊ฐ์ค์น๋ฅผ ๋ฐ๊ฟ ์ด์ ๋ถํฌ ๋ณํ๋ฅผ ๋ชจ์ํ ์ ์๋ค. shift>1์ด๋ฉด ๊ฐ๋๋ฅ ์ด ๋์์ง๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" w = env_w or ENV_W\n",
" unit = rng.choice(UNITS, n)\n",
" env = np.array([rng.choice(ENVS, p=UNIT_ENV[u]) for u in unit])\n",
" et = rng.choice(ETYPES, n, p=[.4, .2, .2, .2])\n",
" rate = rng.lognormal(np.log(3.0 * shift), 0.25, n) # ์ผ ํ๊ท ์ด์ฉ์๊ฐ(h/์ผ)\n",
" h_since = rng.uniform(300, 11500, n) # ๋ง์ง๋ง ์ฐฝ์ ๋น ์ดํ ์ด์ฉ์๊ฐ\n",
" n_ovh = rng.integers(0, 3, n)\n",
" hours = h_since + n_ovh * rng.uniform(5000, 8000, n) # ๋์ ์ด์ฉ์๊ฐ\n",
" km = hours * rng.normal(17, 3, n) * np.where(env == \"rough\", 0.8, 1.0) # ๋์ ๊ธฐ๋๊ฑฐ๋ฆฌ(km)\n",
" age_y = np.clip(hours / (rate * 365) * rng.uniform(0.9, 1.15, n), 0.5, 25)\n",
" unpl = rng.poisson(0.35 * h_since / 1000 * np.array([w[e] for e in env]) / 1.3) # ๋น๊ณํ ์ ๋น ๊ฑด์(ํ๊ฒฝ์ด ๊ฐํนํ ์๋ก ๋ง๋ค)\n",
" n_maint = rng.poisson(0.6 * h_since / 1000 + 1) # ๊ณํ ์ ๋น ๊ฑด์\n",
" scale = 9600 / np.array([w[e] for e in env]) * 0.985 ** unpl * np.exp(rng.normal(0, 0.04, n))\n",
" t_fail = scale * rng.weibull(KSHAPE, n) # ์ฐฝ์ ๋น ํ ๊ณ ์ฅ๊น์ง ์ด์ฉ์๊ฐ(์จ์ ์ ๋ต)\n",
" event = (t_fail <= h_since).astype(int)\n",
" dur = np.where(event == 1, t_fail, h_since)\n",
" df = pd.DataFrame({\"eq_id\": [f\"EQ{seed:02d}{i:04d}\" for i in range(n)], \"unit\": unit, \"env\": env, \"etype\": et, \"rate\": rate, \"hours\": hours, \"km\": km, \"age_y\": age_y, \"n_ovh\": n_ovh,\n",
" \"h_since\": h_since, \"unpl\": unpl, \"n_maint\": n_maint, \"t_fail\": t_fail, \"event\": event, \"dur\": dur})\n",
" df[\"insp\"] = np.clip(dur / t_fail + rng.normal(0, INSP_NOISE, n), 0, 1.3) # ๋ง์ง๋ง ์ ๋น ์ ๊ฒ์์ ์ธก์ ํ ๋ง๋ชจ ๋น์จ(์๋ชจ ์๋ช
/์ด ์๋ช
, ์ค์ฐจ ํฌํจ)\n",
" df[\"rul_h\"] = np.where(event == 0, t_fail - h_since, np.nan) # ์์กด ์ฅ๋น์ ๋จ์ ์ด์ฉ์๊ฐ(์ ๋ต)\n",
" df[\"rul_d\"] = df[\"rul_h\"] / df[\"rate\"] # ๋จ์ ์ผ์(์ ๋ต)\n",
" return df\n",
"\n",
"\n",
"def mape(y, p):\n",
" # [TODO : ํ์ ์ค์ต 5] ์์ฐ ๋ฐ ์ ๋น์ฐฝ ๋ฅ๋ ฅ ์ฐ๊ณ ์ต์ ๊ณํ์ ๋น(Overhaul) ์ผ์ ์ถ์ฒ ์์ง ๊ฐ๋ฐ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def mae(y, p):\n",
" # [TODO : ํ์ ์ค์ต 5] ์์ฐ ๋ฐ ์ ๋น์ฐฝ ๋ฅ๋ ฅ ์ฐ๊ณ ์ต์ ๊ณํ์ ๋น(Overhaul) ์ผ์ ์ถ์ฒ ์์ง ๊ฐ๋ฐ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def rmse(y, p):\n",
" return float(np.sqrt(np.mean((np.asarray(y, float) - np.asarray(p, float)) ** 2)))\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ๋
ธํํ ํ์ ํผ์ฒ์ ํ์ต ํ๋ณธ ๋ง๋ค๊ธฐ. ํ๊ฒฝ ๊ฐ์ค์น W0๋ ๋์
์์ ์ ์ ๋ฌธ๊ฐ ์ถ์ ๊ฐ์ด๋ฉฐ(์ค์ ๊ฐ์ค์น์ ๋ค๋ฆ) M9์์ ์ฌ๋ณด์ ํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"W0 = {\"normal\": 1.00, \"rough\": 1.15, \"salt\": 1.10, \"cold\": 1.05}\n",
"FEATS = [\"h_since\", \"hours\", \"km\", \"age_y\", \"rate\", \"n_maint\", \"unpl\", \"unpl_per_kh\", \"km_per_h\", \"n_ovh\", \"k_rough\", \"k_salt\", \"k_cold\", \"k_truck\", \"k_gen\", \"k_trailer\",\n",
" \"aging_idx\", \"lin_rul_d\", \"hs_frac\", \"insp\", \"insp_eta_h\"]\n",
"\n",
"\n",
"def add_features(df, w=None):\n",
" d = df.copy()\n",
" w = w or W0\n",
" d[\"unpl_per_kh\"] = d[\"unpl\"] / (d[\"h_since\"] / 1000 + 0.5)\n",
" d[\"km_per_h\"] = d[\"km\"] / d[\"hours\"]\n",
" for e in (\"rough\", \"salt\", \"cold\"):\n",
" d[\"k_\" + e] = (d[\"env\"] == e).astype(int)\n",
" d[\"k_truck\"], d[\"k_gen\"], d[\"k_trailer\"] = (d[\"etype\"] == \"truck\").astype(int), (d[\"etype\"] == \"generator\").astype(int), (d[\"etype\"] == \"trailer\").astype(int)\n",
" d[\"aging_idx\"] = d[\"h_since\"] * d[\"env\"].map(w) / CYCLE # ํ๊ฒฝ ๊ฐ์ค ๋
ธํ ์ง์(1์ด๋ฉด ์ฐฝ์ ๋น ์ฃผ๊ธฐ์ ๋๋ฌ)\n",
" d[\"lin_rul_d\"] = (CYCLE - d[\"h_since\"]) / d[\"rate\"] # ์ ํ ๊ฐ๊ฐ์๊ฐ ์์ฌ ์ผ์(Baseline)\n",
" d[\"hs_frac\"] = d[\"h_since\"] / CYCLE\n",
" d[\"insp_eta_h\"] = d[\"h_since\"] / np.maximum(d[\"insp\"], 0.05) # ์ ๊ฒ ๋ง๋ชจ ๋น์จ๋ก ํ์ฐํ ์ด ์๋ช
์ถ์ (h)\n",
" return d\n",
"\n",
"\n",
"def snapshots(df, k=4, seed=0):\n",
" \"\"\"๊ณ ์ฅ ์ด๋ ฅ ์ฅ๋น์ ๊ณผ๊ฑฐ ์์ ํ๋ณธ: ๊ณ ์ฅ ์ ์์ ์์ ์ ์ ๊ฒ๊ฐยท์ด์ฉ์๊ฐ๊ณผ ๊ทธ๋์ ๋จ์ ์ผ์. (์์กด ์ฅ๋น์ ๋ฏธ๋๋ ์ ์ ์์ผ๋ฏ๋ก ๊ณ ์ฅ ์ด๋ ฅ๋ง ์ด๋ค)\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" f = df[df[\"event\"] == 1]\n",
" out = []\n",
" for _ in range(k):\n",
" s = f.copy()\n",
" h = s[\"t_fail\"] * rng.uniform(0.2, 0.98, len(s))\n",
" ratio = h / s[\"h_since\"]\n",
" s[\"hours\"] = s[\"hours\"] - (s[\"h_since\"] - h)\n",
" s[\"km\"] = s[\"km\"] * ratio.clip(upper=1)\n",
" s[\"unpl\"] = np.floor(s[\"unpl\"] * ratio)\n",
" s[\"n_maint\"] = np.floor(s[\"n_maint\"] * ratio)\n",
" s[\"insp\"] = np.clip(h / s[\"t_fail\"] + rng.normal(0, INSP_NOISE, len(s)), 0, 1.3)\n",
" s[\"h_since\"] = h\n",
" s[\"rul_d\"] = (s[\"t_fail\"] - h) / s[\"rate\"]\n",
" s[\"event\"] = 0\n",
" out.append(s)\n",
" return pd.concat(out, ignore_index=True)\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ์์กด ๋ถ์ ๋๊ตฌ(scipy๋ง ์ฌ์ฉ): ์์ด๋ธ AFT(๊ฐ์ ์๋ช
) ๋ชจํ๊ณผ Cox ๋น๋ก์ํ ๋ชจํ\n",
"# ---------------------------------------------------------------------------\n",
"from scipy.optimize import minimize\n",
"\n",
"\n",
"def aft_x(d):\n",
" return np.c_[np.ones(len(d)), d[\"env\"].eq(\"rough\"), d[\"env\"].eq(\"salt\"), d[\"env\"].eq(\"cold\"), d[\"unpl\"] / (d[\"h_since\"] / 1000 + 0.5)].astype(float)\n",
"\n",
"\n",
"AFT_COLS = [\"์ ํธ\", \"ํ์ง\", \"์ผ๋ถ\", \"ํนํ\", \"๋น๊ณํ ์ ๋น์จ\"]\n",
"\n",
"\n",
"def fit_aft(d):\n",
" \"\"\"์ฐฝ์ ๋น ํ ์ด์ฉ์๊ฐ dur(๊ณ ์ฅ์ด๋ฉด event=1, ์์กด์ด๋ฉด ์ค๋์ ๋จ)์ ๋ํ ์์ด๋ธ AFT ์ต๋์ฐ๋ ์ถ์ . ๋ฐํ: ๊ณ์(๋ง์ง๋ง์ log ํ์).\"\"\"\n",
" x, t, e = aft_x(d), d[\"dur\"].values, d[\"event\"].values\n",
"\n",
" def nll(p):\n",
" lam, k = np.exp(x @ p[:-1]), np.exp(p[-1])\n",
" z = (t / lam) ** k\n",
" return -(e * (np.log(k) - np.log(lam) + (k - 1) * (np.log(t) - np.log(lam))) - z).sum()\n",
"\n",
" r = minimize(nll, np.r_[np.log(8000), np.zeros(x.shape[1] - 1), np.log(5)], method=\"BFGS\")\n",
" return r.x\n",
"\n",
"\n",
"def aft_pred(p, d, q=0.5, h=None):\n",
" \"\"\"์ด๋ฏธ h(์ด์ฉ์๊ฐ)๊น์ง ์์กดํ๋ค๋ ์กฐ๊ฑด์์ ๋ถ์์ q ์์กด ์์ (์ด์ฉ์๊ฐ h ๊ธฐ์ค ์ด ์๋ช
)์ ๋๋ ค์ค๋ค. q=0.5๊ฐ ์กฐ๊ฑด๋ถ ์ค์๊ฐ.\"\"\"\n",
" lam, k = np.exp(aft_x(d) @ p[:-1]), np.exp(p[-1])\n",
" h = d[\"h_since\"].values if h is None else h\n",
" return lam * ((h / lam) ** k - np.log(q)) ** (1 / k)\n",
"\n",
"\n",
"def aft_fail_prob(p, d, h0, h1):\n",
" \"\"\"h0๊น์ง ์์กดํ ์ฅ๋น๊ฐ h0~h1 ์ฌ์ด์ ๊ณ ์ฅ ๋ ์กฐ๊ฑด๋ถ ํ๋ฅ .\"\"\"\n",
" lam, k = np.exp(aft_x(d) @ p[:-1]), np.exp(p[-1])\n",
" s0, s1 = np.exp(-(h0 / lam) ** k), np.exp(-(h1 / lam) ** k)\n",
" return 1 - s1 / s0\n",
"\n",
"\n",
"def fit_cox(d, iters=25):\n",
" \"\"\"Cox ๋น๋ก์ํ(Breslow ๊ทผ์ฌ) ๋ดํด-๋ฉ์จ. ๊ณต๋ณ๋: ํ์งยท์ผ๋ถยทํนํยท๋น๊ณํ ์ ๋น์จ. ๋ฐํ: ๊ณ์.\"\"\"\n",
" x = aft_x(d)[:, 1:]\n",
" o = np.argsort(-d[\"dur\"].values)\n",
" x, e = x[o], d[\"event\"].values[o]\n",
" b = np.zeros(x.shape[1])\n",
" for _ in range(iters):\n",
" r = np.exp(x @ b)\n",
" S0 = np.cumsum(r)\n",
" S1 = np.cumsum(r[:, None] * x, axis=0)\n",
" S2 = np.cumsum(r[:, None, None] * x[:, :, None] * x[:, None, :], axis=0)\n",
" m1 = S1 / S0[:, None]\n",
" g = ((x - m1) * e[:, None]).sum(axis=0)\n",
" H = -sum(e[i] * (S2[i] / S0[i] - np.outer(m1[i], m1[i])) for i in np.where(e == 1)[0])\n",
" step = np.linalg.solve(H - 1e-6 * np.eye(len(b)), g)\n",
" b = b - step\n",
" if np.abs(step).max() < 1e-7:\n",
" break\n",
" return b\n",
"\n",
"\n",
"def cindex(time, event, risk):\n",
" \"\"\"์ผ์น๋ C-index: ์ํ ์ ์๊ฐ ๋์ ์ชฝ์ด ๋จผ์ ๊ณ ์ฅ ๋ฌ๋๊ฐ(๊ด์ธก ๊ฐ๋ฅํ ์๋ง).\"\"\"\n",
" t, e, r = np.asarray(time), np.asarray(event), np.asarray(risk)\n",
" num = den = 0.0\n",
" for i in np.where(e == 1)[0]:\n",
" m = t > t[i]\n",
" den += m.sum()\n",
" num += (r[m] < r[i]).sum() + 0.5 * (r[m] == r[i]).sum()\n",
" return num / max(den, 1)\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ์์ฌ ์๋ช
(RUL, ์ผ) ์์ธก ์์ง: ์์ด๋ธ AFT ์กฐ๊ฑด๋ถ ์ค์๊ฐ + ๊ทธ๋๋์ธํธ ๋ถ์คํ
(๊ณ ์ฅ ์ด๋ ฅ ์์ ํ๋ณธ ํ์ต)์ ํ๊ท \n",
"# ---------------------------------------------------------------------------\n",
"from sklearn.ensemble import GradientBoostingRegressor\n",
"\n",
"\n",
"def fit_engine(train, seed=0):\n",
" p = fit_aft(train)\n",
" sn = add_features(snapshots(train, 5, seed))\n",
" sn[\"aft\"] = (aft_pred(p, sn) - sn[\"h_since\"]) / sn[\"rate\"]\n",
" g = GradientBoostingRegressor(n_estimators=250, max_depth=3, learning_rate=0.06, random_state=0).fit(sn[FEATS + [\"aft\"]], sn[\"rul_d\"])\n",
" return {\"p\": p, \"g\": g}\n",
"\n",
"\n",
"def predict_rul(eng, d):\n",
" \"\"\"d๋ add_features๋ฅผ ๊ฑฐ์น ํ. ๋ฐํ: (์์๋ธ RUL, AFT RUL, GBR RUL) - ์ผ ๋จ์.\"\"\"\n",
" aft = ((aft_pred(eng[\"p\"], d) - d[\"h_since\"]) / d[\"rate\"]).values\n",
" x = d[FEATS].copy()\n",
" x[\"aft\"] = aft\n",
" gb = eng[\"g\"].predict(x)\n",
" return np.clip(0.5 * aft + 0.5 * gb, 1, None), aft, gb\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ๊ณํ์ ๋น(์ฐฝ์ ๋น) ์ผ์ ์ถ์ฒ: ์๋ณ ์ ๋น์ฐฝ ๋ฅ๋ ฅยท์์ฐยท์ํจ์ฑ ๋ถํ ํคํธ(์ ํจ๊ธฐํ) ์ ์ฝ ์๋ ๊ธฐ๋ ๋น์ฉ ์ต์ํ (๋น์ฉ ๋จ๊ฐ๋ ๊ต์ก์ฉ ๊ฐ์ ๊ฐ, ๋ง์)\n",
"# ---------------------------------------------------------------------------\n",
"from scipy.optimize import milp, LinearConstraint, Bounds\n",
"from scipy.stats import norm\n",
"\n",
"C_PLAN, C_FAIL, C_WASTE_KH, C_KIT = 600.0, 2400.0, 40.0, 120.0 # ๊ณํ ์ฐฝ์ ๋น, ๋น๊ณํ ๊ณ ์ฅ(๊ธด๊ธ ์ ๋น ํฌํจ), ๋ฏธ์ฌ์ฉ ์๋ช
1000h๋น ์์ค, ์ํจ์ฑ ๋ถํ ํคํธ\n",
"HORIZON, MDAYS = 12, 30 # ๊ณํ 12๊ฐ์, ์ 30์ผ\n",
"CAP_M, BUDGET_M = 22, 15000.0 # ์๋ณ ์ ๋น์ฐฝ ๋ฅ๋ ฅ(๋), ์๋ณ ์์ฐ\n",
"LOTS = [(30, 70), (45, 170), (45, 390), (70, 800)] # ์ํจ์ฑ ํคํธ ๋กํธ(์๋, ์ ํจ๊ธฐํ๊น์ง ๋จ์ ์ผ์)\n",
"KIT_MARGIN = 14 # ์ ๋น์ผ ๊ธฐ์ค ์ต์ ์ ํจ๊ธฐ๊ฐ ์ฌ์ (์ผ)\n",
"\n",
"\n",
"def exp_cost_matrix(pred, rate, month_days):\n",
" \"\"\"์ฅ๋น i๋ฅผ ์ m์ ์ ๋นํ ๋์ ๊ธฐ๋ ๋น์ฉ. ์ฐธ RUL ~ N(pred, sigma), sigma = 35์ผ + 0.12 x pred (๊ฐ์ ). ๋ง์ง๋ง ์ด์ ๊ณํ ๊ธฐ๊ฐ ๋ฐ(๋ฏธ์ ๋น).\"\"\"\n",
" sig = 35 + 0.12 * pred\n",
" C = np.zeros((len(pred), len(month_days) + 1))\n",
" for j, day in enumerate(month_days):\n",
" z = (day - pred) / sig\n",
" pf = norm.cdf(z)\n",
" unused = np.maximum(pred - day, 0) * rate * 0.6 # ์ ๋น๋ฅผ ์๋น๊ธด ๋งํผ ๋ฒ๋ฆฌ๋ ์ด์ฉ์๊ฐ h(๊ทผ์ฌ)\n",
" C[:, j] = C_PLAN + C_KIT + pf * (C_FAIL - C_PLAN) + C_WASTE_KH * unused / 1000 * (1 - pf)\n",
" C[:, -1] = norm.cdf((HORIZON * MDAYS - pred) / sig) * C_FAIL\n",
" return C\n",
"\n",
"\n",
"def plan_milp(pred, rate, cap=CAP_M, budget=BUDGET_M, lots=LOTS, use_kit=True):\n",
" \"\"\"์ ์๊ณํ์ผ๋ก ์ฅ๋น๋ณ ์ ๋น ์(0~11) ๋๋ ๋ฏธ์ ๋น(-1)๋ฅผ ๋ฐฐ์ ํ๋ค. ํ๋ณด๋ ์์ธก RUL์ด ๊ณํ ๊ธฐ๊ฐ +90์ผ ์ด๋ด์ธ ์ฅ๋น.\"\"\"\n",
" md = np.arange(1, HORIZON + 1) * MDAYS\n",
" cand = np.where(pred < HORIZON * MDAYS + 90)[0]\n",
" n, M = len(cand), HORIZON\n",
" C = exp_cost_matrix(pred[cand], rate[cand], md)\n",
" c = C.ravel()\n",
" A, lo, hi = [], [], []\n",
" for i in range(n): # ์ฅ๋น๋น ํ ๋ฒ๋ง ๋ฐฐ์ (์ 12์นธ + ๋ฏธ์ ๋น)\n",
" row = np.zeros(n * (M + 1)); row[i * (M + 1):(i + 1) * (M + 1)] = 1; A.append(row); lo.append(1); hi.append(1)\n",
" for m in range(M): # ์๋ณ ๋ฅ๋ ฅยท์์ฐ\n",
" row = np.zeros(n * (M + 1)); row[m::(M + 1)] = 1\n",
" A.append(row); lo.append(0); hi.append(cap)\n",
" A.append(row * (C_PLAN + C_KIT)); lo.append(0); hi.append(budget)\n",
" if use_kit: # ์ํจ์ฑ ํคํธ: m์ ์ดํ ์ ๋น ์์๋ ๊ทธ ์์ ์ ์ ํจ๊ธฐ๊ฐ์ด ๋จ์ ๋กํธ ์๋์ ๋์ ์ ์๋ค\n",
" for m in range(M):\n",
" sup = sum(q for q, e in lots if e >= md[m] + KIT_MARGIN)\n",
" row = np.zeros(n * (M + 1))\n",
" for mm in range(m, M):\n",
" row[mm::(M + 1)] = 1\n",
" A.append(row); lo.append(0); hi.append(sup)\n",
" r = milp(c, constraints=LinearConstraint(np.array(A), lo, hi), integrality=np.ones(len(c)), bounds=Bounds(0, 1), options={\"time_limit\": 20})\n",
" month = np.full(len(pred), -1)\n",
" if r.x is not None:\n",
" x = np.round(r.x).reshape(n, M + 1)\n",
" assign = np.where(x[:, :M].sum(axis=1) > 0.5, x[:, :M].argmax(axis=1), -1)\n",
" month[cand] = assign\n",
" return month\n",
"\n",
"\n",
"def plan_greedy(pred, margin=45, cap=CAP_M, budget=BUDGET_M):\n",
" \"\"\"๊ฐ๋จํ ํ์ ๋ฐฐ์ : ์ ๋น ๋ชฉํ์ผ(์์ธก RUL - margin)์ด ๋น ๋ฅธ ์์ผ๋ก ๋ชฉํ์ผ ์ด์ ๊ฐ์ฅ ๋ฆ์ ๋น ์, ์์ผ๋ฉด ๊ฐ์ฅ ์ด๋ฅธ ๋น ์์ ๋ฐฐ์ .\"\"\"\n",
" cap = min(cap, int(budget // (C_PLAN + C_KIT))) # ์ ์์ฐ์ด ํ์ฉํ๋ ๋์๋ก ๋ฅ๋ ฅ์ ์ ํ\n",
" month = np.full(len(pred), -1)\n",
" left = np.full(HORIZON, cap)\n",
" for i in np.argsort(pred):\n",
" if pred[i] > HORIZON * MDAYS + 60: break\n",
" tgt = int(np.clip((pred[i] - margin) // MDAYS - 1, 0, HORIZON - 1))\n",
" for m in list(range(tgt, -1, -1)) + list(range(tgt + 1, HORIZON)):\n",
" if left[m] > 0:\n",
" month[i] = m; left[m] -= 1; break\n",
" return month\n",
"\n",
"\n",
"def plan_fixed(lin_rul, cap=CAP_M, budget=BUDGET_M):\n",
" \"\"\"๊ดํ: ๊ท์ ์ฃผ๊ธฐ ๋๋ฌ ์์๋๋ก(์ ํ ์์ฌ ์ผ์) ์๋ณ ๋ฅ๋ ฅ ์์์ ๋ฐฐ์ .\"\"\"\n",
" return plan_greedy(np.asarray(lin_rul, float), margin=0, cap=cap, budget=budget)\n",
"\n",
"\n",
"def evaluate_plan(month, true_rul, rate, cost_kit=True):\n",
" \"\"\"์ฐธ RUL๋ก ๊ณํ์ ์ค์ ๋น์ฉ์ ๊ณ์ฐํ๋ค: ์ ๋น์ผ ์ด์ ์ ๊ณ ์ฅ -> ๋น๊ณํ ๊ณ ์ฅ ๋น์ฉ, ์๋๋ฉด ๊ณํ ๋น์ฉ + ๋ฏธ์ฌ์ฉ ์๋ช
์์ค.\"\"\"\n",
" day = np.where(month >= 0, (month + 1) * MDAYS, 10 ** 9)\n",
" fail = (true_rul < day) & (true_rul < HORIZON * MDAYS)\n",
" planned = (month >= 0) & ~fail\n",
" waste = np.where(planned, (true_rul - day) * rate * 0.6, 0) * C_WASTE_KH / 1000\n",
" cost = fail * C_FAIL + planned * (C_PLAN + (C_KIT if cost_kit else 0)) + waste\n",
" return {\"total\": float(cost.sum()), \"fail\": int(fail.sum()), \"planned\": int(planned.sum()), \"waste\": float(waste.sum()), \"unsched_fail\": int(((month < 0) & fail).sum())}\n",
"\n",
"\n",
"def kit_fefo(month, lots=LOTS):\n",
" \"\"\"์ ํจ๊ธฐํ์ด ๋น ๋ฅธ ๋กํธ๋ถํฐ ์ฐ๋ FEFO๋ก ํคํธ๋ฅผ ๋ฐฐ์ ํ๊ณ ์ ๋น์ผ์ ์ ํจ๊ธฐ๊ฐ์ด ๋ถ์กฑํ ๋ฐฐ์ ์์ ๋ง๋ฃ ํ๊ธฐ ์๋์ ์ผ๋ค.\"\"\"\n",
" q = [[qty, e] for qty, e in sorted(lots, key=lambda t: t[1])]\n",
" short = 0\n",
" for m in sorted(month[month >= 0]):\n",
" day = (m + 1) * MDAYS\n",
" for lot in q:\n",
" if lot[1] >= day + KIT_MARGIN and lot[0] > 0:\n",
" lot[0] -= 1; break\n",
" else:\n",
" short += 1\n",
" expired = sum(l[0] for l in q if l[1] < HORIZON * MDAYS)\n",
" return {\"short\": short, \"expired_unused\": expired, \"left\": sum(l[0] for l in q)}\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"tr = make_fleet(900, seed=1)\n",
"te = add_features(make_fleet(900, seed=3))\n",
"eng = fit_engine(tr)\n",
"al = te[te.event == 0].reset_index(drop=True)\n",
"pred, aft, gb = predict_rul(eng, al)\n",
"y = al[\"rul_d\"].values\n",
"big = y >= 90\n",
"P = {\"๊ธฐ์ค(์ ํ ๊ฐ๊ฐ์๊ฐ)\": al[\"lin_rul_d\"].values, \"์์ด๋ธ AFT\": aft, \"GBR(์ ๊ฒ ๋ง๋ชจ ํฌํจ)\": gb, \"์์๋ธ(์ ์)\": pred}\n",
"tab = pd.DataFrame({k: {\"MAE(์ผ)\": mae(y, v), \"RMSE(์ผ)\": rmse(y, v), \"MAPE(%)\": mape(y[big], v[big]), \"R2\": r2_score(y, v)} for k, v in P.items()}).T\n",
"print(f\"[1] ์์กด ์ฅ๋น {len(al)}๋ RUL ์์ธก ์ฑ๋ฅ(ํ์ต {len(tr)}๋์ ๋ค๋ฅธ ์ฅ๋น, MAPE๋ ์ฐธ RUL 90์ผ ์ด์)\")\n",
"print(tab.round(3).to_string())\n",
"b, e = tab.loc[\"๊ธฐ์ค(์ ํ ๊ฐ๊ฐ์๊ฐ)\"], tab.loc[\"์์๋ธ(์ ์)\"]\n",
"ok = 1 - e[\"MAE(์ผ)\"] / b[\"MAE(์ผ)\"] >= .2 and 1 - e[\"RMSE(์ผ)\"] / b[\"RMSE(์ผ)\"] >= .2\n",
"print(f\" ๊ธฐ์ค ๋๋น MAE ๊ฐ์ {1 - e['MAE(์ผ)'] / b['MAE(์ผ)']:.1%}, RMSE ๊ฐ์ {1 - e['RMSE(์ผ)'] / b['RMSE(์ผ)']:.1%} (๋ชฉํ 20% ์ด์: {'๋ฌ์ฑ' if ok else '๋ฏธ๋ฌ'}), R2 {e['R2']:.3f} (๋ชฉํ 0.85 ์ด์: {'๋ฌ์ฑ' if e['R2'] >= .85 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# ์ ๊ฒ ๋ง๋ชจ ํผ์ฒ์ ํจ๊ณผ: ์ ๊ฑฐ ์คํ\n",
"tr_f = add_features(snapshots(tr, 5, 0))\n",
"cols_no = [c for c in FEATS if c not in (\"insp\", \"insp_eta_h\")]\n",
"g_no = GradientBoostingRegressor(n_estimators=250, max_depth=3, learning_rate=0.06, random_state=0).fit(tr_f[cols_no], tr_f[\"rul_d\"])\n",
"p_no = g_no.predict(al[cols_no])\n",
"print(f\" ์ ๊ฒ ๋ง๋ชจ ํผ์ฒ๋ฅผ ๋นผ๋ฉด GBR MAE {mae(y, gb):.0f}์ผ -> {mae(y, p_no):.0f}์ผ, R2 {r2_score(y, gb):.3f} -> {r2_score(y, p_no):.3f} (์ ๋น ์ ๊ฒ ๊ธฐ๋ก์ ๊ฐ์น)\")\n",
"\n",
"# ๋ถ๊ท์น ์ํฉ๋ณ ์ค์ฐจ\n",
"conds = {\"์ผ๋ฐ\": al.env == \"normal\", \"ํ์ง\": al.env == \"rough\", \"์ผ๋ถ\": al.env == \"salt\", \"ํนํ\": al.env == \"cold\", \"๊ณ ๊ฐ๋(์ผ 4h ์ด์)\": al.rate >= 4, \"๋น๊ณํ ์ ๋น์จ ์์ 25%\": al.unpl_per_kh >= al.unpl_per_kh.quantile(.75)}\n",
"rr = pd.DataFrame([(k, int(m.sum()), mae(y[m.values], P[\"๊ธฐ์ค(์ ํ ๊ฐ๊ฐ์๊ฐ)\"][m.values]), mae(y[m.values], pred[m.values])) for k, m in conds.items()], columns=[\"์กฐ๊ฑด\", \"๋์\", \"๊ธฐ์ค MAE\", \"์์๋ธ MAE\"])\n",
"print(\"\\n[2] ์กฐ๊ฑด๋ณ MAE(์ผ)\\n\" + rr.round(0).to_string(index=False))\n",
"\n",
"# ---------------- ๊ณํ์ ๋น ์ผ์ ์ถ์ฒ ----------------\n",
"rate, true_rul = al[\"rate\"].values, al[\"rul_d\"].values\n",
"mf, mg = plan_fixed(al[\"lin_rul_d\"].values), plan_greedy(pred)\n",
"mm = plan_milp(pred, rate)\n",
"pl = {\"๊ท์ ์ฃผ๊ธฐ ๊ณ ์ ์ผ์ (๊ดํ)\": mf, \"์์ธก ๊ธฐ๋ฐ ํ์ ๋ฐฐ์ \": mg, \"์์ธก ๊ธฐ๋ฐ ์ ์๊ณํ(์ ์)\": mm}\n",
"rows = []\n",
"for k, v in pl.items():\n",
" r, kf = evaluate_plan(v, true_rul, rate), kit_fefo(v)\n",
" rows.append((k, int((v >= 0).sum()), r[\"total\"], r[\"fail\"], r[\"planned\"], kf[\"short\"]))\n",
"pt = pd.DataFrame(rows, columns=[\"์ผ์ ์\", \"๋ฐฐ์ ๋์\", \"์ค์ ์ด๋น์ฉ(๋ง์)\", \"๋น๊ณํ ๊ณ ์ฅ\", \"๊ณํ ์ ๋น\", \"ํคํธ ์ ํจ๊ธฐ๊ฐ ๋ถ์กฑ\"])\n",
"print(f\"\\n[3] 12๊ฐ์ ๊ณํ์ ๋น ์ผ์ ๋น๊ต(์ ๋ฅ๋ ฅ {CAP_M}๋, ์ ์์ฐ {BUDGET_M:,.0f}๋ง์, ํคํธ ์ฌ๊ณ {sum(q for q, _ in LOTS)}๊ฐ ๋กํธ {len(LOTS)}์ข
, ๋น์ฉ ๋จ๊ฐ๋ ๊ต์ก์ฉ ๊ฐ์ ๊ฐ)\")\n",
"print(pt.round(0).to_string(index=False))\n",
"sv = 1 - pt.iloc[2][\"์ค์ ์ด๋น์ฉ(๋ง์)\"] / pt.iloc[0][\"์ค์ ์ด๋น์ฉ(๋ง์)\"]\n",
"print(f\" ์ ์๊ณํ ์์ ๊ดํ ๋๋น ์ด๋น์ฉ {sv:.1%} ์ ๊ฐ, ๋น๊ณํ ๊ณ ์ฅ {int(pt.iloc[0]['๋น๊ณํ ๊ณ ์ฅ'])} -> {int(pt.iloc[2]['๋น๊ณํ ๊ณ ์ฅ'])}๋\")\n",
"cnt = np.bincount(mm[mm >= 0], minlength=HORIZON)\n",
"print(\" ์๋ณ ์ ๋น ๋์(์ ์):\", \" \".join(f\"{i + 1}์ {c}\" for i, c in enumerate(cnt)), f\"(๋ฅ๋ ฅ {CAP_M}๋ ์ดํ ํ์ธ: {'OK' if cnt.max() <= CAP_M else '์ด๊ณผ'}, ์ ์์ฐ ํ์ธ: {'OK' if (cnt * (C_PLAN + C_KIT)).max() <= BUDGET_M else '์ด๊ณผ'})\")\n",
"kf = kit_fefo(mm)\n",
"print(f\" ์ํจ์ฑ ํคํธ FEFO ๋ฐฐ์ : ์ ํจ๊ธฐ๊ฐ ๋ถ์กฑ {kf['short']}๊ฑด, ๋ง๋ฃ ์๋ฐ ํ๊ธฐ ์์ {kf['expired_unused']}๊ฐ -> ์ ํจ๊ธฐํ์ด ๋น ๋ฅธ ๋กํธ๋ถํฐ ์ฐ๋๋ก ์ ๋น ์์ ๋ง์ถค\")\n",
"print(\" ์ถ์ฒ ์ผ์ ์ ์ ๋น๊ณํ๊ด ๊ฒํ ์ ๊ตฐ์์งํ๊ด ์น์ธ(M7) ์ ๊น์ง ํ์ ๋์ง ์๋๋ค. ์ฅ๋นยท๋จ๊ฐ๋ ๊ต์ก์ฉ ๊ฐ์ ์๋ฃ์ด๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 3, figsize=(15, 4.3))\n",
"ax[0].scatter(y, P[\"๊ธฐ์ค(์ ํ ๊ฐ๊ฐ์๊ฐ)\"], s=6, label=\"baseline\"); ax[0].scatter(y, pred, s=6, label=\"ensemble\"); ax[0].plot([0, 4000], [0, 4000], \"k--\"); ax[0].legend(); ax[0].set_title(\"RUL: true vs predicted (days)\")\n",
"ax[1].bar(range(len(pt)), pt[\"์ค์ ์ด๋น์ฉ(๋ง์)\"]); ax[1].set_xticks(range(len(pt))); ax[1].set_xticklabels([\"fixed\", \"greedy\", \"MILP\"]); ax[1].set_title(\"Total cost of 12-month plan (10k KRW)\")\n",
"ax[2].bar(np.arange(1, HORIZON + 1), cnt); ax[2].axhline(CAP_M, c=\"r\", ls=\"--\"); ax[2].set_title(\"Overhauls per month (capacity line)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
05 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[1] ์์กด ์ฅ๋น 601๋ RUL ์์ธก ์ฑ๋ฅ(ํ์ต 900๋์ ๋ค๋ฅธ ์ฅ๋น, MAPE๋ ์ฐธ RUL 90์ผ ์ด์)\n",
" MAE(์ผ) RMSE(์ผ) MAPE(%) R2\n",
"๊ธฐ์ค(์ ํ ๊ฐ๊ฐ์๊ฐ) 354.501 435.605 46.016 0.808\n",
"์์ด๋ธ AFT 212.324 277.206 24.014 0.922\n",
"GBR(์ ๊ฒ ๋ง๋ชจ ํฌํจ) 253.028 396.280 16.536 0.842\n",
"์์๋ธ(์ ์) 210.946 289.055 17.907 0.916\n",
" ๊ธฐ์ค ๋๋น MAE ๊ฐ์ 40.5%, RMSE ๊ฐ์ 33.6% (๋ชฉํ 20% ์ด์: ๋ฌ์ฑ), R2 0.916 (๋ชฉํ 0.85 ์ด์: ๋ฌ์ฑ)\n",
" ์ ๊ฒ ๋ง๋ชจ ํผ์ฒ๋ฅผ ๋นผ๋ฉด GBR MAE 253์ผ -> 491์ผ, R2 0.842 -> 0.578 (์ ๋น ์ ๊ฒ ๊ธฐ๋ก์ ๊ฐ์น)\n",
"\n",
"[2] ์กฐ๊ฑด๋ณ MAE(์ผ)\n",
" ์กฐ๊ฑด ๋์ ๊ธฐ์ค MAE ์์๋ธ MAE\n",
" ์ผ๋ฐ 265 432.0 238.0\n",
" ํ์ง 99 379.0 163.0\n",
" ์ผ๋ถ 124 265.0 192.0\n",
" ํนํ 113 249.0 212.0\n",
" ๊ณ ๊ฐ๋(์ผ 4h ์ด์) 89 240.0 145.0\n",
"๋น๊ณํ ์ ๋น์จ ์์ 25% 151 320.0 198.0\n",
"\n",
"[3] 12๊ฐ์ ๊ณํ์ ๋น ์ผ์ ๋น๊ต(์ ๋ฅ๋ ฅ 22๋, ์ ์์ฐ 15,000๋ง์, ํคํธ ์ฌ๊ณ 190๊ฐ ๋กํธ 4์ข
, ๋น์ฉ ๋จ๊ฐ๋ ๊ต์ก์ฉ ๊ฐ์ ๊ฐ)\n",
" ์ผ์ ์ ๋ฐฐ์ ๋์ ์ค์ ์ด๋น์ฉ(๋ง์) ๋น๊ณํ ๊ณ ์ฅ ๊ณํ ์ ๋น ํคํธ ์ ํจ๊ธฐ๊ฐ ๋ถ์กฑ\n",
"๊ท์ ์ฃผ๊ธฐ ๊ณ ์ ์ผ์ (๊ดํ) 123 184752.0 47 97 0\n",
" ์์ธก ๊ธฐ๋ฐ ํ์ ๋ฐฐ์ 105 141558.0 35 79 0\n",
" ์์ธก ๊ธฐ๋ฐ ์ ์๊ณํ(์ ์) 101 116447.0 21 90 0\n",
" ์ ์๊ณํ ์์ ๊ดํ ๋๋น ์ด๋น์ฉ 37.0% ์ ๊ฐ, ๋น๊ณํ ๊ณ ์ฅ 47 -> 21๋\n",
" ์๋ณ ์ ๋น ๋์(์ ์): 1์ 20 2์ 20 3์ 20 4์ 20 5์ 20 6์ 1 7์ 0 8์ 0 9์ 0 10์ 0 11์ 0 12์ 0 (๋ฅ๋ ฅ 22๋ ์ดํ ํ์ธ: OK, ์ ์์ฐ ํ์ธ: OK)\n",
" ์ํจ์ฑ ํคํธ FEFO ๋ฐฐ์ : ์ ํจ๊ธฐ๊ฐ ๋ถ์กฑ 0๊ฑด, ๋ง๋ฃ ์๋ฐ ํ๊ธฐ ์์ 10๊ฐ -> ์ ํจ๊ธฐํ์ด ๋น ๋ฅธ ๋กํธ๋ถํฐ ์ฐ๋๋ก ์ ๋น ์์ ๋ง์ถค\n",
" ์ถ์ฒ ์ผ์ ์ ์ ๋น๊ณํ๊ด ๊ฒํ ์ ๊ตฐ์์งํ๊ด ์น์ธ(M7) ์ ๊น์ง ํ์ ๋์ง ์๋๋ค. ์ฅ๋นยท๋จ๊ฐ๋ ๊ต์ก์ฉ ๊ฐ์ ์๋ฃ์ด๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
06. ๋ถ๋๋ณ ์ฅ๋น ๋
ธํ๋ ํํฉ ๋ฐ ๊ถ์ฅ ์ ๋น ์บ๋ฆฐ๋ ๋์๋ณด๋ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋ถ๋๋ณ ์ฅ๋น ๋
ธํ๋ ํํฉ ๋ฐ ๊ถ์ฅ ์ ๋น ์บ๋ฆฐ๋ ๋์๋ณด๋๋ฅผ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋ถ๋๋ณ ์ฅ๋น ๋
ธํ๋ ํํฉ๊ณผ ๊ถ์ฅ ์ ๋น ์บ๋ฆฐ๋ ๋์๋ณด๋๋ฅผ ๊ตฌํํ๊ณ ์์ธก ๊ทผ๊ฑฐ๋ฅผ ์ค๋ช
ํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๋ถ๋๋ณ ์ฅ๋น ์, ํ๊ท ๋
ธํ ์ง์, ํ๊ท ์์ธก ์์ฌ ์๋ช
, ๊ณ ์ํ ๋น์จ์ ์ง๊ณ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/explainability/sup02_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": [
"\"\"\"๋ถ๋๋ณ ์ฅ๋น ๋
ธํ๋ ํํฉ๊ณผ ๊ถ์ฅ ์ ๋น ์บ๋ฆฐ๋ ๋์๋ณด๋๋ฅผ ๊ตฌํํ๊ณ , ์ฅ๋น๋ง๋ค ์์ฌ ์๋ช
(RUL) ์์ธก์ ๊ธฐ์ฌ๋ ์์ 3๊ฐ ์์ธ์ ๋ณ์๊ตฐ ํ๊ท ๋์ฒด ๋ฐฉ์์ผ๋ก ์๋ ์ฐ์ถํด ์์ฐ์ด ์ค๋ช
์ผ๋ก ๋ณด์ฌ ์ค๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ฅ๋น ๋ณด์ ํํฉยท๊ณ ์ฅ ์ด๋ ฅ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์ฅ๋นยท์์น ์์)\n",
"# ๊ฐ์ ๋ถ๋ 6๊ฐ(U1~U6)์ ์ฅ๋น๊ฐ ์ด์ฉ ํ๊ฒฝ(normal/rough/salt/cold)์ ๋ฐ๋ผ ๋ค๋ฅธ ์๋๋ก ๋
ธํํํ๋ค. ์ฐฝ์ ๋น ํ ์ด์ฉ์๊ฐ(h_since)์ด\n",
"# ์ฅ๋น๋ง๋ค ์จ์ ์๋ช
(t_fail)์ ๋์ผ๋ฉด ๊ณ ์ฅ(event=1) ์ด๋ ฅ, ๋์ง ์์์ผ๋ฉด ํ์ฌ๊น์ง ์์กด(event=0, ๊ด์ธก ์ค๋์ ๋จ)์ด๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"ENVS = [\"normal\", \"rough\", \"salt\", \"cold\"]\n",
"ENV_W = {\"normal\": 1.00, \"rough\": 1.35, \"salt\": 1.25, \"cold\": 1.12} # ํ๊ฒฝ ๊ฐ์ค์น(์จ์ ์ ๋ต, ๊ฐ์ ๊ฐ): ํด์๋ก ์๋ช
์ด ์งง๋ค\n",
"ENV_KO = {\"normal\": \"์ผ๋ฐ\", \"rough\": \"ํ์ง\", \"salt\": \"์ผ๋ถ\", \"cold\": \"ํนํ\"}\n",
"UNITS = [\"U1\", \"U2\", \"U3\", \"U4\", \"U5\", \"U6\"]\n",
"UNIT_ENV = {\"U1\": [.7, .1, .1, .1], \"U2\": [.2, .6, .1, .1], \"U3\": [.2, .1, .6, .1], \"U4\": [.3, .1, .1, .5], \"U5\": [.5, .3, .1, .1], \"U6\": [.4, .1, .4, .1]}\n",
"ETYPES = [\"truck\", \"generator\", \"trailer\", \"radio\"]\n",
"CYCLE = 8000.0 # ๊ท์ ์ฐฝ์ ๋น ์ฃผ๊ธฐ(์ด์ฉ์๊ฐ h, ๊ฐ์ ๊ฐ)\n",
"KSHAPE = 12.0 # ์๋ช
๋ถํฌ ์์ด๋ธ ํ์(๋ง๋ชจํ ๊ณ ์ฅ, ํด์๋ก ์๋ช
์ด ๋ชจ์ฌ ์๋ค)\n",
"INSP_NOISE = 0.025 # ์ ๋น ์ ๊ฒ ๋ง๋ชจ ๋น์จ ์ธก์ ์ค์ฐจ(๊ฐ์ ๊ฐ)\n",
"\n",
"\n",
"def make_fleet(n=900, seed=0, env_w=None, shift=1.0):\n",
" \"\"\"์ฅ๋น n๋. env_w๋ก ์จ์ ํ๊ฒฝ ๊ฐ์ค์น๋ฅผ ๋ฐ๊ฟ ์ด์ ๋ถํฌ ๋ณํ๋ฅผ ๋ชจ์ํ ์ ์๋ค. shift>1์ด๋ฉด ๊ฐ๋๋ฅ ์ด ๋์์ง๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" w = env_w or ENV_W\n",
" unit = rng.choice(UNITS, n)\n",
" env = np.array([rng.choice(ENVS, p=UNIT_ENV[u]) for u in unit])\n",
" et = rng.choice(ETYPES, n, p=[.4, .2, .2, .2])\n",
" rate = rng.lognormal(np.log(3.0 * shift), 0.25, n) # ์ผ ํ๊ท ์ด์ฉ์๊ฐ(h/์ผ)\n",
" h_since = rng.uniform(300, 11500, n) # ๋ง์ง๋ง ์ฐฝ์ ๋น ์ดํ ์ด์ฉ์๊ฐ\n",
" n_ovh = rng.integers(0, 3, n)\n",
" hours = h_since + n_ovh * rng.uniform(5000, 8000, n) # ๋์ ์ด์ฉ์๊ฐ\n",
" km = hours * rng.normal(17, 3, n) * np.where(env == \"rough\", 0.8, 1.0) # ๋์ ๊ธฐ๋๊ฑฐ๋ฆฌ(km)\n",
" age_y = np.clip(hours / (rate * 365) * rng.uniform(0.9, 1.15, n), 0.5, 25)\n",
" unpl = rng.poisson(0.35 * h_since / 1000 * np.array([w[e] for e in env]) / 1.3) # ๋น๊ณํ ์ ๋น ๊ฑด์(ํ๊ฒฝ์ด ๊ฐํนํ ์๋ก ๋ง๋ค)\n",
" n_maint = rng.poisson(0.6 * h_since / 1000 + 1) # ๊ณํ ์ ๋น ๊ฑด์\n",
" scale = 9600 / np.array([w[e] for e in env]) * 0.985 ** unpl * np.exp(rng.normal(0, 0.04, n))\n",
" t_fail = scale * rng.weibull(KSHAPE, n) # ์ฐฝ์ ๋น ํ ๊ณ ์ฅ๊น์ง ์ด์ฉ์๊ฐ(์จ์ ์ ๋ต)\n",
" event = (t_fail <= h_since).astype(int)\n",
" dur = np.where(event == 1, t_fail, h_since)\n",
" df = pd.DataFrame({\"eq_id\": [f\"EQ{seed:02d}{i:04d}\" for i in range(n)], \"unit\": unit, \"env\": env, \"etype\": et, \"rate\": rate, \"hours\": hours, \"km\": km, \"age_y\": age_y, \"n_ovh\": n_ovh,\n",
" \"h_since\": h_since, \"unpl\": unpl, \"n_maint\": n_maint, \"t_fail\": t_fail, \"event\": event, \"dur\": dur})\n",
" df[\"insp\"] = np.clip(dur / t_fail + rng.normal(0, INSP_NOISE, n), 0, 1.3) # ๋ง์ง๋ง ์ ๋น ์ ๊ฒ์์ ์ธก์ ํ ๋ง๋ชจ ๋น์จ(์๋ชจ ์๋ช
/์ด ์๋ช
, ์ค์ฐจ ํฌํจ)\n",
" df[\"rul_h\"] = np.where(event == 0, t_fail - h_since, np.nan) # ์์กด ์ฅ๋น์ ๋จ์ ์ด์ฉ์๊ฐ(์ ๋ต)\n",
" df[\"rul_d\"] = df[\"rul_h\"] / df[\"rate\"] # ๋จ์ ์ผ์(์ ๋ต)\n",
" return df\n",
"\n",
"\n",
"def mape(y, p):\n",
" # [TODO : ํ์ ์ค์ต 6] ๋ถ๋๋ณ ์ฅ๋น ๋
ธํ๋ ํํฉ ๋ฐ ๊ถ์ฅ ์ ๋น ์บ๋ฆฐ๋ ๋์๋ณด๋ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def mae(y, p):\n",
" # [TODO : ํ์ ์ค์ต 6] ๋ถ๋๋ณ ์ฅ๋น ๋
ธํ๋ ํํฉ ๋ฐ ๊ถ์ฅ ์ ๋น ์บ๋ฆฐ๋ ๋์๋ณด๋ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def rmse(y, p):\n",
" return float(np.sqrt(np.mean((np.asarray(y, float) - np.asarray(p, float)) ** 2)))\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ๋
ธํํ ํ์ ํผ์ฒ์ ํ์ต ํ๋ณธ ๋ง๋ค๊ธฐ. ํ๊ฒฝ ๊ฐ์ค์น W0๋ ๋์
์์ ์ ์ ๋ฌธ๊ฐ ์ถ์ ๊ฐ์ด๋ฉฐ(์ค์ ๊ฐ์ค์น์ ๋ค๋ฆ) M9์์ ์ฌ๋ณด์ ํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"W0 = {\"normal\": 1.00, \"rough\": 1.15, \"salt\": 1.10, \"cold\": 1.05}\n",
"FEATS = [\"h_since\", \"hours\", \"km\", \"age_y\", \"rate\", \"n_maint\", \"unpl\", \"unpl_per_kh\", \"km_per_h\", \"n_ovh\", \"k_rough\", \"k_salt\", \"k_cold\", \"k_truck\", \"k_gen\", \"k_trailer\",\n",
" \"aging_idx\", \"lin_rul_d\", \"hs_frac\", \"insp\", \"insp_eta_h\"]\n",
"\n",
"\n",
"def add_features(df, w=None):\n",
" d = df.copy()\n",
" w = w or W0\n",
" d[\"unpl_per_kh\"] = d[\"unpl\"] / (d[\"h_since\"] / 1000 + 0.5)\n",
" d[\"km_per_h\"] = d[\"km\"] / d[\"hours\"]\n",
" for e in (\"rough\", \"salt\", \"cold\"):\n",
" d[\"k_\" + e] = (d[\"env\"] == e).astype(int)\n",
" d[\"k_truck\"], d[\"k_gen\"], d[\"k_trailer\"] = (d[\"etype\"] == \"truck\").astype(int), (d[\"etype\"] == \"generator\").astype(int), (d[\"etype\"] == \"trailer\").astype(int)\n",
" d[\"aging_idx\"] = d[\"h_since\"] * d[\"env\"].map(w) / CYCLE # ํ๊ฒฝ ๊ฐ์ค ๋
ธํ ์ง์(1์ด๋ฉด ์ฐฝ์ ๋น ์ฃผ๊ธฐ์ ๋๋ฌ)\n",
" d[\"lin_rul_d\"] = (CYCLE - d[\"h_since\"]) / d[\"rate\"] # ์ ํ ๊ฐ๊ฐ์๊ฐ ์์ฌ ์ผ์(Baseline)\n",
" d[\"hs_frac\"] = d[\"h_since\"] / CYCLE\n",
" d[\"insp_eta_h\"] = d[\"h_since\"] / np.maximum(d[\"insp\"], 0.05) # ์ ๊ฒ ๋ง๋ชจ ๋น์จ๋ก ํ์ฐํ ์ด ์๋ช
์ถ์ (h)\n",
" return d\n",
"\n",
"\n",
"def snapshots(df, k=4, seed=0):\n",
" \"\"\"๊ณ ์ฅ ์ด๋ ฅ ์ฅ๋น์ ๊ณผ๊ฑฐ ์์ ํ๋ณธ: ๊ณ ์ฅ ์ ์์ ์์ ์ ์ ๊ฒ๊ฐยท์ด์ฉ์๊ฐ๊ณผ ๊ทธ๋์ ๋จ์ ์ผ์. (์์กด ์ฅ๋น์ ๋ฏธ๋๋ ์ ์ ์์ผ๋ฏ๋ก ๊ณ ์ฅ ์ด๋ ฅ๋ง ์ด๋ค)\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" f = df[df[\"event\"] == 1]\n",
" out = []\n",
" for _ in range(k):\n",
" s = f.copy()\n",
" h = s[\"t_fail\"] * rng.uniform(0.2, 0.98, len(s))\n",
" ratio = h / s[\"h_since\"]\n",
" s[\"hours\"] = s[\"hours\"] - (s[\"h_since\"] - h)\n",
" s[\"km\"] = s[\"km\"] * ratio.clip(upper=1)\n",
" s[\"unpl\"] = np.floor(s[\"unpl\"] * ratio)\n",
" s[\"n_maint\"] = np.floor(s[\"n_maint\"] * ratio)\n",
" s[\"insp\"] = np.clip(h / s[\"t_fail\"] + rng.normal(0, INSP_NOISE, len(s)), 0, 1.3)\n",
" s[\"h_since\"] = h\n",
" s[\"rul_d\"] = (s[\"t_fail\"] - h) / s[\"rate\"]\n",
" s[\"event\"] = 0\n",
" out.append(s)\n",
" return pd.concat(out, ignore_index=True)\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ์์กด ๋ถ์ ๋๊ตฌ(scipy๋ง ์ฌ์ฉ): ์์ด๋ธ AFT(๊ฐ์ ์๋ช
) ๋ชจํ๊ณผ Cox ๋น๋ก์ํ ๋ชจํ\n",
"# ---------------------------------------------------------------------------\n",
"from scipy.optimize import minimize\n",
"\n",
"\n",
"def aft_x(d):\n",
" return np.c_[np.ones(len(d)), d[\"env\"].eq(\"rough\"), d[\"env\"].eq(\"salt\"), d[\"env\"].eq(\"cold\"), d[\"unpl\"] / (d[\"h_since\"] / 1000 + 0.5)].astype(float)\n",
"\n",
"\n",
"AFT_COLS = [\"์ ํธ\", \"ํ์ง\", \"์ผ๋ถ\", \"ํนํ\", \"๋น๊ณํ ์ ๋น์จ\"]\n",
"\n",
"\n",
"def fit_aft(d):\n",
" \"\"\"์ฐฝ์ ๋น ํ ์ด์ฉ์๊ฐ dur(๊ณ ์ฅ์ด๋ฉด event=1, ์์กด์ด๋ฉด ์ค๋์ ๋จ)์ ๋ํ ์์ด๋ธ AFT ์ต๋์ฐ๋ ์ถ์ . ๋ฐํ: ๊ณ์(๋ง์ง๋ง์ log ํ์).\"\"\"\n",
" x, t, e = aft_x(d), d[\"dur\"].values, d[\"event\"].values\n",
"\n",
" def nll(p):\n",
" lam, k = np.exp(x @ p[:-1]), np.exp(p[-1])\n",
" z = (t / lam) ** k\n",
" return -(e * (np.log(k) - np.log(lam) + (k - 1) * (np.log(t) - np.log(lam))) - z).sum()\n",
"\n",
" r = minimize(nll, np.r_[np.log(8000), np.zeros(x.shape[1] - 1), np.log(5)], method=\"BFGS\")\n",
" return r.x\n",
"\n",
"\n",
"def aft_pred(p, d, q=0.5, h=None):\n",
" \"\"\"์ด๋ฏธ h(์ด์ฉ์๊ฐ)๊น์ง ์์กดํ๋ค๋ ์กฐ๊ฑด์์ ๋ถ์์ q ์์กด ์์ (์ด์ฉ์๊ฐ h ๊ธฐ์ค ์ด ์๋ช
)์ ๋๋ ค์ค๋ค. q=0.5๊ฐ ์กฐ๊ฑด๋ถ ์ค์๊ฐ.\"\"\"\n",
" lam, k = np.exp(aft_x(d) @ p[:-1]), np.exp(p[-1])\n",
" h = d[\"h_since\"].values if h is None else h\n",
" return lam * ((h / lam) ** k - np.log(q)) ** (1 / k)\n",
"\n",
"\n",
"def aft_fail_prob(p, d, h0, h1):\n",
" \"\"\"h0๊น์ง ์์กดํ ์ฅ๋น๊ฐ h0~h1 ์ฌ์ด์ ๊ณ ์ฅ ๋ ์กฐ๊ฑด๋ถ ํ๋ฅ .\"\"\"\n",
" lam, k = np.exp(aft_x(d) @ p[:-1]), np.exp(p[-1])\n",
" s0, s1 = np.exp(-(h0 / lam) ** k), np.exp(-(h1 / lam) ** k)\n",
" return 1 - s1 / s0\n",
"\n",
"\n",
"def fit_cox(d, iters=25):\n",
" \"\"\"Cox ๋น๋ก์ํ(Breslow ๊ทผ์ฌ) ๋ดํด-๋ฉ์จ. ๊ณต๋ณ๋: ํ์งยท์ผ๋ถยทํนํยท๋น๊ณํ ์ ๋น์จ. ๋ฐํ: ๊ณ์.\"\"\"\n",
" x = aft_x(d)[:, 1:]\n",
" o = np.argsort(-d[\"dur\"].values)\n",
" x, e = x[o], d[\"event\"].values[o]\n",
" b = np.zeros(x.shape[1])\n",
" for _ in range(iters):\n",
" r = np.exp(x @ b)\n",
" S0 = np.cumsum(r)\n",
" S1 = np.cumsum(r[:, None] * x, axis=0)\n",
" S2 = np.cumsum(r[:, None, None] * x[:, :, None] * x[:, None, :], axis=0)\n",
" m1 = S1 / S0[:, None]\n",
" g = ((x - m1) * e[:, None]).sum(axis=0)\n",
" H = -sum(e[i] * (S2[i] / S0[i] - np.outer(m1[i], m1[i])) for i in np.where(e == 1)[0])\n",
" step = np.linalg.solve(H - 1e-6 * np.eye(len(b)), g)\n",
" b = b - step\n",
" if np.abs(step).max() < 1e-7:\n",
" break\n",
" return b\n",
"\n",
"\n",
"def cindex(time, event, risk):\n",
" \"\"\"์ผ์น๋ C-index: ์ํ ์ ์๊ฐ ๋์ ์ชฝ์ด ๋จผ์ ๊ณ ์ฅ ๋ฌ๋๊ฐ(๊ด์ธก ๊ฐ๋ฅํ ์๋ง).\"\"\"\n",
" t, e, r = np.asarray(time), np.asarray(event), np.asarray(risk)\n",
" num = den = 0.0\n",
" for i in np.where(e == 1)[0]:\n",
" m = t > t[i]\n",
" den += m.sum()\n",
" num += (r[m] < r[i]).sum() + 0.5 * (r[m] == r[i]).sum()\n",
" return num / max(den, 1)\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ์์ฌ ์๋ช
(RUL, ์ผ) ์์ธก ์์ง: ์์ด๋ธ AFT ์กฐ๊ฑด๋ถ ์ค์๊ฐ + ๊ทธ๋๋์ธํธ ๋ถ์คํ
(๊ณ ์ฅ ์ด๋ ฅ ์์ ํ๋ณธ ํ์ต)์ ํ๊ท \n",
"# ---------------------------------------------------------------------------\n",
"from sklearn.ensemble import GradientBoostingRegressor\n",
"\n",
"\n",
"def fit_engine(train, seed=0):\n",
" p = fit_aft(train)\n",
" sn = add_features(snapshots(train, 5, seed))\n",
" sn[\"aft\"] = (aft_pred(p, sn) - sn[\"h_since\"]) / sn[\"rate\"]\n",
" g = GradientBoostingRegressor(n_estimators=250, max_depth=3, learning_rate=0.06, random_state=0).fit(sn[FEATS + [\"aft\"]], sn[\"rul_d\"])\n",
" return {\"p\": p, \"g\": g}\n",
"\n",
"\n",
"def predict_rul(eng, d):\n",
" \"\"\"d๋ add_features๋ฅผ ๊ฑฐ์น ํ. ๋ฐํ: (์์๋ธ RUL, AFT RUL, GBR RUL) - ์ผ ๋จ์.\"\"\"\n",
" aft = ((aft_pred(eng[\"p\"], d) - d[\"h_since\"]) / d[\"rate\"]).values\n",
" x = d[FEATS].copy()\n",
" x[\"aft\"] = aft\n",
" gb = eng[\"g\"].predict(x)\n",
" return np.clip(0.5 * aft + 0.5 * gb, 1, None), aft, gb\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ๊ณํ์ ๋น(์ฐฝ์ ๋น) ์ผ์ ์ถ์ฒ: ์๋ณ ์ ๋น์ฐฝ ๋ฅ๋ ฅยท์์ฐยท์ํจ์ฑ ๋ถํ ํคํธ(์ ํจ๊ธฐํ) ์ ์ฝ ์๋ ๊ธฐ๋ ๋น์ฉ ์ต์ํ (๋น์ฉ ๋จ๊ฐ๋ ๊ต์ก์ฉ ๊ฐ์ ๊ฐ, ๋ง์)\n",
"# ---------------------------------------------------------------------------\n",
"from scipy.optimize import milp, LinearConstraint, Bounds\n",
"from scipy.stats import norm\n",
"\n",
"C_PLAN, C_FAIL, C_WASTE_KH, C_KIT = 600.0, 2400.0, 40.0, 120.0 # ๊ณํ ์ฐฝ์ ๋น, ๋น๊ณํ ๊ณ ์ฅ(๊ธด๊ธ ์ ๋น ํฌํจ), ๋ฏธ์ฌ์ฉ ์๋ช
1000h๋น ์์ค, ์ํจ์ฑ ๋ถํ ํคํธ\n",
"HORIZON, MDAYS = 12, 30 # ๊ณํ 12๊ฐ์, ์ 30์ผ\n",
"CAP_M, BUDGET_M = 22, 15000.0 # ์๋ณ ์ ๋น์ฐฝ ๋ฅ๋ ฅ(๋), ์๋ณ ์์ฐ\n",
"LOTS = [(30, 70), (45, 170), (45, 390), (70, 800)] # ์ํจ์ฑ ํคํธ ๋กํธ(์๋, ์ ํจ๊ธฐํ๊น์ง ๋จ์ ์ผ์)\n",
"KIT_MARGIN = 14 # ์ ๋น์ผ ๊ธฐ์ค ์ต์ ์ ํจ๊ธฐ๊ฐ ์ฌ์ (์ผ)\n",
"\n",
"\n",
"def exp_cost_matrix(pred, rate, month_days):\n",
" \"\"\"์ฅ๋น i๋ฅผ ์ m์ ์ ๋นํ ๋์ ๊ธฐ๋ ๋น์ฉ. ์ฐธ RUL ~ N(pred, sigma), sigma = 35์ผ + 0.12 x pred (๊ฐ์ ). ๋ง์ง๋ง ์ด์ ๊ณํ ๊ธฐ๊ฐ ๋ฐ(๋ฏธ์ ๋น).\"\"\"\n",
" sig = 35 + 0.12 * pred\n",
" C = np.zeros((len(pred), len(month_days) + 1))\n",
" for j, day in enumerate(month_days):\n",
" z = (day - pred) / sig\n",
" pf = norm.cdf(z)\n",
" unused = np.maximum(pred - day, 0) * rate * 0.6 # ์ ๋น๋ฅผ ์๋น๊ธด ๋งํผ ๋ฒ๋ฆฌ๋ ์ด์ฉ์๊ฐ h(๊ทผ์ฌ)\n",
" C[:, j] = C_PLAN + C_KIT + pf * (C_FAIL - C_PLAN) + C_WASTE_KH * unused / 1000 * (1 - pf)\n",
" C[:, -1] = norm.cdf((HORIZON * MDAYS - pred) / sig) * C_FAIL\n",
" return C\n",
"\n",
"\n",
"def plan_milp(pred, rate, cap=CAP_M, budget=BUDGET_M, lots=LOTS, use_kit=True):\n",
" \"\"\"์ ์๊ณํ์ผ๋ก ์ฅ๋น๋ณ ์ ๋น ์(0~11) ๋๋ ๋ฏธ์ ๋น(-1)๋ฅผ ๋ฐฐ์ ํ๋ค. ํ๋ณด๋ ์์ธก RUL์ด ๊ณํ ๊ธฐ๊ฐ +90์ผ ์ด๋ด์ธ ์ฅ๋น.\"\"\"\n",
" md = np.arange(1, HORIZON + 1) * MDAYS\n",
" cand = np.where(pred < HORIZON * MDAYS + 90)[0]\n",
" n, M = len(cand), HORIZON\n",
" C = exp_cost_matrix(pred[cand], rate[cand], md)\n",
" c = C.ravel()\n",
" A, lo, hi = [], [], []\n",
" for i in range(n): # ์ฅ๋น๋น ํ ๋ฒ๋ง ๋ฐฐ์ (์ 12์นธ + ๋ฏธ์ ๋น)\n",
" row = np.zeros(n * (M + 1)); row[i * (M + 1):(i + 1) * (M + 1)] = 1; A.append(row); lo.append(1); hi.append(1)\n",
" for m in range(M): # ์๋ณ ๋ฅ๋ ฅยท์์ฐ\n",
" row = np.zeros(n * (M + 1)); row[m::(M + 1)] = 1\n",
" A.append(row); lo.append(0); hi.append(cap)\n",
" A.append(row * (C_PLAN + C_KIT)); lo.append(0); hi.append(budget)\n",
" if use_kit: # ์ํจ์ฑ ํคํธ: m์ ์ดํ ์ ๋น ์์๋ ๊ทธ ์์ ์ ์ ํจ๊ธฐ๊ฐ์ด ๋จ์ ๋กํธ ์๋์ ๋์ ์ ์๋ค\n",
" for m in range(M):\n",
" sup = sum(q for q, e in lots if e >= md[m] + KIT_MARGIN)\n",
" row = np.zeros(n * (M + 1))\n",
" for mm in range(m, M):\n",
" row[mm::(M + 1)] = 1\n",
" A.append(row); lo.append(0); hi.append(sup)\n",
" r = milp(c, constraints=LinearConstraint(np.array(A), lo, hi), integrality=np.ones(len(c)), bounds=Bounds(0, 1), options={\"time_limit\": 20})\n",
" month = np.full(len(pred), -1)\n",
" if r.x is not None:\n",
" x = np.round(r.x).reshape(n, M + 1)\n",
" assign = np.where(x[:, :M].sum(axis=1) > 0.5, x[:, :M].argmax(axis=1), -1)\n",
" month[cand] = assign\n",
" return month\n",
"\n",
"\n",
"def plan_greedy(pred, margin=45, cap=CAP_M, budget=BUDGET_M):\n",
" \"\"\"๊ฐ๋จํ ํ์ ๋ฐฐ์ : ์ ๋น ๋ชฉํ์ผ(์์ธก RUL - margin)์ด ๋น ๋ฅธ ์์ผ๋ก ๋ชฉํ์ผ ์ด์ ๊ฐ์ฅ ๋ฆ์ ๋น ์, ์์ผ๋ฉด ๊ฐ์ฅ ์ด๋ฅธ ๋น ์์ ๋ฐฐ์ .\"\"\"\n",
" cap = min(cap, int(budget // (C_PLAN + C_KIT))) # ์ ์์ฐ์ด ํ์ฉํ๋ ๋์๋ก ๋ฅ๋ ฅ์ ์ ํ\n",
" month = np.full(len(pred), -1)\n",
" left = np.full(HORIZON, cap)\n",
" for i in np.argsort(pred):\n",
" if pred[i] > HORIZON * MDAYS + 60: break\n",
" tgt = int(np.clip((pred[i] - margin) // MDAYS - 1, 0, HORIZON - 1))\n",
" for m in list(range(tgt, -1, -1)) + list(range(tgt + 1, HORIZON)):\n",
" if left[m] > 0:\n",
" month[i] = m; left[m] -= 1; break\n",
" return month\n",
"\n",
"\n",
"def plan_fixed(lin_rul, cap=CAP_M, budget=BUDGET_M):\n",
" \"\"\"๊ดํ: ๊ท์ ์ฃผ๊ธฐ ๋๋ฌ ์์๋๋ก(์ ํ ์์ฌ ์ผ์) ์๋ณ ๋ฅ๋ ฅ ์์์ ๋ฐฐ์ .\"\"\"\n",
" return plan_greedy(np.asarray(lin_rul, float), margin=0, cap=cap, budget=budget)\n",
"\n",
"\n",
"def evaluate_plan(month, true_rul, rate, cost_kit=True):\n",
" \"\"\"์ฐธ RUL๋ก ๊ณํ์ ์ค์ ๋น์ฉ์ ๊ณ์ฐํ๋ค: ์ ๋น์ผ ์ด์ ์ ๊ณ ์ฅ -> ๋น๊ณํ ๊ณ ์ฅ ๋น์ฉ, ์๋๋ฉด ๊ณํ ๋น์ฉ + ๋ฏธ์ฌ์ฉ ์๋ช
์์ค.\"\"\"\n",
" day = np.where(month >= 0, (month + 1) * MDAYS, 10 ** 9)\n",
" fail = (true_rul < day) & (true_rul < HORIZON * MDAYS)\n",
" planned = (month >= 0) & ~fail\n",
" waste = np.where(planned, (true_rul - day) * rate * 0.6, 0) * C_WASTE_KH / 1000\n",
" cost = fail * C_FAIL + planned * (C_PLAN + (C_KIT if cost_kit else 0)) + waste\n",
" return {\"total\": float(cost.sum()), \"fail\": int(fail.sum()), \"planned\": int(planned.sum()), \"waste\": float(waste.sum()), \"unsched_fail\": int(((month < 0) & fail).sum())}\n",
"\n",
"\n",
"def kit_fefo(month, lots=LOTS):\n",
" \"\"\"์ ํจ๊ธฐํ์ด ๋น ๋ฅธ ๋กํธ๋ถํฐ ์ฐ๋ FEFO๋ก ํคํธ๋ฅผ ๋ฐฐ์ ํ๊ณ ์ ๋น์ผ์ ์ ํจ๊ธฐ๊ฐ์ด ๋ถ์กฑํ ๋ฐฐ์ ์์ ๋ง๋ฃ ํ๊ธฐ ์๋์ ์ผ๋ค.\"\"\"\n",
" q = [[qty, e] for qty, e in sorted(lots, key=lambda t: t[1])]\n",
" short = 0\n",
" for m in sorted(month[month >= 0]):\n",
" day = (m + 1) * MDAYS\n",
" for lot in q:\n",
" if lot[1] >= day + KIT_MARGIN and lot[0] > 0:\n",
" lot[0] -= 1; break\n",
" else:\n",
" short += 1\n",
" expired = sum(l[0] for l in q if l[1] < HORIZON * MDAYS)\n",
" return {\"short\": short, \"expired_unused\": expired, \"left\": sum(l[0] for l in q)}\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"tr = make_fleet(900, seed=1)\n",
"eng = fit_engine(tr)\n",
"fl = add_features(make_fleet(900, seed=3))\n",
"al = fl[fl.event == 0].reset_index(drop=True)\n",
"al[\"rul_pred\"], _, _ = predict_rul(eng, al)\n",
"al[\"month\"] = plan_milp(al[\"rul_pred\"].values, al[\"rate\"].values)\n",
"al[\"p90\"] = aft_fail_prob(eng[\"p\"], al, al[\"h_since\"].values, al[\"h_since\"].values + 90 * al[\"rate\"].values)\n",
"\n",
"# ---------------- 1) ๋ถ๋๋ณ ๋
ธํ๋ ํํฉ ----------------\n",
"g = al.groupby(\"unit\")\n",
"board = pd.DataFrame({\"์ฅ๋น ์\": g.size(), \"ํ๊ท ๋
ธํ ์ง์\": g[\"aging_idx\"].mean(), \"ํ๊ท ์์ธก RUL(์ผ)\": g[\"rul_pred\"].mean(), \"180์ผ ๋ด ๊ณ ์ํ\": g.apply(lambda x: int((x.rul_pred < 180).sum())),\n",
" \"ํ์งยท์ผ๋ถ ๋น์จ\": g.apply(lambda x: x.env.isin([\"rough\", \"salt\"]).mean()), \"12๊ฐ์ ๋ด ๊ถ์ฅ ์ ๋น\": g.apply(lambda x: int((x.month >= 0).sum()))})\n",
"board[\"๊ณ ์ํ ๋น์จ\"] = board[\"180์ผ ๋ด ๊ณ ์ํ\"] / board[\"์ฅ๋น ์\"]\n",
"print(f\"[1] ๋ถ๋๋ณ ๋
ธํ๋ ํํฉ(์์กด ์ฅ๋น {len(al)}๋, ๊ฐ์ ๋ถ๋ 6๊ฐ) [๊ต์ก์ฉ๊ฐ์์๋ฃ]\")\n",
"print(board.round(2).to_string())\n",
"worst = board[\"๊ณ ์ํ ๋น์จ\"].idxmax()\n",
"print(f\" ๊ณ ์ํ ๋น์จ์ด ๊ฐ์ฅ ๋์ ๋ถ๋ {worst}({board.loc[worst, '๊ณ ์ํ ๋น์จ']:.0%}), ํ๊ฒฝ ๊ตฌ์ฑ(ํ์งยท์ผ๋ถ {board.loc[worst, 'ํ์งยท์ผ๋ถ ๋น์จ']:.0%})๊ณผ ํจ๊ป ๋ณธ๋ค.\")\n",
"\n",
"# ---------------- 2) ๊ถ์ฅ ์ ๋น ์บ๋ฆฐ๋ ----------------\n",
"cal = pd.crosstab(al.loc[al.month >= 0, \"unit\"], al.loc[al.month >= 0, \"month\"] + 1).reindex(columns=range(1, HORIZON + 1), fill_value=0)\n",
"print(\"\\n[2] ๊ถ์ฅ ์ ๋น ์บ๋ฆฐ๋(๋ถ๋ x ์, ๋์; ์ ์์ฐยท๋ฅ๋ ฅยทํคํธ ์ ํจ๊ธฐ๊ฐ ๋ฐ์)\")\n",
"print(cal.to_string())\n",
"print(f\" ์ ํฉ๊ณ: \" + \" \".join(f\"{m}์ {int(c)}\" for m, c in cal.sum().items()) + f\" / ์ ๋น ์ผ์ ์ด ์กํ์ง ์์ ๊ณ ์ํ(RUL 180์ผ ๋ฏธ๋ง) ์ฅ๋น {int(((al.month < 0) & (al.rul_pred < 180)).sum())}๋\")\n",
"top = al.sort_values(\"rul_pred\").head(8)\n",
"print(\"\\n ์ํ ์์ 8๋ [๊ถ์ฅ ์์ ์น์ธ ์ ์ถ์ฒ์]\")\n",
"print(pd.DataFrame({\"์ฅ๋น\": top.eq_id, \"๋ถ๋\": top.unit, \"ํ๊ฒฝ\": top.env.map(ENV_KO), \"์์ธก RUL(์ผ)\": top.rul_pred, \"90์ผ ๋ด ๊ณ ์ฅ ํ๋ฅ \": top.p90, \"๊ถ์ฅ ์\": top.month.map(lambda m: f\"{m + 1}์\" if m >= 0 else \"-\")}).round(2).to_string(index=False))\n",
"\n",
"# ---------------- 3) ๊ธฐ์ฌ๋ ์์ 3๊ฐ(๋ณ์๊ตฐ ํ๊ท ๋์ฒด) ----------------\n",
"GROUPS = {\"์ด์ฉ ์ด๋ ฅ(๊ฒฝ๊ณผ ์๊ฐยท๊ฑฐ๋ฆฌยท์ฐ์)\": [\"h_since\", \"hours\", \"km\", \"age_y\"], \"์ด์ฉ ํ๊ฒฝ\": [\"env\"], \"์ ๋น ์ด๋ ฅ(๋น๊ณํยท๊ณํ ์ ๋น)\": [\"unpl\", \"n_maint\"], \"์ ๊ฒ ๋ง๋ชจ ๋น์จ\": [\"insp\"], \"์ผ ๊ฐ๋๋ฅ \": [\"rate\"]}\n",
"MEAN = tr[[\"h_since\", \"hours\", \"km\", \"age_y\", \"unpl\", \"n_maint\", \"insp\", \"rate\"]].mean()\n",
"raw_cols = [\"eq_id\", \"unit\", \"env\", \"etype\", \"rate\", \"hours\", \"km\", \"age_y\", \"n_ovh\", \"h_since\", \"unpl\", \"n_maint\", \"insp\"]\n",
"base_raw = al[raw_cols].copy()\n",
"full = al[\"rul_pred\"].values\n",
"contrib = {}\n",
"for gname, cols in GROUPS.items():\n",
" x = base_raw.copy()\n",
" for c in cols:\n",
" x[c] = \"normal\" if c == \"env\" else MEAN[c]\n",
" xf = add_features(x.assign(event=0, t_fail=0.0, dur=x[\"h_since\"]))\n",
" contrib[gname] = full - predict_rul(eng, xf)[0] # ์์ = ์ด ์์ธ ๋๋ฌธ์ ํ๊ท ์ฅ๋น๋ณด๋ค ์์ฌ ์๋ช
์ด ์งง๋ค\n",
"C = pd.DataFrame(contrib)\n",
"t3 = C.apply(lambda r: r.reindex(r.abs().sort_values(ascending=False).index)[:3], axis=1)\n",
"okn = int(sum(t3.iloc[i].notna().sum() == 3 and np.isfinite(t3.iloc[i].dropna().values).all() for i in range(len(t3))))\n",
"print(f\"\\n[3] ์ฅ๋น {len(al)}๋ ์ค ๊ธฐ์ฌ๋ ์์ 3๊ฐ ์์ธ ์๋ ์ฐ์ถ {okn}๋ ({okn / len(al):.0%}, ๋ชฉํ 100%)\")\n",
"print(\" ์์ธ๊ตฐ๋ณ ํ๊ท ๊ธฐ์ฌ ํฌ๊ธฐ(์ผ):\", \", \".join(f\"{k} {v:.0f}\" for k, v in C.abs().mean().sort_values(ascending=False).items()))\n",
"\n",
"\n",
"def explain(i):\n",
" r, row = al.loc[i], C.loc[i]\n",
" ordered = row.reindex(row.abs().sort_values(ascending=False).index)[:3]\n",
" main = ordered.index[0]\n",
" why = {\"์ด์ฉ ์ด๋ ฅ(๊ฒฝ๊ณผ ์๊ฐยท๊ฑฐ๋ฆฌยท์ฐ์)\": \"์ฐฝ์ ๋น ์ดํ ์ด์ฉ ์๊ฐ์ด ๊ธธ์ด\", \"์ด์ฉ ํ๊ฒฝ\": f\"{ENV_KO[r.env]} ํ๊ฒฝ ๊ฐํน๋ ๋๋ฌธ์\", \"์ ๋น ์ด๋ ฅ(๋น๊ณํยท๊ณํ ์ ๋น)\": \"๋น๊ณํ ์ ๋น๊ฐ ์ฆ์\", \"์ ๊ฒ ๋ง๋ชจ ๋น์จ\": \"์ ๊ฒ ๋ง๋ชจ ๋น์จ์ด ๋์\", \"์ผ ๊ฐ๋๋ฅ \": \"์ผ ๊ฐ๋๋ฅ ์ด ๋์\"}[main]\n",
" return f\"{r.eq_id}({r.unit}, {ENV_KO[r.env]}): {why} ์์ฌ ์๋ช
{r.rul_pred:.0f}์ผ ์์ธก. ์ฃผ์ ์์ธ \" + \", \".join(f\"{k.split('(')[0]}({v:+.0f}์ผ)\" for k, v in ordered.items())\n",
"\n",
"\n",
"print(\"[4] ์ํ ์์ 3๋ ์์ฐ์ด ์ค๋ช
(์์ = ํ๊ท ์ฅ๋น๋ณด๋ค ์์ฌ ์๋ช
์ด ์งง์์ง๋ ์์ธ)\")\n",
"for i in top.index[:3]:\n",
" print(\" -\", explain(i))\n",
"print(\" ๋์๋ณด๋์ ์ค๋ช
์ ์ ๋น๊ณํ๊ด์ ํ๋จ ๋ณด์กฐ์ด๋ฉฐ ์ผ์ ํ์ ๊ณผ ์ฅ๋น ๋ํ ๊ฑด์๋ ๊ตฐ์์งํ๊ด ์น์ธ(M7)์ ๋ฐ๋ฅธ๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(2, 2, figsize=(13, 8))\n",
"ax[0, 0].bar(board.index, board[\"๊ณ ์ํ ๋น์จ\"] * 100); ax[0, 0].set_title(\"1 High-risk share by unit (%, RUL < 180 days)\")\n",
"im = ax[0, 1].imshow(cal.values, aspect=\"auto\", cmap=\"YlOrRd\"); ax[0, 1].set_xticks(range(HORIZON)); ax[0, 1].set_xticklabels(range(1, HORIZON + 1)); ax[0, 1].set_yticks(range(len(cal))); ax[0, 1].set_yticklabels(cal.index)\n",
"ax[0, 1].set_title(\"2 Recommended overhaul calendar (unit x month)\"); plt.colorbar(im, ax=ax[0, 1])\n",
"ax[1, 0].bar(board.index, board[\"ํ๊ท ๋
ธํ ์ง์\"]); ax[1, 0].axhline(1, c=\"r\", ls=\"--\"); ax[1, 0].set_title(\"3 Mean aging index by unit\")\n",
"r0 = C.loc[top.index[0]]\n",
"ax[1, 1].barh([\"usage history\", \"environment\", \"maintenance history\", \"inspection wear\", \"daily usage\"][::-1], r0.values[::-1]); ax[1, 1].set_title(\"4 Contribution to RUL, riskiest unit (days)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
06 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[1] ๋ถ๋๋ณ ๋
ธํ๋ ํํฉ(์์กด ์ฅ๋น 601๋, ๊ฐ์ ๋ถ๋ 6๊ฐ) [๊ต์ก์ฉ๊ฐ์์๋ฃ]\n",
" ์ฅ๋น ์ ํ๊ท ๋
ธํ ์ง์ ํ๊ท ์์ธก RUL(์ผ) 180์ผ ๋ด ๊ณ ์ํ ํ์งยท์ผ๋ถ ๋น์จ 12๊ฐ์ ๋ด ๊ถ์ฅ ์ ๋น ๊ณ ์ํ ๋น์จ\n",
"unit \n",
"U1 110 0.58 1267.69 5 0.13 21 0.05\n",
"U2 87 0.50 1340.48 5 0.64 11 0.06\n",
"U3 101 0.53 1200.60 4 0.63 15 0.04\n",
"U4 110 0.57 1259.99 7 0.13 24 0.06\n",
"U5 106 0.58 1287.43 6 0.31 18 0.06\n",
"U6 87 0.51 1370.82 5 0.48 12 0.06\n",
" ๊ณ ์ํ ๋น์จ์ด ๊ฐ์ฅ ๋์ ๋ถ๋ U4(6%), ํ๊ฒฝ ๊ตฌ์ฑ(ํ์งยท์ผ๋ถ 13%)๊ณผ ํจ๊ป ๋ณธ๋ค.\n",
"\n",
"[2] ๊ถ์ฅ ์ ๋น ์บ๋ฆฐ๋(๋ถ๋ x ์, ๋์; ์ ์์ฐยท๋ฅ๋ ฅยทํคํธ ์ ํจ๊ธฐ๊ฐ ๋ฐ์)\n",
"month 1 2 3 4 5 6 7 8 9 10 11 12\n",
"unit \n",
"U1 1 8 2 6 4 0 0 0 0 0 0 0\n",
"U2 3 2 3 1 2 0 0 0 0 0 0 0\n",
"U3 4 1 3 4 3 0 0 0 0 0 0 0\n",
"U4 4 4 7 3 5 1 0 0 0 0 0 0\n",
"U5 6 1 3 4 4 0 0 0 0 0 0 0\n",
"U6 2 4 2 2 2 0 0 0 0 0 0 0\n",
" ์ ํฉ๊ณ: 1์ 20 2์ 20 3์ 20 4์ 20 5์ 20 6์ 1 7์ 0 8์ 0 9์ 0 10์ 0 11์ 0 12์ 0 / ์ ๋น ์ผ์ ์ด ์กํ์ง ์์ ๊ณ ์ํ(RUL 180์ผ ๋ฏธ๋ง) ์ฅ๋น 0๋\n",
"\n",
" ์ํ ์์ 8๋ [๊ถ์ฅ ์์ ์น์ธ ์ ์ถ์ฒ์]\n",
" ์ฅ๋น ๋ถ๋ ํ๊ฒฝ ์์ธก RUL(์ผ) 90์ผ ๋ด ๊ณ ์ฅ ํ๋ฅ ๊ถ์ฅ ์\n",
"EQ030723 U6 ์ผ๋ถ 78.41 0.49 1์\n",
"EQ030395 U2 ํ์ง 83.07 0.72 1์\n",
"EQ030208 U5 ํนํ 88.97 0.73 1์\n",
"EQ030501 U4 ์ผ๋ฐ 90.25 0.43 1์\n",
"EQ030658 U3 ํนํ 98.04 0.49 1์\n",
"EQ030648 U2 ํ์ง 98.17 0.41 1์\n",
"EQ030142 U4 ์ผ๋ฐ 107.32 0.66 1์\n",
"EQ030652 U3 ํนํ 107.97 0.34 1์\n",
"\n",
"[3] ์ฅ๋น 601๋ ์ค ๊ธฐ์ฌ๋ ์์ 3๊ฐ ์์ธ ์๋ ์ฐ์ถ 601๋ (100%, ๋ชฉํ 100%)\n",
" ์์ธ๊ตฐ๋ณ ํ๊ท ๊ธฐ์ฌ ํฌ๊ธฐ(์ผ): ์ด์ฉ ์ด๋ ฅ(๊ฒฝ๊ณผ ์๊ฐยท๊ฑฐ๋ฆฌยท์ฐ์) 487, ์ ๊ฒ ๋ง๋ชจ ๋น์จ 344, ์ผ ๊ฐ๋๋ฅ 260, ์ด์ฉ ํ๊ฒฝ 207, ์ ๋น ์ด๋ ฅ(๋น๊ณํยท๊ณํ ์ ๋น) 94\n",
"[4] ์ํ ์์ 3๋ ์์ฐ์ด ์ค๋ช
(์์ = ํ๊ท ์ฅ๋น๋ณด๋ค ์์ฌ ์๋ช
์ด ์งง์์ง๋ ์์ธ)\n",
" - EQ030723(U6, ์ผ๋ถ): ์ ๊ฒ ๋ง๋ชจ ๋น์จ์ด ๋์ ์์ฌ ์๋ช
78์ผ ์์ธก. ์ฃผ์ ์์ธ ์ ๊ฒ ๋ง๋ชจ ๋น์จ(-286์ผ), ์ด์ฉ ํ๊ฒฝ(-172์ผ), ์ด์ฉ ์ด๋ ฅ(-128์ผ)\n",
" - EQ030395(U2, ํ์ง): ์ ๊ฒ ๋ง๋ชจ ๋น์จ์ด ๋์ ์์ฌ ์๋ช
83์ผ ์์ธก. ์ฃผ์ ์์ธ ์ ๊ฒ ๋ง๋ชจ ๋น์จ(-279์ผ), ์ด์ฉ ํ๊ฒฝ(-218์ผ), ์ด์ฉ ์ด๋ ฅ(-110์ผ)\n",
" - EQ030208(U5, ํนํ): ์ฐฝ์ ๋น ์ดํ ์ด์ฉ ์๊ฐ์ด ๊ธธ์ด ์์ฌ ์๋ช
89์ผ ์์ธก. ์ฃผ์ ์์ธ ์ด์ฉ ์ด๋ ฅ(-382์ผ), ์ ๊ฒ ๋ง๋ชจ ๋น์จ(-351์ผ), ์ด์ฉ ํ๊ฒฝ(-64์ผ)\n",
" ๋์๋ณด๋์ ์ค๋ช
์ ์ ๋น๊ณํ๊ด์ ํ๋จ ๋ณด์กฐ์ด๋ฉฐ ์ผ์ ํ์ ๊ณผ ์ฅ๋น ๋ํ ๊ฑด์๋ ๊ตฐ์์งํ๊ด ์น์ธ(M7)์ ๋ฐ๋ฅธ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
07. ์ ๋น ์ผ์ ํ์ ๋ฐ ์ฅ๋น ๋ํ ๊ฑด์๋ฅผ ์ํ ๊ตฐ์์งํ๊ด HITL ์น์ธ ์ ์ฐจ ์ค๊ณ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ ๋น ์ผ์ ํ์ ์ ์ํ ๊ตฐ์์งํ๊ด HITL ์น์ธ ์ ์ฐจ๋ฅผ ์ค๊ณํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ ๋น ์ผ์ ํ์ ๊ณผ ์ฅ๋น ๋ํ ๊ฑด์๋ฅผ ์ํ ๊ตฐ์์งํ๊ด HITL ์น์ธ ์ ์ฐจ๋ฅผ ์ค๊ณํ๊ณ ๋น์ฉ ์ ๊ฐ ํจ๊ณผ๋ฅผ ๊ฒ์ฆํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ดํ ์ผ์ ๋๋น ์ ์ ์ผ์ ์ 12๊ฐ์ ๋น์ฉ์ ํ์, ์ ๋น์ฐฝ ๋ฅ๋ ฅ ๊ฐ์, ์์ฐ ์ญ๊ฐ, ํคํธ ์ฌ๊ณ ๋ถ์กฑ, ๊ฐ๋๋ฅ ์์น ๋ฑ ์๋๋ฆฌ์ค๋ณ๋ก ๋น๊ตํ๊ณ ๋ฐฑํ
์คํธ์ ๋ฏผ๊ฐ๋ ํ์ธํ๊ณ ์ ํจ์ฑ์ ์ ๊ฒํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/optimization/sup02_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": [
"\"\"\"์ ๋น์ฐฝ ๋ฅ๋ ฅยท์์ฐยท์ํจ์ฑ ํคํธ ์ ์ฝ ์๋ ์์ธก ๊ธฐ๋ฐ ๊ณํ์ ๋น ์ผ์ ์ ๊ท์ ์ฃผ๊ธฐ ๊ณ ์ ์ผ์ ๊ณผ ๋น๊ตํด ๋น์ฉยทํคํธ ์์ง์จ์ ์๋๋ฆฌ์คยท๋ฐฑํ
์คํธ๋ก ๊ฒ์ฆํ๊ณ (๋น์ฉ 15% ์ด์ ์ ๊ฐ), ์ ๋น ์ผ์ ํ์ ๊ณผ ์ฅ๋น ๋ํ ๊ฑด์๋ฅผ ๊ตฐ์์งํ๊ด(๋ํ๋ ์ฌ์ ๋ด๋น 2๋จ๊ณ)์ด ์ง์ ์น์ธยท๋ฐ๋ คํ๋ HITL ๊ฒฐ์ฌ ์ํฌํ๋ก์ฐ(๊ถํยท์ฌ์ ยทํด์ ์ฒด์ธ ๊ฐ์ฌ ๋ก๊ทธ)๋ฅผ ๊ตฌํํ๋ค.\"\"\"\n",
"import hashlib\n",
"import json\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ฅ๋น ๋ณด์ ํํฉยท๊ณ ์ฅ ์ด๋ ฅ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์ฅ๋นยท์์น ์์)\n",
"# ๊ฐ์ ๋ถ๋ 6๊ฐ(U1~U6)์ ์ฅ๋น๊ฐ ์ด์ฉ ํ๊ฒฝ(normal/rough/salt/cold)์ ๋ฐ๋ผ ๋ค๋ฅธ ์๋๋ก ๋
ธํํํ๋ค. ์ฐฝ์ ๋น ํ ์ด์ฉ์๊ฐ(h_since)์ด\n",
"# ์ฅ๋น๋ง๋ค ์จ์ ์๋ช
(t_fail)์ ๋์ผ๋ฉด ๊ณ ์ฅ(event=1) ์ด๋ ฅ, ๋์ง ์์์ผ๋ฉด ํ์ฌ๊น์ง ์์กด(event=0, ๊ด์ธก ์ค๋์ ๋จ)์ด๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"ENVS = [\"normal\", \"rough\", \"salt\", \"cold\"]\n",
"ENV_W = {\"normal\": 1.00, \"rough\": 1.35, \"salt\": 1.25, \"cold\": 1.12} # ํ๊ฒฝ ๊ฐ์ค์น(์จ์ ์ ๋ต, ๊ฐ์ ๊ฐ): ํด์๋ก ์๋ช
์ด ์งง๋ค\n",
"ENV_KO = {\"normal\": \"์ผ๋ฐ\", \"rough\": \"ํ์ง\", \"salt\": \"์ผ๋ถ\", \"cold\": \"ํนํ\"}\n",
"UNITS = [\"U1\", \"U2\", \"U3\", \"U4\", \"U5\", \"U6\"]\n",
"UNIT_ENV = {\"U1\": [.7, .1, .1, .1], \"U2\": [.2, .6, .1, .1], \"U3\": [.2, .1, .6, .1], \"U4\": [.3, .1, .1, .5], \"U5\": [.5, .3, .1, .1], \"U6\": [.4, .1, .4, .1]}\n",
"ETYPES = [\"truck\", \"generator\", \"trailer\", \"radio\"]\n",
"CYCLE = 8000.0 # ๊ท์ ์ฐฝ์ ๋น ์ฃผ๊ธฐ(์ด์ฉ์๊ฐ h, ๊ฐ์ ๊ฐ)\n",
"KSHAPE = 12.0 # ์๋ช
๋ถํฌ ์์ด๋ธ ํ์(๋ง๋ชจํ ๊ณ ์ฅ, ํด์๋ก ์๋ช
์ด ๋ชจ์ฌ ์๋ค)\n",
"INSP_NOISE = 0.025 # ์ ๋น ์ ๊ฒ ๋ง๋ชจ ๋น์จ ์ธก์ ์ค์ฐจ(๊ฐ์ ๊ฐ)\n",
"\n",
"\n",
"def make_fleet(n=900, seed=0, env_w=None, shift=1.0):\n",
" \"\"\"์ฅ๋น n๋. env_w๋ก ์จ์ ํ๊ฒฝ ๊ฐ์ค์น๋ฅผ ๋ฐ๊ฟ ์ด์ ๋ถํฌ ๋ณํ๋ฅผ ๋ชจ์ํ ์ ์๋ค. shift>1์ด๋ฉด ๊ฐ๋๋ฅ ์ด ๋์์ง๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" w = env_w or ENV_W\n",
" unit = rng.choice(UNITS, n)\n",
" env = np.array([rng.choice(ENVS, p=UNIT_ENV[u]) for u in unit])\n",
" et = rng.choice(ETYPES, n, p=[.4, .2, .2, .2])\n",
" rate = rng.lognormal(np.log(3.0 * shift), 0.25, n) # ์ผ ํ๊ท ์ด์ฉ์๊ฐ(h/์ผ)\n",
" h_since = rng.uniform(300, 11500, n) # ๋ง์ง๋ง ์ฐฝ์ ๋น ์ดํ ์ด์ฉ์๊ฐ\n",
" n_ovh = rng.integers(0, 3, n)\n",
" hours = h_since + n_ovh * rng.uniform(5000, 8000, n) # ๋์ ์ด์ฉ์๊ฐ\n",
" km = hours * rng.normal(17, 3, n) * np.where(env == \"rough\", 0.8, 1.0) # ๋์ ๊ธฐ๋๊ฑฐ๋ฆฌ(km)\n",
" age_y = np.clip(hours / (rate * 365) * rng.uniform(0.9, 1.15, n), 0.5, 25)\n",
" unpl = rng.poisson(0.35 * h_since / 1000 * np.array([w[e] for e in env]) / 1.3) # ๋น๊ณํ ์ ๋น ๊ฑด์(ํ๊ฒฝ์ด ๊ฐํนํ ์๋ก ๋ง๋ค)\n",
" n_maint = rng.poisson(0.6 * h_since / 1000 + 1) # ๊ณํ ์ ๋น ๊ฑด์\n",
" scale = 9600 / np.array([w[e] for e in env]) * 0.985 ** unpl * np.exp(rng.normal(0, 0.04, n))\n",
" t_fail = scale * rng.weibull(KSHAPE, n) # ์ฐฝ์ ๋น ํ ๊ณ ์ฅ๊น์ง ์ด์ฉ์๊ฐ(์จ์ ์ ๋ต)\n",
" event = (t_fail <= h_since).astype(int)\n",
" dur = np.where(event == 1, t_fail, h_since)\n",
" df = pd.DataFrame({\"eq_id\": [f\"EQ{seed:02d}{i:04d}\" for i in range(n)], \"unit\": unit, \"env\": env, \"etype\": et, \"rate\": rate, \"hours\": hours, \"km\": km, \"age_y\": age_y, \"n_ovh\": n_ovh,\n",
" \"h_since\": h_since, \"unpl\": unpl, \"n_maint\": n_maint, \"t_fail\": t_fail, \"event\": event, \"dur\": dur})\n",
" df[\"insp\"] = np.clip(dur / t_fail + rng.normal(0, INSP_NOISE, n), 0, 1.3) # ๋ง์ง๋ง ์ ๋น ์ ๊ฒ์์ ์ธก์ ํ ๋ง๋ชจ ๋น์จ(์๋ชจ ์๋ช
/์ด ์๋ช
, ์ค์ฐจ ํฌํจ)\n",
" df[\"rul_h\"] = np.where(event == 0, t_fail - h_since, np.nan) # ์์กด ์ฅ๋น์ ๋จ์ ์ด์ฉ์๊ฐ(์ ๋ต)\n",
" df[\"rul_d\"] = df[\"rul_h\"] / df[\"rate\"] # ๋จ์ ์ผ์(์ ๋ต)\n",
" return df\n",
"\n",
"\n",
"def mape(y, p):\n",
" # [TODO : ํ์ ์ค์ต 7] ์ ๋น ์ผ์ ํ์ ๋ฐ ์ฅ๋น ๋ํ ๊ฑด์๋ฅผ ์ํ ๊ตฐ์์งํ๊ด HITL ์น์ธ ์ ์ฐจ ์ค๊ณ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def mae(y, p):\n",
" # [TODO : ํ์ ์ค์ต 7] ์ ๋น ์ผ์ ํ์ ๋ฐ ์ฅ๋น ๋ํ ๊ฑด์๋ฅผ ์ํ ๊ตฐ์์งํ๊ด HITL ์น์ธ ์ ์ฐจ ์ค๊ณ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def rmse(y, p):\n",
" return float(np.sqrt(np.mean((np.asarray(y, float) - np.asarray(p, float)) ** 2)))\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ๋
ธํํ ํ์ ํผ์ฒ์ ํ์ต ํ๋ณธ ๋ง๋ค๊ธฐ. ํ๊ฒฝ ๊ฐ์ค์น W0๋ ๋์
์์ ์ ์ ๋ฌธ๊ฐ ์ถ์ ๊ฐ์ด๋ฉฐ(์ค์ ๊ฐ์ค์น์ ๋ค๋ฆ) M9์์ ์ฌ๋ณด์ ํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"W0 = {\"normal\": 1.00, \"rough\": 1.15, \"salt\": 1.10, \"cold\": 1.05}\n",
"FEATS = [\"h_since\", \"hours\", \"km\", \"age_y\", \"rate\", \"n_maint\", \"unpl\", \"unpl_per_kh\", \"km_per_h\", \"n_ovh\", \"k_rough\", \"k_salt\", \"k_cold\", \"k_truck\", \"k_gen\", \"k_trailer\",\n",
" \"aging_idx\", \"lin_rul_d\", \"hs_frac\", \"insp\", \"insp_eta_h\"]\n",
"\n",
"\n",
"def add_features(df, w=None):\n",
" d = df.copy()\n",
" w = w or W0\n",
" d[\"unpl_per_kh\"] = d[\"unpl\"] / (d[\"h_since\"] / 1000 + 0.5)\n",
" d[\"km_per_h\"] = d[\"km\"] / d[\"hours\"]\n",
" for e in (\"rough\", \"salt\", \"cold\"):\n",
" d[\"k_\" + e] = (d[\"env\"] == e).astype(int)\n",
" d[\"k_truck\"], d[\"k_gen\"], d[\"k_trailer\"] = (d[\"etype\"] == \"truck\").astype(int), (d[\"etype\"] == \"generator\").astype(int), (d[\"etype\"] == \"trailer\").astype(int)\n",
" d[\"aging_idx\"] = d[\"h_since\"] * d[\"env\"].map(w) / CYCLE # ํ๊ฒฝ ๊ฐ์ค ๋
ธํ ์ง์(1์ด๋ฉด ์ฐฝ์ ๋น ์ฃผ๊ธฐ์ ๋๋ฌ)\n",
" d[\"lin_rul_d\"] = (CYCLE - d[\"h_since\"]) / d[\"rate\"] # ์ ํ ๊ฐ๊ฐ์๊ฐ ์์ฌ ์ผ์(Baseline)\n",
" d[\"hs_frac\"] = d[\"h_since\"] / CYCLE\n",
" d[\"insp_eta_h\"] = d[\"h_since\"] / np.maximum(d[\"insp\"], 0.05) # ์ ๊ฒ ๋ง๋ชจ ๋น์จ๋ก ํ์ฐํ ์ด ์๋ช
์ถ์ (h)\n",
" return d\n",
"\n",
"\n",
"def snapshots(df, k=4, seed=0):\n",
" \"\"\"๊ณ ์ฅ ์ด๋ ฅ ์ฅ๋น์ ๊ณผ๊ฑฐ ์์ ํ๋ณธ: ๊ณ ์ฅ ์ ์์ ์์ ์ ์ ๊ฒ๊ฐยท์ด์ฉ์๊ฐ๊ณผ ๊ทธ๋์ ๋จ์ ์ผ์. (์์กด ์ฅ๋น์ ๋ฏธ๋๋ ์ ์ ์์ผ๋ฏ๋ก ๊ณ ์ฅ ์ด๋ ฅ๋ง ์ด๋ค)\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" f = df[df[\"event\"] == 1]\n",
" out = []\n",
" for _ in range(k):\n",
" s = f.copy()\n",
" h = s[\"t_fail\"] * rng.uniform(0.2, 0.98, len(s))\n",
" ratio = h / s[\"h_since\"]\n",
" s[\"hours\"] = s[\"hours\"] - (s[\"h_since\"] - h)\n",
" s[\"km\"] = s[\"km\"] * ratio.clip(upper=1)\n",
" s[\"unpl\"] = np.floor(s[\"unpl\"] * ratio)\n",
" s[\"n_maint\"] = np.floor(s[\"n_maint\"] * ratio)\n",
" s[\"insp\"] = np.clip(h / s[\"t_fail\"] + rng.normal(0, INSP_NOISE, len(s)), 0, 1.3)\n",
" s[\"h_since\"] = h\n",
" s[\"rul_d\"] = (s[\"t_fail\"] - h) / s[\"rate\"]\n",
" s[\"event\"] = 0\n",
" out.append(s)\n",
" return pd.concat(out, ignore_index=True)\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ์์กด ๋ถ์ ๋๊ตฌ(scipy๋ง ์ฌ์ฉ): ์์ด๋ธ AFT(๊ฐ์ ์๋ช
) ๋ชจํ๊ณผ Cox ๋น๋ก์ํ ๋ชจํ\n",
"# ---------------------------------------------------------------------------\n",
"from scipy.optimize import minimize\n",
"\n",
"\n",
"def aft_x(d):\n",
" return np.c_[np.ones(len(d)), d[\"env\"].eq(\"rough\"), d[\"env\"].eq(\"salt\"), d[\"env\"].eq(\"cold\"), d[\"unpl\"] / (d[\"h_since\"] / 1000 + 0.5)].astype(float)\n",
"\n",
"\n",
"AFT_COLS = [\"์ ํธ\", \"ํ์ง\", \"์ผ๋ถ\", \"ํนํ\", \"๋น๊ณํ ์ ๋น์จ\"]\n",
"\n",
"\n",
"def fit_aft(d):\n",
" \"\"\"์ฐฝ์ ๋น ํ ์ด์ฉ์๊ฐ dur(๊ณ ์ฅ์ด๋ฉด event=1, ์์กด์ด๋ฉด ์ค๋์ ๋จ)์ ๋ํ ์์ด๋ธ AFT ์ต๋์ฐ๋ ์ถ์ . ๋ฐํ: ๊ณ์(๋ง์ง๋ง์ log ํ์).\"\"\"\n",
" x, t, e = aft_x(d), d[\"dur\"].values, d[\"event\"].values\n",
"\n",
" def nll(p):\n",
" lam, k = np.exp(x @ p[:-1]), np.exp(p[-1])\n",
" z = (t / lam) ** k\n",
" return -(e * (np.log(k) - np.log(lam) + (k - 1) * (np.log(t) - np.log(lam))) - z).sum()\n",
"\n",
" r = minimize(nll, np.r_[np.log(8000), np.zeros(x.shape[1] - 1), np.log(5)], method=\"BFGS\")\n",
" return r.x\n",
"\n",
"\n",
"def aft_pred(p, d, q=0.5, h=None):\n",
" \"\"\"์ด๋ฏธ h(์ด์ฉ์๊ฐ)๊น์ง ์์กดํ๋ค๋ ์กฐ๊ฑด์์ ๋ถ์์ q ์์กด ์์ (์ด์ฉ์๊ฐ h ๊ธฐ์ค ์ด ์๋ช
)์ ๋๋ ค์ค๋ค. q=0.5๊ฐ ์กฐ๊ฑด๋ถ ์ค์๊ฐ.\"\"\"\n",
" lam, k = np.exp(aft_x(d) @ p[:-1]), np.exp(p[-1])\n",
" h = d[\"h_since\"].values if h is None else h\n",
" return lam * ((h / lam) ** k - np.log(q)) ** (1 / k)\n",
"\n",
"\n",
"def aft_fail_prob(p, d, h0, h1):\n",
" \"\"\"h0๊น์ง ์์กดํ ์ฅ๋น๊ฐ h0~h1 ์ฌ์ด์ ๊ณ ์ฅ ๋ ์กฐ๊ฑด๋ถ ํ๋ฅ .\"\"\"\n",
" lam, k = np.exp(aft_x(d) @ p[:-1]), np.exp(p[-1])\n",
" s0, s1 = np.exp(-(h0 / lam) ** k), np.exp(-(h1 / lam) ** k)\n",
" return 1 - s1 / s0\n",
"\n",
"\n",
"def fit_cox(d, iters=25):\n",
" \"\"\"Cox ๋น๋ก์ํ(Breslow ๊ทผ์ฌ) ๋ดํด-๋ฉ์จ. ๊ณต๋ณ๋: ํ์งยท์ผ๋ถยทํนํยท๋น๊ณํ ์ ๋น์จ. ๋ฐํ: ๊ณ์.\"\"\"\n",
" x = aft_x(d)[:, 1:]\n",
" o = np.argsort(-d[\"dur\"].values)\n",
" x, e = x[o], d[\"event\"].values[o]\n",
" b = np.zeros(x.shape[1])\n",
" for _ in range(iters):\n",
" r = np.exp(x @ b)\n",
" S0 = np.cumsum(r)\n",
" S1 = np.cumsum(r[:, None] * x, axis=0)\n",
" S2 = np.cumsum(r[:, None, None] * x[:, :, None] * x[:, None, :], axis=0)\n",
" m1 = S1 / S0[:, None]\n",
" g = ((x - m1) * e[:, None]).sum(axis=0)\n",
" H = -sum(e[i] * (S2[i] / S0[i] - np.outer(m1[i], m1[i])) for i in np.where(e == 1)[0])\n",
" step = np.linalg.solve(H - 1e-6 * np.eye(len(b)), g)\n",
" b = b - step\n",
" if np.abs(step).max() < 1e-7:\n",
" break\n",
" return b\n",
"\n",
"\n",
"def cindex(time, event, risk):\n",
" \"\"\"์ผ์น๋ C-index: ์ํ ์ ์๊ฐ ๋์ ์ชฝ์ด ๋จผ์ ๊ณ ์ฅ ๋ฌ๋๊ฐ(๊ด์ธก ๊ฐ๋ฅํ ์๋ง).\"\"\"\n",
" t, e, r = np.asarray(time), np.asarray(event), np.asarray(risk)\n",
" num = den = 0.0\n",
" for i in np.where(e == 1)[0]:\n",
" m = t > t[i]\n",
" den += m.sum()\n",
" num += (r[m] < r[i]).sum() + 0.5 * (r[m] == r[i]).sum()\n",
" return num / max(den, 1)\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ์์ฌ ์๋ช
(RUL, ์ผ) ์์ธก ์์ง: ์์ด๋ธ AFT ์กฐ๊ฑด๋ถ ์ค์๊ฐ + ๊ทธ๋๋์ธํธ ๋ถ์คํ
(๊ณ ์ฅ ์ด๋ ฅ ์์ ํ๋ณธ ํ์ต)์ ํ๊ท \n",
"# ---------------------------------------------------------------------------\n",
"from sklearn.ensemble import GradientBoostingRegressor\n",
"\n",
"\n",
"def fit_engine(train, seed=0):\n",
" p = fit_aft(train)\n",
" sn = add_features(snapshots(train, 5, seed))\n",
" sn[\"aft\"] = (aft_pred(p, sn) - sn[\"h_since\"]) / sn[\"rate\"]\n",
" g = GradientBoostingRegressor(n_estimators=250, max_depth=3, learning_rate=0.06, random_state=0).fit(sn[FEATS + [\"aft\"]], sn[\"rul_d\"])\n",
" return {\"p\": p, \"g\": g}\n",
"\n",
"\n",
"def predict_rul(eng, d):\n",
" \"\"\"d๋ add_features๋ฅผ ๊ฑฐ์น ํ. ๋ฐํ: (์์๋ธ RUL, AFT RUL, GBR RUL) - ์ผ ๋จ์.\"\"\"\n",
" aft = ((aft_pred(eng[\"p\"], d) - d[\"h_since\"]) / d[\"rate\"]).values\n",
" x = d[FEATS].copy()\n",
" x[\"aft\"] = aft\n",
" gb = eng[\"g\"].predict(x)\n",
" return np.clip(0.5 * aft + 0.5 * gb, 1, None), aft, gb\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ๊ณํ์ ๋น(์ฐฝ์ ๋น) ์ผ์ ์ถ์ฒ: ์๋ณ ์ ๋น์ฐฝ ๋ฅ๋ ฅยท์์ฐยท์ํจ์ฑ ๋ถํ ํคํธ(์ ํจ๊ธฐํ) ์ ์ฝ ์๋ ๊ธฐ๋ ๋น์ฉ ์ต์ํ (๋น์ฉ ๋จ๊ฐ๋ ๊ต์ก์ฉ ๊ฐ์ ๊ฐ, ๋ง์)\n",
"# ---------------------------------------------------------------------------\n",
"from scipy.optimize import milp, LinearConstraint, Bounds\n",
"from scipy.stats import norm\n",
"\n",
"C_PLAN, C_FAIL, C_WASTE_KH, C_KIT = 600.0, 2400.0, 40.0, 120.0 # ๊ณํ ์ฐฝ์ ๋น, ๋น๊ณํ ๊ณ ์ฅ(๊ธด๊ธ ์ ๋น ํฌํจ), ๋ฏธ์ฌ์ฉ ์๋ช
1000h๋น ์์ค, ์ํจ์ฑ ๋ถํ ํคํธ\n",
"HORIZON, MDAYS = 12, 30 # ๊ณํ 12๊ฐ์, ์ 30์ผ\n",
"CAP_M, BUDGET_M = 22, 15000.0 # ์๋ณ ์ ๋น์ฐฝ ๋ฅ๋ ฅ(๋), ์๋ณ ์์ฐ\n",
"LOTS = [(30, 70), (45, 170), (45, 390), (70, 800)] # ์ํจ์ฑ ํคํธ ๋กํธ(์๋, ์ ํจ๊ธฐํ๊น์ง ๋จ์ ์ผ์)\n",
"KIT_MARGIN = 14 # ์ ๋น์ผ ๊ธฐ์ค ์ต์ ์ ํจ๊ธฐ๊ฐ ์ฌ์ (์ผ)\n",
"\n",
"\n",
"def exp_cost_matrix(pred, rate, month_days):\n",
" \"\"\"์ฅ๋น i๋ฅผ ์ m์ ์ ๋นํ ๋์ ๊ธฐ๋ ๋น์ฉ. ์ฐธ RUL ~ N(pred, sigma), sigma = 35์ผ + 0.12 x pred (๊ฐ์ ). ๋ง์ง๋ง ์ด์ ๊ณํ ๊ธฐ๊ฐ ๋ฐ(๋ฏธ์ ๋น).\"\"\"\n",
" sig = 35 + 0.12 * pred\n",
" C = np.zeros((len(pred), len(month_days) + 1))\n",
" for j, day in enumerate(month_days):\n",
" z = (day - pred) / sig\n",
" pf = norm.cdf(z)\n",
" unused = np.maximum(pred - day, 0) * rate * 0.6 # ์ ๋น๋ฅผ ์๋น๊ธด ๋งํผ ๋ฒ๋ฆฌ๋ ์ด์ฉ์๊ฐ h(๊ทผ์ฌ)\n",
" C[:, j] = C_PLAN + C_KIT + pf * (C_FAIL - C_PLAN) + C_WASTE_KH * unused / 1000 * (1 - pf)\n",
" C[:, -1] = norm.cdf((HORIZON * MDAYS - pred) / sig) * C_FAIL\n",
" return C\n",
"\n",
"\n",
"def plan_milp(pred, rate, cap=CAP_M, budget=BUDGET_M, lots=LOTS, use_kit=True):\n",
" \"\"\"์ ์๊ณํ์ผ๋ก ์ฅ๋น๋ณ ์ ๋น ์(0~11) ๋๋ ๋ฏธ์ ๋น(-1)๋ฅผ ๋ฐฐ์ ํ๋ค. ํ๋ณด๋ ์์ธก RUL์ด ๊ณํ ๊ธฐ๊ฐ +90์ผ ์ด๋ด์ธ ์ฅ๋น.\"\"\"\n",
" md = np.arange(1, HORIZON + 1) * MDAYS\n",
" cand = np.where(pred < HORIZON * MDAYS + 90)[0]\n",
" n, M = len(cand), HORIZON\n",
" C = exp_cost_matrix(pred[cand], rate[cand], md)\n",
" c = C.ravel()\n",
" A, lo, hi = [], [], []\n",
" for i in range(n): # ์ฅ๋น๋น ํ ๋ฒ๋ง ๋ฐฐ์ (์ 12์นธ + ๋ฏธ์ ๋น)\n",
" row = np.zeros(n * (M + 1)); row[i * (M + 1):(i + 1) * (M + 1)] = 1; A.append(row); lo.append(1); hi.append(1)\n",
" for m in range(M): # ์๋ณ ๋ฅ๋ ฅยท์์ฐ\n",
" row = np.zeros(n * (M + 1)); row[m::(M + 1)] = 1\n",
" A.append(row); lo.append(0); hi.append(cap)\n",
" A.append(row * (C_PLAN + C_KIT)); lo.append(0); hi.append(budget)\n",
" if use_kit: # ์ํจ์ฑ ํคํธ: m์ ์ดํ ์ ๋น ์์๋ ๊ทธ ์์ ์ ์ ํจ๊ธฐ๊ฐ์ด ๋จ์ ๋กํธ ์๋์ ๋์ ์ ์๋ค\n",
" for m in range(M):\n",
" sup = sum(q for q, e in lots if e >= md[m] + KIT_MARGIN)\n",
" row = np.zeros(n * (M + 1))\n",
" for mm in range(m, M):\n",
" row[mm::(M + 1)] = 1\n",
" A.append(row); lo.append(0); hi.append(sup)\n",
" r = milp(c, constraints=LinearConstraint(np.array(A), lo, hi), integrality=np.ones(len(c)), bounds=Bounds(0, 1), options={\"time_limit\": 20})\n",
" month = np.full(len(pred), -1)\n",
" if r.x is not None:\n",
" x = np.round(r.x).reshape(n, M + 1)\n",
" assign = np.where(x[:, :M].sum(axis=1) > 0.5, x[:, :M].argmax(axis=1), -1)\n",
" month[cand] = assign\n",
" return month\n",
"\n",
"\n",
"def plan_greedy(pred, margin=45, cap=CAP_M, budget=BUDGET_M):\n",
" \"\"\"๊ฐ๋จํ ํ์ ๋ฐฐ์ : ์ ๋น ๋ชฉํ์ผ(์์ธก RUL - margin)์ด ๋น ๋ฅธ ์์ผ๋ก ๋ชฉํ์ผ ์ด์ ๊ฐ์ฅ ๋ฆ์ ๋น ์, ์์ผ๋ฉด ๊ฐ์ฅ ์ด๋ฅธ ๋น ์์ ๋ฐฐ์ .\"\"\"\n",
" cap = min(cap, int(budget // (C_PLAN + C_KIT))) # ์ ์์ฐ์ด ํ์ฉํ๋ ๋์๋ก ๋ฅ๋ ฅ์ ์ ํ\n",
" month = np.full(len(pred), -1)\n",
" left = np.full(HORIZON, cap)\n",
" for i in np.argsort(pred):\n",
" if pred[i] > HORIZON * MDAYS + 60: break\n",
" tgt = int(np.clip((pred[i] - margin) // MDAYS - 1, 0, HORIZON - 1))\n",
" for m in list(range(tgt, -1, -1)) + list(range(tgt + 1, HORIZON)):\n",
" if left[m] > 0:\n",
" month[i] = m; left[m] -= 1; break\n",
" return month\n",
"\n",
"\n",
"def plan_fixed(lin_rul, cap=CAP_M, budget=BUDGET_M):\n",
" \"\"\"๊ดํ: ๊ท์ ์ฃผ๊ธฐ ๋๋ฌ ์์๋๋ก(์ ํ ์์ฌ ์ผ์) ์๋ณ ๋ฅ๋ ฅ ์์์ ๋ฐฐ์ .\"\"\"\n",
" return plan_greedy(np.asarray(lin_rul, float), margin=0, cap=cap, budget=budget)\n",
"\n",
"\n",
"def evaluate_plan(month, true_rul, rate, cost_kit=True):\n",
" \"\"\"์ฐธ RUL๋ก ๊ณํ์ ์ค์ ๋น์ฉ์ ๊ณ์ฐํ๋ค: ์ ๋น์ผ ์ด์ ์ ๊ณ ์ฅ -> ๋น๊ณํ ๊ณ ์ฅ ๋น์ฉ, ์๋๋ฉด ๊ณํ ๋น์ฉ + ๋ฏธ์ฌ์ฉ ์๋ช
์์ค.\"\"\"\n",
" day = np.where(month >= 0, (month + 1) * MDAYS, 10 ** 9)\n",
" fail = (true_rul < day) & (true_rul < HORIZON * MDAYS)\n",
" planned = (month >= 0) & ~fail\n",
" waste = np.where(planned, (true_rul - day) * rate * 0.6, 0) * C_WASTE_KH / 1000\n",
" cost = fail * C_FAIL + planned * (C_PLAN + (C_KIT if cost_kit else 0)) + waste\n",
" return {\"total\": float(cost.sum()), \"fail\": int(fail.sum()), \"planned\": int(planned.sum()), \"waste\": float(waste.sum()), \"unsched_fail\": int(((month < 0) & fail).sum())}\n",
"\n",
"\n",
"def kit_fefo(month, lots=LOTS):\n",
" \"\"\"์ ํจ๊ธฐํ์ด ๋น ๋ฅธ ๋กํธ๋ถํฐ ์ฐ๋ FEFO๋ก ํคํธ๋ฅผ ๋ฐฐ์ ํ๊ณ ์ ๋น์ผ์ ์ ํจ๊ธฐ๊ฐ์ด ๋ถ์กฑํ ๋ฐฐ์ ์์ ๋ง๋ฃ ํ๊ธฐ ์๋์ ์ผ๋ค.\"\"\"\n",
" q = [[qty, e] for qty, e in sorted(lots, key=lambda t: t[1])]\n",
" short = 0\n",
" for m in sorted(month[month >= 0]):\n",
" day = (m + 1) * MDAYS\n",
" for lot in q:\n",
" if lot[1] >= day + KIT_MARGIN and lot[0] > 0:\n",
" lot[0] -= 1; break\n",
" else:\n",
" short += 1\n",
" expired = sum(l[0] for l in q if l[1] < HORIZON * MDAYS)\n",
" return {\"short\": short, \"expired_unused\": expired, \"left\": sum(l[0] for l in q)}\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"tr = make_fleet(900, seed=1)\n",
"eng = fit_engine(tr)\n",
"\n",
"\n",
"def scenario(seed, shift=1.0, cap=CAP_M, budget=BUDGET_M, lots=LOTS, use_kit=True):\n",
" fl = add_features(make_fleet(900, seed=seed, shift=shift))\n",
" al = fl[fl.event == 0].reset_index(drop=True)\n",
" pred, _, _ = predict_rul(eng, al)\n",
" mf = plan_fixed(al[\"lin_rul_d\"].values, cap=cap, budget=budget)\n",
" mm = plan_milp(pred, al[\"rate\"].values, cap=cap, budget=budget, lots=lots, use_kit=use_kit)\n",
" rf, rm = evaluate_plan(mf, al[\"rul_d\"].values, al[\"rate\"].values), evaluate_plan(mm, al[\"rul_d\"].values, al[\"rate\"].values)\n",
" kf, km = kit_fefo(mf, lots), kit_fefo(mm, lots)\n",
" stock = sum(q for q, _ in lots)\n",
" return al, pred, mf, mm, rf, rm, kf, km, stock\n",
"\n",
"\n",
"# ---------------- 1) ์๋๋ฆฌ์ค 6์ข
----------------\n",
"SC = [(\"ํ์\", dict(seed=3)), (\"์ ๋น์ฐฝ ๋ฅ๋ ฅ 70%(15๋/์)\", dict(seed=3, cap=15)), (\"์์ฐ ์ญ๊ฐ(์ 10,000๋ง์)\", dict(seed=3, budget=10000.0)), (\"์ํจ์ฑ ํคํธ ์ฌ๊ณ ์ ๋ฐยท๋จ๊ธฐ ์ ํจ\", dict(seed=3, lots=[(15, 70), (20, 170), (20, 240), (30, 330)])),\n",
" (\"๊ฐ๋๋ฅ +30%\", dict(seed=3, shift=1.3)), (\"๋ค๋ฅธ ๋ณด์ ์ฅ๋น(์๋ 4)\", dict(seed=4))]\n",
"rows, cache = [], {}\n",
"for name, kw in SC:\n",
" al, pred, mf, mm, rf, rm, kf, km, stock = scenario(**kw)\n",
" cache[name] = (al, pred, mm)\n",
" rows.append((name, len(al), rf[\"total\"], rm[\"total\"], 1 - rm[\"total\"] / rf[\"total\"], rf[\"fail\"], rm[\"fail\"], kf[\"expired_unused\"], km[\"expired_unused\"], km[\"short\"]))\n",
"res = pd.DataFrame(rows, columns=[\"์๋๋ฆฌ์ค\", \"์์กด ์ฅ๋น\", \"๊ดํ ๋น์ฉ(๋ง์)\", \"์ ์ ๋น์ฉ(๋ง์)\", \"์ ๊ฐ๋ฅ \", \"๊ดํ ๋น๊ณํ ๊ณ ์ฅ\", \"์ ์ ๋น๊ณํ ๊ณ ์ฅ\", \"๊ดํ ํคํธ ๋ง๋ฃ ํ๊ธฐ\", \"์ ์ ํคํธ ๋ง๋ฃ ํ๊ธฐ\", \"์ ์ ํคํธ ์ ํจ๊ธฐ๊ฐ ๋ถ์กฑ\"])\n",
"print(\"[1] ์๋๋ฆฌ์ค๋ณ 12๊ฐ์ ๋น์ฉ ๋น๊ต(๊ดํ = ๊ท์ ์ฃผ๊ธฐ ๋๋ฌ ์ ๊ณ ์ ์ผ์ , ๋จ๊ฐ๋ ๊ต์ก์ฉ ๊ฐ์ ๊ฐ)\")\n",
"print(res.assign(์ ๊ฐ๋ฅ =res[\"์ ๊ฐ๋ฅ \"].map(\"{:.1%}\".format)).round(0).to_string(index=False))\n",
"avg = res[\"์ ๊ฐ๋ฅ \"].mean()\n",
"print(f\" ์๋๋ฆฌ์ค 6์ข
ํ๊ท ์ ๊ฐ {avg:.1%} (๋ชฉํ 15% ์ด์: {'๋ฌ์ฑ' if avg >= .15 else '๋ฏธ๋ฌ'}), ์ต์ {res['์ ๊ฐ๋ฅ '].min():.1%}({res.loc[res['์ ๊ฐ๋ฅ '].idxmin(), '์๋๋ฆฌ์ค']})\")\n",
"\n",
"# ---------------- 2) ๋ฐฑํ
์คํธ: ์๋ก ๋ค๋ฅธ ๋ณด์ ์ฅ๋น 5์ธํธ ----------------\n",
"bt = []\n",
"for sd in (10, 11, 12, 13, 14):\n",
" _, _, _, _, rf, rm, _, _, _ = scenario(seed=sd)\n",
" bt.append((sd, 1 - rm[\"total\"] / rf[\"total\"], rf[\"fail\"], rm[\"fail\"]))\n",
"bt = pd.DataFrame(bt, columns=[\"์ฅ๋น ์ธํธ\", \"์ ๊ฐ๋ฅ \", \"๊ดํ ๊ณ ์ฅ\", \"์ ์ ๊ณ ์ฅ\"])\n",
"print(f\"\\n[2] ๋ฐฑํ
์คํธ(์๋ก ๋ค๋ฅธ ๋ณด์ ์ฅ๋น 5์ธํธ): ํ๊ท ์ ๊ฐ {bt['์ ๊ฐ๋ฅ '].mean():.1%}, ์ต์ {bt['์ ๊ฐ๋ฅ '].min():.1%}, ์ต๋ {bt['์ ๊ฐ๋ฅ '].max():.1%}, ๋น๊ณํ ๊ณ ์ฅ ํ๊ท {bt['๊ดํ ๊ณ ์ฅ'].mean():.0f} -> {bt['์ ์ ๊ณ ์ฅ'].mean():.0f}๋\")\n",
"\n",
"# ---------------- 3) ๋จ๊ฐ ๋ฏผ๊ฐ๋: ๋น๊ณํ ๊ณ ์ฅ ๋น์ฉ ----------------\n",
"sens = []\n",
"base_cfail = C_FAIL\n",
"for mult in (0.5, 1.0, 1.5, 2.0):\n",
" C_FAIL = base_cfail * mult\n",
" _, _, _, _, rf, rm, _, _, _ = scenario(seed=3)\n",
" sens.append((mult, 1 - rm[\"total\"] / rf[\"total\"]))\n",
"C_FAIL = base_cfail\n",
"print(\"[3] ๋น๊ณํ ๊ณ ์ฅ ๋น์ฉ ๋ฏผ๊ฐ๋:\", \"; \".join(f\"x{m} -> ์ ๊ฐ {s:.0%}\" for m, s in sens))\n",
"print(\" ๊ณ ์ฅ ๋น์ฉ์ด ๋ฎ์์ง๋ฉด ์๋น๊ธด ์ ๋น์ ๋ญ๋น๊ฐ ์๋์ ์ผ๋ก ์ปค์ ธ ์ ๊ฐ ํญ์ด ์ค์ด๋ ๋ค. ๋จ๊ฐ๋ ์ฌ์ ๋ถ์์ ์ค์ ๊ฐ์ผ๋ก ๋ค์ ํ์ธํด์ผ ํ๋ค.\")\n",
"\n",
"# ---------------- 4) HITL ๊ฒฐ์ฌ ์ํฌํ๋ก์ฐ ----------------\n",
"USERS = {\"ai_bot\": \"AI\", \"pln_kim\": \"PLN\", \"pln_lee\": \"PLN\", \"log_park\": \"LOG\", \"fin_choi\": \"FIN\", \"aud_han\": \"AUD\"}\n",
"PERM = {\"AI\": {\"submit\"}, \"PLN\": {\"submit\", \"redraft\"}, \"LOG\": {\"approve\", \"reject\"}, \"FIN\": {\"approve\", \"reject\"}, \"AUD\": {\"read\"}}\n",
"\n",
"\n",
"class Flow:\n",
" def __init__(self):\n",
" self.docs, self.log, self.blocked = {}, [], []\n",
"\n",
" def _audit(self, user, action, doc, detail):\n",
" prev = self.log[-1][\"hash\"] if self.log else \"GENESIS\"\n",
" rec = {\"seq\": len(self.log) + 1, \"user\": user, \"action\": action, \"doc\": doc, \"detail\": detail, \"prev\": prev}\n",
" rec[\"hash\"] = hashlib.sha256(json.dumps(rec, ensure_ascii=False, sort_keys=True).encode()).hexdigest()[:12]\n",
" self.log.append(rec)\n",
"\n",
" def verify(self):\n",
" prev = \"GENESIS\"\n",
" for r in self.log:\n",
" body = {k: v for k, v in r.items() if k != \"hash\"}\n",
" if r[\"prev\"] != prev or hashlib.sha256(json.dumps(body, ensure_ascii=False, sort_keys=True).encode()).hexdigest()[:12] != r[\"hash\"]:\n",
" return False, r[\"seq\"]\n",
" prev = r[\"hash\"]\n",
" return True, None\n",
"\n",
" def deny(self, user, act, doc, why):\n",
" self.blocked.append((user, act, doc, why)); self._audit(user, \"DENIED:\" + act, doc, why); return False\n",
"\n",
" def submit(self, user, kind, title, n, parent=None):\n",
" need = \"redraft\" if parent else \"submit\"\n",
" if need not in PERM.get(USERS.get(user), set()):\n",
" return self.deny(user, need, \"-\", \"์์ ๊ถํ ์์\")\n",
" did = f\"MNT{len(self.docs) + 1:02d}\"\n",
" self.docs[did] = {\"kind\": kind, \"title\": title, \"line\": [\"LOG\", \"FIN\"] if kind == \"๋ํ ๊ฑด์\" else [\"LOG\"], \"step\": 0, \"status\": \"๊ฒํ ๋๊ธฐ\", \"drafter\": user, \"done\": [], \"parent\": parent}\n",
" self._audit(user, \"submit\", did, f\"{kind} {title} {n}๋\")\n",
" return did\n",
"\n",
" def decide(self, user, did, act, reason=\"\"):\n",
" role, d = USERS.get(user), self.docs.get(did)\n",
" if role is None: return self.deny(user, act, did, \"๋ฏธ๋ฑ๋ก ์ฌ์ฉ์\")\n",
" if d is None or d[\"status\"] != \"๊ฒํ ๋๊ธฐ\": return self.deny(user, act, did, \"๊ฒฐ์ฌ ๋์ ์๋(์ฒ๋ฆฌ ์๋ฃ)\")\n",
" if act not in PERM[role]: return self.deny(user, act, did, f\"{role} ์ญํ ์ {act} ๋ถ๊ฐ\")\n",
" if d[\"line\"][d[\"step\"]] != role: return self.deny(user, act, did, \"๊ฒฐ์ฌ ์์ ์๋ฐ\")\n",
" if user == d[\"drafter\"] or user in d[\"done\"]: return self.deny(user, act, did, \"๊ธฐ์์ยท๊ธฐ๊ฒฐ์ฌ์ ์ฌ๊ฒฐ์ฌ ๊ธ์ง\")\n",
" if len(reason.strip()) < 10: return self.deny(user, act, did, \"์ฌ์ 10์ ์ด์ ํ์\")\n",
" if act == \"reject\":\n",
" d[\"status\"] = \"๋ฐ๋ ค\"\n",
" else:\n",
" d[\"done\"].append(user); d[\"step\"] += 1\n",
" d[\"status\"] = \"์น์ธ\" if d[\"step\"] == len(d[\"line\"]) else \"๊ฒํ ๋๊ธฐ\"\n",
" self._audit(user, act, did, reason); return True\n",
"\n",
" def edit(self, user, did):\n",
" if self.docs[did][\"status\"] == \"์น์ธ\": return self.deny(user, \"edit\", did, \"์น์ธ ํ ์ผ์ ๋ณ๊ฒฝ ๊ธ์ง(์ฌ๊ธฐ์)\")\n",
" return True\n",
"\n",
"\n",
"al, pred, mm = cache[\"ํ์\"]\n",
"al = al.assign(rul_pred=pred, month=mm)\n",
"scrap = al[(al[\"age_y\"] >= 14) & (al[\"rul_pred\"] < 365)] # ๋ํ ๊ฑด์ ํ๋ณด: ์ฌ์ฉ 14๋
์ด์์ด๊ณ 1๋
๋ด ์๋ช
์ข
๋ฃ ์์ธก\n",
"wf = Flow()\n",
"docs = [wf.submit(\"ai_bot\" if q % 2 == 0 else \"pln_kim\", \"์ ๋น ์ผ์ \", f\"{3 * q + 1}~{3 * q + 3}์ ์ผ์ ์\", int(((al.month >= 3 * q) & (al.month < 3 * q + 3)).sum())) for q in range(2)]\n",
"docs += [wf.submit(\"pln_lee\", \"๋ํ ๊ฑด์\", f\"{u} ๋ถ๋ ๋
ธํ ์ฅ๋น\", int((scrap.unit == u).sum())) for u in scrap[\"unit\"].value_counts().index[:2]]\n",
"two = [i for i in docs if len(wf.docs[i][\"line\"]) == 2]\n",
"print(f\"\\n[4] AI ์ถ์ฒ์ {len(docs)}๊ฑด(์ ๋น ์ผ์ 2๊ฑด, ๋ํ ๊ฑด์ {len(two)}๊ฑด; ๋ํ ํ๋ณด {len(scrap)}๋) ์ ๋ถ '๊ฒํ ๋๊ธฐ' ๋ฑ๋ก, ์๋ ์น์ธ {sum(wf.docs[i]['status'] == '์น์ธ' for i in docs)}๊ฑด (๋ชฉํ 0๊ฑด). 2๋จ๊ณ ๊ฒฐ์ฌ(๊ตฐ์์งํ๊ด + ์ฌ์ ๋ด๋น) ๋์ {two}\")\n",
"wf.decide(\"log_park\", docs[0], \"reject\", \"1~3์ ์ ๋น ์์์ ํคํธ ์ ํจ๊ธฐ๊ฐ ์ฌํ์ธ ํ์, ์ฌ๊ธฐ์ ์์ฒญ\")\n",
"new = wf.submit(\"pln_kim\", \"์ ๋น ์ผ์ \", \"1~3์ ์ผ์ ์(์ฌ๊ธฐ์)\", int(((al.month >= 0) & (al.month < 3)).sum()), parent=docs[0])\n",
"wf.decide(\"log_park\", new, \"approve\", \"ํคํธ ์ ํจ๊ธฐ๊ฐ ํ์ธ ์๋ฃ, ์ผ์ ํ์ \")\n",
"wf.decide(\"log_park\", docs[1], \"approve\", \"4~6์ ์ผ์ , ์ ๋น์ฐฝ ๋ฅ๋ ฅ ๋ฒ์ ๋ด ํ์ธํจ\")\n",
"if two:\n",
" wf.decide(\"log_park\", two[0], \"approve\", \"๋ํ ๊ฑด์ ์ฌ์ ํ๋น, ์ฌ์ ๋ด๋น ๊ฒํ ์์ \")\n",
" wf.decide(\"fin_choi\", two[0], \"approve\", \"์์กด ๊ฐ์น ๋๋น ์๋ฆฌ๋น ์ด๊ณผ ํ์ธ, ๋ํ ๊ฑด์ ๋์\")\n",
"tgt = two[-1] if two else docs[-1]\n",
"attacks = [(\"pln_kim\", tgt, \"approve\", \"์์ ์๊ฐ ์ง์ ์น์ธํ๋ค๊ณ ์ฒ๋ฆฌํจ\"), (\"ai_bot\", tgt, \"approve\", \"AI ๊ณ์ ์ด ์ง์ ์น์ธํ๋ ค๊ณ ์๋ํจ\"), (\"aud_han\", tgt, \"approve\", \"๊ฐ์ฌ ๊ณ์ ์ผ๋ก ์น์ธ ์๋ํจ\"), (\"ghost\", tgt, \"approve\", \"์ธ๋ถ ๊ณ์ ์ผ๋ก ์น์ธ ์๋ํจ\"),\n",
" (\"log_park\", tgt, \"approve\", \"\"), (\"log_park\", tgt, \"reject\", \"์งง์\"), (\"fin_choi\", tgt, \"approve\", \"์ฌ์ ๋ด๋น์ด ์์๋ฅผ ๊ฑด๋๋ฐ์ด ์น์ธ\"), (\"log_park\", new, \"reject\", \"์น์ธ๋ ์ผ์ ์ ๋ค์ง์ด ๋ฐ๋ คํจ\"),\n",
" (\"pln_lee\", tgt, \"approve\", \"์์ ์ ๊ณ์ ์ผ๋ก ์น์ธ ์๋ํจ\")]\n",
"for u, doc, a, rs in attacks:\n",
" wf.decide(u, doc, a, rs)\n",
"wf.edit(\"pln_kim\", new)\n",
"print(f\" ๊ถํ ์ฐํ ์๋ 10๊ฑด ์ค ์ฐจ๋จ {len(wf.blocked)}๊ฑด (์ฐจ๋จ์จ {len(wf.blocked) / 10:.0%}, ๋ชฉํ 100%)\")\n",
"for u, a, doc, why in wf.blocked[:4]:\n",
" print(f\" ์ฐจ๋จ {u:<9} {a:<8} {doc}: {why}\")\n",
"ok, _ = wf.verify()\n",
"snap = json.loads(json.dumps(wf.log))\n",
"wf.log[3][\"detail\"] = \"์ฌํ์ ๊ณ ์ณ ์ด ์ฌ์ \"\n",
"ok2, at = wf.verify()\n",
"wf.log = snap\n",
"print(f\" ๊ฐ์ฌ ๋ก๊ทธ {len(wf.log)}๊ฑด ํด์ ์ฒด์ธ ๋ฌด๊ฒฐ์ฑ {'100%' if ok else 'ํผ์'}; 4๋ฒ ๊ธฐ๋ก ์ฌํ ์์ ์ {'ํต๊ณผ' if ok2 else f'์๋ณ์กฐ ๊ฒ์ถ({at}๋ฒ)'}\")\n",
"print(f\" {docs[0]}(๋ฐ๋ ค) -> {new}(์ฌ๊ธฐ์, parent ์ฐ๊ฒฐ) ์ด๋ ฅ:\", [(r['action'], r['user']) for r in wf.log if r['doc'] in (docs[0], new)])\n",
"print(\" ๋ฌธ์ ์ํ:\", pd.Series({k: v[\"status\"] for k, v in wf.docs.items()}).value_counts().to_dict(), \"| ์ ๋น ์ผ์ ํ์ ๊ณผ ์ฅ๋น ๋ํ๋ ๊ตฐ์์งํ๊ด ๊ฒฐ์ฌ(๋ํ๋ ์ฌ์ ๋ด๋น ํฌํจ)๋ก๋ง ํ์ ๋๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(12, 4))\n",
"x = np.arange(len(res)); ax[0].bar(x - .2, res[\"๊ดํ ๋น์ฉ(๋ง์)\"], .4, label=\"fixed-cycle\"); ax[0].bar(x + .2, res[\"์ ์ ๋น์ฉ(๋ง์)\"], .4, label=\"risk-based plan\")\n",
"ax[0].set_xticks(x); ax[0].set_xticklabels([f\"S{i + 1}\" for i in x]); ax[0].legend(); ax[0].set_title(\"12-month cost by scenario (10k KRW)\")\n",
"ax[1].plot([s[0] for s in sens], [s[1] * 100 for s in sens], \"o-\"); ax[1].set_title(\"Saving (%) vs unplanned-failure cost multiplier\"); ax[1].set_xlabel(\"multiplier\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
07 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[1] ์๋๋ฆฌ์ค๋ณ 12๊ฐ์ ๋น์ฉ ๋น๊ต(๊ดํ = ๊ท์ ์ฃผ๊ธฐ ๋๋ฌ ์ ๊ณ ์ ์ผ์ , ๋จ๊ฐ๋ ๊ต์ก์ฉ ๊ฐ์ ๊ฐ)\n",
" ์๋๋ฆฌ์ค ์์กด ์ฅ๋น ๊ดํ ๋น์ฉ(๋ง์) ์ ์ ๋น์ฉ(๋ง์) ์ ๊ฐ๋ฅ ๊ดํ ๋น๊ณํ ๊ณ ์ฅ ์ ์ ๋น๊ณํ ๊ณ ์ฅ ๊ดํ ํคํธ ๋ง๋ฃ ํ๊ธฐ ์ ์ ํคํธ ๋ง๋ฃ ํ๊ธฐ ์ ์ ํคํธ ์ ํจ๊ธฐ๊ฐ ๋ถ์กฑ\n",
" ํ์ 601 184752.0 116447.0 37.0% 47 21 10 10 0\n",
" ์ ๋น์ฐฝ ๋ฅ๋ ฅ 70%(15๋/์) 601 184691.0 122971.0 33.4% 47 25 15 15 0\n",
" ์์ฐ ์ญ๊ฐ(์ 10,000๋ง์) 601 184655.0 124543.0 32.6% 47 26 17 17 0\n",
"์ํจ์ฑ ํคํธ ์ฌ๊ณ ์ ๋ฐยท๋จ๊ธฐ ์ ํจ 601 184752.0 114897.0 37.8% 47 25 0 0 0\n",
" ๊ฐ๋๋ฅ +30% 601 225885.0 149598.0 33.8% 61 29 10 10 0\n",
" ๋ค๋ฅธ ๋ณด์ ์ฅ๋น(์๋ 4) 590 164663.0 102951.0 37.5% 46 19 17 10 0\n",
" ์๋๋ฆฌ์ค 6์ข
ํ๊ท ์ ๊ฐ 35.3% (๋ชฉํ 15% ์ด์: ๋ฌ์ฑ), ์ต์ 32.6%(์์ฐ ์ญ๊ฐ(์ 10,000๋ง์))\n",
"\n",
"[2] ๋ฐฑํ
์คํธ(์๋ก ๋ค๋ฅธ ๋ณด์ ์ฅ๋น 5์ธํธ): ํ๊ท ์ ๊ฐ 35.4%, ์ต์ 32.4%, ์ต๋ 41.6%, ๋น๊ณํ ๊ณ ์ฅ ํ๊ท 54 -> 27๋\n",
"[3] ๋น๊ณํ ๊ณ ์ฅ ๋น์ฉ ๋ฏผ๊ฐ๋: x0.5 -> ์ ๊ฐ 35%; x1.0 -> ์ ๊ฐ 37%; x1.5 -> ์ ๊ฐ 41%; x2.0 -> ์ ๊ฐ 45%\n",
" ๊ณ ์ฅ ๋น์ฉ์ด ๋ฎ์์ง๋ฉด ์๋น๊ธด ์ ๋น์ ๋ญ๋น๊ฐ ์๋์ ์ผ๋ก ์ปค์ ธ ์ ๊ฐ ํญ์ด ์ค์ด๋ ๋ค. ๋จ๊ฐ๋ ์ฌ์ ๋ถ์์ ์ค์ ๊ฐ์ผ๋ก ๋ค์ ํ์ธํด์ผ ํ๋ค.\n",
"\n",
"[4] AI ์ถ์ฒ์ 4๊ฑด(์ ๋น ์ผ์ 2๊ฑด, ๋ํ ๊ฑด์ 2๊ฑด; ๋ํ ํ๋ณด 22๋) ์ ๋ถ '๊ฒํ ๋๊ธฐ' ๋ฑ๋ก, ์๋ ์น์ธ 0๊ฑด (๋ชฉํ 0๊ฑด). 2๋จ๊ณ ๊ฒฐ์ฌ(๊ตฐ์์งํ๊ด + ์ฌ์ ๋ด๋น) ๋์ ['MNT03', 'MNT04']\n",
" ๊ถํ ์ฐํ ์๋ 10๊ฑด ์ค ์ฐจ๋จ 10๊ฑด (์ฐจ๋จ์จ 100%, ๋ชฉํ 100%)\n",
" ์ฐจ๋จ pln_kim approve MNT04: PLN ์ญํ ์ approve ๋ถ๊ฐ\n",
" ์ฐจ๋จ ai_bot approve MNT04: AI ์ญํ ์ approve ๋ถ๊ฐ\n",
" ์ฐจ๋จ aud_han approve MNT04: AUD ์ญํ ์ approve ๋ถ๊ฐ\n",
" ์ฐจ๋จ ghost approve MNT04: ๋ฏธ๋ฑ๋ก ์ฌ์ฉ์\n",
" ๊ฐ์ฌ ๋ก๊ทธ 20๊ฑด ํด์ ์ฒด์ธ ๋ฌด๊ฒฐ์ฑ 100%; 4๋ฒ ๊ธฐ๋ก ์ฌํ ์์ ์ ์๋ณ์กฐ ๊ฒ์ถ(4๋ฒ)\n",
" MNT01(๋ฐ๋ ค) -> MNT05(์ฌ๊ธฐ์, parent ์ฐ๊ฒฐ) ์ด๋ ฅ: [('submit', 'ai_bot'), ('reject', 'log_park'), ('submit', 'pln_kim'), ('approve', 'log_park'), ('DENIED:reject', 'log_park'), ('DENIED:edit', 'pln_kim')]\n",
" ๋ฌธ์ ์ํ: {'์น์ธ': 3, '๋ฐ๋ ค': 1, '๊ฒํ ๋๊ธฐ': 1} | ์ ๋น ์ผ์ ํ์ ๊ณผ ์ฅ๋น ๋ํ๋ ๊ตฐ์์งํ๊ด ๊ฒฐ์ฌ(๋ํ๋ ์ฌ์ ๋ด๋น ํฌํจ)๋ก๋ง ํ์ ๋๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
08. ์ ์ฒด ๊ตฐ๋จ ๋ณด์ ์ฅ๋น 1๋ง๋ ์๋ช
์ผ๊ด ์๋ฎฌ๋ ์ด์
๋ถํ ํ
์คํธ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋ณด์ ์ฅ๋น 1๋ง๋ ์๋ช
์ผ๊ด ์๋ฎฌ๋ ์ด์
๋ถํ ํ
์คํธ๋ฅผ ์ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋ณด์ ์ฅ๋น 1๋ง ๋ ์๋ช
์ผ๊ด ์๋ฎฌ๋ ์ด์
๋ถํ ์ํ๊ณผ ๊ฒฝ๋ณด ์ ํ๋ ์ํ์ ์ํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๋ณด์ ์ฅ๋น 1์ฒ~10๋ง ๋ ๊ท๋ชจ์์ ํผ์ฒ ์์ฑ, ์๋ช
์์ธก, ์ผ์ ๋ฐฐ์ , ๋์๋ณด๋ ์ง๊ณ ์๊ฐ์ ์ฐ๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/dashboard/sup02_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์ฒ~10๋ง ๋์ ์๋ช
(RUL) ์ผ๊ด ์์ธกยท์ํ ๊ฒฝ๋ณดยท๊ณํ์ ๋น ์ผ์ ๋ฐฐ์ ยท๋์๋ณด๋ ์ง๊ณ ์๋ฎฌ๋ ์ด์
์ผ๋ก ๋จ๊ณ๋ณ ์ฒ๋ฆฌ ์๊ฐ๊ณผ ๋์๋ณด๋ ๋ก๋ฉ ์๊ฐ์ ์ฌ๊ณ , ๊ฒฐ์ธกยทํ๊ฒฝ ์ค๊ธฐ์ฌยท๋จ์ ์ค๋ฅ ์ฃผ์
์กฐ๊ฑด์์ ์ํ ๊ฒฝ๋ณด ์ ํ๋๋ฅผ ์ธก์ ํ๋ฉฐ ์ ์๊ณํ์ ๊ท๋ชจ ํ๊ณ์ ๋์(ํ์ ๋ฐฐ์ )์ ์ ๋ฆฌํ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ฅ๋น ๋ณด์ ํํฉยท๊ณ ์ฅ ์ด๋ ฅ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์ฅ๋นยท์์น ์์)\n",
"# ๊ฐ์ ๋ถ๋ 6๊ฐ(U1~U6)์ ์ฅ๋น๊ฐ ์ด์ฉ ํ๊ฒฝ(normal/rough/salt/cold)์ ๋ฐ๋ผ ๋ค๋ฅธ ์๋๋ก ๋
ธํํํ๋ค. ์ฐฝ์ ๋น ํ ์ด์ฉ์๊ฐ(h_since)์ด\n",
"# ์ฅ๋น๋ง๋ค ์จ์ ์๋ช
(t_fail)์ ๋์ผ๋ฉด ๊ณ ์ฅ(event=1) ์ด๋ ฅ, ๋์ง ์์์ผ๋ฉด ํ์ฌ๊น์ง ์์กด(event=0, ๊ด์ธก ์ค๋์ ๋จ)์ด๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"ENVS = [\"normal\", \"rough\", \"salt\", \"cold\"]\n",
"ENV_W = {\"normal\": 1.00, \"rough\": 1.35, \"salt\": 1.25, \"cold\": 1.12} # ํ๊ฒฝ ๊ฐ์ค์น(์จ์ ์ ๋ต, ๊ฐ์ ๊ฐ): ํด์๋ก ์๋ช
์ด ์งง๋ค\n",
"ENV_KO = {\"normal\": \"์ผ๋ฐ\", \"rough\": \"ํ์ง\", \"salt\": \"์ผ๋ถ\", \"cold\": \"ํนํ\"}\n",
"UNITS = [\"U1\", \"U2\", \"U3\", \"U4\", \"U5\", \"U6\"]\n",
"UNIT_ENV = {\"U1\": [.7, .1, .1, .1], \"U2\": [.2, .6, .1, .1], \"U3\": [.2, .1, .6, .1], \"U4\": [.3, .1, .1, .5], \"U5\": [.5, .3, .1, .1], \"U6\": [.4, .1, .4, .1]}\n",
"ETYPES = [\"truck\", \"generator\", \"trailer\", \"radio\"]\n",
"CYCLE = 8000.0 # ๊ท์ ์ฐฝ์ ๋น ์ฃผ๊ธฐ(์ด์ฉ์๊ฐ h, ๊ฐ์ ๊ฐ)\n",
"KSHAPE = 12.0 # ์๋ช
๋ถํฌ ์์ด๋ธ ํ์(๋ง๋ชจํ ๊ณ ์ฅ, ํด์๋ก ์๋ช
์ด ๋ชจ์ฌ ์๋ค)\n",
"INSP_NOISE = 0.025 # ์ ๋น ์ ๊ฒ ๋ง๋ชจ ๋น์จ ์ธก์ ์ค์ฐจ(๊ฐ์ ๊ฐ)\n",
"\n",
"\n",
"def make_fleet(n=900, seed=0, env_w=None, shift=1.0):\n",
" \"\"\"์ฅ๋น n๋. env_w๋ก ์จ์ ํ๊ฒฝ ๊ฐ์ค์น๋ฅผ ๋ฐ๊ฟ ์ด์ ๋ถํฌ ๋ณํ๋ฅผ ๋ชจ์ํ ์ ์๋ค. shift>1์ด๋ฉด ๊ฐ๋๋ฅ ์ด ๋์์ง๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" w = env_w or ENV_W\n",
" unit = rng.choice(UNITS, n)\n",
" env = np.array([rng.choice(ENVS, p=UNIT_ENV[u]) for u in unit])\n",
" et = rng.choice(ETYPES, n, p=[.4, .2, .2, .2])\n",
" rate = rng.lognormal(np.log(3.0 * shift), 0.25, n) # ์ผ ํ๊ท ์ด์ฉ์๊ฐ(h/์ผ)\n",
" h_since = rng.uniform(300, 11500, n) # ๋ง์ง๋ง ์ฐฝ์ ๋น ์ดํ ์ด์ฉ์๊ฐ\n",
" n_ovh = rng.integers(0, 3, n)\n",
" hours = h_since + n_ovh * rng.uniform(5000, 8000, n) # ๋์ ์ด์ฉ์๊ฐ\n",
" km = hours * rng.normal(17, 3, n) * np.where(env == \"rough\", 0.8, 1.0) # ๋์ ๊ธฐ๋๊ฑฐ๋ฆฌ(km)\n",
" age_y = np.clip(hours / (rate * 365) * rng.uniform(0.9, 1.15, n), 0.5, 25)\n",
" unpl = rng.poisson(0.35 * h_since / 1000 * np.array([w[e] for e in env]) / 1.3) # ๋น๊ณํ ์ ๋น ๊ฑด์(ํ๊ฒฝ์ด ๊ฐํนํ ์๋ก ๋ง๋ค)\n",
" n_maint = rng.poisson(0.6 * h_since / 1000 + 1) # ๊ณํ ์ ๋น ๊ฑด์\n",
" scale = 9600 / np.array([w[e] for e in env]) * 0.985 ** unpl * np.exp(rng.normal(0, 0.04, n))\n",
" t_fail = scale * rng.weibull(KSHAPE, n) # ์ฐฝ์ ๋น ํ ๊ณ ์ฅ๊น์ง ์ด์ฉ์๊ฐ(์จ์ ์ ๋ต)\n",
" event = (t_fail <= h_since).astype(int)\n",
" dur = np.where(event == 1, t_fail, h_since)\n",
" df = pd.DataFrame({\"eq_id\": [f\"EQ{seed:02d}{i:04d}\" for i in range(n)], \"unit\": unit, \"env\": env, \"etype\": et, \"rate\": rate, \"hours\": hours, \"km\": km, \"age_y\": age_y, \"n_ovh\": n_ovh,\n",
" \"h_since\": h_since, \"unpl\": unpl, \"n_maint\": n_maint, \"t_fail\": t_fail, \"event\": event, \"dur\": dur})\n",
" df[\"insp\"] = np.clip(dur / t_fail + rng.normal(0, INSP_NOISE, n), 0, 1.3) # ๋ง์ง๋ง ์ ๋น ์ ๊ฒ์์ ์ธก์ ํ ๋ง๋ชจ ๋น์จ(์๋ชจ ์๋ช
/์ด ์๋ช
, ์ค์ฐจ ํฌํจ)\n",
" df[\"rul_h\"] = np.where(event == 0, t_fail - h_since, np.nan) # ์์กด ์ฅ๋น์ ๋จ์ ์ด์ฉ์๊ฐ(์ ๋ต)\n",
" df[\"rul_d\"] = df[\"rul_h\"] / df[\"rate\"] # ๋จ์ ์ผ์(์ ๋ต)\n",
" return df\n",
"\n",
"\n",
"def mape(y, p):\n",
" # [TODO : ํ์ ์ค์ต 8] ์ ์ฒด ๊ตฐ๋จ ๋ณด์ ์ฅ๋น 1๋ง๋ ์๋ช
์ผ๊ด ์๋ฎฌ๋ ์ด์
๋ถํ ํ
์คํธ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def mae(y, p):\n",
" # [TODO : ํ์ ์ค์ต 8] ์ ์ฒด ๊ตฐ๋จ ๋ณด์ ์ฅ๋น 1๋ง๋ ์๋ช
์ผ๊ด ์๋ฎฌ๋ ์ด์
๋ถํ ํ
์คํธ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def rmse(y, p):\n",
" return float(np.sqrt(np.mean((np.asarray(y, float) - np.asarray(p, float)) ** 2)))\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ๋
ธํํ ํ์ ํผ์ฒ์ ํ์ต ํ๋ณธ ๋ง๋ค๊ธฐ. ํ๊ฒฝ ๊ฐ์ค์น W0๋ ๋์
์์ ์ ์ ๋ฌธ๊ฐ ์ถ์ ๊ฐ์ด๋ฉฐ(์ค์ ๊ฐ์ค์น์ ๋ค๋ฆ) M9์์ ์ฌ๋ณด์ ํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"W0 = {\"normal\": 1.00, \"rough\": 1.15, \"salt\": 1.10, \"cold\": 1.05}\n",
"FEATS = [\"h_since\", \"hours\", \"km\", \"age_y\", \"rate\", \"n_maint\", \"unpl\", \"unpl_per_kh\", \"km_per_h\", \"n_ovh\", \"k_rough\", \"k_salt\", \"k_cold\", \"k_truck\", \"k_gen\", \"k_trailer\",\n",
" \"aging_idx\", \"lin_rul_d\", \"hs_frac\", \"insp\", \"insp_eta_h\"]\n",
"\n",
"\n",
"def add_features(df, w=None):\n",
" d = df.copy()\n",
" w = w or W0\n",
" d[\"unpl_per_kh\"] = d[\"unpl\"] / (d[\"h_since\"] / 1000 + 0.5)\n",
" d[\"km_per_h\"] = d[\"km\"] / d[\"hours\"]\n",
" for e in (\"rough\", \"salt\", \"cold\"):\n",
" d[\"k_\" + e] = (d[\"env\"] == e).astype(int)\n",
" d[\"k_truck\"], d[\"k_gen\"], d[\"k_trailer\"] = (d[\"etype\"] == \"truck\").astype(int), (d[\"etype\"] == \"generator\").astype(int), (d[\"etype\"] == \"trailer\").astype(int)\n",
" d[\"aging_idx\"] = d[\"h_since\"] * d[\"env\"].map(w) / CYCLE # ํ๊ฒฝ ๊ฐ์ค ๋
ธํ ์ง์(1์ด๋ฉด ์ฐฝ์ ๋น ์ฃผ๊ธฐ์ ๋๋ฌ)\n",
" d[\"lin_rul_d\"] = (CYCLE - d[\"h_since\"]) / d[\"rate\"] # ์ ํ ๊ฐ๊ฐ์๊ฐ ์์ฌ ์ผ์(Baseline)\n",
" d[\"hs_frac\"] = d[\"h_since\"] / CYCLE\n",
" d[\"insp_eta_h\"] = d[\"h_since\"] / np.maximum(d[\"insp\"], 0.05) # ์ ๊ฒ ๋ง๋ชจ ๋น์จ๋ก ํ์ฐํ ์ด ์๋ช
์ถ์ (h)\n",
" return d\n",
"\n",
"\n",
"def snapshots(df, k=4, seed=0):\n",
" \"\"\"๊ณ ์ฅ ์ด๋ ฅ ์ฅ๋น์ ๊ณผ๊ฑฐ ์์ ํ๋ณธ: ๊ณ ์ฅ ์ ์์ ์์ ์ ์ ๊ฒ๊ฐยท์ด์ฉ์๊ฐ๊ณผ ๊ทธ๋์ ๋จ์ ์ผ์. (์์กด ์ฅ๋น์ ๋ฏธ๋๋ ์ ์ ์์ผ๋ฏ๋ก ๊ณ ์ฅ ์ด๋ ฅ๋ง ์ด๋ค)\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" f = df[df[\"event\"] == 1]\n",
" out = []\n",
" for _ in range(k):\n",
" s = f.copy()\n",
" h = s[\"t_fail\"] * rng.uniform(0.2, 0.98, len(s))\n",
" ratio = h / s[\"h_since\"]\n",
" s[\"hours\"] = s[\"hours\"] - (s[\"h_since\"] - h)\n",
" s[\"km\"] = s[\"km\"] * ratio.clip(upper=1)\n",
" s[\"unpl\"] = np.floor(s[\"unpl\"] * ratio)\n",
" s[\"n_maint\"] = np.floor(s[\"n_maint\"] * ratio)\n",
" s[\"insp\"] = np.clip(h / s[\"t_fail\"] + rng.normal(0, INSP_NOISE, len(s)), 0, 1.3)\n",
" s[\"h_since\"] = h\n",
" s[\"rul_d\"] = (s[\"t_fail\"] - h) / s[\"rate\"]\n",
" s[\"event\"] = 0\n",
" out.append(s)\n",
" return pd.concat(out, ignore_index=True)\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ์์กด ๋ถ์ ๋๊ตฌ(scipy๋ง ์ฌ์ฉ): ์์ด๋ธ AFT(๊ฐ์ ์๋ช
) ๋ชจํ๊ณผ Cox ๋น๋ก์ํ ๋ชจํ\n",
"# ---------------------------------------------------------------------------\n",
"from scipy.optimize import minimize\n",
"\n",
"\n",
"def aft_x(d):\n",
" return np.c_[np.ones(len(d)), d[\"env\"].eq(\"rough\"), d[\"env\"].eq(\"salt\"), d[\"env\"].eq(\"cold\"), d[\"unpl\"] / (d[\"h_since\"] / 1000 + 0.5)].astype(float)\n",
"\n",
"\n",
"AFT_COLS = [\"์ ํธ\", \"ํ์ง\", \"์ผ๋ถ\", \"ํนํ\", \"๋น๊ณํ ์ ๋น์จ\"]\n",
"\n",
"\n",
"def fit_aft(d):\n",
" \"\"\"์ฐฝ์ ๋น ํ ์ด์ฉ์๊ฐ dur(๊ณ ์ฅ์ด๋ฉด event=1, ์์กด์ด๋ฉด ์ค๋์ ๋จ)์ ๋ํ ์์ด๋ธ AFT ์ต๋์ฐ๋ ์ถ์ . ๋ฐํ: ๊ณ์(๋ง์ง๋ง์ log ํ์).\"\"\"\n",
" x, t, e = aft_x(d), d[\"dur\"].values, d[\"event\"].values\n",
"\n",
" def nll(p):\n",
" lam, k = np.exp(x @ p[:-1]), np.exp(p[-1])\n",
" z = (t / lam) ** k\n",
" return -(e * (np.log(k) - np.log(lam) + (k - 1) * (np.log(t) - np.log(lam))) - z).sum()\n",
"\n",
" r = minimize(nll, np.r_[np.log(8000), np.zeros(x.shape[1] - 1), np.log(5)], method=\"BFGS\")\n",
" return r.x\n",
"\n",
"\n",
"def aft_pred(p, d, q=0.5, h=None):\n",
" \"\"\"์ด๋ฏธ h(์ด์ฉ์๊ฐ)๊น์ง ์์กดํ๋ค๋ ์กฐ๊ฑด์์ ๋ถ์์ q ์์กด ์์ (์ด์ฉ์๊ฐ h ๊ธฐ์ค ์ด ์๋ช
)์ ๋๋ ค์ค๋ค. q=0.5๊ฐ ์กฐ๊ฑด๋ถ ์ค์๊ฐ.\"\"\"\n",
" lam, k = np.exp(aft_x(d) @ p[:-1]), np.exp(p[-1])\n",
" h = d[\"h_since\"].values if h is None else h\n",
" return lam * ((h / lam) ** k - np.log(q)) ** (1 / k)\n",
"\n",
"\n",
"def aft_fail_prob(p, d, h0, h1):\n",
" \"\"\"h0๊น์ง ์์กดํ ์ฅ๋น๊ฐ h0~h1 ์ฌ์ด์ ๊ณ ์ฅ ๋ ์กฐ๊ฑด๋ถ ํ๋ฅ .\"\"\"\n",
" lam, k = np.exp(aft_x(d) @ p[:-1]), np.exp(p[-1])\n",
" s0, s1 = np.exp(-(h0 / lam) ** k), np.exp(-(h1 / lam) ** k)\n",
" return 1 - s1 / s0\n",
"\n",
"\n",
"def fit_cox(d, iters=25):\n",
" \"\"\"Cox ๋น๋ก์ํ(Breslow ๊ทผ์ฌ) ๋ดํด-๋ฉ์จ. ๊ณต๋ณ๋: ํ์งยท์ผ๋ถยทํนํยท๋น๊ณํ ์ ๋น์จ. ๋ฐํ: ๊ณ์.\"\"\"\n",
" x = aft_x(d)[:, 1:]\n",
" o = np.argsort(-d[\"dur\"].values)\n",
" x, e = x[o], d[\"event\"].values[o]\n",
" b = np.zeros(x.shape[1])\n",
" for _ in range(iters):\n",
" r = np.exp(x @ b)\n",
" S0 = np.cumsum(r)\n",
" S1 = np.cumsum(r[:, None] * x, axis=0)\n",
" S2 = np.cumsum(r[:, None, None] * x[:, :, None] * x[:, None, :], axis=0)\n",
" m1 = S1 / S0[:, None]\n",
" g = ((x - m1) * e[:, None]).sum(axis=0)\n",
" H = -sum(e[i] * (S2[i] / S0[i] - np.outer(m1[i], m1[i])) for i in np.where(e == 1)[0])\n",
" step = np.linalg.solve(H - 1e-6 * np.eye(len(b)), g)\n",
" b = b - step\n",
" if np.abs(step).max() < 1e-7:\n",
" break\n",
" return b\n",
"\n",
"\n",
"def cindex(time, event, risk):\n",
" \"\"\"์ผ์น๋ C-index: ์ํ ์ ์๊ฐ ๋์ ์ชฝ์ด ๋จผ์ ๊ณ ์ฅ ๋ฌ๋๊ฐ(๊ด์ธก ๊ฐ๋ฅํ ์๋ง).\"\"\"\n",
" t, e, r = np.asarray(time), np.asarray(event), np.asarray(risk)\n",
" num = den = 0.0\n",
" for i in np.where(e == 1)[0]:\n",
" m = t > t[i]\n",
" den += m.sum()\n",
" num += (r[m] < r[i]).sum() + 0.5 * (r[m] == r[i]).sum()\n",
" return num / max(den, 1)\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ์์ฌ ์๋ช
(RUL, ์ผ) ์์ธก ์์ง: ์์ด๋ธ AFT ์กฐ๊ฑด๋ถ ์ค์๊ฐ + ๊ทธ๋๋์ธํธ ๋ถ์คํ
(๊ณ ์ฅ ์ด๋ ฅ ์์ ํ๋ณธ ํ์ต)์ ํ๊ท \n",
"# ---------------------------------------------------------------------------\n",
"from sklearn.ensemble import GradientBoostingRegressor\n",
"\n",
"\n",
"def fit_engine(train, seed=0):\n",
" p = fit_aft(train)\n",
" sn = add_features(snapshots(train, 5, seed))\n",
" sn[\"aft\"] = (aft_pred(p, sn) - sn[\"h_since\"]) / sn[\"rate\"]\n",
" g = GradientBoostingRegressor(n_estimators=250, max_depth=3, learning_rate=0.06, random_state=0).fit(sn[FEATS + [\"aft\"]], sn[\"rul_d\"])\n",
" return {\"p\": p, \"g\": g}\n",
"\n",
"\n",
"def predict_rul(eng, d):\n",
" \"\"\"d๋ add_features๋ฅผ ๊ฑฐ์น ํ. ๋ฐํ: (์์๋ธ RUL, AFT RUL, GBR RUL) - ์ผ ๋จ์.\"\"\"\n",
" aft = ((aft_pred(eng[\"p\"], d) - d[\"h_since\"]) / d[\"rate\"]).values\n",
" x = d[FEATS].copy()\n",
" x[\"aft\"] = aft\n",
" gb = eng[\"g\"].predict(x)\n",
" return np.clip(0.5 * aft + 0.5 * gb, 1, None), aft, gb\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ๊ณํ์ ๋น(์ฐฝ์ ๋น) ์ผ์ ์ถ์ฒ: ์๋ณ ์ ๋น์ฐฝ ๋ฅ๋ ฅยท์์ฐยท์ํจ์ฑ ๋ถํ ํคํธ(์ ํจ๊ธฐํ) ์ ์ฝ ์๋ ๊ธฐ๋ ๋น์ฉ ์ต์ํ (๋น์ฉ ๋จ๊ฐ๋ ๊ต์ก์ฉ ๊ฐ์ ๊ฐ, ๋ง์)\n",
"# ---------------------------------------------------------------------------\n",
"from scipy.optimize import milp, LinearConstraint, Bounds\n",
"from scipy.stats import norm\n",
"\n",
"C_PLAN, C_FAIL, C_WASTE_KH, C_KIT = 600.0, 2400.0, 40.0, 120.0 # ๊ณํ ์ฐฝ์ ๋น, ๋น๊ณํ ๊ณ ์ฅ(๊ธด๊ธ ์ ๋น ํฌํจ), ๋ฏธ์ฌ์ฉ ์๋ช
1000h๋น ์์ค, ์ํจ์ฑ ๋ถํ ํคํธ\n",
"HORIZON, MDAYS = 12, 30 # ๊ณํ 12๊ฐ์, ์ 30์ผ\n",
"CAP_M, BUDGET_M = 22, 15000.0 # ์๋ณ ์ ๋น์ฐฝ ๋ฅ๋ ฅ(๋), ์๋ณ ์์ฐ\n",
"LOTS = [(30, 70), (45, 170), (45, 390), (70, 800)] # ์ํจ์ฑ ํคํธ ๋กํธ(์๋, ์ ํจ๊ธฐํ๊น์ง ๋จ์ ์ผ์)\n",
"KIT_MARGIN = 14 # ์ ๋น์ผ ๊ธฐ์ค ์ต์ ์ ํจ๊ธฐ๊ฐ ์ฌ์ (์ผ)\n",
"\n",
"\n",
"def exp_cost_matrix(pred, rate, month_days):\n",
" \"\"\"์ฅ๋น i๋ฅผ ์ m์ ์ ๋นํ ๋์ ๊ธฐ๋ ๋น์ฉ. ์ฐธ RUL ~ N(pred, sigma), sigma = 35์ผ + 0.12 x pred (๊ฐ์ ). ๋ง์ง๋ง ์ด์ ๊ณํ ๊ธฐ๊ฐ ๋ฐ(๋ฏธ์ ๋น).\"\"\"\n",
" sig = 35 + 0.12 * pred\n",
" C = np.zeros((len(pred), len(month_days) + 1))\n",
" for j, day in enumerate(month_days):\n",
" z = (day - pred) / sig\n",
" pf = norm.cdf(z)\n",
" unused = np.maximum(pred - day, 0) * rate * 0.6 # ์ ๋น๋ฅผ ์๋น๊ธด ๋งํผ ๋ฒ๋ฆฌ๋ ์ด์ฉ์๊ฐ h(๊ทผ์ฌ)\n",
" C[:, j] = C_PLAN + C_KIT + pf * (C_FAIL - C_PLAN) + C_WASTE_KH * unused / 1000 * (1 - pf)\n",
" C[:, -1] = norm.cdf((HORIZON * MDAYS - pred) / sig) * C_FAIL\n",
" return C\n",
"\n",
"\n",
"def plan_milp(pred, rate, cap=CAP_M, budget=BUDGET_M, lots=LOTS, use_kit=True):\n",
" \"\"\"์ ์๊ณํ์ผ๋ก ์ฅ๋น๋ณ ์ ๋น ์(0~11) ๋๋ ๋ฏธ์ ๋น(-1)๋ฅผ ๋ฐฐ์ ํ๋ค. ํ๋ณด๋ ์์ธก RUL์ด ๊ณํ ๊ธฐ๊ฐ +90์ผ ์ด๋ด์ธ ์ฅ๋น.\"\"\"\n",
" md = np.arange(1, HORIZON + 1) * MDAYS\n",
" cand = np.where(pred < HORIZON * MDAYS + 90)[0]\n",
" n, M = len(cand), HORIZON\n",
" C = exp_cost_matrix(pred[cand], rate[cand], md)\n",
" c = C.ravel()\n",
" A, lo, hi = [], [], []\n",
" for i in range(n): # ์ฅ๋น๋น ํ ๋ฒ๋ง ๋ฐฐ์ (์ 12์นธ + ๋ฏธ์ ๋น)\n",
" row = np.zeros(n * (M + 1)); row[i * (M + 1):(i + 1) * (M + 1)] = 1; A.append(row); lo.append(1); hi.append(1)\n",
" for m in range(M): # ์๋ณ ๋ฅ๋ ฅยท์์ฐ\n",
" row = np.zeros(n * (M + 1)); row[m::(M + 1)] = 1\n",
" A.append(row); lo.append(0); hi.append(cap)\n",
" A.append(row * (C_PLAN + C_KIT)); lo.append(0); hi.append(budget)\n",
" if use_kit: # ์ํจ์ฑ ํคํธ: m์ ์ดํ ์ ๋น ์์๋ ๊ทธ ์์ ์ ์ ํจ๊ธฐ๊ฐ์ด ๋จ์ ๋กํธ ์๋์ ๋์ ์ ์๋ค\n",
" for m in range(M):\n",
" sup = sum(q for q, e in lots if e >= md[m] + KIT_MARGIN)\n",
" row = np.zeros(n * (M + 1))\n",
" for mm in range(m, M):\n",
" row[mm::(M + 1)] = 1\n",
" A.append(row); lo.append(0); hi.append(sup)\n",
" r = milp(c, constraints=LinearConstraint(np.array(A), lo, hi), integrality=np.ones(len(c)), bounds=Bounds(0, 1), options={\"time_limit\": 20})\n",
" month = np.full(len(pred), -1)\n",
" if r.x is not None:\n",
" x = np.round(r.x).reshape(n, M + 1)\n",
" assign = np.where(x[:, :M].sum(axis=1) > 0.5, x[:, :M].argmax(axis=1), -1)\n",
" month[cand] = assign\n",
" return month\n",
"\n",
"\n",
"def plan_greedy(pred, margin=45, cap=CAP_M, budget=BUDGET_M):\n",
" \"\"\"๊ฐ๋จํ ํ์ ๋ฐฐ์ : ์ ๋น ๋ชฉํ์ผ(์์ธก RUL - margin)์ด ๋น ๋ฅธ ์์ผ๋ก ๋ชฉํ์ผ ์ด์ ๊ฐ์ฅ ๋ฆ์ ๋น ์, ์์ผ๋ฉด ๊ฐ์ฅ ์ด๋ฅธ ๋น ์์ ๋ฐฐ์ .\"\"\"\n",
" cap = min(cap, int(budget // (C_PLAN + C_KIT))) # ์ ์์ฐ์ด ํ์ฉํ๋ ๋์๋ก ๋ฅ๋ ฅ์ ์ ํ\n",
" month = np.full(len(pred), -1)\n",
" left = np.full(HORIZON, cap)\n",
" for i in np.argsort(pred):\n",
" if pred[i] > HORIZON * MDAYS + 60: break\n",
" tgt = int(np.clip((pred[i] - margin) // MDAYS - 1, 0, HORIZON - 1))\n",
" for m in list(range(tgt, -1, -1)) + list(range(tgt + 1, HORIZON)):\n",
" if left[m] > 0:\n",
" month[i] = m; left[m] -= 1; break\n",
" return month\n",
"\n",
"\n",
"def plan_fixed(lin_rul, cap=CAP_M, budget=BUDGET_M):\n",
" \"\"\"๊ดํ: ๊ท์ ์ฃผ๊ธฐ ๋๋ฌ ์์๋๋ก(์ ํ ์์ฌ ์ผ์) ์๋ณ ๋ฅ๋ ฅ ์์์ ๋ฐฐ์ .\"\"\"\n",
" return plan_greedy(np.asarray(lin_rul, float), margin=0, cap=cap, budget=budget)\n",
"\n",
"\n",
"def evaluate_plan(month, true_rul, rate, cost_kit=True):\n",
" \"\"\"์ฐธ RUL๋ก ๊ณํ์ ์ค์ ๋น์ฉ์ ๊ณ์ฐํ๋ค: ์ ๋น์ผ ์ด์ ์ ๊ณ ์ฅ -> ๋น๊ณํ ๊ณ ์ฅ ๋น์ฉ, ์๋๋ฉด ๊ณํ ๋น์ฉ + ๋ฏธ์ฌ์ฉ ์๋ช
์์ค.\"\"\"\n",
" day = np.where(month >= 0, (month + 1) * MDAYS, 10 ** 9)\n",
" fail = (true_rul < day) & (true_rul < HORIZON * MDAYS)\n",
" planned = (month >= 0) & ~fail\n",
" waste = np.where(planned, (true_rul - day) * rate * 0.6, 0) * C_WASTE_KH / 1000\n",
" cost = fail * C_FAIL + planned * (C_PLAN + (C_KIT if cost_kit else 0)) + waste\n",
" return {\"total\": float(cost.sum()), \"fail\": int(fail.sum()), \"planned\": int(planned.sum()), \"waste\": float(waste.sum()), \"unsched_fail\": int(((month < 0) & fail).sum())}\n",
"\n",
"\n",
"def kit_fefo(month, lots=LOTS):\n",
" \"\"\"์ ํจ๊ธฐํ์ด ๋น ๋ฅธ ๋กํธ๋ถํฐ ์ฐ๋ FEFO๋ก ํคํธ๋ฅผ ๋ฐฐ์ ํ๊ณ ์ ๋น์ผ์ ์ ํจ๊ธฐ๊ฐ์ด ๋ถ์กฑํ ๋ฐฐ์ ์์ ๋ง๋ฃ ํ๊ธฐ ์๋์ ์ผ๋ค.\"\"\"\n",
" q = [[qty, e] for qty, e in sorted(lots, key=lambda t: t[1])]\n",
" short = 0\n",
" for m in sorted(month[month >= 0]):\n",
" day = (m + 1) * MDAYS\n",
" for lot in q:\n",
" if lot[1] >= day + KIT_MARGIN and lot[0] > 0:\n",
" lot[0] -= 1; break\n",
" else:\n",
" short += 1\n",
" expired = sum(l[0] for l in q if l[1] < HORIZON * MDAYS)\n",
" return {\"short\": short, \"expired_unused\": expired, \"left\": sum(l[0] for l in q)}\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"tr = make_fleet(900, seed=1)\n",
"t0 = time.perf_counter()\n",
"eng = fit_engine(tr)\n",
"print(f\"[0] ์์ธก ์์ง ํ์ต {time.perf_counter() - t0:.2f}์ด(ํ์ต {len(tr)}๋, ์์ ํ๋ณธ {int(tr.event.sum()) * 5}๊ฑด)\")\n",
"ALARM_D, MARGIN = 180, 60 # ์ํ ์ ์: ์ฐธ RUL 180์ผ ๋ฏธ๋ง / ๊ฒฝ๋ณด ๊ธฐ์ค: ์์ธก RUL์ด 180+60์ผ ๋ฏธ๋ง(์์ธก ์ค์ฐจ๋ฅผ ๊ณ ๋ คํ ์ฌ์ )\n",
"\n",
"\n",
"def pipeline(fl):\n",
" \"\"\"์ผ๊ด ์ฒ๋ฆฌ: ํผ์ฒ -> RUL ์์ธก -> ๊ฒฝ๋ณด -> ์ผ์ ๋ฐฐ์ (ํ์) -> ๋์๋ณด๋ ์ง๊ณ. ๋จ๊ณ๋ณ ์๊ฐ์ ๋ฐํํ๋ค.\"\"\"\n",
" t = {}\n",
" s = time.perf_counter(); d = add_features(fl); t[\"ํผ์ฒ\"] = time.perf_counter() - s\n",
" al = d[d.event == 0].reset_index(drop=True)\n",
" s = time.perf_counter(); pred, _, _ = predict_rul(eng, al); t[\"์์ธก\"] = time.perf_counter() - s\n",
" s = time.perf_counter(); month = plan_greedy(pred, cap=max(CAP_M, int(len(al) * 0.03)), budget=1e9); t[\"์ผ์ \"] = time.perf_counter() - s\n",
" s = time.perf_counter()\n",
" al[\"pred\"], al[\"month\"] = pred, month\n",
" summ = al.groupby(\"unit\").agg(์ฅ๋น=(\"pred\", \"size\"), ํ๊ท RUL=(\"pred\", \"mean\"), ์ํ=(\"pred\", lambda x: int((x < ALARM_D + MARGIN).sum())))\n",
" cal = pd.crosstab(al.loc[al.month >= 0, \"unit\"], al.loc[al.month >= 0, \"month\"])\n",
" low = al.nsmallest(20, \"pred\")\n",
" fg, ax = plt.subplots(1, 2, figsize=(9, 3.5)); ax[0].bar(summ.index, summ[\"์ํ\"]); ax[1].imshow(cal.values, aspect=\"auto\"); fg.canvas.draw(); plt.close(fg)\n",
" t[\"๋์๋ณด๋\"] = time.perf_counter() - s\n",
" return al, t\n",
"\n",
"\n",
"# ---------------- 1) ๊ท๋ชจ๋ณ ์๋ฎฌ๋ ์ด์
----------------\n",
"rows = []\n",
"for N in (1000, 5000, 10000, 50000, 100000):\n",
" s = time.perf_counter(); fl = make_fleet(N, seed=100 + N % 7); tg = time.perf_counter() - s\n",
" al, t = pipeline(fl)\n",
" tot = t[\"ํผ์ฒ\"] + t[\"์์ธก\"] + t[\"์ผ์ \"]\n",
" rows.append((N, len(al), tg, t[\"ํผ์ฒ\"], t[\"์์ธก\"], t[\"์ผ์ \"], t[\"๋์๋ณด๋\"], len(al) / tot))\n",
"ld = pd.DataFrame(rows, columns=[\"๋ณด์ ์ฅ๋น\", \"์์กด ์ฅ๋น\", \"์์ฑ(์ด)\", \"ํผ์ฒ(์ด)\", \"์์ธก(์ด)\", \"์ผ์ (์ด)\", \"๋์๋ณด๋ ๋ก๋ฉ(์ด)\", \"์ฒ๋ฆฌ๋(๋/์ด)\"])\n",
"print(\"\\n[1] ๊ท๋ชจ๋ณ ์ผ๊ด ์ฒ๋ฆฌ ์๊ฐ(์์ฑ์ ์๋ฎฌ๋ ์ด์
์ฉ์ด๋ฉฐ ์ค์ ์ด์์๋ ์์)\")\n",
"print(ld.round(3).to_string(index=False))\n",
"r10 = ld[ld[\"๋ณด์ ์ฅ๋น\"] == 10000].iloc[0]\n",
"print(f\" 1๋ง ๋ ์ผ๊ด ์๋ช
์๋ฎฌ๋ ์ด์
: ํผ์ฒ+์์ธก+์ผ์ {r10['ํผ์ฒ(์ด)'] + r10['์์ธก(์ด)'] + r10['์ผ์ (์ด)']:.2f}์ด, ๋์๋ณด๋ ๋ก๋ฉ {r10['๋์๋ณด๋ ๋ก๋ฉ(์ด)']:.3f}์ด(๋ชฉํ 2์ด ์ด๋ด: {'๋ฌ์ฑ' if r10['๋์๋ณด๋ ๋ก๋ฉ(์ด)'] <= 2 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# ---------------- 2) ์ํ ๊ฒฝ๋ณด ์ ํ๋(1๋ง ๋) ----------------\n",
"fl10 = make_fleet(10000, seed=5)\n",
"d10 = add_features(fl10)\n",
"a10 = d10[d10.event == 0].reset_index(drop=True)\n",
"pred10, _, _ = predict_rul(eng, a10)\n",
"\n",
"\n",
"def alarm_metrics(pred, truth_d, margin=MARGIN):\n",
" al_, tr_ = pred < ALARM_D + margin, truth_d < ALARM_D\n",
" tp, fp, fn, tn = int((al_ & tr_).sum()), int((al_ & ~tr_).sum()), int((~al_ & tr_).sum()), int((~al_ & ~tr_).sum())\n",
" return (tp + tn) / len(pred), tp / max(tp + fn, 1), tp / max(tp + fp, 1), fp, fn\n",
"\n",
"\n",
"acc, rec, prec, fp, fn = alarm_metrics(pred10, a10[\"rul_d\"].values)\n",
"sw = pd.DataFrame([(m, *alarm_metrics(pred10, a10[\"rul_d\"].values, m)) for m in (0, 60, 120, 180)], columns=[\"์ฌ์ (์ผ)\", \"์ ํ๋\", \"์ฌํ์จ\", \"์ ๋ฐ๋\", \"์ค๊ฒฝ๋ณด\", \"๋ฏธํ\"])\n",
"print(f\"\\n[2] ์ํ ๊ฒฝ๋ณด ์ ํ๋: ์์กด {len(a10)}๋, ์ค์ ์ํ(์ฐธ RUL {ALARM_D}์ผ ๋ฏธ๋ง) {int((a10.rul_d < ALARM_D).sum())}๋({(a10.rul_d < ALARM_D).mean():.1%})\")\n",
"print(\" ๊ฒฝ๋ณด ๊ธฐ์ค(์์ธก RUL < 180 + ์ฌ์ ) ๋น๊ต\\n\" + sw.assign(์ ํ๋=sw[\"์ ํ๋\"].map(\"{:.1%}\".format), ์ฌํ์จ=sw[\"์ฌํ์จ\"].map(\"{:.1%}\".format), ์ ๋ฐ๋=sw[\"์ ๋ฐ๋\"].map(\"{:.1%}\".format)).to_string(index=False))\n",
"print(f\" ์ฑํ ์ฌ์ {MARGIN}์ผ: ์ ํ๋ {acc:.1%} (๋ชฉํ 95% ์ด์: {'๋ฌ์ฑ' if acc >= .95 else '๋ฏธ๋ฌ'}), ์ฌํ์จ {rec:.1%}, ์ ๋ฐ๋ {prec:.1%}, ์ค๊ฒฝ๋ณด {fp}, ๋ฏธํ {fn}\")\n",
"print(\" ์ํ ์ฅ๋น๊ฐ ๋๋ฌผ์ด ์ ํ๋๋ง์ผ๋ก๋ ๋ถํ๋ ค์ง๋ฏ๋ก ์ฌํ์จยท์ ๋ฐ๋๋ฅผ ํจ๊ป ๋ณธ๋ค. ์ฌ์ ๋ฅผ ๋๋ฆฌ๋ฉด ์ฌํ์จ์ ์ค๋ฅด์ง๋ง ์ค๊ฒฝ๋ณด๊ฐ ๋์ด ์ ํ๋๊ฐ 95% ์๋๋ก ๋ด๋ ค๊ฐ๋ค. ๋์น ์ฅ๋น๋ ์ ๊ธฐ ์ ๊ฒ(์ ๊ฒ ๋ง๋ชจ)์ผ๋ก ๋ณด์ํ๋ค.\")\n",
"\n",
"# ---------------- 3) ์ค๋ฅ ์ฃผ์
----------------\n",
"rng = np.random.default_rng(7)\n",
"W_FIX = {\"normal\": 1.0, \"rough\": 1.15, \"salt\": 1.1, \"cold\": 1.05}\n",
"\n",
"\n",
"def corrupt(kind):\n",
" x = a10.copy()\n",
" if kind == \"์ ๊ฒ ๋ง๋ชจ 20% ๊ฒฐ์ธก(๋์ฒด)\":\n",
" m = rng.random(len(x)) < 0.2\n",
" x.loc[m, \"insp\"] = (x.loc[m, \"h_since\"] / CYCLE * x.loc[m, \"env\"].map(W_FIX)).clip(0, 1.3) # ์ ์ ๊ท์น: ๋
ธํ ์ง์๋ก ๋์ฒด\n",
" elif kind == \"์ ๊ฒ ๋ง๋ชจ 20% ๊ฒฐ์ธก(0์ผ๋ก ๋ฐฉ์น)\":\n",
" m = rng.random(len(x)) < 0.2\n",
" x.loc[m, \"insp\"] = 0.0\n",
" elif kind == \"ํ๊ฒฝ 10% ์ค๊ธฐ์ฌ\":\n",
" m = rng.random(len(x)) < 0.1\n",
" x.loc[m, \"env\"] = rng.choice(ENVS, int(m.sum()))\n",
" elif kind == \"๊ฐ๋๋ฅ 5% ๋จ์ ์ค๋ฅ(x30)\":\n",
" m = rng.random(len(x)) < 0.05\n",
" x.loc[m, \"rate\"] = x.loc[m, \"rate\"] * 30\n",
" elif kind == \"๋ณตํฉ(๊ฒฐ์ธก ๋์ฒด+ํ๊ฒฝ ์ค๊ธฐ์ฌ+๋จ์ ์ค๋ฅ)\":\n",
" m = rng.random(len(x)) < 0.2; x.loc[m, \"insp\"] = (x.loc[m, \"h_since\"] / CYCLE * x.loc[m, \"env\"].map(W_FIX)).clip(0, 1.3)\n",
" m = rng.random(len(x)) < 0.1; x.loc[m, \"env\"] = rng.choice(ENVS, int(m.sum()))\n",
" m = rng.random(len(x)) < 0.05; x.loc[m, \"rate\"] = x.loc[m, \"rate\"] * 30\n",
" return add_features(x[[\"eq_id\", \"unit\", \"env\", \"etype\", \"rate\", \"hours\", \"km\", \"age_y\", \"n_ovh\", \"h_since\", \"unpl\", \"n_maint\", \"insp\", \"t_fail\", \"event\", \"dur\", \"rul_h\", \"rul_d\"]])\n",
"\n",
"\n",
"rows = [(\"์ ์\", acc, rec, prec, fp, fn)]\n",
"for kind in (\"์ ๊ฒ ๋ง๋ชจ 20% ๊ฒฐ์ธก(๋์ฒด)\", \"์ ๊ฒ ๋ง๋ชจ 20% ๊ฒฐ์ธก(0์ผ๋ก ๋ฐฉ์น)\", \"ํ๊ฒฝ 10% ์ค๊ธฐ์ฌ\", \"๊ฐ๋๋ฅ 5% ๋จ์ ์ค๋ฅ(x30)\", \"๋ณตํฉ(๊ฒฐ์ธก ๋์ฒด+ํ๊ฒฝ ์ค๊ธฐ์ฌ+๋จ์ ์ค๋ฅ)\"):\n",
" x = corrupt(kind)\n",
" p, _, _ = predict_rul(eng, x)\n",
" rows.append((kind, *alarm_metrics(p, a10[\"rul_d\"].values)))\n",
"er = pd.DataFrame(rows, columns=[\"์กฐ๊ฑด\", \"์ ํ๋\", \"์ฌํ์จ\", \"์ ๋ฐ๋\", \"์ค๊ฒฝ๋ณด\", \"๋ฏธํ\"])\n",
"print(\"\\n[3] ์ค๋ฅ ์ฃผ์
์ ๊ฒฝ๋ณด ์ฑ๋ฅ(1๋ง ๋)\\n\" + er.assign(์ ํ๋=er[\"์ ํ๋\"].map(\"{:.1%}\".format), ์ฌํ์จ=er[\"์ฌํ์จ\"].map(\"{:.1%}\".format), ์ ๋ฐ๋=er[\"์ ๋ฐ๋\"].map(\"{:.1%}\".format)).to_string(index=False))\n",
"worst = er.iloc[1:].sort_values(\"์ ํ๋\").iloc[0]\n",
"z = er[er[\"์กฐ๊ฑด\"].str.contains(\"0์ผ๋ก\")].iloc[0]\n",
"print(f\" ์ ํ๋๊ฐ ๊ฐ์ฅ ๋ฎ์ ์กฐ๊ฑด์ '{worst['์กฐ๊ฑด']}'({worst['์ ํ๋']:.1%}, ์ค๊ฒฝ๋ณด {int(worst['์ค๊ฒฝ๋ณด'])}๊ฑด). ๊ฒฐ์ธก์ 0์ผ๋ก ๋ฐฉ์นํ๋ฉด ์ฌํ์จ์ด {z['์ฌํ์จ']:.1%}๋ก ์ ์({er.iloc[0]['์ฌํ์จ']:.1%})๋ณด๋ค ๋จ์ด์ง๋ฏ๋ก ์ ์ ๊ท์น(M2)์ ๋์ฒด๊ฐ ์ฒ๋ฆฌ๊ฐ ํ์ํ๋ค.\")\n",
"print(f\" 95% ๋ชฉํ๋ ์ ์ ์กฐ๊ฑด์์๋ง ๋ฌ์ฑํ๋ฉฐ ์ค๋ฅ๊ฐ ๊ฒน์น๋ฉด {er.iloc[1:]['์ ํ๋'].min():.1%}๊น์ง ๋ฎ์์ง๋ค -> ์
๋ ฅ ๊ฒ์ฆ(M1ยทM2)์ด ๊ฒฝ๋ณด ํ์ง์ ์ ์ ์ด๋ค.\")\n",
"\n",
"# ---------------- 4) ์ ์๊ณํ ๊ท๋ชจ ํ๊ณ ----------------\n",
"mt = []\n",
"for n in (50, 150, 300, 600):\n",
" sub = a10.head(20000).copy()\n",
" pr_ = np.sort(pred10)[:n] * 0 + np.linspace(30, 440, n) # ์์ธก RUL์ด ๊ณํ ๊ธฐ๊ฐ ์์ ๋ถํฌํ๋ ํ๋ณด n๋\n",
" s = time.perf_counter(); m = plan_milp(pr_, np.full(n, 3.0), cap=max(CAP_M, n // 8), budget=1e9); dt = time.perf_counter() - s\n",
" mt.append((n, dt, int((m >= 0).sum())))\n",
"print(\"\\n[4] ์ ์๊ณํ ํ๋ณด ์ฅ๋น ์๋ณ ํ์ด ์๊ฐ:\", \", \".join(f\"{n}๋ {s:.2f}์ด\" for n, s, _ in mt))\n",
"print(\" ํ๋ณด๊ฐ ์์ฒ ๋๋ฅผ ๋์ผ๋ฉด ์ ์๊ณํ์ด ๋๋ ค์ง๋ฏ๋ก ์ํ ์์ ํ๋ณด๋ง ์ ์๊ณํ, ๋๋จธ์ง๋ ํ์ ๋ฐฐ์ ์ผ๋ก ๋๋๋ ๊ตฌ์กฐ๋ฅผ ์ด๋ค. ๊ฒฝ๋ณดยท์ผ์ ์ ํ๋จ ๋ณด์กฐ์ด๋ฉฐ ํ์ ์ ๊ตฐ์์งํ๊ด ์น์ธ(M7)์ ๋ฐ๋ฅธ๋ค.\")\n",
"print(\" ๋ชจ๋ ๋ถ๋ยท์ฅ๋นยท์์น๋ ๊ต์ก์ฉ ๊ฐ์ ์๋ฃ์ด๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 3, figsize=(15, 4.2))\n",
"ax[0].loglog(ld[\"๋ณด์ ์ฅ๋น\"], ld[\"ํผ์ฒ(์ด)\"] + ld[\"์์ธก(์ด)\"] + ld[\"์ผ์ (์ด)\"], \"o-\", label=\"batch total\"); ax[0].loglog(ld[\"๋ณด์ ์ฅ๋น\"], ld[\"๋์๋ณด๋ ๋ก๋ฉ(์ด)\"], \"s-\", label=\"dashboard\"); ax[0].axhline(2, c=\"r\", ls=\"--\"); ax[0].legend(); ax[0].set_title(\"Processing time vs fleet size (s)\")\n",
"ax[1].barh(range(len(er)), er[\"์ฌํ์จ\"] * 100); ax[1].set_yticks(range(len(er))); ax[1].set_yticklabels([f\"C{i + 1}\" for i in range(len(er))]); ax[1].set_title(\"Alarm recall by fault case (%)\")\n",
"ax[2].plot([m[0] for m in mt], [m[1] for m in mt], \"o-\"); ax[2].set_title(\"MILP solve time vs candidate units (s)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
08 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[0] ์์ธก ์์ง ํ์ต 1.44์ด(ํ์ต 900๋, ์์ ํ๋ณธ 1440๊ฑด)\n",
"\n",
"[1] ๊ท๋ชจ๋ณ ์ผ๊ด ์ฒ๋ฆฌ ์๊ฐ(์์ฑ์ ์๋ฎฌ๋ ์ด์
์ฉ์ด๋ฉฐ ์ค์ ์ด์์๋ ์์)\n",
" ๋ณด์ ์ฅ๋น ์์กด ์ฅ๋น ์์ฑ(์ด) ํผ์ฒ(์ด) ์์ธก(์ด) ์ผ์ (์ด) ๋์๋ณด๋ ๋ก๋ฉ(์ด) ์ฒ๋ฆฌ๋(๋/์ด)\n",
" 1000 676 0.021 0.007 0.008 0.001 0.125 41807.644\n",
" 5000 3346 0.132 0.010 0.020 0.005 0.111 95891.024\n",
" 10000 6643 0.282 0.013 0.036 0.010 0.107 113284.295\n",
" 50000 33436 0.989 0.051 0.177 0.038 0.127 125508.675\n",
"100000 66873 2.168 0.083 0.388 0.079 0.132 121715.318\n",
" 1๋ง ๋ ์ผ๊ด ์๋ช
์๋ฎฌ๋ ์ด์
: ํผ์ฒ+์์ธก+์ผ์ 0.06์ด, ๋์๋ณด๋ ๋ก๋ฉ 0.107์ด(๋ชฉํ 2์ด ์ด๋ด: ๋ฌ์ฑ)\n",
"\n",
"[2] ์ํ ๊ฒฝ๋ณด ์ ํ๋: ์์กด 6741๋, ์ค์ ์ํ(์ฐธ RUL 180์ผ ๋ฏธ๋ง) 474๋(7.0%)\n",
" ๊ฒฝ๋ณด ๊ธฐ์ค(์์ธก RUL < 180 + ์ฌ์ ) ๋น๊ต\n",
" ์ฌ์ (์ผ) ์ ํ๋ ์ฌํ์จ ์ ๋ฐ๋ ์ค๊ฒฝ๋ณด ๋ฏธํ\n",
" 0 95.7% 51.1% 80.7% 58 232\n",
" 60 95.5% 70.9% 67.5% 162 138\n",
" 120 94.7% 84.6% 58.3% 287 73\n",
" 180 92.5% 92.0% 48.1% 470 38\n",
" ์ฑํ ์ฌ์ 60์ผ: ์ ํ๋ 95.5% (๋ชฉํ 95% ์ด์: ๋ฌ์ฑ), ์ฌํ์จ 70.9%, ์ ๋ฐ๋ 67.5%, ์ค๊ฒฝ๋ณด 162, ๋ฏธํ 138\n",
" ์ํ ์ฅ๋น๊ฐ ๋๋ฌผ์ด ์ ํ๋๋ง์ผ๋ก๋ ๋ถํ๋ ค์ง๋ฏ๋ก ์ฌํ์จยท์ ๋ฐ๋๋ฅผ ํจ๊ป ๋ณธ๋ค. ์ฌ์ ๋ฅผ ๋๋ฆฌ๋ฉด ์ฌํ์จ์ ์ค๋ฅด์ง๋ง ์ค๊ฒฝ๋ณด๊ฐ ๋์ด ์ ํ๋๊ฐ 95% ์๋๋ก ๋ด๋ ค๊ฐ๋ค. ๋์น ์ฅ๋น๋ ์ ๊ธฐ ์ ๊ฒ(์ ๊ฒ ๋ง๋ชจ)์ผ๋ก ๋ณด์ํ๋ค.\n",
"\n",
"[3] ์ค๋ฅ ์ฃผ์
์ ๊ฒฝ๋ณด ์ฑ๋ฅ(1๋ง ๋)\n",
" ์กฐ๊ฑด ์ ํ๋ ์ฌํ์จ ์ ๋ฐ๋ ์ค๊ฒฝ๋ณด ๋ฏธํ\n",
" ์ ์ 95.5% 70.9% 67.5% 162 138\n",
" ์ ๊ฒ ๋ง๋ชจ 20% ๊ฒฐ์ธก(๋์ฒด) 94.9% 69.2% 62.1% 200 146\n",
" ์ ๊ฒ ๋ง๋ชจ 20% ๊ฒฐ์ธก(0์ผ๋ก ๋ฐฉ์น) 94.9% 54.6% 67.1% 127 215\n",
" ํ๊ฒฝ 10% ์ค๊ธฐ์ฌ 95.4% 70.5% 66.0% 172 140\n",
" ๊ฐ๋๋ฅ 5% ๋จ์ ์ค๋ฅ(x30) 94.7% 72.6% 60.4% 226 130\n",
"๋ณตํฉ(๊ฒฐ์ธก ๋์ฒด+ํ๊ฒฝ ์ค๊ธฐ์ฌ+๋จ์ ์ค๋ฅ) 93.5% 69.2% 52.9% 292 146\n",
" ์ ํ๋๊ฐ ๊ฐ์ฅ ๋ฎ์ ์กฐ๊ฑด์ '๋ณตํฉ(๊ฒฐ์ธก ๋์ฒด+ํ๊ฒฝ ์ค๊ธฐ์ฌ+๋จ์ ์ค๋ฅ)'(93.5%, ์ค๊ฒฝ๋ณด 292๊ฑด). ๊ฒฐ์ธก์ 0์ผ๋ก ๋ฐฉ์นํ๋ฉด ์ฌํ์จ์ด 54.6%๋ก ์ ์(70.9%)๋ณด๋ค ๋จ์ด์ง๋ฏ๋ก ์ ์ ๊ท์น(M2)์ ๋์ฒด๊ฐ ์ฒ๋ฆฌ๊ฐ ํ์ํ๋ค.\n",
" 95% ๋ชฉํ๋ ์ ์ ์กฐ๊ฑด์์๋ง ๋ฌ์ฑํ๋ฉฐ ์ค๋ฅ๊ฐ ๊ฒน์น๋ฉด 93.5%๊น์ง ๋ฎ์์ง๋ค -> ์
๋ ฅ ๊ฒ์ฆ(M1ยทM2)์ด ๊ฒฝ๋ณด ํ์ง์ ์ ์ ์ด๋ค.\n",
"\n",
"[4] ์ ์๊ณํ ํ๋ณด ์ฅ๋น ์๋ณ ํ์ด ์๊ฐ: 50๋ 0.04์ด, 150๋ 0.11์ด, 300๋ 0.22์ด, 600๋ 0.50์ด\n",
" ํ๋ณด๊ฐ ์์ฒ ๋๋ฅผ ๋์ผ๋ฉด ์ ์๊ณํ์ด ๋๋ ค์ง๋ฏ๋ก ์ํ ์์ ํ๋ณด๋ง ์ ์๊ณํ, ๋๋จธ์ง๋ ํ์ ๋ฐฐ์ ์ผ๋ก ๋๋๋ ๊ตฌ์กฐ๋ฅผ ์ด๋ค. ๊ฒฝ๋ณดยท์ผ์ ์ ํ๋จ ๋ณด์กฐ์ด๋ฉฐ ํ์ ์ ๊ตฐ์์งํ๊ด ์น์ธ(M7)์ ๋ฐ๋ฅธ๋ค.\n",
" ๋ชจ๋ ๋ถ๋ยท์ฅ๋นยท์์น๋ ๊ต์ก์ฉ ๊ฐ์ ์๋ฃ์ด๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
09. AAR ๊ธฐ๋ฐ ์๋ช
์์ธก ์ค์ฐจ ๋ถ์ ๋ฐ ํ๊ฒฝ ๊ฐ์ค์น ํ๋ผ๋ฏธํฐ ์ฌ๋ณด์ \n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"AAR ๊ธฐ๋ฐ ์๋ช
์์ธก ์ค์ฐจ๋ฅผ ๋ถ์ํ๊ณ ํ๊ฒฝ ๊ฐ์ค์น๋ฅผ ์ฌ๋ณด์ ํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **AAR๋ก ์๋ช
์์ธก ์ค์ฐจ๋ฅผ ๋ถ์ํ๊ณ ํ๊ฒฝ ๊ฐ์ค์น๋ฅผ ์ฌ๋ณด์ ํด ROI๋ฅผ ์ฐ์ถํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ด์ ์ฅ๋น์ ์์ธก ์ค์ฐจ๋ฅผ ํ๊ฒฝ, ๊ฐ๋๋ฅ , ๋น๊ณํ ์ ๋น์จ๋ณ๋ก ๋๋๊ณ ํฐ ์ค์ฐจ์ ์์ธ์ ๋ถ๋ฅ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `reports/sup02_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๋ก ํ๊ฒฝ๋ณยท์์ธ๋ณ ๋ถ์ํ๊ณ , ํ๊ฒฝ ๊ฐ์ค์น๋ฅผ ์ด์ ๊ณ ์ฅ ์ด๋ ฅ์ AFT ๊ณ์๋ก ์ฌ๋ณด์ ํ๊ณ ์ด์ ๋ฐ์ดํฐ๋ก ์์ธก ์์ง์ ์ฌํ์ตํด ๋ณด๋ฅ ์ฅ๋น ์ง๋จ์์ ๊ฐ์ ์ ๊ฒ์ฆํ ๋ค ๋น์ฉ ์ ๊ฐ์กยท๊ฐ๋ ์ค๋จ์ผ์ ๋จ์ถ๋ฅ ยทํฌ์ ํ์ ๊ธฐ๊ฐ 3๋ ํจ๊ณผ ์งํ๋ฅผ ์ฐ์๊ณผ ๋ฏผ๊ฐ๋๋ก ์ฐ์ถํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.metrics import r2_score\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ฅ๋น ๋ณด์ ํํฉยท๊ณ ์ฅ ์ด๋ ฅ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์ฅ๋นยท์์น ์์)\n",
"# ๊ฐ์ ๋ถ๋ 6๊ฐ(U1~U6)์ ์ฅ๋น๊ฐ ์ด์ฉ ํ๊ฒฝ(normal/rough/salt/cold)์ ๋ฐ๋ผ ๋ค๋ฅธ ์๋๋ก ๋
ธํํํ๋ค. ์ฐฝ์ ๋น ํ ์ด์ฉ์๊ฐ(h_since)์ด\n",
"# ์ฅ๋น๋ง๋ค ์จ์ ์๋ช
(t_fail)์ ๋์ผ๋ฉด ๊ณ ์ฅ(event=1) ์ด๋ ฅ, ๋์ง ์์์ผ๋ฉด ํ์ฌ๊น์ง ์์กด(event=0, ๊ด์ธก ์ค๋์ ๋จ)์ด๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"ENVS = [\"normal\", \"rough\", \"salt\", \"cold\"]\n",
"ENV_W = {\"normal\": 1.00, \"rough\": 1.35, \"salt\": 1.25, \"cold\": 1.12} # ํ๊ฒฝ ๊ฐ์ค์น(์จ์ ์ ๋ต, ๊ฐ์ ๊ฐ): ํด์๋ก ์๋ช
์ด ์งง๋ค\n",
"ENV_KO = {\"normal\": \"์ผ๋ฐ\", \"rough\": \"ํ์ง\", \"salt\": \"์ผ๋ถ\", \"cold\": \"ํนํ\"}\n",
"UNITS = [\"U1\", \"U2\", \"U3\", \"U4\", \"U5\", \"U6\"]\n",
"UNIT_ENV = {\"U1\": [.7, .1, .1, .1], \"U2\": [.2, .6, .1, .1], \"U3\": [.2, .1, .6, .1], \"U4\": [.3, .1, .1, .5], \"U5\": [.5, .3, .1, .1], \"U6\": [.4, .1, .4, .1]}\n",
"ETYPES = [\"truck\", \"generator\", \"trailer\", \"radio\"]\n",
"CYCLE = 8000.0 # ๊ท์ ์ฐฝ์ ๋น ์ฃผ๊ธฐ(์ด์ฉ์๊ฐ h, ๊ฐ์ ๊ฐ)\n",
"KSHAPE = 12.0 # ์๋ช
๋ถํฌ ์์ด๋ธ ํ์(๋ง๋ชจํ ๊ณ ์ฅ, ํด์๋ก ์๋ช
์ด ๋ชจ์ฌ ์๋ค)\n",
"INSP_NOISE = 0.025 # ์ ๋น ์ ๊ฒ ๋ง๋ชจ ๋น์จ ์ธก์ ์ค์ฐจ(๊ฐ์ ๊ฐ)\n",
"\n",
"\n",
"def make_fleet(n=900, seed=0, env_w=None, shift=1.0):\n",
" \"\"\"์ฅ๋น n๋. env_w๋ก ์จ์ ํ๊ฒฝ ๊ฐ์ค์น๋ฅผ ๋ฐ๊ฟ ์ด์ ๋ถํฌ ๋ณํ๋ฅผ ๋ชจ์ํ ์ ์๋ค. shift>1์ด๋ฉด ๊ฐ๋๋ฅ ์ด ๋์์ง๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" w = env_w or ENV_W\n",
" unit = rng.choice(UNITS, n)\n",
" env = np.array([rng.choice(ENVS, p=UNIT_ENV[u]) for u in unit])\n",
" et = rng.choice(ETYPES, n, p=[.4, .2, .2, .2])\n",
" rate = rng.lognormal(np.log(3.0 * shift), 0.25, n) # ์ผ ํ๊ท ์ด์ฉ์๊ฐ(h/์ผ)\n",
" h_since = rng.uniform(300, 11500, n) # ๋ง์ง๋ง ์ฐฝ์ ๋น ์ดํ ์ด์ฉ์๊ฐ\n",
" n_ovh = rng.integers(0, 3, n)\n",
" hours = h_since + n_ovh * rng.uniform(5000, 8000, n) # ๋์ ์ด์ฉ์๊ฐ\n",
" km = hours * rng.normal(17, 3, n) * np.where(env == \"rough\", 0.8, 1.0) # ๋์ ๊ธฐ๋๊ฑฐ๋ฆฌ(km)\n",
" age_y = np.clip(hours / (rate * 365) * rng.uniform(0.9, 1.15, n), 0.5, 25)\n",
" unpl = rng.poisson(0.35 * h_since / 1000 * np.array([w[e] for e in env]) / 1.3) # ๋น๊ณํ ์ ๋น ๊ฑด์(ํ๊ฒฝ์ด ๊ฐํนํ ์๋ก ๋ง๋ค)\n",
" n_maint = rng.poisson(0.6 * h_since / 1000 + 1) # ๊ณํ ์ ๋น ๊ฑด์\n",
" scale = 9600 / np.array([w[e] for e in env]) * 0.985 ** unpl * np.exp(rng.normal(0, 0.04, n))\n",
" t_fail = scale * rng.weibull(KSHAPE, n) # ์ฐฝ์ ๋น ํ ๊ณ ์ฅ๊น์ง ์ด์ฉ์๊ฐ(์จ์ ์ ๋ต)\n",
" event = (t_fail <= h_since).astype(int)\n",
" dur = np.where(event == 1, t_fail, h_since)\n",
" df = pd.DataFrame({\"eq_id\": [f\"EQ{seed:02d}{i:04d}\" for i in range(n)], \"unit\": unit, \"env\": env, \"etype\": et, \"rate\": rate, \"hours\": hours, \"km\": km, \"age_y\": age_y, \"n_ovh\": n_ovh,\n",
" \"h_since\": h_since, \"unpl\": unpl, \"n_maint\": n_maint, \"t_fail\": t_fail, \"event\": event, \"dur\": dur})\n",
" df[\"insp\"] = np.clip(dur / t_fail + rng.normal(0, INSP_NOISE, n), 0, 1.3) # ๋ง์ง๋ง ์ ๋น ์ ๊ฒ์์ ์ธก์ ํ ๋ง๋ชจ ๋น์จ(์๋ชจ ์๋ช
/์ด ์๋ช
, ์ค์ฐจ ํฌํจ)\n",
" df[\"rul_h\"] = np.where(event == 0, t_fail - h_since, np.nan) # ์์กด ์ฅ๋น์ ๋จ์ ์ด์ฉ์๊ฐ(์ ๋ต)\n",
" df[\"rul_d\"] = df[\"rul_h\"] / df[\"rate\"] # ๋จ์ ์ผ์(์ ๋ต)\n",
" return df\n",
"\n",
"\n",
"def mape(y, p):\n",
" # [TODO : ํ์ ์ค์ต 9] AAR ๊ธฐ๋ฐ ์๋ช
์์ธก ์ค์ฐจ ๋ถ์ ๋ฐ ํ๊ฒฝ ๊ฐ์ค์น ํ๋ผ๋ฏธํฐ ์ฌ๋ณด์ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def mae(y, p):\n",
" # [TODO : ํ์ ์ค์ต 9] AAR ๊ธฐ๋ฐ ์๋ช
์์ธก ์ค์ฐจ ๋ถ์ ๋ฐ ํ๊ฒฝ ๊ฐ์ค์น ํ๋ผ๋ฏธํฐ ์ฌ๋ณด์ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def rmse(y, p):\n",
" return float(np.sqrt(np.mean((np.asarray(y, float) - np.asarray(p, float)) ** 2)))\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ๋
ธํํ ํ์ ํผ์ฒ์ ํ์ต ํ๋ณธ ๋ง๋ค๊ธฐ. ํ๊ฒฝ ๊ฐ์ค์น W0๋ ๋์
์์ ์ ์ ๋ฌธ๊ฐ ์ถ์ ๊ฐ์ด๋ฉฐ(์ค์ ๊ฐ์ค์น์ ๋ค๋ฆ) M9์์ ์ฌ๋ณด์ ํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"W0 = {\"normal\": 1.00, \"rough\": 1.15, \"salt\": 1.10, \"cold\": 1.05}\n",
"FEATS = [\"h_since\", \"hours\", \"km\", \"age_y\", \"rate\", \"n_maint\", \"unpl\", \"unpl_per_kh\", \"km_per_h\", \"n_ovh\", \"k_rough\", \"k_salt\", \"k_cold\", \"k_truck\", \"k_gen\", \"k_trailer\",\n",
" \"aging_idx\", \"lin_rul_d\", \"hs_frac\", \"insp\", \"insp_eta_h\"]\n",
"\n",
"\n",
"def add_features(df, w=None):\n",
" d = df.copy()\n",
" w = w or W0\n",
" d[\"unpl_per_kh\"] = d[\"unpl\"] / (d[\"h_since\"] / 1000 + 0.5)\n",
" d[\"km_per_h\"] = d[\"km\"] / d[\"hours\"]\n",
" for e in (\"rough\", \"salt\", \"cold\"):\n",
" d[\"k_\" + e] = (d[\"env\"] == e).astype(int)\n",
" d[\"k_truck\"], d[\"k_gen\"], d[\"k_trailer\"] = (d[\"etype\"] == \"truck\").astype(int), (d[\"etype\"] == \"generator\").astype(int), (d[\"etype\"] == \"trailer\").astype(int)\n",
" d[\"aging_idx\"] = d[\"h_since\"] * d[\"env\"].map(w) / CYCLE # ํ๊ฒฝ ๊ฐ์ค ๋
ธํ ์ง์(1์ด๋ฉด ์ฐฝ์ ๋น ์ฃผ๊ธฐ์ ๋๋ฌ)\n",
" d[\"lin_rul_d\"] = (CYCLE - d[\"h_since\"]) / d[\"rate\"] # ์ ํ ๊ฐ๊ฐ์๊ฐ ์์ฌ ์ผ์(Baseline)\n",
" d[\"hs_frac\"] = d[\"h_since\"] / CYCLE\n",
" d[\"insp_eta_h\"] = d[\"h_since\"] / np.maximum(d[\"insp\"], 0.05) # ์ ๊ฒ ๋ง๋ชจ ๋น์จ๋ก ํ์ฐํ ์ด ์๋ช
์ถ์ (h)\n",
" return d\n",
"\n",
"\n",
"def snapshots(df, k=4, seed=0):\n",
" \"\"\"๊ณ ์ฅ ์ด๋ ฅ ์ฅ๋น์ ๊ณผ๊ฑฐ ์์ ํ๋ณธ: ๊ณ ์ฅ ์ ์์ ์์ ์ ์ ๊ฒ๊ฐยท์ด์ฉ์๊ฐ๊ณผ ๊ทธ๋์ ๋จ์ ์ผ์. (์์กด ์ฅ๋น์ ๋ฏธ๋๋ ์ ์ ์์ผ๋ฏ๋ก ๊ณ ์ฅ ์ด๋ ฅ๋ง ์ด๋ค)\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" f = df[df[\"event\"] == 1]\n",
" out = []\n",
" for _ in range(k):\n",
" s = f.copy()\n",
" h = s[\"t_fail\"] * rng.uniform(0.2, 0.98, len(s))\n",
" ratio = h / s[\"h_since\"]\n",
" s[\"hours\"] = s[\"hours\"] - (s[\"h_since\"] - h)\n",
" s[\"km\"] = s[\"km\"] * ratio.clip(upper=1)\n",
" s[\"unpl\"] = np.floor(s[\"unpl\"] * ratio)\n",
" s[\"n_maint\"] = np.floor(s[\"n_maint\"] * ratio)\n",
" s[\"insp\"] = np.clip(h / s[\"t_fail\"] + rng.normal(0, INSP_NOISE, len(s)), 0, 1.3)\n",
" s[\"h_since\"] = h\n",
" s[\"rul_d\"] = (s[\"t_fail\"] - h) / s[\"rate\"]\n",
" s[\"event\"] = 0\n",
" out.append(s)\n",
" return pd.concat(out, ignore_index=True)\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ์์กด ๋ถ์ ๋๊ตฌ(scipy๋ง ์ฌ์ฉ): ์์ด๋ธ AFT(๊ฐ์ ์๋ช
) ๋ชจํ๊ณผ Cox ๋น๋ก์ํ ๋ชจํ\n",
"# ---------------------------------------------------------------------------\n",
"from scipy.optimize import minimize\n",
"\n",
"\n",
"def aft_x(d):\n",
" return np.c_[np.ones(len(d)), d[\"env\"].eq(\"rough\"), d[\"env\"].eq(\"salt\"), d[\"env\"].eq(\"cold\"), d[\"unpl\"] / (d[\"h_since\"] / 1000 + 0.5)].astype(float)\n",
"\n",
"\n",
"AFT_COLS = [\"์ ํธ\", \"ํ์ง\", \"์ผ๋ถ\", \"ํนํ\", \"๋น๊ณํ ์ ๋น์จ\"]\n",
"\n",
"\n",
"def fit_aft(d):\n",
" \"\"\"์ฐฝ์ ๋น ํ ์ด์ฉ์๊ฐ dur(๊ณ ์ฅ์ด๋ฉด event=1, ์์กด์ด๋ฉด ์ค๋์ ๋จ)์ ๋ํ ์์ด๋ธ AFT ์ต๋์ฐ๋ ์ถ์ . ๋ฐํ: ๊ณ์(๋ง์ง๋ง์ log ํ์).\"\"\"\n",
" x, t, e = aft_x(d), d[\"dur\"].values, d[\"event\"].values\n",
"\n",
" def nll(p):\n",
" lam, k = np.exp(x @ p[:-1]), np.exp(p[-1])\n",
" z = (t / lam) ** k\n",
" return -(e * (np.log(k) - np.log(lam) + (k - 1) * (np.log(t) - np.log(lam))) - z).sum()\n",
"\n",
" r = minimize(nll, np.r_[np.log(8000), np.zeros(x.shape[1] - 1), np.log(5)], method=\"BFGS\")\n",
" return r.x\n",
"\n",
"\n",
"def aft_pred(p, d, q=0.5, h=None):\n",
" \"\"\"์ด๋ฏธ h(์ด์ฉ์๊ฐ)๊น์ง ์์กดํ๋ค๋ ์กฐ๊ฑด์์ ๋ถ์์ q ์์กด ์์ (์ด์ฉ์๊ฐ h ๊ธฐ์ค ์ด ์๋ช
)์ ๋๋ ค์ค๋ค. q=0.5๊ฐ ์กฐ๊ฑด๋ถ ์ค์๊ฐ.\"\"\"\n",
" lam, k = np.exp(aft_x(d) @ p[:-1]), np.exp(p[-1])\n",
" h = d[\"h_since\"].values if h is None else h\n",
" return lam * ((h / lam) ** k - np.log(q)) ** (1 / k)\n",
"\n",
"\n",
"def aft_fail_prob(p, d, h0, h1):\n",
" \"\"\"h0๊น์ง ์์กดํ ์ฅ๋น๊ฐ h0~h1 ์ฌ์ด์ ๊ณ ์ฅ ๋ ์กฐ๊ฑด๋ถ ํ๋ฅ .\"\"\"\n",
" lam, k = np.exp(aft_x(d) @ p[:-1]), np.exp(p[-1])\n",
" s0, s1 = np.exp(-(h0 / lam) ** k), np.exp(-(h1 / lam) ** k)\n",
" return 1 - s1 / s0\n",
"\n",
"\n",
"def fit_cox(d, iters=25):\n",
" \"\"\"Cox ๋น๋ก์ํ(Breslow ๊ทผ์ฌ) ๋ดํด-๋ฉ์จ. ๊ณต๋ณ๋: ํ์งยท์ผ๋ถยทํนํยท๋น๊ณํ ์ ๋น์จ. ๋ฐํ: ๊ณ์.\"\"\"\n",
" x = aft_x(d)[:, 1:]\n",
" o = np.argsort(-d[\"dur\"].values)\n",
" x, e = x[o], d[\"event\"].values[o]\n",
" b = np.zeros(x.shape[1])\n",
" for _ in range(iters):\n",
" r = np.exp(x @ b)\n",
" S0 = np.cumsum(r)\n",
" S1 = np.cumsum(r[:, None] * x, axis=0)\n",
" S2 = np.cumsum(r[:, None, None] * x[:, :, None] * x[:, None, :], axis=0)\n",
" m1 = S1 / S0[:, None]\n",
" g = ((x - m1) * e[:, None]).sum(axis=0)\n",
" H = -sum(e[i] * (S2[i] / S0[i] - np.outer(m1[i], m1[i])) for i in np.where(e == 1)[0])\n",
" step = np.linalg.solve(H - 1e-6 * np.eye(len(b)), g)\n",
" b = b - step\n",
" if np.abs(step).max() < 1e-7:\n",
" break\n",
" return b\n",
"\n",
"\n",
"def cindex(time, event, risk):\n",
" \"\"\"์ผ์น๋ C-index: ์ํ ์ ์๊ฐ ๋์ ์ชฝ์ด ๋จผ์ ๊ณ ์ฅ ๋ฌ๋๊ฐ(๊ด์ธก ๊ฐ๋ฅํ ์๋ง).\"\"\"\n",
" t, e, r = np.asarray(time), np.asarray(event), np.asarray(risk)\n",
" num = den = 0.0\n",
" for i in np.where(e == 1)[0]:\n",
" m = t > t[i]\n",
" den += m.sum()\n",
" num += (r[m] < r[i]).sum() + 0.5 * (r[m] == r[i]).sum()\n",
" return num / max(den, 1)\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ์์ฌ ์๋ช
(RUL, ์ผ) ์์ธก ์์ง: ์์ด๋ธ AFT ์กฐ๊ฑด๋ถ ์ค์๊ฐ + ๊ทธ๋๋์ธํธ ๋ถ์คํ
(๊ณ ์ฅ ์ด๋ ฅ ์์ ํ๋ณธ ํ์ต)์ ํ๊ท \n",
"# ---------------------------------------------------------------------------\n",
"from sklearn.ensemble import GradientBoostingRegressor\n",
"\n",
"\n",
"def fit_engine(train, seed=0):\n",
" p = fit_aft(train)\n",
" sn = add_features(snapshots(train, 5, seed))\n",
" sn[\"aft\"] = (aft_pred(p, sn) - sn[\"h_since\"]) / sn[\"rate\"]\n",
" g = GradientBoostingRegressor(n_estimators=250, max_depth=3, learning_rate=0.06, random_state=0).fit(sn[FEATS + [\"aft\"]], sn[\"rul_d\"])\n",
" return {\"p\": p, \"g\": g}\n",
"\n",
"\n",
"def predict_rul(eng, d):\n",
" \"\"\"d๋ add_features๋ฅผ ๊ฑฐ์น ํ. ๋ฐํ: (์์๋ธ RUL, AFT RUL, GBR RUL) - ์ผ ๋จ์.\"\"\"\n",
" aft = ((aft_pred(eng[\"p\"], d) - d[\"h_since\"]) / d[\"rate\"]).values\n",
" x = d[FEATS].copy()\n",
" x[\"aft\"] = aft\n",
" gb = eng[\"g\"].predict(x)\n",
" return np.clip(0.5 * aft + 0.5 * gb, 1, None), aft, gb\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ๊ณํ์ ๋น(์ฐฝ์ ๋น) ์ผ์ ์ถ์ฒ: ์๋ณ ์ ๋น์ฐฝ ๋ฅ๋ ฅยท์์ฐยท์ํจ์ฑ ๋ถํ ํคํธ(์ ํจ๊ธฐํ) ์ ์ฝ ์๋ ๊ธฐ๋ ๋น์ฉ ์ต์ํ (๋น์ฉ ๋จ๊ฐ๋ ๊ต์ก์ฉ ๊ฐ์ ๊ฐ, ๋ง์)\n",
"# ---------------------------------------------------------------------------\n",
"from scipy.optimize import milp, LinearConstraint, Bounds\n",
"from scipy.stats import norm\n",
"\n",
"C_PLAN, C_FAIL, C_WASTE_KH, C_KIT = 600.0, 2400.0, 40.0, 120.0 # ๊ณํ ์ฐฝ์ ๋น, ๋น๊ณํ ๊ณ ์ฅ(๊ธด๊ธ ์ ๋น ํฌํจ), ๋ฏธ์ฌ์ฉ ์๋ช
1000h๋น ์์ค, ์ํจ์ฑ ๋ถํ ํคํธ\n",
"HORIZON, MDAYS = 12, 30 # ๊ณํ 12๊ฐ์, ์ 30์ผ\n",
"CAP_M, BUDGET_M = 22, 15000.0 # ์๋ณ ์ ๋น์ฐฝ ๋ฅ๋ ฅ(๋), ์๋ณ ์์ฐ\n",
"LOTS = [(30, 70), (45, 170), (45, 390), (70, 800)] # ์ํจ์ฑ ํคํธ ๋กํธ(์๋, ์ ํจ๊ธฐํ๊น์ง ๋จ์ ์ผ์)\n",
"KIT_MARGIN = 14 # ์ ๋น์ผ ๊ธฐ์ค ์ต์ ์ ํจ๊ธฐ๊ฐ ์ฌ์ (์ผ)\n",
"\n",
"\n",
"def exp_cost_matrix(pred, rate, month_days):\n",
" \"\"\"์ฅ๋น i๋ฅผ ์ m์ ์ ๋นํ ๋์ ๊ธฐ๋ ๋น์ฉ. ์ฐธ RUL ~ N(pred, sigma), sigma = 35์ผ + 0.12 x pred (๊ฐ์ ). ๋ง์ง๋ง ์ด์ ๊ณํ ๊ธฐ๊ฐ ๋ฐ(๋ฏธ์ ๋น).\"\"\"\n",
" sig = 35 + 0.12 * pred\n",
" C = np.zeros((len(pred), len(month_days) + 1))\n",
" for j, day in enumerate(month_days):\n",
" z = (day - pred) / sig\n",
" pf = norm.cdf(z)\n",
" unused = np.maximum(pred - day, 0) * rate * 0.6 # ์ ๋น๋ฅผ ์๋น๊ธด ๋งํผ ๋ฒ๋ฆฌ๋ ์ด์ฉ์๊ฐ h(๊ทผ์ฌ)\n",
" C[:, j] = C_PLAN + C_KIT + pf * (C_FAIL - C_PLAN) + C_WASTE_KH * unused / 1000 * (1 - pf)\n",
" C[:, -1] = norm.cdf((HORIZON * MDAYS - pred) / sig) * C_FAIL\n",
" return C\n",
"\n",
"\n",
"def plan_milp(pred, rate, cap=CAP_M, budget=BUDGET_M, lots=LOTS, use_kit=True):\n",
" \"\"\"์ ์๊ณํ์ผ๋ก ์ฅ๋น๋ณ ์ ๋น ์(0~11) ๋๋ ๋ฏธ์ ๋น(-1)๋ฅผ ๋ฐฐ์ ํ๋ค. ํ๋ณด๋ ์์ธก RUL์ด ๊ณํ ๊ธฐ๊ฐ +90์ผ ์ด๋ด์ธ ์ฅ๋น.\"\"\"\n",
" md = np.arange(1, HORIZON + 1) * MDAYS\n",
" cand = np.where(pred < HORIZON * MDAYS + 90)[0]\n",
" n, M = len(cand), HORIZON\n",
" C = exp_cost_matrix(pred[cand], rate[cand], md)\n",
" c = C.ravel()\n",
" A, lo, hi = [], [], []\n",
" for i in range(n): # ์ฅ๋น๋น ํ ๋ฒ๋ง ๋ฐฐ์ (์ 12์นธ + ๋ฏธ์ ๋น)\n",
" row = np.zeros(n * (M + 1)); row[i * (M + 1):(i + 1) * (M + 1)] = 1; A.append(row); lo.append(1); hi.append(1)\n",
" for m in range(M): # ์๋ณ ๋ฅ๋ ฅยท์์ฐ\n",
" row = np.zeros(n * (M + 1)); row[m::(M + 1)] = 1\n",
" A.append(row); lo.append(0); hi.append(cap)\n",
" A.append(row * (C_PLAN + C_KIT)); lo.append(0); hi.append(budget)\n",
" if use_kit: # ์ํจ์ฑ ํคํธ: m์ ์ดํ ์ ๋น ์์๋ ๊ทธ ์์ ์ ์ ํจ๊ธฐ๊ฐ์ด ๋จ์ ๋กํธ ์๋์ ๋์ ์ ์๋ค\n",
" for m in range(M):\n",
" sup = sum(q for q, e in lots if e >= md[m] + KIT_MARGIN)\n",
" row = np.zeros(n * (M + 1))\n",
" for mm in range(m, M):\n",
" row[mm::(M + 1)] = 1\n",
" A.append(row); lo.append(0); hi.append(sup)\n",
" r = milp(c, constraints=LinearConstraint(np.array(A), lo, hi), integrality=np.ones(len(c)), bounds=Bounds(0, 1), options={\"time_limit\": 20})\n",
" month = np.full(len(pred), -1)\n",
" if r.x is not None:\n",
" x = np.round(r.x).reshape(n, M + 1)\n",
" assign = np.where(x[:, :M].sum(axis=1) > 0.5, x[:, :M].argmax(axis=1), -1)\n",
" month[cand] = assign\n",
" return month\n",
"\n",
"\n",
"def plan_greedy(pred, margin=45, cap=CAP_M, budget=BUDGET_M):\n",
" \"\"\"๊ฐ๋จํ ํ์ ๋ฐฐ์ : ์ ๋น ๋ชฉํ์ผ(์์ธก RUL - margin)์ด ๋น ๋ฅธ ์์ผ๋ก ๋ชฉํ์ผ ์ด์ ๊ฐ์ฅ ๋ฆ์ ๋น ์, ์์ผ๋ฉด ๊ฐ์ฅ ์ด๋ฅธ ๋น ์์ ๋ฐฐ์ .\"\"\"\n",
" cap = min(cap, int(budget // (C_PLAN + C_KIT))) # ์ ์์ฐ์ด ํ์ฉํ๋ ๋์๋ก ๋ฅ๋ ฅ์ ์ ํ\n",
" month = np.full(len(pred), -1)\n",
" left = np.full(HORIZON, cap)\n",
" for i in np.argsort(pred):\n",
" if pred[i] > HORIZON * MDAYS + 60: break\n",
" tgt = int(np.clip((pred[i] - margin) // MDAYS - 1, 0, HORIZON - 1))\n",
" for m in list(range(tgt, -1, -1)) + list(range(tgt + 1, HORIZON)):\n",
" if left[m] > 0:\n",
" month[i] = m; left[m] -= 1; break\n",
" return month\n",
"\n",
"\n",
"def plan_fixed(lin_rul, cap=CAP_M, budget=BUDGET_M):\n",
" \"\"\"๊ดํ: ๊ท์ ์ฃผ๊ธฐ ๋๋ฌ ์์๋๋ก(์ ํ ์์ฌ ์ผ์) ์๋ณ ๋ฅ๋ ฅ ์์์ ๋ฐฐ์ .\"\"\"\n",
" return plan_greedy(np.asarray(lin_rul, float), margin=0, cap=cap, budget=budget)\n",
"\n",
"\n",
"def evaluate_plan(month, true_rul, rate, cost_kit=True):\n",
" \"\"\"์ฐธ RUL๋ก ๊ณํ์ ์ค์ ๋น์ฉ์ ๊ณ์ฐํ๋ค: ์ ๋น์ผ ์ด์ ์ ๊ณ ์ฅ -> ๋น๊ณํ ๊ณ ์ฅ ๋น์ฉ, ์๋๋ฉด ๊ณํ ๋น์ฉ + ๋ฏธ์ฌ์ฉ ์๋ช
์์ค.\"\"\"\n",
" day = np.where(month >= 0, (month + 1) * MDAYS, 10 ** 9)\n",
" fail = (true_rul < day) & (true_rul < HORIZON * MDAYS)\n",
" planned = (month >= 0) & ~fail\n",
" waste = np.where(planned, (true_rul - day) * rate * 0.6, 0) * C_WASTE_KH / 1000\n",
" cost = fail * C_FAIL + planned * (C_PLAN + (C_KIT if cost_kit else 0)) + waste\n",
" return {\"total\": float(cost.sum()), \"fail\": int(fail.sum()), \"planned\": int(planned.sum()), \"waste\": float(waste.sum()), \"unsched_fail\": int(((month < 0) & fail).sum())}\n",
"\n",
"\n",
"def kit_fefo(month, lots=LOTS):\n",
" \"\"\"์ ํจ๊ธฐํ์ด ๋น ๋ฅธ ๋กํธ๋ถํฐ ์ฐ๋ FEFO๋ก ํคํธ๋ฅผ ๋ฐฐ์ ํ๊ณ ์ ๋น์ผ์ ์ ํจ๊ธฐ๊ฐ์ด ๋ถ์กฑํ ๋ฐฐ์ ์์ ๋ง๋ฃ ํ๊ธฐ ์๋์ ์ผ๋ค.\"\"\"\n",
" q = [[qty, e] for qty, e in sorted(lots, key=lambda t: t[1])]\n",
" short = 0\n",
" for m in sorted(month[month >= 0]):\n",
" day = (m + 1) * MDAYS\n",
" for lot in q:\n",
" if lot[1] >= day + KIT_MARGIN and lot[0] > 0:\n",
" lot[0] -= 1; break\n",
" else:\n",
" short += 1\n",
" expired = sum(l[0] for l in q if l[1] < HORIZON * MDAYS)\n",
" return {\"short\": short, \"expired_unused\": expired, \"left\": sum(l[0] for l in q)}\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"OPS_W = {\"normal\": 1.00, \"rough\": 1.55, \"salt\": 1.15, \"cold\": 1.30} # ์ด์ ๊ธฐ๊ฐ์ ์จ์ ํ๊ฒฝ ๊ฐ์ค์น(๋์
๋น์์ ๋ฌ๋ผ์ง ๊ฐ์ )\n",
"tr = make_fleet(900, seed=1)\n",
"eng0 = fit_engine(tr)\n",
"A = make_fleet(700, seed=60, env_w=OPS_W, shift=1.2) # AAR ๋์ ์ด์ ์ฅ๋น\n",
"B = make_fleet(700, seed=70, env_w=OPS_W, shift=1.2) # ์ฌ๋ณด์ ์ ์ฐ์ง ์๋ ๋ณด๋ฅ ์ฅ๋น\n",
"\n",
"# ---------------- 1) AAR: ์ด์ ์์ธก ์ค์ฐจ ๋ถ์ ----------------\n",
"Af = add_features(A)\n",
"al = Af[Af.event == 0].reset_index(drop=True)\n",
"pred, _, _ = predict_rul(eng0, al)\n",
"y = al[\"rul_d\"].values\n",
"al[\"err\"] = pred - y # ์์ = ์๋ช
์ ๊ธธ๊ฒ ์์ธก(์ํ)\n",
"big = y >= 90\n",
"print(f\"[1] AAR ๋์ ์ด์ ์ฅ๋น {len(A)}๋(์์กด {len(al)}๋, ๊ณ ์ฅ {int(A.event.sum())}๋): ํํ ์์ง MAE {mae(y, pred):.0f}์ผ, RMSE {rmse(y, pred):.0f}์ผ, MAPE {mape(y[big], pred[big]):.1f}%, R2 {r2_score(y, pred):.3f}, ํ๊ท ํธํฅ {al.err.mean():+.0f}์ผ\")\n",
"ct = pd.DataFrame([(k, int(m.sum()), mae(y[m.values], pred[m.values]), al.err[m].mean()) for k, m in {\"์ผ๋ฐ\": al.env == \"normal\", \"ํ์ง\": al.env == \"rough\", \"์ผ๋ถ\": al.env == \"salt\", \"ํนํ\": al.env == \"cold\",\n",
" \"์ผ ๊ฐ๋๋ฅ 4h ์ด์\": al.rate >= 4, \"๋น๊ณํ ์ ๋น์จ ์์ 25%\": al.unpl_per_kh >= al.unpl_per_kh.quantile(.75)}.items()], columns=[\"์กฐ๊ฑด\", \"๋์\", \"MAE(์ผ)\", \"ํ๊ท ํธํฅ(์ผ)\"])\n",
"print(ct.round(0).to_string(index=False))\n",
"worst = al.reindex(al.err.abs().sort_values(ascending=False).index).head(40)\n",
"\n",
"\n",
"def cause(r):\n",
" if r.env == \"rough\" and r.err > 0: return \"ํ์ง ๊ฐํน๋ ๊ณผ์ ๋ฐ์\"\n",
" if r.env == \"cold\" and r.err > 0: return \"ํนํ ๊ฐํน๋ ๊ณผ์ ๋ฐ์\"\n",
" if r.env == \"salt\" and r.err < 0: return \"์ผ๋ถ ๊ฐํน๋ ๊ณผ๋ ๋ฐ์\"\n",
" if r.rate >= 4: return \"๊ฐ๋๋ฅ ์์น(์ด์ฉ ๊ฐ๋ ๋ณํ)\"\n",
" if r.unpl_per_kh >= 1.2: return \"๋น๊ณํ ์ ๋น ๋ค์\"\n",
" return \"๊ธฐํ(๊ฐ์ฒด ํธ์ฐจ)\"\n",
"\n",
"\n",
"cs = worst.apply(cause, axis=1).value_counts()\n",
"print(\" ์ค์ฐจ ํฐ ์์ 40๋์ ์์ธ ๋ถ๋ฅ:\", \", \".join(f\"{k} {v}๋({v / 40:.0%})\" for k, v in cs.items()))\n",
"\n",
"# ---------------- 2) ์ฌ๋ณด์ : ํ๊ฒฝ ๊ฐ์ค์น -> ์ด์ ๋ฐ์ดํฐ ์ฌํ์ต ----------------\n",
"pa = fit_aft(A)\n",
"life_ratio = np.exp(pa[1:4])\n",
"W1 = {\"normal\": 1.0, \"rough\": 1 / life_ratio[0], \"salt\": 1 / life_ratio[1], \"cold\": 1 / life_ratio[2]}\n",
"wt = pd.DataFrame({\"๋์
์ ์ถ์ (W0)\": W0, \"์ฌ๋ณด์ (์ด์ AFT)\": W1, \"์จ์ ์ ๋ต(์ด์)\": OPS_W}).loc[ENVS]\n",
"wt[\"๋ณํ์จ\"] = wt[\"์ฌ๋ณด์ (์ด์ AFT)\"] / wt[\"๋์
์ ์ถ์ (W0)\"] - 1\n",
"print(\"\\n[2] ํ๊ฒฝ ๊ฐ์ค์น ๋งคํธ๋ฆญ์ค(ํด์๋ก ๋นจ๋ฆฌ ๋
ธํ)\")\n",
"print(wt.round(3).to_string())\n",
"flag = wt[wt[\"๋ณํ์จ\"].abs() > 0.2].index.tolist()\n",
"print(f\" ๋ณํ์จ 20% ์ด๊ณผ ํ๊ฒฝ {flag or '์์'}๋ ์ ๋น๊ณํ๊ด ๊ฒํ ยท๊ตฐ์์งํ๊ด ์น์ธ ๋ค ์ ์ฉํ๋ค(์๋ ๋ฐ์ ๊ธ์ง).\")\n",
"eng1 = fit_engine(A, seed=1)\n",
"Bf0, Bf1 = add_features(B, W0), add_features(B, W1)\n",
"bl0, bl1 = Bf0[Bf0.event == 0].reset_index(drop=True), Bf1[Bf1.event == 0].reset_index(drop=True)\n",
"p0, _, _ = predict_rul(eng0, bl0)\n",
"p1, _, _ = predict_rul(eng1, bl1)\n",
"yb = bl0[\"rul_d\"].values\n",
"bb = yb >= 90\n",
"tab = pd.DataFrame({\"์ฌ๋ณด์ ์ \": [mae(yb, p0), rmse(yb, p0), mape(yb[bb], p0[bb]), r2_score(yb, p0), (p0 - yb).mean()], \"์ฌ๋ณด์ ํ\": [mae(yb, p1), rmse(yb, p1), mape(yb[bb], p1[bb]), r2_score(yb, p1), (p1 - yb).mean()]},\n",
" index=[\"MAE(์ผ)\", \"RMSE(์ผ)\", \"MAPE(%)\", \"R2\", \"ํ๊ท ํธํฅ(์ผ)\"])\n",
"print(f\"\\n[3] ๋ณด๋ฅ ์ฅ๋น {len(bl0)}๋(์ฌ๋ณด์ ์ ์ฐ์ง ์์) ๊ฒ์ฆ\\n\" + tab.round(3).to_string())\n",
"print(f\" MAE {1 - tab.loc['MAE(์ผ)', '์ฌ๋ณด์ ํ'] / tab.loc['MAE(์ผ)', '์ฌ๋ณด์ ์ ']:.1%} ๊ฐ์, ํ๊ฒฝ๋ณ ํธํฅ(์ -> ํ):\", \", \".join(f\"{e} {(p0 - yb)[(bl0.env == e).values].mean():+.0f} -> {(p1 - yb)[(bl0.env == e).values].mean():+.0f}\" for e in ENVS))\n",
"\n",
"# ---------------- 3) ROI 3๋ ์งํ(๊ต์ก์ฉ ๊ฐ์ ) ----------------\n",
"rate_b, true_b = bl0[\"rate\"].values, yb\n",
"m0, m1 = plan_milp(p0, rate_b), plan_milp(p1, rate_b)\n",
"r0, r1 = evaluate_plan(m0, true_b, rate_b), evaluate_plan(m1, true_b, rate_b)\n",
"D_FAIL, D_PLAN = 45, 12 # ๋น๊ณํ ๊ณ ์ฅ/๊ณํ ์ฐฝ์ ๋น ๊ฐ๋ ์ค๋จ ์ผ์(๊ฐ์ )\n",
"down0, down1 = r0[\"fail\"] * D_FAIL + r0[\"planned\"] * D_PLAN, r1[\"fail\"] * D_FAIL + r1[\"planned\"] * D_PLAN\n",
"SCALE, INV, OPEX = 1.0, 12000.0, 1200.0 # ๊ด๋ฆฌ ์ฅ๋น ๊ท๋ชจ ๋ฐฐ์(์ฐ), ๋์
๋น์ฉ, ์ฐ ์ด์๋น(๋ง์)\n",
"\n",
"\n",
"def roi(c0, c1, d0, d1, scale=SCALE, inv=INV):\n",
" save = (c0 - c1) * scale - OPEX\n",
" return save, 1 - d1 / d0, inv / max(save / 12, 1e-9)\n",
"\n",
"\n",
"sv, tr_, pb = roi(r0[\"total\"], r1[\"total\"], down0, down1)\n",
"print(f\"\\n[4] 12๊ฐ์ ๊ณํ ๋น๊ต(๋ณด๋ฅ ์ฅ๋น): ๋น์ฉ {r0['total']:,.0f} -> {r1['total']:,.0f}๋ง์, ๋น๊ณํ ๊ณ ์ฅ {r0['fail']} -> {r1['fail']}๋, ๊ณํ ์ ๋น {r0['planned']} -> {r1['planned']}๋\")\n",
"print(f\" ROI 3๋ ์งํ [๊ต์ก์ฉ ๊ฐ์ : ๊ด๋ฆฌ ๊ท๋ชจ x{SCALE:.0f}, ๋์
{INV:.0f}๋ง์, ์ด์ {OPEX:.0f}๋ง์/๋
, ๊ณ ์ฅ ์ค๋จ {D_FAIL}์ผยท์ฐฝ์ ๋น {D_PLAN}์ผ]\")\n",
"print(f\" (1) ์ฐ๊ฐ ๋น์ฉ ์ ๊ฐ์ก {sv:,.0f}๋ง์ (2) ๊ฐ๋ ์ค๋จ์ผ์ ๋จ์ถ๋ฅ {tr_:.1%}({down0:,}์ผ -> {down1:,}์ผ) (3) ํฌ์ ํ์ ๊ธฐ๊ฐ {pb:.1f}๊ฐ์\")\n",
"print(\" ์ฐ์: ์ ๊ฐ์ก = (์ฌ๋ณด์ ์ - ํ 12๊ฐ์ ๋น์ฉ) x ๊ท๋ชจ - ์ด์๋น, ๋จ์ถ๋ฅ = 1 - ํ ์ค๋จ์ผ์/์ ์ค๋จ์ผ์, ํ์ ๊ธฐ๊ฐ = ๋์
๋น / (์ ์ ๊ฐ์ก)\")\n",
"print(\" ๊ด๋ฆฌ ๊ท๋ชจ ๋ฏผ๊ฐ๋:\", \"; \".join(f\"x{s} -> ์ ๊ฐ {roi(r0['total'], r1['total'], down0, down1, scale=s)[0]:,.0f}๋ง์, ํ์ {roi(r0['total'], r1['total'], down0, down1, scale=s)[2]:.0f}๊ฐ์\" for s in (0.5, 1, 2, 3)))\n",
"half = r0[\"total\"] - (r0[\"total\"] - r1[\"total\"]) / 2\n",
"print(f\" ๋น๊ด(์ฌ๋ณด์ ํจ๊ณผ ์ ๋ฐ๋ง ์คํ, x1): ์ ๊ฐ {roi(r0['total'], half, down0, down1, scale=1)[0]:,.0f}๋ง์, ํ์ {roi(r0['total'], half, down0, down1, scale=1)[2]:.0f}๊ฐ์\")\n",
"print(\" ๊ฐ์ ๊ฐ์ด๋ฏ๋ก ์ฌ์ ยท๊ธฐํ ๋ถ์์ ์ค์ ๋จ๊ฐ๋ก ๋ค์ ์ฐ์ ํด์ผ ํ๋ค. ์ฌ๋ณด์ ๊ฐ์ค์น๋ ๊ตฐ์์งํ๊ด ์น์ธ ํ ์ด์์ ๋ฐ์ํ๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 3, figsize=(15, 4.3))\n",
"ax[0].bar(range(len(ct)), ct[\"ํ๊ท ํธํฅ(์ผ)\"]); ax[0].set_xticks(range(len(ct))); ax[0].set_xticklabels([f\"C{i + 1}\" for i in range(len(ct))]); ax[0].set_title(\"AAR: mean bias by condition (days)\")\n",
"x = np.arange(4); ax[1].bar(x - .27, wt[\"๋์
์ ์ถ์ (W0)\"], .27, label=\"initial W0\"); ax[1].bar(x, wt[\"์ฌ๋ณด์ (์ด์ AFT)\"], .27, label=\"recalibrated\"); ax[1].bar(x + .27, wt[\"์จ์ ์ ๋ต(์ด์)\"], .27, label=\"hidden truth\")\n",
"ax[1].set_xticks(x); ax[1].set_xticklabels(ENVS); ax[1].legend(); ax[1].set_title(\"Environment weight matrix\")\n",
"ax[2].scatter(yb, p0, s=6, alpha=.5, label=\"before\"); ax[2].scatter(yb, p1, s=6, alpha=.5, label=\"after\"); ax[2].plot([0, 4000], [0, 4000], \"k--\"); ax[2].legend(); ax[2].set_title(\"Hold-out RUL: true vs predicted (days)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
09 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[1] AAR ๋์ ์ด์ ์ฅ๋น 700๋(์์กด 448๋, ๊ณ ์ฅ 252๋): ํํ ์์ง MAE 180์ผ, RMSE 241์ผ, MAPE 21.1%, R2 0.909, ํ๊ท ํธํฅ -55์ผ\n",
" ์กฐ๊ฑด ๋์ MAE(์ผ) ํ๊ท ํธํฅ(์ผ)\n",
" ์ผ๋ฐ 202 196.0 -106.0\n",
" ํ์ง 75 132.0 100.0\n",
" ์ผ๋ถ 106 205.0 -169.0\n",
" ํนํ 65 144.0 111.0\n",
" ์ผ ๊ฐ๋๋ฅ 4h ์ด์ 157 127.0 -9.0\n",
"๋น๊ณํ ์ ๋น์จ ์์ 25% 112 195.0 -63.0\n",
" ์ค์ฐจ ํฐ ์์ 40๋์ ์์ธ ๋ถ๋ฅ: ๊ธฐํ(๊ฐ์ฒด ํธ์ฐจ) 22๋(55%), ์ผ๋ถ ๊ฐํน๋ ๊ณผ๋ ๋ฐ์ 14๋(35%), ํนํ ๊ฐํน๋ ๊ณผ์ ๋ฐ์ 2๋(5%), ๊ฐ๋๋ฅ ์์น(์ด์ฉ ๊ฐ๋ ๋ณํ) 1๋(2%), ๋น๊ณํ ์ ๋น ๋ค์ 1๋(2%)\n",
"\n",
"[2] ํ๊ฒฝ ๊ฐ์ค์น ๋งคํธ๋ฆญ์ค(ํด์๋ก ๋นจ๋ฆฌ ๋
ธํ)\n",
" ๋์
์ ์ถ์ (W0) ์ฌ๋ณด์ (์ด์ AFT) ์จ์ ์ ๋ต(์ด์) ๋ณํ์จ\n",
"normal 1.00 1.000 1.00 0.000\n",
"rough 1.15 1.580 1.55 0.374\n",
"salt 1.10 1.164 1.15 0.058\n",
"cold 1.05 1.283 1.30 0.222\n",
" ๋ณํ์จ 20% ์ด๊ณผ ํ๊ฒฝ ['rough', 'cold']๋ ์ ๋น๊ณํ๊ด ๊ฒํ ยท๊ตฐ์์งํ๊ด ์น์ธ ๋ค ์ ์ฉํ๋ค(์๋ ๋ฐ์ ๊ธ์ง).\n",
"\n",
"[3] ๋ณด๋ฅ ์ฅ๋น 461๋(์ฌ๋ณด์ ์ ์ฐ์ง ์์) ๊ฒ์ฆ\n",
" ์ฌ๋ณด์ ์ ์ฌ๋ณด์ ํ\n",
"MAE(์ผ) 184.536 163.118\n",
"RMSE(์ผ) 258.982 234.928\n",
"MAPE(%) 24.450 19.425\n",
"R2 0.887 0.907\n",
"ํ๊ท ํธํฅ(์ผ) -34.864 -64.634\n",
" MAE 11.6% ๊ฐ์, ํ๊ฒฝ๋ณ ํธํฅ(์ -> ํ): normal -98 -> -83, rough +101 -> -73, salt -135 -> -55, cold +141 -> -18\n",
"\n",
"[4] 12๊ฐ์ ๊ณํ ๋น๊ต(๋ณด๋ฅ ์ฅ๋น): ๋น์ฉ 118,444 -> 101,921๋ง์, ๋น๊ณํ ๊ณ ์ฅ 27 -> 18๋, ๊ณํ ์ ๋น 73 -> 80๋\n",
" ROI 3๋ ์งํ [๊ต์ก์ฉ ๊ฐ์ : ๊ด๋ฆฌ ๊ท๋ชจ x1, ๋์
12000๋ง์, ์ด์ 1200๋ง์/๋
, ๊ณ ์ฅ ์ค๋จ 45์ผยท์ฐฝ์ ๋น 12์ผ]\n",
" (1) ์ฐ๊ฐ ๋น์ฉ ์ ๊ฐ์ก 15,323๋ง์ (2) ๊ฐ๋ ์ค๋จ์ผ์ ๋จ์ถ๋ฅ 15.4%(2,091์ผ -> 1,770์ผ) (3) ํฌ์ ํ์ ๊ธฐ๊ฐ 9.4๊ฐ์\n",
" ์ฐ์: ์ ๊ฐ์ก = (์ฌ๋ณด์ ์ - ํ 12๊ฐ์ ๋น์ฉ) x ๊ท๋ชจ - ์ด์๋น, ๋จ์ถ๋ฅ = 1 - ํ ์ค๋จ์ผ์/์ ์ค๋จ์ผ์, ํ์ ๊ธฐ๊ฐ = ๋์
๋น / (์ ์ ๊ฐ์ก)\n",
" ๊ด๋ฆฌ ๊ท๋ชจ ๋ฏผ๊ฐ๋: x0.5 -> ์ ๊ฐ 7,061๋ง์, ํ์ 20๊ฐ์; x1 -> ์ ๊ฐ 15,323๋ง์, ํ์ 9๊ฐ์; x2 -> ์ ๊ฐ 31,846๋ง์, ํ์ 5๊ฐ์; x3 -> ์ ๊ฐ 48,368๋ง์, ํ์ 3๊ฐ์\n",
" ๋น๊ด(์ฌ๋ณด์ ํจ๊ณผ ์ ๋ฐ๋ง ์คํ, x1): ์ ๊ฐ 7,061๋ง์, ํ์ 20๊ฐ์\n",
" ๊ฐ์ ๊ฐ์ด๋ฏ๋ก ์ฌ์ ยท๊ธฐํ ๋ถ์์ ์ค์ ๋จ๊ฐ๋ก ๋ค์ ์ฐ์ ํด์ผ ํ๋ค. ์ฌ๋ณด์ ๊ฐ์ค์น๋ ๊ตฐ์์งํ๊ด ์น์ธ ํ ์ด์์ ๋ฐ์ํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
10. ์ฅ๋น ๋
ธํ๋ ๊ธฐ๋ฐ ๊ธฐ๋์๋ช
์์ธก ๋ฐ ๊ณํ์ ๋น ์ถ์ฒ ์์คํ
ํตํฉ ์์ฑ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ฅ๋น ๋
ธํ๋ ๊ธฐ๋ฐ ๊ธฐ๋์๋ช
์์ธก ๋ฐ ๊ณํ์ ๋น ์ผ์ ์ถ์ฒ ๋๊ตฌ๋ฅผ ์์ฑํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๊ธฐ๋์๋ช
์์ธก๊ณผ ๊ณํ์ ๋น ์ถ์ฒ ์์คํ
์ ํตํฉ ์คํํ๊ณ ์ด์ ๋ฌธ์์ ์๋ ์ฌํ์ต ํ์ดํ๋ผ์ธ์ ์์ฑํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๋ณด์ ํํฉ ๊ฐฑ์ , ์ ํฉ์ฑ ๊ฒ์ฆ, ๋ถํฌ ๋ณํ(PSI) ์ ๊ฒ, ์ฑํผ์ธ/์ฑ๋ฆฐ์ ์ฌํ์ต, ์๋ช
์์ธก, 12๊ฐ์ ์ ๋น ์ผ์ ์ถ์ฒ์ ํ ๋ฒ์ ์คํ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `docs/sup02_operations_guide.md ๋ฐ pipeline/sup02_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": [
"\"\"\"์ฅ๋น ๋ณด์ ํํฉ ๊ฐฑ์ , ์ ํฉ์ฑ ๊ฒ์ฆ, ๋ถํฌ ๋ณํ(PSI) ์ ๊ฒ, ์๋ ์ฌํ์ต(์ฑํผ์ธ/์ฑ๋ฆฐ์ ), ์์ฌ ์๋ช
(RUL) ์์ธก, ์ ๋น์ฐฝ ๋ฅ๋ ฅยท์์ฐยท์ํจ์ฑ ํคํธ ์ ์ฝ์ 12๊ฐ์ ๊ณํ์ ๋น ์ผ์ ์ถ์ฒ๊ณผ ์น์ธ๋๊ธฐยท๋ํ ํ๋ณด ๋ถ๋ฆฌ๊น์ง ํ ๋ฒ์ ์คํํ๋ ํตํฉ ํ์ดํ๋ผ์ธ์ ์์ฑํ๊ณ ์ด์ ๋ฌธ์ ์๋น์จยท์์ฒด ์ ๊ฒยท์ธํฐํ์ด์ค ๋ช
์ธยท๋ธ๋ฆฌํ ์์ฝ์ ์ถ๋ ฅํ๋ค.\"\"\"\n",
"import importlib\n",
"import json\n",
"import sys\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.metrics import r2_score\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์ฅ๋น ๋ณด์ ํํฉยท๊ณ ์ฅ ์ด๋ ฅ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์ฅ๋นยท์์น ์์)\n",
"# ๊ฐ์ ๋ถ๋ 6๊ฐ(U1~U6)์ ์ฅ๋น๊ฐ ์ด์ฉ ํ๊ฒฝ(normal/rough/salt/cold)์ ๋ฐ๋ผ ๋ค๋ฅธ ์๋๋ก ๋
ธํํํ๋ค. ์ฐฝ์ ๋น ํ ์ด์ฉ์๊ฐ(h_since)์ด\n",
"# ์ฅ๋น๋ง๋ค ์จ์ ์๋ช
(t_fail)์ ๋์ผ๋ฉด ๊ณ ์ฅ(event=1) ์ด๋ ฅ, ๋์ง ์์์ผ๋ฉด ํ์ฌ๊น์ง ์์กด(event=0, ๊ด์ธก ์ค๋์ ๋จ)์ด๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"ENVS = [\"normal\", \"rough\", \"salt\", \"cold\"]\n",
"ENV_W = {\"normal\": 1.00, \"rough\": 1.35, \"salt\": 1.25, \"cold\": 1.12} # ํ๊ฒฝ ๊ฐ์ค์น(์จ์ ์ ๋ต, ๊ฐ์ ๊ฐ): ํด์๋ก ์๋ช
์ด ์งง๋ค\n",
"ENV_KO = {\"normal\": \"์ผ๋ฐ\", \"rough\": \"ํ์ง\", \"salt\": \"์ผ๋ถ\", \"cold\": \"ํนํ\"}\n",
"UNITS = [\"U1\", \"U2\", \"U3\", \"U4\", \"U5\", \"U6\"]\n",
"UNIT_ENV = {\"U1\": [.7, .1, .1, .1], \"U2\": [.2, .6, .1, .1], \"U3\": [.2, .1, .6, .1], \"U4\": [.3, .1, .1, .5], \"U5\": [.5, .3, .1, .1], \"U6\": [.4, .1, .4, .1]}\n",
"ETYPES = [\"truck\", \"generator\", \"trailer\", \"radio\"]\n",
"CYCLE = 8000.0 # ๊ท์ ์ฐฝ์ ๋น ์ฃผ๊ธฐ(์ด์ฉ์๊ฐ h, ๊ฐ์ ๊ฐ)\n",
"KSHAPE = 12.0 # ์๋ช
๋ถํฌ ์์ด๋ธ ํ์(๋ง๋ชจํ ๊ณ ์ฅ, ํด์๋ก ์๋ช
์ด ๋ชจ์ฌ ์๋ค)\n",
"INSP_NOISE = 0.025 # ์ ๋น ์ ๊ฒ ๋ง๋ชจ ๋น์จ ์ธก์ ์ค์ฐจ(๊ฐ์ ๊ฐ)\n",
"\n",
"\n",
"def make_fleet(n=900, seed=0, env_w=None, shift=1.0):\n",
" \"\"\"์ฅ๋น n๋. env_w๋ก ์จ์ ํ๊ฒฝ ๊ฐ์ค์น๋ฅผ ๋ฐ๊ฟ ์ด์ ๋ถํฌ ๋ณํ๋ฅผ ๋ชจ์ํ ์ ์๋ค. shift>1์ด๋ฉด ๊ฐ๋๋ฅ ์ด ๋์์ง๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" w = env_w or ENV_W\n",
" unit = rng.choice(UNITS, n)\n",
" env = np.array([rng.choice(ENVS, p=UNIT_ENV[u]) for u in unit])\n",
" et = rng.choice(ETYPES, n, p=[.4, .2, .2, .2])\n",
" rate = rng.lognormal(np.log(3.0 * shift), 0.25, n) # ์ผ ํ๊ท ์ด์ฉ์๊ฐ(h/์ผ)\n",
" h_since = rng.uniform(300, 11500, n) # ๋ง์ง๋ง ์ฐฝ์ ๋น ์ดํ ์ด์ฉ์๊ฐ\n",
" n_ovh = rng.integers(0, 3, n)\n",
" hours = h_since + n_ovh * rng.uniform(5000, 8000, n) # ๋์ ์ด์ฉ์๊ฐ\n",
" km = hours * rng.normal(17, 3, n) * np.where(env == \"rough\", 0.8, 1.0) # ๋์ ๊ธฐ๋๊ฑฐ๋ฆฌ(km)\n",
" age_y = np.clip(hours / (rate * 365) * rng.uniform(0.9, 1.15, n), 0.5, 25)\n",
" unpl = rng.poisson(0.35 * h_since / 1000 * np.array([w[e] for e in env]) / 1.3) # ๋น๊ณํ ์ ๋น ๊ฑด์(ํ๊ฒฝ์ด ๊ฐํนํ ์๋ก ๋ง๋ค)\n",
" n_maint = rng.poisson(0.6 * h_since / 1000 + 1) # ๊ณํ ์ ๋น ๊ฑด์\n",
" scale = 9600 / np.array([w[e] for e in env]) * 0.985 ** unpl * np.exp(rng.normal(0, 0.04, n))\n",
" t_fail = scale * rng.weibull(KSHAPE, n) # ์ฐฝ์ ๋น ํ ๊ณ ์ฅ๊น์ง ์ด์ฉ์๊ฐ(์จ์ ์ ๋ต)\n",
" event = (t_fail <= h_since).astype(int)\n",
" dur = np.where(event == 1, t_fail, h_since)\n",
" df = pd.DataFrame({\"eq_id\": [f\"EQ{seed:02d}{i:04d}\" for i in range(n)], \"unit\": unit, \"env\": env, \"etype\": et, \"rate\": rate, \"hours\": hours, \"km\": km, \"age_y\": age_y, \"n_ovh\": n_ovh,\n",
" \"h_since\": h_since, \"unpl\": unpl, \"n_maint\": n_maint, \"t_fail\": t_fail, \"event\": event, \"dur\": dur})\n",
" df[\"insp\"] = np.clip(dur / t_fail + rng.normal(0, INSP_NOISE, n), 0, 1.3) # ๋ง์ง๋ง ์ ๋น ์ ๊ฒ์์ ์ธก์ ํ ๋ง๋ชจ ๋น์จ(์๋ชจ ์๋ช
/์ด ์๋ช
, ์ค์ฐจ ํฌํจ)\n",
" df[\"rul_h\"] = np.where(event == 0, t_fail - h_since, np.nan) # ์์กด ์ฅ๋น์ ๋จ์ ์ด์ฉ์๊ฐ(์ ๋ต)\n",
" df[\"rul_d\"] = df[\"rul_h\"] / df[\"rate\"] # ๋จ์ ์ผ์(์ ๋ต)\n",
" return df\n",
"\n",
"\n",
"def mape(y, p):\n",
" # [TODO : ํ์ ์ค์ต 10] ์ฅ๋น ๋
ธํ๋ ๊ธฐ๋ฐ ๊ธฐ๋์๋ช
์์ธก ๋ฐ ๊ณํ์ ๋น ์ถ์ฒ ์์คํ
ํตํฉ ์์ฑ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def mae(y, p):\n",
" # [TODO : ํ์ ์ค์ต 10] ์ฅ๋น ๋
ธํ๋ ๊ธฐ๋ฐ ๊ธฐ๋์๋ช
์์ธก ๋ฐ ๊ณํ์ ๋น ์ถ์ฒ ์์คํ
ํตํฉ ์์ฑ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def rmse(y, p):\n",
" return float(np.sqrt(np.mean((np.asarray(y, float) - np.asarray(p, float)) ** 2)))\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ๋
ธํํ ํ์ ํผ์ฒ์ ํ์ต ํ๋ณธ ๋ง๋ค๊ธฐ. ํ๊ฒฝ ๊ฐ์ค์น W0๋ ๋์
์์ ์ ์ ๋ฌธ๊ฐ ์ถ์ ๊ฐ์ด๋ฉฐ(์ค์ ๊ฐ์ค์น์ ๋ค๋ฆ) M9์์ ์ฌ๋ณด์ ํ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"W0 = {\"normal\": 1.00, \"rough\": 1.15, \"salt\": 1.10, \"cold\": 1.05}\n",
"FEATS = [\"h_since\", \"hours\", \"km\", \"age_y\", \"rate\", \"n_maint\", \"unpl\", \"unpl_per_kh\", \"km_per_h\", \"n_ovh\", \"k_rough\", \"k_salt\", \"k_cold\", \"k_truck\", \"k_gen\", \"k_trailer\",\n",
" \"aging_idx\", \"lin_rul_d\", \"hs_frac\", \"insp\", \"insp_eta_h\"]\n",
"\n",
"\n",
"def add_features(df, w=None):\n",
" d = df.copy()\n",
" w = w or W0\n",
" d[\"unpl_per_kh\"] = d[\"unpl\"] / (d[\"h_since\"] / 1000 + 0.5)\n",
" d[\"km_per_h\"] = d[\"km\"] / d[\"hours\"]\n",
" for e in (\"rough\", \"salt\", \"cold\"):\n",
" d[\"k_\" + e] = (d[\"env\"] == e).astype(int)\n",
" d[\"k_truck\"], d[\"k_gen\"], d[\"k_trailer\"] = (d[\"etype\"] == \"truck\").astype(int), (d[\"etype\"] == \"generator\").astype(int), (d[\"etype\"] == \"trailer\").astype(int)\n",
" d[\"aging_idx\"] = d[\"h_since\"] * d[\"env\"].map(w) / CYCLE # ํ๊ฒฝ ๊ฐ์ค ๋
ธํ ์ง์(1์ด๋ฉด ์ฐฝ์ ๋น ์ฃผ๊ธฐ์ ๋๋ฌ)\n",
" d[\"lin_rul_d\"] = (CYCLE - d[\"h_since\"]) / d[\"rate\"] # ์ ํ ๊ฐ๊ฐ์๊ฐ ์์ฌ ์ผ์(Baseline)\n",
" d[\"hs_frac\"] = d[\"h_since\"] / CYCLE\n",
" d[\"insp_eta_h\"] = d[\"h_since\"] / np.maximum(d[\"insp\"], 0.05) # ์ ๊ฒ ๋ง๋ชจ ๋น์จ๋ก ํ์ฐํ ์ด ์๋ช
์ถ์ (h)\n",
" return d\n",
"\n",
"\n",
"def snapshots(df, k=4, seed=0):\n",
" \"\"\"๊ณ ์ฅ ์ด๋ ฅ ์ฅ๋น์ ๊ณผ๊ฑฐ ์์ ํ๋ณธ: ๊ณ ์ฅ ์ ์์ ์์ ์ ์ ๊ฒ๊ฐยท์ด์ฉ์๊ฐ๊ณผ ๊ทธ๋์ ๋จ์ ์ผ์. (์์กด ์ฅ๋น์ ๋ฏธ๋๋ ์ ์ ์์ผ๋ฏ๋ก ๊ณ ์ฅ ์ด๋ ฅ๋ง ์ด๋ค)\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" f = df[df[\"event\"] == 1]\n",
" out = []\n",
" for _ in range(k):\n",
" s = f.copy()\n",
" h = s[\"t_fail\"] * rng.uniform(0.2, 0.98, len(s))\n",
" ratio = h / s[\"h_since\"]\n",
" s[\"hours\"] = s[\"hours\"] - (s[\"h_since\"] - h)\n",
" s[\"km\"] = s[\"km\"] * ratio.clip(upper=1)\n",
" s[\"unpl\"] = np.floor(s[\"unpl\"] * ratio)\n",
" s[\"n_maint\"] = np.floor(s[\"n_maint\"] * ratio)\n",
" s[\"insp\"] = np.clip(h / s[\"t_fail\"] + rng.normal(0, INSP_NOISE, len(s)), 0, 1.3)\n",
" s[\"h_since\"] = h\n",
" s[\"rul_d\"] = (s[\"t_fail\"] - h) / s[\"rate\"]\n",
" s[\"event\"] = 0\n",
" out.append(s)\n",
" return pd.concat(out, ignore_index=True)\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ์์กด ๋ถ์ ๋๊ตฌ(scipy๋ง ์ฌ์ฉ): ์์ด๋ธ AFT(๊ฐ์ ์๋ช
) ๋ชจํ๊ณผ Cox ๋น๋ก์ํ ๋ชจํ\n",
"# ---------------------------------------------------------------------------\n",
"from scipy.optimize import minimize\n",
"\n",
"\n",
"def aft_x(d):\n",
" return np.c_[np.ones(len(d)), d[\"env\"].eq(\"rough\"), d[\"env\"].eq(\"salt\"), d[\"env\"].eq(\"cold\"), d[\"unpl\"] / (d[\"h_since\"] / 1000 + 0.5)].astype(float)\n",
"\n",
"\n",
"AFT_COLS = [\"์ ํธ\", \"ํ์ง\", \"์ผ๋ถ\", \"ํนํ\", \"๋น๊ณํ ์ ๋น์จ\"]\n",
"\n",
"\n",
"def fit_aft(d):\n",
" \"\"\"์ฐฝ์ ๋น ํ ์ด์ฉ์๊ฐ dur(๊ณ ์ฅ์ด๋ฉด event=1, ์์กด์ด๋ฉด ์ค๋์ ๋จ)์ ๋ํ ์์ด๋ธ AFT ์ต๋์ฐ๋ ์ถ์ . ๋ฐํ: ๊ณ์(๋ง์ง๋ง์ log ํ์).\"\"\"\n",
" x, t, e = aft_x(d), d[\"dur\"].values, d[\"event\"].values\n",
"\n",
" def nll(p):\n",
" lam, k = np.exp(x @ p[:-1]), np.exp(p[-1])\n",
" z = (t / lam) ** k\n",
" return -(e * (np.log(k) - np.log(lam) + (k - 1) * (np.log(t) - np.log(lam))) - z).sum()\n",
"\n",
" r = minimize(nll, np.r_[np.log(8000), np.zeros(x.shape[1] - 1), np.log(5)], method=\"BFGS\")\n",
" return r.x\n",
"\n",
"\n",
"def aft_pred(p, d, q=0.5, h=None):\n",
" \"\"\"์ด๋ฏธ h(์ด์ฉ์๊ฐ)๊น์ง ์์กดํ๋ค๋ ์กฐ๊ฑด์์ ๋ถ์์ q ์์กด ์์ (์ด์ฉ์๊ฐ h ๊ธฐ์ค ์ด ์๋ช
)์ ๋๋ ค์ค๋ค. q=0.5๊ฐ ์กฐ๊ฑด๋ถ ์ค์๊ฐ.\"\"\"\n",
" lam, k = np.exp(aft_x(d) @ p[:-1]), np.exp(p[-1])\n",
" h = d[\"h_since\"].values if h is None else h\n",
" return lam * ((h / lam) ** k - np.log(q)) ** (1 / k)\n",
"\n",
"\n",
"def aft_fail_prob(p, d, h0, h1):\n",
" \"\"\"h0๊น์ง ์์กดํ ์ฅ๋น๊ฐ h0~h1 ์ฌ์ด์ ๊ณ ์ฅ ๋ ์กฐ๊ฑด๋ถ ํ๋ฅ .\"\"\"\n",
" lam, k = np.exp(aft_x(d) @ p[:-1]), np.exp(p[-1])\n",
" s0, s1 = np.exp(-(h0 / lam) ** k), np.exp(-(h1 / lam) ** k)\n",
" return 1 - s1 / s0\n",
"\n",
"\n",
"def fit_cox(d, iters=25):\n",
" \"\"\"Cox ๋น๋ก์ํ(Breslow ๊ทผ์ฌ) ๋ดํด-๋ฉ์จ. ๊ณต๋ณ๋: ํ์งยท์ผ๋ถยทํนํยท๋น๊ณํ ์ ๋น์จ. ๋ฐํ: ๊ณ์.\"\"\"\n",
" x = aft_x(d)[:, 1:]\n",
" o = np.argsort(-d[\"dur\"].values)\n",
" x, e = x[o], d[\"event\"].values[o]\n",
" b = np.zeros(x.shape[1])\n",
" for _ in range(iters):\n",
" r = np.exp(x @ b)\n",
" S0 = np.cumsum(r)\n",
" S1 = np.cumsum(r[:, None] * x, axis=0)\n",
" S2 = np.cumsum(r[:, None, None] * x[:, :, None] * x[:, None, :], axis=0)\n",
" m1 = S1 / S0[:, None]\n",
" g = ((x - m1) * e[:, None]).sum(axis=0)\n",
" H = -sum(e[i] * (S2[i] / S0[i] - np.outer(m1[i], m1[i])) for i in np.where(e == 1)[0])\n",
" step = np.linalg.solve(H - 1e-6 * np.eye(len(b)), g)\n",
" b = b - step\n",
" if np.abs(step).max() < 1e-7:\n",
" break\n",
" return b\n",
"\n",
"\n",
"def cindex(time, event, risk):\n",
" \"\"\"์ผ์น๋ C-index: ์ํ ์ ์๊ฐ ๋์ ์ชฝ์ด ๋จผ์ ๊ณ ์ฅ ๋ฌ๋๊ฐ(๊ด์ธก ๊ฐ๋ฅํ ์๋ง).\"\"\"\n",
" t, e, r = np.asarray(time), np.asarray(event), np.asarray(risk)\n",
" num = den = 0.0\n",
" for i in np.where(e == 1)[0]:\n",
" m = t > t[i]\n",
" den += m.sum()\n",
" num += (r[m] < r[i]).sum() + 0.5 * (r[m] == r[i]).sum()\n",
" return num / max(den, 1)\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ์์ฌ ์๋ช
(RUL, ์ผ) ์์ธก ์์ง: ์์ด๋ธ AFT ์กฐ๊ฑด๋ถ ์ค์๊ฐ + ๊ทธ๋๋์ธํธ ๋ถ์คํ
(๊ณ ์ฅ ์ด๋ ฅ ์์ ํ๋ณธ ํ์ต)์ ํ๊ท \n",
"# ---------------------------------------------------------------------------\n",
"from sklearn.ensemble import GradientBoostingRegressor\n",
"\n",
"\n",
"def fit_engine(train, seed=0):\n",
" p = fit_aft(train)\n",
" sn = add_features(snapshots(train, 5, seed))\n",
" sn[\"aft\"] = (aft_pred(p, sn) - sn[\"h_since\"]) / sn[\"rate\"]\n",
" g = GradientBoostingRegressor(n_estimators=250, max_depth=3, learning_rate=0.06, random_state=0).fit(sn[FEATS + [\"aft\"]], sn[\"rul_d\"])\n",
" return {\"p\": p, \"g\": g}\n",
"\n",
"\n",
"def predict_rul(eng, d):\n",
" \"\"\"d๋ add_features๋ฅผ ๊ฑฐ์น ํ. ๋ฐํ: (์์๋ธ RUL, AFT RUL, GBR RUL) - ์ผ ๋จ์.\"\"\"\n",
" aft = ((aft_pred(eng[\"p\"], d) - d[\"h_since\"]) / d[\"rate\"]).values\n",
" x = d[FEATS].copy()\n",
" x[\"aft\"] = aft\n",
" gb = eng[\"g\"].predict(x)\n",
" return np.clip(0.5 * aft + 0.5 * gb, 1, None), aft, gb\n",
"# ---------------------------------------------------------------------------\n",
"# ---------------------------------------------------------------------------\n",
"# ๊ณํ์ ๋น(์ฐฝ์ ๋น) ์ผ์ ์ถ์ฒ: ์๋ณ ์ ๋น์ฐฝ ๋ฅ๋ ฅยท์์ฐยท์ํจ์ฑ ๋ถํ ํคํธ(์ ํจ๊ธฐํ) ์ ์ฝ ์๋ ๊ธฐ๋ ๋น์ฉ ์ต์ํ (๋น์ฉ ๋จ๊ฐ๋ ๊ต์ก์ฉ ๊ฐ์ ๊ฐ, ๋ง์)\n",
"# ---------------------------------------------------------------------------\n",
"from scipy.optimize import milp, LinearConstraint, Bounds\n",
"from scipy.stats import norm\n",
"\n",
"C_PLAN, C_FAIL, C_WASTE_KH, C_KIT = 600.0, 2400.0, 40.0, 120.0 # ๊ณํ ์ฐฝ์ ๋น, ๋น๊ณํ ๊ณ ์ฅ(๊ธด๊ธ ์ ๋น ํฌํจ), ๋ฏธ์ฌ์ฉ ์๋ช
1000h๋น ์์ค, ์ํจ์ฑ ๋ถํ ํคํธ\n",
"HORIZON, MDAYS = 12, 30 # ๊ณํ 12๊ฐ์, ์ 30์ผ\n",
"CAP_M, BUDGET_M = 22, 15000.0 # ์๋ณ ์ ๋น์ฐฝ ๋ฅ๋ ฅ(๋), ์๋ณ ์์ฐ\n",
"LOTS = [(30, 70), (45, 170), (45, 390), (70, 800)] # ์ํจ์ฑ ํคํธ ๋กํธ(์๋, ์ ํจ๊ธฐํ๊น์ง ๋จ์ ์ผ์)\n",
"KIT_MARGIN = 14 # ์ ๋น์ผ ๊ธฐ์ค ์ต์ ์ ํจ๊ธฐ๊ฐ ์ฌ์ (์ผ)\n",
"\n",
"\n",
"def exp_cost_matrix(pred, rate, month_days):\n",
" \"\"\"์ฅ๋น i๋ฅผ ์ m์ ์ ๋นํ ๋์ ๊ธฐ๋ ๋น์ฉ. ์ฐธ RUL ~ N(pred, sigma), sigma = 35์ผ + 0.12 x pred (๊ฐ์ ). ๋ง์ง๋ง ์ด์ ๊ณํ ๊ธฐ๊ฐ ๋ฐ(๋ฏธ์ ๋น).\"\"\"\n",
" sig = 35 + 0.12 * pred\n",
" C = np.zeros((len(pred), len(month_days) + 1))\n",
" for j, day in enumerate(month_days):\n",
" z = (day - pred) / sig\n",
" pf = norm.cdf(z)\n",
" unused = np.maximum(pred - day, 0) * rate * 0.6 # ์ ๋น๋ฅผ ์๋น๊ธด ๋งํผ ๋ฒ๋ฆฌ๋ ์ด์ฉ์๊ฐ h(๊ทผ์ฌ)\n",
" C[:, j] = C_PLAN + C_KIT + pf * (C_FAIL - C_PLAN) + C_WASTE_KH * unused / 1000 * (1 - pf)\n",
" C[:, -1] = norm.cdf((HORIZON * MDAYS - pred) / sig) * C_FAIL\n",
" return C\n",
"\n",
"\n",
"def plan_milp(pred, rate, cap=CAP_M, budget=BUDGET_M, lots=LOTS, use_kit=True):\n",
" \"\"\"์ ์๊ณํ์ผ๋ก ์ฅ๋น๋ณ ์ ๋น ์(0~11) ๋๋ ๋ฏธ์ ๋น(-1)๋ฅผ ๋ฐฐ์ ํ๋ค. ํ๋ณด๋ ์์ธก RUL์ด ๊ณํ ๊ธฐ๊ฐ +90์ผ ์ด๋ด์ธ ์ฅ๋น.\"\"\"\n",
" md = np.arange(1, HORIZON + 1) * MDAYS\n",
" cand = np.where(pred < HORIZON * MDAYS + 90)[0]\n",
" n, M = len(cand), HORIZON\n",
" C = exp_cost_matrix(pred[cand], rate[cand], md)\n",
" c = C.ravel()\n",
" A, lo, hi = [], [], []\n",
" for i in range(n): # ์ฅ๋น๋น ํ ๋ฒ๋ง ๋ฐฐ์ (์ 12์นธ + ๋ฏธ์ ๋น)\n",
" row = np.zeros(n * (M + 1)); row[i * (M + 1):(i + 1) * (M + 1)] = 1; A.append(row); lo.append(1); hi.append(1)\n",
" for m in range(M): # ์๋ณ ๋ฅ๋ ฅยท์์ฐ\n",
" row = np.zeros(n * (M + 1)); row[m::(M + 1)] = 1\n",
" A.append(row); lo.append(0); hi.append(cap)\n",
" A.append(row * (C_PLAN + C_KIT)); lo.append(0); hi.append(budget)\n",
" if use_kit: # ์ํจ์ฑ ํคํธ: m์ ์ดํ ์ ๋น ์์๋ ๊ทธ ์์ ์ ์ ํจ๊ธฐ๊ฐ์ด ๋จ์ ๋กํธ ์๋์ ๋์ ์ ์๋ค\n",
" for m in range(M):\n",
" sup = sum(q for q, e in lots if e >= md[m] + KIT_MARGIN)\n",
" row = np.zeros(n * (M + 1))\n",
" for mm in range(m, M):\n",
" row[mm::(M + 1)] = 1\n",
" A.append(row); lo.append(0); hi.append(sup)\n",
" r = milp(c, constraints=LinearConstraint(np.array(A), lo, hi), integrality=np.ones(len(c)), bounds=Bounds(0, 1), options={\"time_limit\": 20})\n",
" month = np.full(len(pred), -1)\n",
" if r.x is not None:\n",
" x = np.round(r.x).reshape(n, M + 1)\n",
" assign = np.where(x[:, :M].sum(axis=1) > 0.5, x[:, :M].argmax(axis=1), -1)\n",
" month[cand] = assign\n",
" return month\n",
"\n",
"\n",
"def plan_greedy(pred, margin=45, cap=CAP_M, budget=BUDGET_M):\n",
" \"\"\"๊ฐ๋จํ ํ์ ๋ฐฐ์ : ์ ๋น ๋ชฉํ์ผ(์์ธก RUL - margin)์ด ๋น ๋ฅธ ์์ผ๋ก ๋ชฉํ์ผ ์ด์ ๊ฐ์ฅ ๋ฆ์ ๋น ์, ์์ผ๋ฉด ๊ฐ์ฅ ์ด๋ฅธ ๋น ์์ ๋ฐฐ์ .\"\"\"\n",
" cap = min(cap, int(budget // (C_PLAN + C_KIT))) # ์ ์์ฐ์ด ํ์ฉํ๋ ๋์๋ก ๋ฅ๋ ฅ์ ์ ํ\n",
" month = np.full(len(pred), -1)\n",
" left = np.full(HORIZON, cap)\n",
" for i in np.argsort(pred):\n",
" if pred[i] > HORIZON * MDAYS + 60: break\n",
" tgt = int(np.clip((pred[i] - margin) // MDAYS - 1, 0, HORIZON - 1))\n",
" for m in list(range(tgt, -1, -1)) + list(range(tgt + 1, HORIZON)):\n",
" if left[m] > 0:\n",
" month[i] = m; left[m] -= 1; break\n",
" return month\n",
"\n",
"\n",
"def plan_fixed(lin_rul, cap=CAP_M, budget=BUDGET_M):\n",
" \"\"\"๊ดํ: ๊ท์ ์ฃผ๊ธฐ ๋๋ฌ ์์๋๋ก(์ ํ ์์ฌ ์ผ์) ์๋ณ ๋ฅ๋ ฅ ์์์ ๋ฐฐ์ .\"\"\"\n",
" return plan_greedy(np.asarray(lin_rul, float), margin=0, cap=cap, budget=budget)\n",
"\n",
"\n",
"def evaluate_plan(month, true_rul, rate, cost_kit=True):\n",
" \"\"\"์ฐธ RUL๋ก ๊ณํ์ ์ค์ ๋น์ฉ์ ๊ณ์ฐํ๋ค: ์ ๋น์ผ ์ด์ ์ ๊ณ ์ฅ -> ๋น๊ณํ ๊ณ ์ฅ ๋น์ฉ, ์๋๋ฉด ๊ณํ ๋น์ฉ + ๋ฏธ์ฌ์ฉ ์๋ช
์์ค.\"\"\"\n",
" day = np.where(month >= 0, (month + 1) * MDAYS, 10 ** 9)\n",
" fail = (true_rul < day) & (true_rul < HORIZON * MDAYS)\n",
" planned = (month >= 0) & ~fail\n",
" waste = np.where(planned, (true_rul - day) * rate * 0.6, 0) * C_WASTE_KH / 1000\n",
" cost = fail * C_FAIL + planned * (C_PLAN + (C_KIT if cost_kit else 0)) + waste\n",
" return {\"total\": float(cost.sum()), \"fail\": int(fail.sum()), \"planned\": int(planned.sum()), \"waste\": float(waste.sum()), \"unsched_fail\": int(((month < 0) & fail).sum())}\n",
"\n",
"\n",
"def kit_fefo(month, lots=LOTS):\n",
" \"\"\"์ ํจ๊ธฐํ์ด ๋น ๋ฅธ ๋กํธ๋ถํฐ ์ฐ๋ FEFO๋ก ํคํธ๋ฅผ ๋ฐฐ์ ํ๊ณ ์ ๋น์ผ์ ์ ํจ๊ธฐ๊ฐ์ด ๋ถ์กฑํ ๋ฐฐ์ ์์ ๋ง๋ฃ ํ๊ธฐ ์๋์ ์ผ๋ค.\"\"\"\n",
" q = [[qty, e] for qty, e in sorted(lots, key=lambda t: t[1])]\n",
" short = 0\n",
" for m in sorted(month[month >= 0]):\n",
" day = (m + 1) * MDAYS\n",
" for lot in q:\n",
" if lot[1] >= day + KIT_MARGIN and lot[0] > 0:\n",
" lot[0] -= 1; break\n",
" else:\n",
" short += 1\n",
" expired = sum(l[0] for l in q if l[1] < HORIZON * MDAYS)\n",
" return {\"short\": short, \"expired_unused\": expired, \"left\": sum(l[0] for l in q)}\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"libs = [\"numpy\", \"pandas\", \"scipy\", \"sklearn\", \"matplotlib\"]\n",
"env_ok = sum(importlib.util.find_spec(m) is not None for m in libs)\n",
"print(f\"[0] ํ๊ฒฝ ์ ๊ฒ: Python {sys.version_info.major}.{sys.version_info.minor}, ํ์ ๋ชจ๋ {env_ok}/{len(libs)}\")\n",
"\n",
"# ---------------- 1) ๋ณด์ ํํฉ ๊ฐฑ์ ยท์ ํฉ์ฑ ๊ฒ์ฆ(๋ฏธ์
1~2 ์์ฝ) ----------------\n",
"OPS_W = {\"normal\": 1.00, \"rough\": 1.55, \"salt\": 1.15, \"cold\": 1.30}\n",
"base = make_fleet(900, seed=1) # ๊ธฐ์กด ๋ณด์ ํํฉ\n",
"new = make_fleet(500, seed=90, env_w=OPS_W, shift=1.1) # ์ ๊ท ์
๋ ฅ๋ถ(์ด์ ํ๊ฒฝ์ด ๋ฌ๋ผ์ง)\n",
"raw = pd.concat([base, new], ignore_index=True)\n",
"chk = {\"์ฅ๋น ID ์ ์ผ\": not raw[\"eq_id\"].duplicated().any(), \"๊ฒฐ์ธก 0\": int(raw[[\"rate\", \"hours\", \"h_since\", \"insp\", \"unpl\"]].isna().sum().sum()) == 0, \"ํ๊ฒฝ ๊ฐ 4์ข
\": bool(raw[\"env\"].isin(ENVS).all()),\n",
" \"๋ถ๋ ์ฝ๋ ๋ฑ๋ก\": bool(raw[\"unit\"].isin(UNITS).all()), \"์ด์ฉ์๊ฐ 0 ์ด์\": bool((raw[\"hours\"] >= 0).all()), \"์ฐฝ์ ๋น ํ <= ๋์ \": bool((raw[\"h_since\"] <= raw[\"hours\"] + 1e-6).all()),\n",
" \"๊ธฐ๋ ์๋ 5~40km/h\": bool((raw[\"km\"] / raw[\"hours\"]).between(5, 40).all()), \"์ ๊ฒ ๋ง๋ชจ 0~1.3\": bool(raw[\"insp\"].between(0, 1.3).all())}\n",
"print(f\"[1] ์ ๊ท {len(new)}๋ ํฌํจ {len(raw)}๋ ์ ํฉ์ฑ ๊ฒ์ฆ {sum(chk.values())}/{len(chk)} ํต๊ณผ\")\n",
"\n",
"# ---------------- 2) ๋ถํฌ ๋ณํ ์ ๊ฒ๊ณผ ์๋ ์ฌํ์ต ----------------\n",
"def psi(a, b, bins=8):\n",
" q = np.unique(np.quantile(a, np.linspace(0, 1, bins + 1)))\n",
" q[0], q[-1] = -np.inf, np.inf\n",
" pa, pb = np.histogram(a, q)[0] / len(a) + 1e-4, np.histogram(b, q)[0] / len(b) + 1e-4\n",
" return float(np.sum((pa - pb) * np.log(pa / pb)))\n",
"\n",
"\n",
"fb, fn = add_features(base), add_features(new)\n",
"drift = {c: psi(fb[c], fn[c]) for c in [\"rate\", \"unpl_per_kh\", \"insp_eta_h\", \"aging_idx\", \"km_per_h\"]}\n",
"worst = max(drift, key=drift.get)\n",
"print(\"[2] ๋ถํฌ ๋ณํ PSI(๊ธฐ์กด vs ์ ๊ท):\", \", \".join(f\"{k} {v:.2f}\" for k, v in drift.items()), f\"-> ๊ฐ์ฅ ํฐ ๋ณํ {worst}(0.2 ์ด์์ด๋ฉด ์ฌํ์ต ๊ฒํ )\")\n",
"new_tr, new_ho = new.iloc[:300].reset_index(drop=True), new.iloc[300:].reset_index(drop=True)\n",
"champ = fit_engine(base)\n",
"chall = fit_engine(pd.concat([base.sample(300, random_state=0), new_tr], ignore_index=True), seed=1) # ์ต๊ทผ ๋ถํฌ๋ฅผ ๋ฐ์ํด ๊ธฐ์กด ์ผ๋ถ + ์ ๊ท ํ์ต๋ถ์ผ๋ก ์ฌํ์ต\n",
"ho = add_features(new_ho)\n",
"ho = ho[ho.event == 0].reset_index(drop=True)\n",
"pc, _, _ = predict_rul(champ, ho)\n",
"ph, _, _ = predict_rul(chall, ho)\n",
"y = ho[\"rul_d\"].values\n",
"big = y >= 90\n",
"ac, bc = mae(y, pc), mae(y, ph)\n",
"promote = (ac - bc) / ac >= 0.03\n",
"print(f\" ๊ฒ์ฆ {len(ho)}๋(์ ๊ท ๋ถํฌ) MAE ์ฑํผ์ธ {ac:.0f}์ผ vs ์ฑ๋ฆฐ์ {bc:.0f}์ผ, MAPE {mape(y[big], pc[big]):.1f}% vs {mape(y[big], ph[big]):.1f}% -> {'์ฑ๋ฆฐ์ ์น๊ฒฉ' if promote else '๊ธฐ์กด ๋ชจ๋ธ ์ ์ง'}(MAE 3% ์ด์ ๊ฐ์ ์ผ ๋๋ง ์น๊ฒฉ)\")\n",
"model = chall if promote else champ\n",
"\n",
"# ---------------- 3) RUL ์์ธก๊ณผ 12๊ฐ์ ์ ๋น ์ผ์ ----------------\n",
"fl = add_features(new)\n",
"al = fl[fl.event == 0].reset_index(drop=True)\n",
"al[\"rul_pred\"], _, _ = predict_rul(model, al)\n",
"al[\"month\"] = plan_milp(al[\"rul_pred\"].values, al[\"rate\"].values)\n",
"mf = plan_fixed(al[\"lin_rul_d\"].values)\n",
"r_fix, r_new = evaluate_plan(mf, al[\"rul_d\"].values, al[\"rate\"].values), evaluate_plan(al[\"month\"].values, al[\"rul_d\"].values, al[\"rate\"].values)\n",
"kf = kit_fefo(al[\"month\"].values)\n",
"print(f\"\\n[3] ์ ๊ท ๋ณด์ {len(al)}๋ 12๊ฐ์ ์ ๋น ์ผ์ [๊ต์ก์ฉ๊ฐ์์๋ฃ]: ์ ๋น ๋ฐฐ์ {int((al.month >= 0).sum())}๋, ์๋ณ \" + \" \".join(f\"{m + 1}์ {c}\" for m, c in enumerate(np.bincount(al.loc[al.month >= 0, 'month'], minlength=HORIZON))))\n",
"print(f\" ๊ท์ ์ฃผ๊ธฐ ๊ณ ์ ์ผ์ ๋๋น ์ค์ ๋น์ฉ {r_fix['total']:,.0f} -> {r_new['total']:,.0f}๋ง์({1 - r_new['total'] / r_fix['total']:.1%} ์ ๊ฐ), ๋น๊ณํ ๊ณ ์ฅ {r_fix['fail']} -> {r_new['fail']}๋, ํคํธ ์ ํจ๊ธฐ๊ฐ ๋ถ์กฑ {kf['short']}๊ฑด\")\n",
"al[\"์ฒ๋ฆฌ\"] = np.select([al.rul_pred < 90, (al.age_y >= 14) & (al.rul_pred < 365)], [\"๊ธด๊ธ ์ ๊ฒ(ํ์ฅ ํ์ธ ํ ๊ตฐ์์งํ๊ด ์น์ธ)\", \"๋ํ ๊ฑด์ ํ๋ณด(2๋จ๊ณ ๊ฒฐ์ฌ)\"], np.where(al.month >= 0, \"์ ๋น ์ผ์ ์น์ธ๋๊ธฐ\", \"์ ๊ธฐ ์ ๊ฒ ์ ์ง\"))\n",
"top = al.sort_values(\"rul_pred\").head(6)\n",
"print(pd.DataFrame({\"์ฅ๋น\": top.eq_id, \"๋ถ๋\": top.unit, \"ํ๊ฒฝ\": top.env.map(ENV_KO), \"์์ธก RUL(์ผ)\": top.rul_pred, \"๊ถ์ฅ ์\": top.month.map(lambda m: f\"{m + 1}์\" if m >= 0 else \"-\"), \"์ฒ๋ฆฌ\": top[\"์ฒ๋ฆฌ\"]}).round(0).to_string(index=False))\n",
"vc = al[\"์ฒ๋ฆฌ\"].value_counts()\n",
"print(\" ์ฒ๋ฆฌ ๊ตฌ๋ถ:\", vc.to_dict(), \"/ ์๋ ํ์ 0๊ฑด(๋ชจ๋ ์ผ์ ยท๋ํ๋ ๊ตฐ์์งํ๊ด ์น์ธ ํ์)\")\n",
"\n",
"# ---------------- 4) ์ด์ ๋ฌธ์ ์๋น์จยท์ธํฐํ์ด์ค ๋ช
์ธ ----------------\n",
"docs = {\"๋ฐ์ดํฐ ์ฌ์ ยท์คํค๋ง(M1)\": 30, \"์ ์ ๊ท์น 11์ข
(M2)\": 11, \"BaselineยทEDA(M3)\": 8, \"์์กด ๋ถ์ ๋ชจ๋ธ(M4)\": 12, \"RUL ์์งยท์ ๋น ์ผ์ (M5)\": 4, \"๋์๋ณด๋ยท์ค๋ช
(M6)\": 5, \"์น์ธ ์ ์ฐจยท๊ถํ(M7)\": 4,\n",
" \"์ผ๊ด ์๋ฎฌ๋ ์ด์
ยท๋ถํ(M8)\": 5, \"AARยทROI(M9)\": 4, \"์ด์ยท์ฌํ์ต ์ ์ฐจ(M10)\": 6, \"์ฅ์ ๋์\": 5, \"์ธํฐํ์ด์ค ๊ฐ์ด๋\": 4}\n",
"done = sum(v > 0 for v in docs.values())\n",
"iface = {\"input\": {\"fleet\": [\"eq_id:str\", \"unit:str\", \"env:str\", \"rate:float\", \"hours:float\", \"h_since:float\", \"unpl:int\", \"insp:float\"], \"lots\": [\"lot_id\", \"qty\", \"expiry_d\"], \"format\": \"json-lines\"},\n",
" \"output\": {\"plan\": [\"eq_id\", \"rul_days\", \"month\", \"status\"], \"approval\": \"PENDING_ONLY\"}, \"auth\": \"role-based(๊ฐ์)\", \"note\": \"์์ ๊ตฐ์์ ๋ณด์ฒด๊ณ๋ ๊ฐ์ ์ธํฐํ์ด์ค๋ก๋ง ์ ์\"}\n",
"print(f\"\\n[4] ์ด์ ๋ฌธ์ {len(docs)}๊ฐ ํญ๋ชฉ ์ค ์์ฑ ์๋ฃ {done}๊ฐ ({done / len(docs):.0%}) / ์ธํฐํ์ด์ค ๋ช
์ธ(JSON):\")\n",
"print(\" \", json.dumps(iface, ensure_ascii=False))\n",
"\n",
"# ---------------- 5) ์์ฒด ์ ๊ฒยท๋ธ๋ฆฌํ ----------------\n",
"pm, _, _ = predict_rul(model, ho)\n",
"fin_mae, fin_mape, base_mae = mae(y, pm), mape(y[big], pm[big]), mae(y, ho[\"lin_rul_d\"].values)\n",
"checks = [(\"ํ๊ฒฝ ์ ๊ฒ ํต๊ณผ\", env_ok == len(libs)), (\"๋ฐ์ดํฐ ์ ํฉ์ฑ ์ ํญ๋ชฉ ํต๊ณผ\", all(chk.values())), (\"์ฌํ์ต ํ์ดํ๋ผ์ธ ์ ์ ์คํ(์ฑํผ์ธ/์ฑ๋ฆฐ์ ํ์ ์ถ๋ ฅ)\", ac > 0 and bc > 0),\n",
" (\"RUL MAPE 25% ์ดํ(์ฐธ RUL 90์ผ ์ด์)\", fin_mape <= 25), (\"์ ํ ๊ฐ๊ฐ์๊ฐ ๋๋น MAE 20% ์ด์ ๊ฐ์ \", fin_mae <= 0.8 * base_mae), (\"๊ณ ์ ์ฃผ๊ธฐ ๋๋น ๊ณํ ๋น์ฉ 15% ์ด์ ์ ๊ฐ\", r_new[\"total\"] <= 0.85 * r_fix[\"total\"]),\n",
" (\"์๋ ํ์ 0๊ฑดยทํคํธ ์ ํจ๊ธฐ๊ฐ ๋ถ์กฑ 0๊ฑด\", bool(al[\"์ฒ๋ฆฌ\"].str.contains(\"์น์ธ|์ ๊ฒ|๊ฑด์\").all()) and kf[\"short\"] == 0), (\"์ด์ ๋ฌธ์ 100% ์๋น\", done == len(docs))]\n",
"print(\"\\n[5] ์์ฒด ์ ๊ฒ\")\n",
"for n, ok in checks:\n",
" print(f\" [{'PASS' if ok else 'FAIL'}] {n}\")\n",
"print(\"\\n[6] ๋ธ๋ฆฌํ ์์ฝ\")\n",
"print(f\" - ์ ๊ท {len(new)}๋๋ก ๋ถํฌ ๋ณํ๋ฅผ ์ ๊ฒ({worst} PSI {drift[worst]:.2f})ํ๊ณ ์ฌํ์ต ํ๋จ์ ์๋ ์ํํ๋ค({'์ฑ๋ฆฐ์ ์น๊ฒฉ' if promote else '๊ธฐ์กด ๋ชจ๋ธ ์ ์ง'}).\")\n",
"print(f\" - ์ ๊ท ๋ถํฌ RUL MAE {base_mae:.0f}์ผ(์ ํ ๊ฐ๊ฐ์๊ฐ) -> {fin_mae:.0f}์ผ, MAPE {fin_mape:.1f}%. 12๊ฐ์ ๊ณํ ๋น์ฉ {1 - r_new['total'] / r_fix['total']:.1%} ์ ๊ฐ, ๋น๊ณํ ๊ณ ์ฅ {r_fix['fail']} -> {r_new['fail']}๋(๊ฐ์ ๋จ๊ฐ ๊ธฐ์ค).\")\n",
"print(\" - ์ ๋น ์ผ์ ยท๊ธด๊ธ ์ ๊ฒยท๋ํ ํ๋ณด๋ ๋ชจ๋ ์น์ธ๋๊ธฐ๋ก ๋ถ๋ฆฌํ๋ค. ํ์ ์ ๊ตฐ์์งํ๊ด(๋ํ๋ ์ฌ์ ๋ด๋น ํฌํจ) ๊ฒฐ์ฌ์ ๋ฐ๋ฅธ๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(12, 4))\n",
"ax[0].bar(list(drift), list(drift.values())); ax[0].axhline(0.2, c=\"r\", ls=\"--\"); ax[0].set_title(\"Feature drift (PSI), old vs new fleet\"); ax[0].tick_params(axis=\"x\", rotation=20)\n",
"ax[1].scatter(y, pc, s=6, alpha=.5, label=\"champion\"); ax[1].scatter(y, ph, s=6, alpha=.5, label=\"challenger\"); ax[1].plot([0, 4000], [0, 4000], \"k--\"); ax[1].legend(); ax[1].set_title(\"Hold-out RUL: true vs predicted (days)\")\n",
"plt.suptitle(\"SUP02 integrated pipeline (synthetic)\"); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
10 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[0] ํ๊ฒฝ ์ ๊ฒ: Python 3.11, ํ์ ๋ชจ๋ 5/5\n",
"[1] ์ ๊ท 500๋ ํฌํจ 1400๋ ์ ํฉ์ฑ ๊ฒ์ฆ 8/8 ํต๊ณผ\n",
"[2] ๋ถํฌ ๋ณํ PSI(๊ธฐ์กด vs ์ ๊ท): rate 0.09, unpl_per_kh 0.03, insp_eta_h 0.03, aging_idx 0.02, km_per_h 0.03 -> ๊ฐ์ฅ ํฐ ๋ณํ rate(0.2 ์ด์์ด๋ฉด ์ฌํ์ต ๊ฒํ )\n",
" ๊ฒ์ฆ 132๋(์ ๊ท ๋ถํฌ) MAE ์ฑํผ์ธ 216์ผ vs ์ฑ๋ฆฐ์ 209์ผ, MAPE 24.9% vs 22.8% -> ์ฑ๋ฆฐ์ ์น๊ฒฉ(MAE 3% ์ด์ ๊ฐ์ ์ผ ๋๋ง ์น๊ฒฉ)\n",
"\n",
"[3] ์ ๊ท ๋ณด์ 322๋ 12๊ฐ์ ์ ๋น ์ผ์ [๊ต์ก์ฉ๊ฐ์์๋ฃ]: ์ ๋น ๋ฐฐ์ 56๋, ์๋ณ 1์ 20 2์ 20 3์ 8 4์ 7 5์ 1 6์ 0 7์ 0 8์ 0 9์ 0 10์ 0 11์ 0 12์ 0\n",
" ๊ท์ ์ฃผ๊ธฐ ๊ณ ์ ์ผ์ ๋๋น ์ค์ ๋น์ฉ 117,515 -> 76,804๋ง์(34.6% ์ ๊ฐ), ๋น๊ณํ ๊ณ ์ฅ 36 -> 17๋, ํคํธ ์ ํจ๊ธฐ๊ฐ ๋ถ์กฑ 0๊ฑด\n",
" ์ฅ๋น ๋ถ๋ ํ๊ฒฝ ์์ธก RUL(์ผ) ๊ถ์ฅ ์ ์ฒ๋ฆฌ\n",
"EQ900423 U6 ์ผ๋ฐ 74.0 1์ ๊ธด๊ธ ์ ๊ฒ(ํ์ฅ ํ์ธ ํ ๊ตฐ์์งํ๊ด ์น์ธ)\n",
"EQ900136 U5 ํ์ง 75.0 1์ ๊ธด๊ธ ์ ๊ฒ(ํ์ฅ ํ์ธ ํ ๊ตฐ์์งํ๊ด ์น์ธ)\n",
"EQ900455 U6 ์ผ๋ถ 90.0 1์ ์ ๋น ์ผ์ ์น์ธ๋๊ธฐ\n",
"EQ900280 U3 ์ผ๋ถ 111.0 1์ ์ ๋น ์ผ์ ์น์ธ๋๊ธฐ\n",
"EQ900040 U1 ์ผ๋ถ 124.0 1์ ์ ๋น ์ผ์ ์น์ธ๋๊ธฐ\n",
"EQ900035 U1 ์ผ๋ฐ 134.0 1์ ์ ๋น ์ผ์ ์น์ธ๋๊ธฐ\n",
" ์ฒ๋ฆฌ ๊ตฌ๋ถ: {'์ ๊ธฐ ์ ๊ฒ ์ ์ง': 266, '์ ๋น ์ผ์ ์น์ธ๋๊ธฐ': 37, '๋ํ ๊ฑด์ ํ๋ณด(2๋จ๊ณ ๊ฒฐ์ฌ)': 17, '๊ธด๊ธ ์ ๊ฒ(ํ์ฅ ํ์ธ ํ ๊ตฐ์์งํ๊ด ์น์ธ)': 2} / ์๋ ํ์ 0๊ฑด(๋ชจ๋ ์ผ์ ยท๋ํ๋ ๊ตฐ์์งํ๊ด ์น์ธ ํ์)\n",
"\n",
"[4] ์ด์ ๋ฌธ์ 12๊ฐ ํญ๋ชฉ ์ค ์์ฑ ์๋ฃ 12๊ฐ (100%) / ์ธํฐํ์ด์ค ๋ช
์ธ(JSON):\n",
" {\"input\": {\"fleet\": [\"eq_id:str\", \"unit:str\", \"env:str\", \"rate:float\", \"hours:float\", \"h_since:float\", \"unpl:int\", \"insp:float\"], \"lots\": [\"lot_id\", \"qty\", \"expiry_d\"], \"format\": \"json-lines\"}, \"output\": {\"plan\": [\"eq_id\", \"rul_days\", \"month\", \"status\"], \"approval\": \"PENDING_ONLY\"}, \"auth\": \"role-based(๊ฐ์)\", \"note\": \"์์ ๊ตฐ์์ ๋ณด์ฒด๊ณ๋ ๊ฐ์ ์ธํฐํ์ด์ค๋ก๋ง ์ ์\"}\n",
"\n",
"[5] ์์ฒด ์ ๊ฒ\n",
" [PASS] ํ๊ฒฝ ์ ๊ฒ ํต๊ณผ\n",
" [PASS] ๋ฐ์ดํฐ ์ ํฉ์ฑ ์ ํญ๋ชฉ ํต๊ณผ\n",
" [PASS] ์ฌํ์ต ํ์ดํ๋ผ์ธ ์ ์ ์คํ(์ฑํผ์ธ/์ฑ๋ฆฐ์ ํ์ ์ถ๋ ฅ)\n",
" [PASS] RUL MAPE 25% ์ดํ(์ฐธ RUL 90์ผ ์ด์)\n",
" [PASS] ์ ํ ๊ฐ๊ฐ์๊ฐ ๋๋น MAE 20% ์ด์ ๊ฐ์ \n",
" [PASS] ๊ณ ์ ์ฃผ๊ธฐ ๋๋น ๊ณํ ๋น์ฉ 15% ์ด์ ์ ๊ฐ\n",
" [PASS] ์๋ ํ์ 0๊ฑดยทํคํธ ์ ํจ๊ธฐ๊ฐ ๋ถ์กฑ 0๊ฑด\n",
" [PASS] ์ด์ ๋ฌธ์ 100% ์๋น\n",
"\n",
"[6] ๋ธ๋ฆฌํ ์์ฝ\n",
" - ์ ๊ท 500๋๋ก ๋ถํฌ ๋ณํ๋ฅผ ์ ๊ฒ(rate PSI 0.09)ํ๊ณ ์ฌํ์ต ํ๋จ์ ์๋ ์ํํ๋ค(์ฑ๋ฆฐ์ ์น๊ฒฉ).\n",
" - ์ ๊ท ๋ถํฌ RUL MAE 377์ผ(์ ํ ๊ฐ๊ฐ์๊ฐ) -> 209์ผ, MAPE 22.8%. 12๊ฐ์ ๊ณํ ๋น์ฉ 34.6% ์ ๊ฐ, ๋น๊ณํ ๊ณ ์ฅ 36 -> 17๋(๊ฐ์ ๋จ๊ฐ ๊ธฐ์ค).\n",
" - ์ ๋น ์ผ์ ยท๊ธด๊ธ ์ ๊ฒยท๋ํ ํ๋ณด๋ ๋ชจ๋ ์น์ธ๋๊ธฐ๋ก ๋ถ๋ฆฌํ๋ค. ํ์ ์ ๊ตฐ์์งํ๊ด(๋ํ๋ ์ฌ์ ๋ด๋น ํฌํจ) ๊ฒฐ์ฌ์ ๋ฐ๋ฅธ๋ค.\n",
"```\n",
" \n"
]
}
],
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}