{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# ๐ฏ [MNT04] ๊ณ ์ฅ์ค์ ยท์๋ฆฌ๋ถ์ ์๋ชจ ๊ธฐ๋ฐ PBL ํ๋ชฉ๊ด๋ฆฌ\n",
"### Google Colab ์ค์ต ํ๊ฒฝ (์ฅ๋น์ ๋นยท์๋ฆฌ๋ถ์ / Level 3)\n",
"\n",
"[](https://colab.research.google.com/github/edunlab/milicontent2/blob/main/uploads/MNT04/MNT04_solution.ipynb)\n",
"\n",
"---\n",
"\n",
"## ๐ 1. ํ๋ก์ ํธ ๊ฐ์ ๋ฐ ๋ฐฐ๊ฒฝ\n",
"\n",
"| ํญ๋ชฉ | ๋ด์ฉ |\n",
"| :--- | :--- |\n",
"| **๊ณผ์ ๋ถ๋ฅ** | ์ฅ๋น์ ๋นยท์๋ฆฌ๋ถ์ (Level 3) |\n",
"| **ํ์ต ์์ ์๊ฐ** | 10๊ฐ ๋ฏธ์
(5์ผ / ์ด 28.5์๊ฐ) |\n",
"| **ํ๊ฒฝ ์ฌ์** | Python 3.10+, numpy, pandas, matplotlib, scipy, scikit-learn |\n",
"\n",
"### ๐ ํ๋ก์ ํธ ๋ชฉํ\n",
"ํญ๊ณตยท๊ธฐ๊ฐ ์ฅ๋น์ ์์ง๊ณผ ๊ตฌ๋๊ณ๋ ๊ณ ์ฅ์ด ๋๊ธฐ ์ ์ ์ง๋, ์จ๋, ์๋ ฅ์ ๋ฏธ์ธํ ๋ณํ๊ฐ ๋จผ์ ๋ํ๋์ง๋ง, ๊ณ ์ ์๊ณ๊ฐ๋ง์ผ๋ก๋ ๊ณ ๋ถํ ์ด์ฉ์ผ์ ์ด์์ผ๋ก ์ค์ธํ๊ฑฐ๋ ์ด๊ธฐ ์ดํ๋ฅผ ๋์ณ ์๋ฆฌ๋ถ์์ ์ ๋ ํ๋ณดํ์ง ๋ชปํ๋ค. ๋ณธ ํ๋ก์ ํธ๋ ๊ณ ์ฅ ์ค์ ๊ณผ ์๋ฆฌ๋ถ์ ์๋ชจ๋ฅผ ์ฐ๊ฒฐํด ์ ๋น์ ๋ถํ ๊ด๋ฆฌ์ ์ฐ๊ณ ๋ฌธ์ ๋ฅผ ํ์ตํ๋ ๊ฒ์ ๋ชฉํ๋ก, ๊ต์ก์ฉ ๊ฐ์ ๊ธฐ๋์ฒด 48๋์ ์ผ์ ์๊ณ์ด๊ณผ ๋ถํ ์๋ชจ๋ฅผ ์ฝ๋๋ก ๋ง๋ค์ด ๋ถ์ํ๋ค. ์ผ์ ๋ฐ์ดํฐ์
์ ์, ์ก์ ์ ๊ฑฐ์ FFT ํน์ง๋ ์ถ์ถ, ์๊ณ๊ฐ Baseline๊ณผ ๋ถํ ์๋ชจ ํ๋ ํ ๋ถ์, ์คํ ์ธ์ฝ๋ ๊ธฐ๋ฐ ๋ฏธ์ธ ๋ณํ ๊ฐ์ง์ ํ์ ํผ์ฒ ๋ชจ๋ธ, ์์กด์๋ช
(RUL) ์์ธก๊ณผ ์ด์ ์ ๋ขฐ๋ ์ ์, ์ค๋ช
๊ฐ๋ฅํ ๊ฒฝ๋ณด ํ๋ฉด, ๊ฒ์ฌ๊ด ์น์ธ(HITL) ๊ธฐ๋ฐ ์กฐ๊ธฐ ์
๊ณ ์๋ฎฌ๋ ์ดํฐ, ๋์ฉ๋ ์คํธ๋ฆผ ์คํธ๋ ์ค ์ํ, 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",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ผ์ ์์ง๋ถํฐ ์ ์ฒ๋ฆฌ, ํน์ง ์ถ์ถ, ์ด์ ๊ฐ์ง, RUL ์์ธก, ๊ฒ์ฌ๊ด ์น์ธ, ์ ๋นยท๋ถํ ์๋ชจ, ์ฌํ๊ฒํ ๊น์ง ์
๋ฌด ํ๋ฆ์ ๋จ๊ณ๋ณ๋ก ์ ๋ฆฌํ๊ณ , ์ผ์ยทํน์งยท๊ณ ์ฅยท๋ถํ ๋ฌถ์์ผ๋ก ํผ์ฒ 20๊ฐ ์ด์ ์ ์ํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `docs/mnt04_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๊ฐ ์ด์)์ ์์ง์ ๋นยทPBL ํ๋ชฉ๊ด๋ฆฌ ์
๋ฌด ํ๋ก์ธ์ค ๋งต์ ์ ์ํ๊ณ , ๊ฐ์ ์ผ์ ์๋ฃ๋ก ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import json\n",
"import re\n",
"import numpy as np\n",
"import pandas as pd\n",
"from scipy.stats import kurtosis\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๊ธฐ๋์ฒด 48๋์ ์์งยท๊ตฌ๋๊ณ ์ง๋ยท์จ๋ยท์๋ ฅ ์ผ์ ์๋ฃ ์์ฑ๊ธฐ (์ค์ ์ฅ๋นยท๋ถ๋ยท์ ๋น ์ค์ ์์)\n",
"# ๊ฐ์ฒด๋ง๋ค ์ ์ -> ์ดํ -> ๊ณ ์ฅ(์๋ช
์ข
๋ฃ)๊น์ง ํ๋ฃจ 1ํ ์ธก์ (์ง๋ ํํ 256ํ๋ณธ + ์จ๋ + ์๋ ฅ + ๋ถํ)์ ๋ง๋ ๋ค.\n",
"# ์ดํ ์ ํ 4๊ฐ์ง: bearing(๋ฒ ์ด๋ง), gear(๊ธฐ์ด), oil_pump(์ค์ผ ํํ), cooling(๋๊ฐ ํฌ ๋ชจํฐ)\n",
"# ---------------------------------------------------------------------------\n",
"FS, NS = 1024, 256 # ๊ฐ์ ํ๋ณธํ ์ฃผํ์(Hz), ํํ 1๊ฐ๋น ํ๋ณธ ์\n",
"TT = np.arange(NS) / FS\n",
"MODES = { # ์ดํ ์ ํ: (๋ฐ์ ํ๋ฅ , ํ๊ท ์๋ช
(์ผ), ๊ต์ฒด ์๋ฆฌ๋ถ์, ๊ฐ์ ๋จ๊ฐ์ง์)\n",
" \"bearing\": (0.40, 160, \"์ฃผ ๋ฒ ์ด๋ง ์ธํธ\", 6.0), \"gear\": (0.25, 175, \"๊ตฌ๋ ๊ธฐ์ด ์ธํธ\", 9.0),\n",
" \"oil_pump\": (0.20, 150, \"์ค์ผ ํํ\", 4.0), \"cooling\": (0.15, 140, \"๋๊ฐ ํฌ ๋ชจํฐ\", 3.0)}\n",
"KIT_COST = 1.0 # ๋ชจ๋ ์ ๋น์ ํจ๊ป ์๋ชจ๋๋ ์ฐยทํํฐ ํคํธ ๋จ๊ฐ์ง์\n",
"BANDS = {\"b50\": (40, 60), \"b100\": (90, 110), \"b150\": (140, 156), \"b165\": (156, 176), \"b200\": (190, 215)}\n",
"\n",
"\n",
"def make_runs(n=48, seed=1):\n",
" \"\"\"๋ฐํ: units(๊ฐ์ฒด ์ ๋ณด), snap(์ผ๋ณ ์ธก์ ์์ฝ), W(์ง๋ ํํ [์ธก์ ์, 256])\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" names = list(MODES)\n",
" mode = rng.choice(names, n, p=[MODES[m][0] for m in names])\n",
" life = np.clip(np.array([rng.normal(MODES[m][1], 18) for m in mode]), 90, 230).astype(int)\n",
" start = np.sort(rng.integers(0, 240, n))\n",
" units = pd.DataFrame({\"unit\": [f\"U{i:02d}\" for i in range(n)], \"mode\": mode, \"life\": life, \"start\": start, \"base_load\": rng.uniform(0.4, 0.75, n)})\n",
" rows = []\n",
" for u in units.itertuples():\n",
" d = np.arange(u.life + 1)\n",
" onset = 0.45 * u.life\n",
" deg = np.clip((d - onset) / (u.life - onset), 0, 1) ** 1.5\n",
" load = np.clip(u.base_load + rng.normal(0, 0.06, len(d)), 0.2, 1.0)\n",
" hi = rng.random(len(d)) < 0.07 # ๊ณ ๋ถํ ์ด์ฉ์ผ(์ ์์ด์ง๋ง ์ง๋ยท์จ๋ ์์น)\n",
" load = np.where(hi, rng.uniform(0.95, 1.1, len(d)), load)\n",
" rows.append(pd.DataFrame({\"unit\": u.unit, \"day\": d, \"cal\": u.start + d, \"rul\": u.life - d, \"deg\": deg, \"load\": load, \"hi_load\": hi, \"mode\": u.mode}))\n",
" snap = pd.concat(rows, ignore_index=True)\n",
" S = len(snap)\n",
" snap[\"ambient\"] = 15 + 12 * np.sin(2 * np.pi * snap[\"cal\"] / 365) + rng.normal(0, 2, S)\n",
" dg, ld, md = snap[\"deg\"].values, snap[\"load\"].values, snap[\"mode\"].values\n",
" isb, isg, iso, isc = (md == \"bearing\"), (md == \"gear\"), (md == \"oil_pump\"), (md == \"cooling\")\n",
" snap[\"temp\"] = 55 + 22 * ld + 0.5 * snap[\"ambient\"] + 9 * dg * iso + 26 * dg * isc + 3 * dg * (isb | isg) + rng.normal(0, 1.5, S)\n",
" snap[\"press\"] = 450 - 12 * ld - 85 * dg * iso - 15 * dg * isc + rng.normal(0, 6, S)\n",
" ph = rng.uniform(0, 2 * np.pi, (S, 3))\n",
" a1, a2 = 1.0 * (0.5 + ld), 0.4 * (0.5 + ld)\n",
" W = (a1[:, None] * np.sin(2 * np.pi * 50 * TT + ph[:, :1]) + a2[:, None] * np.sin(2 * np.pi * 100 * TT + ph[:, 1:2])\n",
" + rng.normal(0, 0.35, (S, NS)))\n",
" W += (0.9 * dg * isb)[:, None] * np.sin(2 * np.pi * 165 * TT + ph[:, 2:3])\n",
" W += (0.8 * dg * isg)[:, None] * (np.sin(2 * np.pi * 150 * TT + ph[:, 2:3]) + 0.8 * np.sin(2 * np.pi * 100 * TT))\n",
" W += (0.5 * dg * iso)[:, None] * np.sin(2 * np.pi * 200 * TT + ph[:, 2:3])\n",
" k = rng.poisson(10 * dg * isb) # ๋ฒ ์ด๋ง ์ดํ: ์ถฉ๊ฒฉ์ฑ ํ์ค(์ฒจ๋ ์์น)\n",
" pos, amp = rng.integers(0, NS, (S, 30)), rng.normal(0, 3.0, (S, 30)) * dg[:, None]\n",
" amp = amp * (np.arange(30)[None, :] < k[:, None])\n",
" np.add.at(W, (np.repeat(np.arange(S), 30), pos.ravel()), amp.ravel())\n",
" return units, snap, W\n",
"\n",
"\n",
"def extract(W):\n",
" \"\"\"์ง๋ ํํ -> ํน์ง๋: RMS, ์ฒจ๋, ํ๊ณ ์จ, ์ต๋-์ต์, FFT ๋์ญ ์๋์ง(50/100/150/165/200Hz ๋ถ๊ทผ)\"\"\"\n",
" x = W - W.mean(axis=1, keepdims=True)\n",
" rms = np.sqrt((x ** 2).mean(axis=1))\n",
" out = {\"rms\": rms, \"kurt\": kurtosis(x, axis=1, fisher=False), \"crest\": np.abs(x).max(axis=1) / rms, \"p2p\": x.max(axis=1) - x.min(axis=1)}\n",
" A = np.abs(np.fft.rfft(x * np.hanning(NS), axis=1)) / NS\n",
" f = np.fft.rfftfreq(NS, 1 / FS)\n",
" for nm, (lo, hi) in BANDS.items():\n",
" out[nm] = np.sqrt((A[:, (f >= lo) & (f < hi)] ** 2).sum(axis=1))\n",
" return pd.DataFrame(out)\n",
"\n",
"# 1) ์
๋ฌด ํ๋ก์ธ์ค ๋งต: ์ผ์ ์์ง๋ถํฐ ์๋ฆฌ๋ถ์ ์์ ํ๋ฅ๊น์ง (๊ฐ ๋จ๊ณ์์ ์๊ธฐ๋ ๋ฐ์ดํฐ)\n",
"PROCESS = [(\"์ผ์ ์์ง\", \"๊ฐ์ฒด ID, ์ธก์ ์ผ, ์ง๋ ํํ, ์จ๋, ์๋ ฅ, ๋ถํ\"), (\"์ ์ฒ๋ฆฌ\", \"์ก์ ์ ๊ฑฐ, ๋จ์ยทํ๊ธฐ ํต์ผ, ๊ฒฐ์ธก ๋ณด์ \"),\n",
" (\"ํน์ง ์ถ์ถ\", \"RMS, ์ฒจ๋, ํ๊ณ ์จ, FFT ๋์ญ ์๋์ง\"), (\"์ด์ ๊ฐ์ง\", \"์ด์ ์ ์, ๊ฒฝ๋ณด ๋จ๊ณ\"),\n",
" (\"์์กด์๋ช
(RUL) ์์ธก\", \"๊ณ ์ฅ ์๋ฐ ์์ , ์ ๋ขฐ ๊ตฌ๊ฐ\"), (\"๊ฒ์ฌ๊ด ์น์ธ\", \"์กฐ๊ธฐ ์
๊ณ ์ ์, ์น์ธยท๋ฐ๋ ค ์ฌ์ \"),\n",
" (\"์ ๋นยท๋ถํ ์๋ชจ\", \"๊ต์ฒด ๋ถํ, ์๋, ์ ๋น ์์์๊ฐ\"), (\"๊ณ ์ฅ ์ค์ ์ฐ๊ฒฐ\", \"์ ๋น ๊ฑด ID๋ก ์ผ์ ์ด๋ ฅ๊ณผ ๊ณ ์ฅ ์ค์ ์ฐ๊ฒฐ\"),\n",
" (\"๋ถํ ์์ยท๊ฐ๋๋ฅ ์ง๊ณ\", \"ํ๋ชฉ๋ณ ์๋ชจ, ๊ฐ์ฉ๋(PBL ์งํ)\"), (\"์ฌํ๊ฒํ (AAR)\", \"์ค๊ฒฝ๋ณด ์์ธ, ์๊ณ ํ๋ผ๋ฏธํฐ ์กฐ์ \")]\n",
"\n",
"# 2) ์คํค๋ง(๋ฐ์ดํฐ ๋์
๋๋ฆฌ): ๊ทธ๋ฃน / ํ๋ / ์๋ฃํ / ๊ท์น / ์ค๋ช
\n",
"S = {\n",
" \"sensor\": {\n",
" \"unit_id\": (str, {\"pattern\": r\"^U\\d{2}$\"}, \"๊ฐ์ฒด ID(๊ฐ์ ๊ธฐ๋์ฒด)\"),\n",
" \"day\": (int, {\"min\": 0, \"max\": 260}, \"์ด์ฉ ๊ฒฝ๊ณผ์ผ(์ธก์ ์ผ)\"),\n",
" \"vib_rms\": (float, {\"min\": 0, \"max\": 10}, \"์ง๋ RMS(๊ฐ์ ๋จ์)\"),\n",
" \"vib_kurt\": (float, {\"min\": 1, \"max\": 30}, \"์ง๋ ์ฒจ๋(์ถฉ๊ฒฉ์ฑ ์งํ)\"),\n",
" \"temp\": (float, {\"min\": -30, \"max\": 150}, \"์์ง ์จ๋(์ญ์จ)\"),\n",
" \"press\": (float, {\"min\": 0, \"max\": 700}, \"์ค์ผ ์๋ ฅ(kPa)\"),\n",
" \"load\": (float, {\"min\": 0, \"max\": 1.2}, \"์ด์ฉ ๋ถํ์จ\"),\n",
" },\n",
" \"feature\": {\n",
" \"band_50\": (float, {\"min\": 0}, \"50Hz ๋ถ๊ทผ FFT ๋์ญ ์๋์ง\"),\n",
" \"band_165\": (float, {\"min\": 0}, \"165Hz ๋ถ๊ทผ(๋ฒ ์ด๋ง ๊ฒฐํจ ๋์ญ) ์๋์ง\"),\n",
" \"crest\": (float, {\"min\": 1, \"max\": 30}, \"ํ๊ณ ์จ(์ต๋๊ฐ/RMS)\"),\n",
" \"hf_ratio\": (float, {\"min\": 0}, \"๊ณ ์ฃผํ/๊ธฐ๋ณธ์ฃผํ ์๋์ง ๋น\"),\n",
" \"temp_res\": (float, {\"min\": -60, \"max\": 80}, \"๋ถํยท์ธ๊ธฐ ๋๋น ์จ๋ ์์ฐจ\"),\n",
" },\n",
" \"failure\": {\n",
" \"event_id\": (str, {\"pattern\": r\"^F\\d{3}$\"}, \"๊ณ ์ฅยท์ ๋น ๊ฑด ID(์ผ์ ์ด๋ ฅ๊ณผ ์ฐ๊ฒฐํ๋ ํค)\"),\n",
" \"mode\": (str, {\"enum\": list(MODES)}, \"์ดํ(๊ณ ์ฅ) ์ ํ\"),\n",
" \"life\": (int, {\"min\": 60, \"max\": 260}, \"๊ณ ์ฅ๊น์ง ์ด์ฉ์ผ์\"),\n",
" \"unplanned\": (bool, {}, \"๋น๊ณํ ์ ๋น ์ฌ๋ถ\"),\n",
" },\n",
" \"parts\": {\n",
" \"part_name\": (str, {\"enum\": [v[2] for v in MODES.values()]}, \"์๋ชจ ์๋ฆฌ๋ถ์\"),\n",
" \"qty\": (int, {\"min\": 1, \"max\": 10}, \"์๋ชจ ์๋\"),\n",
" \"unit_cost\": (float, {\"min\": 0, \"max\": 50}, \"๊ฐ์ ๋จ๊ฐ์ง์\"),\n",
" },\n",
" \"target\": {\n",
" \"rul\": (float, {\"min\": 0, \"max\": 260}, \"์์กด์๋ช
(์ผ) - ์์ธก๋์ 1\"),\n",
" \"anomaly_score\": (float, {\"min\": 0}, \"์ด์ ์ ์(์ฌ๊ตฌ์ฑ ์ค์ฐจ) - ์์ธก๋์ 2\"),\n",
" \"availability\": (float, {\"min\": 0, \"max\": 1}, \"๊ฐ์ฉ๋(๊ฐ๋์ผ/์ ์ฒด์ผ) - PBL ์ฑ๊ณผ ์งํ\"),\n",
" },\n",
"}\n",
"\n",
"\n",
"def check(name, v, typ, rule):\n",
" if not isinstance(v, typ) or (typ in (int, float) and isinstance(v, bool)):\n",
" return [f\"{name}: ์๋ฃํ ์ค๋ฅ({type(v).__name__})\"]\n",
" e = []\n",
" if \"enum\" in rule and v not in rule[\"enum\"]:\n",
" e.append(f\"{name}: ํ์ฉ๋์ง ์์ ๊ฐ '{v}'\")\n",
" if \"min\" in rule and v < rule[\"min\"]:\n",
" e.append(f\"{name}: ์ต์๊ฐ {rule['min']} ๋ฏธ๋ง\")\n",
" if \"max\" in rule and v > rule[\"max\"]:\n",
" e.append(f\"{name}: ์ต๋๊ฐ {rule['max']} ์ด๊ณผ\")\n",
" if \"pattern\" in rule and not re.match(rule[\"pattern\"], v):\n",
" e.append(f\"{name}: ํ์ ๋ถ์ผ์น '{v}'\")\n",
" return e\n",
"\n",
"\n",
"def validate(rec):\n",
" errs = []\n",
" for g, fields in S.items():\n",
" for k, (typ, rule, _) in fields.items():\n",
" if k not in rec.get(g, {}):\n",
" errs.append(f\"{g}.{k}: ํ์ ํ๋ ๋๋ฝ\")\n",
" else:\n",
" errs += check(f\"{g}.{k}\", rec[g][k], typ, rule)\n",
" return errs\n",
"\n",
"\n",
"# 3) [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์์ฑ ์๋ฃ์์ 20๊ฑด์ ๋ ์ฝ๋๋ก ๋ง๋ค์ด ๊ฒ์ฆ\n",
"units, snap, W = make_runs()\n",
"F = extract(W)\n",
"snap = pd.concat([snap, F], axis=1)\n",
"snap[\"temp_res\"] = snap[\"temp\"] - (55 + 22 * snap[\"load\"] + 0.5 * snap[\"ambient\"])\n",
"snap[\"hf_ratio\"] = (snap[\"b150\"] + snap[\"b165\"] + snap[\"b200\"]) / (snap[\"b50\"] + 1e-6)\n",
"ui = units.set_index(\"unit\")\n",
"recs = []\n",
"for i, r in enumerate(snap.sample(20, random_state=0).itertuples()):\n",
" m = r.mode\n",
" recs.append({\n",
" \"sensor\": {\"unit_id\": r.unit, \"day\": int(r.day), \"vib_rms\": float(round(r.rms, 3)), \"vib_kurt\": float(round(r.kurt, 2)),\n",
" \"temp\": float(round(r.temp, 1)), \"press\": float(round(r.press, 1)), \"load\": float(round(r.load, 2))},\n",
" \"feature\": {\"band_50\": float(round(r.b50, 4)), \"band_165\": float(round(r.b165, 4)), \"crest\": float(round(r.crest, 2)),\n",
" \"hf_ratio\": float(round(r.hf_ratio, 3)), \"temp_res\": float(round(r.temp_res, 2))},\n",
" \"failure\": {\"event_id\": f\"F{int(r.unit[1:]):03d}\", \"mode\": m, \"life\": int(ui.loc[r.unit, \"life\"]), \"unplanned\": bool(r.rul < 5)},\n",
" \"parts\": {\"part_name\": MODES[m][2], \"qty\": 1, \"unit_cost\": float(MODES[m][3])},\n",
" \"target\": {\"rul\": float(r.rul), \"anomaly_score\": float(round(r.deg, 3)), \"availability\": 0.92}})\n",
"ok = sum(not validate(r) for r in recs)\n",
"print(f\"๊ฐ์ ์ผ์ ์๋ฃ {len(snap)}๊ฑด(๊ฐ์ฒด {len(units)}๋), ํ๋ณธ {len(recs)}๊ฑด ์ค ๊ฒ์ฆ ํต๊ณผ {ok}๊ฑด, ์ค๋ฅ {len(recs) - ok}๊ฑด\")\n",
"print(f\"์ ์ฒด ํํ {W.shape[0]}๊ฐ x {W.shape[1]}ํ๋ณธ, ์จ๋ยท์๋ ฅ ๊ฒฐ์ธก {int(snap[['temp', 'press']].isna().sum().sum())}๊ฑด\")\n",
"\n",
"# 4) ์ค๋ฅ ์ฃผ์
์ํ\n",
"bad = json.loads(json.dumps(recs[0]))\n",
"bad[\"sensor\"][\"unit_id\"] = \"ENGINE-3\" # ID ํ์ ์ค๋ฅ\n",
"bad[\"sensor\"][\"temp\"] = 480.0 # ์ผ์ ๋ฒ์ ์ด๊ณผ(๋จ์ ํผ๋ ์์ฌ)\n",
"bad[\"failure\"][\"mode\"] = \"turbine\" # ์ ์๋์ง ์์ ์ดํ ์ ํ\n",
"del bad[\"parts\"][\"qty\"] # ํ์ ํ๋ ๋๋ฝ\n",
"bad[\"target\"][\"rul\"] = \"30์ผ\" # ์๋ฃํ ์ค๋ฅ\n",
"print(\"\\n[์ค๋ฅ ์ฃผ์
๊ฒฐ๊ณผ]\")\n",
"for e in validate(bad):\n",
" print(\" -\", e)\n",
"\n",
"# 5) ํ๋ก์ธ์ค ๋งต๊ณผ ๋ฐ์ดํฐ ๋์
๋๋ฆฌ\n",
"print(\"\\n[์
๋ฌด ํ๋ก์ธ์ค ๋งต]\")\n",
"print(\" -> \".join(f\"{i}.{p}\" for i, (p, _) in enumerate(PROCESS, 1)))\n",
"for p, d in PROCESS:\n",
" print(f\" {p:12s}: {d}\")\n",
"rows = [(g, k, t.__name__, d) for g, fl in S.items() for k, (t, r, d) in fl.items()]\n",
"dd = pd.DataFrame(rows, columns=[\"๊ทธ๋ฃน\", \"ํ๋\", \"์๋ฃํ\", \"์ค๋ช
\"])\n",
"print(f\"\\n[๋ฐ์ดํฐ ๋์
๋๋ฆฌ] ํผ์ฒ {len(dd)}๊ฐ (๋ชฉํ 20๊ฐ ์ด์: {'๋ฌ์ฑ' if len(dd) >= 20 else '๋ฏธ๋ฌ'})\")\n",
"print(dd.to_string(index=False))\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
01 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"๊ฐ์ ์ผ์ ์๋ฃ 7733๊ฑด(๊ฐ์ฒด 48๋), ํ๋ณธ 20๊ฑด ์ค ๊ฒ์ฆ ํต๊ณผ 20๊ฑด, ์ค๋ฅ 0๊ฑด\n",
"์ ์ฒด ํํ 7733๊ฐ x 256ํ๋ณธ, ์จ๋ยท์๋ ฅ ๊ฒฐ์ธก 0๊ฑด\n",
"\n",
"[์ค๋ฅ ์ฃผ์
๊ฒฐ๊ณผ]\n",
" - sensor.unit_id: ํ์ ๋ถ์ผ์น 'ENGINE-3'\n",
" - sensor.temp: ์ต๋๊ฐ 150 ์ด๊ณผ\n",
" - failure.mode: ํ์ฉ๋์ง ์์ ๊ฐ 'turbine'\n",
" - parts.qty: ํ์ ํ๋ ๋๋ฝ\n",
" - target.rul: ์๋ฃํ ์ค๋ฅ(str)\n",
"\n",
"[์
๋ฌด ํ๋ก์ธ์ค ๋งต]\n",
"1.์ผ์ ์์ง -> 2.์ ์ฒ๋ฆฌ -> 3.ํน์ง ์ถ์ถ -> 4.์ด์ ๊ฐ์ง -> 5.์์กด์๋ช
(RUL) ์์ธก -> 6.๊ฒ์ฌ๊ด ์น์ธ -> 7.์ ๋นยท๋ถํ ์๋ชจ -> 8.๊ณ ์ฅ ์ค์ ์ฐ๊ฒฐ -> 9.๋ถํ ์์ยท๊ฐ๋๋ฅ ์ง๊ณ -> 10.์ฌํ๊ฒํ (AAR)\n",
" ์ผ์ ์์ง : ๊ฐ์ฒด ID, ์ธก์ ์ผ, ์ง๋ ํํ, ์จ๋, ์๋ ฅ, ๋ถํ\n",
" ์ ์ฒ๋ฆฌ : ์ก์ ์ ๊ฑฐ, ๋จ์ยทํ๊ธฐ ํต์ผ, ๊ฒฐ์ธก ๋ณด์ \n",
" ํน์ง ์ถ์ถ : RMS, ์ฒจ๋, ํ๊ณ ์จ, FFT ๋์ญ ์๋์ง\n",
" ์ด์ ๊ฐ์ง : ์ด์ ์ ์, ๊ฒฝ๋ณด ๋จ๊ณ\n",
" ์์กด์๋ช
(RUL) ์์ธก: ๊ณ ์ฅ ์๋ฐ ์์ , ์ ๋ขฐ ๊ตฌ๊ฐ\n",
" ๊ฒ์ฌ๊ด ์น์ธ : ์กฐ๊ธฐ ์
๊ณ ์ ์, ์น์ธยท๋ฐ๋ ค ์ฌ์ \n",
" ์ ๋นยท๋ถํ ์๋ชจ : ๊ต์ฒด ๋ถํ, ์๋, ์ ๋น ์์์๊ฐ\n",
" ๊ณ ์ฅ ์ค์ ์ฐ๊ฒฐ : ์ ๋น ๊ฑด ID๋ก ์ผ์ ์ด๋ ฅ๊ณผ ๊ณ ์ฅ ์ค์ ์ฐ๊ฒฐ\n",
" ๋ถํ ์์ยท๊ฐ๋๋ฅ ์ง๊ณ: ํ๋ชฉ๋ณ ์๋ชจ, ๊ฐ์ฉ๋(PBL ์งํ)\n",
" ์ฌํ๊ฒํ (AAR) : ์ค๊ฒฝ๋ณด ์์ธ, ์๊ณ ํ๋ผ๋ฏธํฐ ์กฐ์ \n",
"\n",
"[๋ฐ์ดํฐ ๋์
๋๋ฆฌ] ํผ์ฒ 22๊ฐ (๋ชฉํ 20๊ฐ ์ด์: ๋ฌ์ฑ)\n",
" ๊ทธ๋ฃน ํ๋ ์๋ฃํ ์ค๋ช
\n",
" sensor unit_id str ๊ฐ์ฒด ID(๊ฐ์ ๊ธฐ๋์ฒด)\n",
" sensor day int ์ด์ฉ ๊ฒฝ๊ณผ์ผ(์ธก์ ์ผ)\n",
" sensor vib_rms float ์ง๋ RMS(๊ฐ์ ๋จ์)\n",
" sensor vib_kurt float ์ง๋ ์ฒจ๋(์ถฉ๊ฒฉ์ฑ ์งํ)\n",
" sensor temp float ์์ง ์จ๋(์ญ์จ)\n",
" sensor press float ์ค์ผ ์๋ ฅ(kPa)\n",
" sensor load float ์ด์ฉ ๋ถํ์จ\n",
"feature band_50 float 50Hz ๋ถ๊ทผ FFT ๋์ญ ์๋์ง\n",
"feature band_165 float 165Hz ๋ถ๊ทผ(๋ฒ ์ด๋ง ๊ฒฐํจ ๋์ญ) ์๋์ง\n",
"feature crest float ํ๊ณ ์จ(์ต๋๊ฐ/RMS)\n",
"feature hf_ratio float ๊ณ ์ฃผํ/๊ธฐ๋ณธ์ฃผํ ์๋์ง ๋น\n",
"feature temp_res float ๋ถํยท์ธ๊ธฐ ๋๋น ์จ๋ ์์ฐจ\n",
"failure event_id str ๊ณ ์ฅยท์ ๋น ๊ฑด ID(์ผ์ ์ด๋ ฅ๊ณผ ์ฐ๊ฒฐํ๋ ํค)\n",
"failure mode str ์ดํ(๊ณ ์ฅ) ์ ํ\n",
"failure life int ๊ณ ์ฅ๊น์ง ์ด์ฉ์ผ์\n",
"... (์ดํ ์๋ต)\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
02. ๊ณ ์ฃผํ ์ผ์ ์ ํธ ์ก์ ์ ๊ฑฐ, FFT ๋ณํ ๋ฐ ์ฃผ์ ํน์ง๋(RMS, Kurtosis) ์ ์ฒ๋ฆฌ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ผ์ ์ก์ ์ ๊ฑฐ, FFT ๋ณํ ๋ฐ ์ฃผ์ ํน์ง๋(RMS ๋ฑ) ์ถ์ถ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๊ณ ์ฃผํ ์ผ์ ์ ํธ์ ์ก์์ ์ ๊ฑฐํ๊ณ FFT ๋ณํ์ผ๋ก RMS, ์ฒจ๋ ๋ฑ ์ฃผ์ ํน์ง๋์ ์ถ์ถํ๋ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **ํ๊ธฐยท๋จ์(์จ๋ F/C, ์๋ ฅ psi/kPa) ํต์ผ, ์ค๋ณต ์ ๊ฑฐ, ๋ถ๊ฐ๋ฅ๊ฐ ๊ฒฐ์ธก ์ฒ๋ฆฌ, ํํ ๋จ๋ฐ ์คํ์ดํฌ์ ์ค์๊ฐ ํํฐ ์ ๊ฑฐ, ๊ฐ์ญ ์ก์ ์ฐฝ์ ์ ์ญํต๊ณผ ํํฐ, KNN ๊ฒฐ์ธก ๋ณด์ ๋ฑ ์ ์ ๊ท์น 10์ข
์ ์ ์ฉ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/etl/mnt04_data_cleansing.py ๋ฐ ์ ์ ํ์ง ๋ณด๊ณ ์` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ตฐ์ ํน์ ์์ธ(๋น๊ณํ ์ ๋น, ๊ธด๊ธ ๋ถ์ถ ๋ฑ) ๋ฐ์ดํฐ ๋ณด์กด์ ํ๋น์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๋ฐ์ดํฐ ์ ์ ๊ท์น 10์ข
์ ์ฉ, ๋ฐ์ดํฐ ์ ํฉ์ฑ ๊ฒ์ฆ ํต๊ณผ์จ 100%\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ETL ํ์ดํ๋ผ์ธ ๊ตฌํ ๋ฐ ์ ์ ๋ ๋ง์คํฐ ๋ฐ์ดํฐ์
๊ตฌ์ถ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ์ ํ ๋ฐ์ดํฐ ํต๊ณ๋(๊ธฐ์ ํต๊ณ) ๋น๊ต ๋ฐ ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"ํ๊ธฐ ๋ถ์ผ์นยท์ค๋ณตยท๋จ์ ํผ์ฌยท๋ถ๊ฐ๋ฅ๊ฐยท๊ฒฐ์ธกยท๋จ๋ฐ ์คํ์ดํฌ๊ฐ ์์ธ ๊ฐ์ ์ผ์ ์์ฒ ์๋ฃ์ ์ ์ ๊ท์น 10์ข
(ํํ ์คํ์ดํฌ ์ ๊ฑฐ, ์ ์ญํต๊ณผ ์ก์ ์ ๊ฑฐ, KNN ๊ฒฐ์ธก ๋ณด์ ํฌํจ)์ ์ ์ฉํ๊ณ , FFT ๋ณํ์ผ๋ก RMSยท์ฒจ๋ ๋ฑ ์ฃผ์ ํน์ง๋์ ์ถ์ถํ๋ค. ๊ณ ์ฅ ์๋ฐ ์์ ์ ๊ธด๊ธ ์ ๊ฒ ์ธก์ ์ ๋ณด์กดํ๋ฉฐ ์ ํฉ์ฑ ๊ฒ์ฆ ํต๊ณผ์จ๊ณผ ์ ์ ์ ํ ํต๊ณ๋ฅผ ํ์ธํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy.stats import kurtosis\n",
"from scipy.signal import medfilt, butter, sosfiltfilt\n",
"from sklearn.impute import KNNImputer\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๊ธฐ๋์ฒด 48๋์ ์์งยท๊ตฌ๋๊ณ ์ง๋ยท์จ๋ยท์๋ ฅ ์ผ์ ์๋ฃ ์์ฑ๊ธฐ (์ค์ ์ฅ๋นยท๋ถ๋ยท์ ๋น ์ค์ ์์)\n",
"# ๊ฐ์ฒด๋ง๋ค ์ ์ -> ์ดํ -> ๊ณ ์ฅ(์๋ช
์ข
๋ฃ)๊น์ง ํ๋ฃจ 1ํ ์ธก์ (์ง๋ ํํ 256ํ๋ณธ + ์จ๋ + ์๋ ฅ + ๋ถํ)์ ๋ง๋ ๋ค.\n",
"# ์ดํ ์ ํ 4๊ฐ์ง: bearing(๋ฒ ์ด๋ง), gear(๊ธฐ์ด), oil_pump(์ค์ผ ํํ), cooling(๋๊ฐ ํฌ ๋ชจํฐ)\n",
"# ---------------------------------------------------------------------------\n",
"FS, NS = 1024, 256 # ๊ฐ์ ํ๋ณธํ ์ฃผํ์(Hz), ํํ 1๊ฐ๋น ํ๋ณธ ์\n",
"TT = np.arange(NS) / FS\n",
"MODES = { # ์ดํ ์ ํ: (๋ฐ์ ํ๋ฅ , ํ๊ท ์๋ช
(์ผ), ๊ต์ฒด ์๋ฆฌ๋ถ์, ๊ฐ์ ๋จ๊ฐ์ง์)\n",
" \"bearing\": (0.40, 160, \"์ฃผ ๋ฒ ์ด๋ง ์ธํธ\", 6.0), \"gear\": (0.25, 175, \"๊ตฌ๋ ๊ธฐ์ด ์ธํธ\", 9.0),\n",
" \"oil_pump\": (0.20, 150, \"์ค์ผ ํํ\", 4.0), \"cooling\": (0.15, 140, \"๋๊ฐ ํฌ ๋ชจํฐ\", 3.0)}\n",
"KIT_COST = 1.0 # ๋ชจ๋ ์ ๋น์ ํจ๊ป ์๋ชจ๋๋ ์ฐยทํํฐ ํคํธ ๋จ๊ฐ์ง์\n",
"BANDS = {\"b50\": (40, 60), \"b100\": (90, 110), \"b150\": (140, 156), \"b165\": (156, 176), \"b200\": (190, 215)}\n",
"\n",
"\n",
"def make_runs(n=48, seed=1):\n",
" \"\"\"๋ฐํ: units(๊ฐ์ฒด ์ ๋ณด), snap(์ผ๋ณ ์ธก์ ์์ฝ), W(์ง๋ ํํ [์ธก์ ์, 256])\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" names = list(MODES)\n",
" mode = rng.choice(names, n, p=[MODES[m][0] for m in names])\n",
" life = np.clip(np.array([rng.normal(MODES[m][1], 18) for m in mode]), 90, 230).astype(int)\n",
" start = np.sort(rng.integers(0, 240, n))\n",
" units = pd.DataFrame({\"unit\": [f\"U{i:02d}\" for i in range(n)], \"mode\": mode, \"life\": life, \"start\": start, \"base_load\": rng.uniform(0.4, 0.75, n)})\n",
" rows = []\n",
" for u in units.itertuples():\n",
" d = np.arange(u.life + 1)\n",
" onset = 0.45 * u.life\n",
" deg = np.clip((d - onset) / (u.life - onset), 0, 1) ** 1.5\n",
" load = np.clip(u.base_load + rng.normal(0, 0.06, len(d)), 0.2, 1.0)\n",
" hi = rng.random(len(d)) < 0.07 # ๊ณ ๋ถํ ์ด์ฉ์ผ(์ ์์ด์ง๋ง ์ง๋ยท์จ๋ ์์น)\n",
" load = np.where(hi, rng.uniform(0.95, 1.1, len(d)), load)\n",
" rows.append(pd.DataFrame({\"unit\": u.unit, \"day\": d, \"cal\": u.start + d, \"rul\": u.life - d, \"deg\": deg, \"load\": load, \"hi_load\": hi, \"mode\": u.mode}))\n",
" snap = pd.concat(rows, ignore_index=True)\n",
" S = len(snap)\n",
" snap[\"ambient\"] = 15 + 12 * np.sin(2 * np.pi * snap[\"cal\"] / 365) + rng.normal(0, 2, S)\n",
" dg, ld, md = snap[\"deg\"].values, snap[\"load\"].values, snap[\"mode\"].values\n",
" isb, isg, iso, isc = (md == \"bearing\"), (md == \"gear\"), (md == \"oil_pump\"), (md == \"cooling\")\n",
" snap[\"temp\"] = 55 + 22 * ld + 0.5 * snap[\"ambient\"] + 9 * dg * iso + 26 * dg * isc + 3 * dg * (isb | isg) + rng.normal(0, 1.5, S)\n",
" snap[\"press\"] = 450 - 12 * ld - 85 * dg * iso - 15 * dg * isc + rng.normal(0, 6, S)\n",
" ph = rng.uniform(0, 2 * np.pi, (S, 3))\n",
" a1, a2 = 1.0 * (0.5 + ld), 0.4 * (0.5 + ld)\n",
" W = (a1[:, None] * np.sin(2 * np.pi * 50 * TT + ph[:, :1]) + a2[:, None] * np.sin(2 * np.pi * 100 * TT + ph[:, 1:2])\n",
" + rng.normal(0, 0.35, (S, NS)))\n",
" W += (0.9 * dg * isb)[:, None] * np.sin(2 * np.pi * 165 * TT + ph[:, 2:3])\n",
" W += (0.8 * dg * isg)[:, None] * (np.sin(2 * np.pi * 150 * TT + ph[:, 2:3]) + 0.8 * np.sin(2 * np.pi * 100 * TT))\n",
" W += (0.5 * dg * iso)[:, None] * np.sin(2 * np.pi * 200 * TT + ph[:, 2:3])\n",
" k = rng.poisson(10 * dg * isb) # ๋ฒ ์ด๋ง ์ดํ: ์ถฉ๊ฒฉ์ฑ ํ์ค(์ฒจ๋ ์์น)\n",
" pos, amp = rng.integers(0, NS, (S, 30)), rng.normal(0, 3.0, (S, 30)) * dg[:, None]\n",
" amp = amp * (np.arange(30)[None, :] < k[:, None])\n",
" np.add.at(W, (np.repeat(np.arange(S), 30), pos.ravel()), amp.ravel())\n",
" return units, snap, W\n",
"\n",
"\n",
"def extract(W):\n",
" \"\"\"์ง๋ ํํ -> ํน์ง๋: RMS, ์ฒจ๋, ํ๊ณ ์จ, ์ต๋-์ต์, FFT ๋์ญ ์๋์ง(50/100/150/165/200Hz ๋ถ๊ทผ)\"\"\"\n",
" x = W - W.mean(axis=1, keepdims=True)\n",
" rms = np.sqrt((x ** 2).mean(axis=1))\n",
" out = {\"rms\": rms, \"kurt\": kurtosis(x, axis=1, fisher=False), \"crest\": np.abs(x).max(axis=1) / rms, \"p2p\": x.max(axis=1) - x.min(axis=1)}\n",
" A = np.abs(np.fft.rfft(x * np.hanning(NS), axis=1)) / NS\n",
" f = np.fft.rfftfreq(NS, 1 / FS)\n",
" for nm, (lo, hi) in BANDS.items():\n",
" out[nm] = np.sqrt((A[:, (f >= lo) & (f < hi)] ** 2).sum(axis=1))\n",
" return pd.DataFrame(out)\n",
"\n",
"# 1) [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์์ฒ ์๋ฃ: ์์ฑ ์๋ฃ์ ์ผ๋ถ๋ฌ ์ค๋ฅ๋ฅผ ์๋๋ค\n",
"units, snap, W0 = make_runs()\n",
"rng = np.random.default_rng(2)\n",
"S = len(snap)\n",
"W = W0.copy()\n",
"raw = pd.DataFrame({\"unit\": snap[\"unit\"].values, \"date\": [(pd.Timestamp(\"2026-01-01\") + pd.Timedelta(days=int(c))).strftime(\"%Y-%m-%d\") for c in snap[\"cal\"]],\n",
" \"temp\": snap[\"temp\"].round(1).values, \"temp_unit\": \"C\", \"press\": snap[\"press\"].round(1).values, \"press_unit\": \"kPa\",\n",
" \"load\": snap[\"load\"].round(2).values, \"emergency\": snap[\"rul\"].values <= 3})\n",
"idx = rng.choice(S, 1500, replace=False)\n",
"a, b, c, d_, e, f, g, h_ = np.split(idx, [150, 350, 600, 750, 800, 850, 1050])\n",
"raw.loc[a, \"unit\"] = rng.choice([\"u\", \" U\", \"unit-\"], len(a)) + raw.loc[a, \"unit\"].str[1:] # ID ํ๊ธฐ ๋ถ์ผ์น\n",
"raw.loc[b, \"date\"] = pd.to_datetime(raw.loc[b, \"date\"]).dt.strftime(\"%Y/%m/%d\") # ๋ ์ง ํ์ ํผ์ฌ\n",
"raw.loc[c, \"temp\"] = (raw.loc[c, \"temp\"] * 9 / 5 + 32).round(1); raw.loc[c, \"temp_unit\"] = \"F\" # ์จ๋ ๋จ์ ํผ์ฌ\n",
"raw.loc[d_, \"press\"] = (raw.loc[d_, \"press\"] / 6.895).round(1); raw.loc[d_, \"press_unit\"] = \"psi\" # ์๋ ฅ ๋จ์ ํผ์ฌ\n",
"raw.loc[e, \"temp\"] = -999.0; raw.loc[f, \"press\"] = -5.0 # ๋ถ๊ฐ๋ฅ ๊ฐ\n",
"raw.loc[g, [\"temp\", \"press\"]] = np.nan # ๊ฒฐ์ธก\n",
"sp = h_[:200] # ํํ ๋จ๋ฐ ์คํ์ดํฌ(์ผ์ ๊ธ๋ฆฌ์น)\n",
"for i in sp:\n",
" W[i, rng.integers(0, NS, 2)] += rng.choice([-9.0, 9.0], 2)\n",
"nz = h_[200:] # ์ ์๊ธฐ ๊ฐ์ญ: ํํ ์ ์ฒด ์ก์ ์ฆ๊ฐ\n",
"W[nz] += rng.normal(0, 0.9, (len(nz), NS))\n",
"raw = pd.concat([raw, raw.sample(80, random_state=1)], ignore_index=True) # ์ค๋ณต 80๊ฑด\n",
"print(f\"์์ฒ ์๋ฃ {len(raw)}ํ (์ ์ {S}ํ + ์ค๋ณต 80ํ, ์ค๋ฅ ์ฃผ์
{len(idx)}ํ, ํํ ์คํ์ดํฌ {len(sp)}๊ฐยท๊ฐ์ญ {len(nz)}๊ฐ ํฌํจ)\")\n",
"\n",
"# 2) ์ ์ ๊ท์น 10์ข
(ํ ๊ท์น R1~R6, ๊ฒฐ์ธก R9, ์์ธ R10 / ํํ ๊ท์น R7~R8)\n",
"df, applied = raw.iloc[:S].copy(), []\n",
"def rule(name, n):\n",
" applied.append((name, int(n)))\n",
"\n",
"rule(\"R1 ์ค๋ณต ํ ์ ๊ฑฐ\", raw.duplicated().sum())\n",
"uid = \"U\" + df[\"unit\"].str.extract(r\"(\\d+)\")[0]; rule(\"R2 ๊ฐ์ฒด ID ํ๊ธฐ ํต์ผ\", (uid != df[\"unit\"]).sum()); df[\"unit\"] = uid\n",
"t1 = pd.to_datetime(df[\"date\"], format=\"%Y-%m-%d\", errors=\"coerce\"); t2 = pd.to_datetime(df[\"date\"], format=\"%Y/%m/%d\", errors=\"coerce\")\n",
"rule(\"R3 ์ธก์ ์ผ ํ์ ํต์ผ\", t1.isna().sum()); df[\"date\"] = t1.fillna(t2)\n",
"fm = df[\"temp_unit\"] == \"F\"; df.loc[fm, \"temp\"] = ((df.loc[fm, \"temp\"] - 32) * 5 / 9).round(1); df[\"temp_unit\"] = \"C\"; rule(\"R4 ์จ๋ ๋จ์ ํต์ผ(F -> C)\", fm.sum())\n",
"pm = df[\"press_unit\"] == \"psi\"; df.loc[pm, \"press\"] = (df.loc[pm, \"press\"] * 6.895).round(1); df[\"press_unit\"] = \"kPa\"; rule(\"R5 ์๋ ฅ ๋จ์ ํต์ผ(psi -> kPa)\", pm.sum())\n",
"bt, bp = ~df[\"temp\"].between(-30, 150) & df[\"temp\"].notna(), (df[\"press\"] < 0) & df[\"press\"].notna()\n",
"df.loc[bt, \"temp\"] = np.nan; df.loc[bp, \"press\"] = np.nan; rule(\"R6 ๋ถ๊ฐ๋ฅ ๊ฐ ๊ฒฐ์ธก ์ฒ๋ฆฌ\", bt.sum() + bp.sum())\n",
"Wc = W.copy()\n",
"glitch = np.abs(Wc - np.median(Wc, axis=1, keepdims=True)).max(axis=1) > 8 # ๋จ๋ฐ ์คํ์ดํฌ ์ฐฝ\n",
"for i in np.where(glitch)[0]:\n",
" Wc[i] = medfilt(Wc[i], 3)\n",
"rule(\"R7 ํํ ๋จ๋ฐ ์คํ์ดํฌ ์ ๊ฑฐ(์ค์๊ฐ ํํฐ)\", glitch.sum())\n",
"sos = butter(4, 250, fs=FS, output=\"sos\")\n",
"hf = (Wc - sosfiltfilt(sos, Wc, axis=1)).std(axis=1) # 250Hz ์ด์ ์ฑ๋ถ์ ํฌ๊ธฐ\n",
"noisy = hf > 0.4 # ๊ฐ์ญ ์ก์ ์ฐฝ\n",
"Wc[noisy] = sosfiltfilt(sos, Wc[noisy], axis=1); rule(\"R8 ๊ฐ์ญ ์ก์ ์ฐฝ ์ ์ญํต๊ณผ(250Hz) ํํฐ\", noisy.sum())\n",
"miss = df[[\"temp\", \"press\"]].isna().sum().sum()\n",
"X = df.assign(un=df[\"unit\"].str[1:].astype(int) / 50)[[\"temp\", \"press\", \"load\", \"un\"]]\n",
"df[[\"temp\", \"press\"]] = KNNImputer(n_neighbors=5).fit_transform(X)[:, :2]; rule(\"R9 ์ผ์ ๊ฒฐ์ธก KNN ๋ณด์ \", miss)\n",
"df[\"rms\"] = np.sqrt(((Wc - Wc.mean(axis=1, keepdims=True)) ** 2).mean(axis=1)); lim = df[\"rms\"].quantile(0.99)\n",
"df[\"outlier_flag\"] = (df[\"rms\"] > lim) & ~df[\"emergency\"]; rule(\"R10 ๊ธด๊ธ ์ ๊ฒ ์ธก์ ๋ณด์กด(์ผ๋ฐ ์ด์๊ฐ์ ํ์๋ง)\", (df[\"rms\"] > lim).sum() - df[\"outlier_flag\"].sum())\n",
"print(\"\\n[์ ์ฉ ๊ท์น๊ณผ ์ฒ๋ฆฌ ๊ฑด์]\")\n",
"for nme, n in applied:\n",
" print(f\" {nme:36s} {n:5d}๊ฑด\")\n",
"\n",
"# 3) ์ ํฉ์ฑ ๊ฒ์ฆ\n",
"checks = {\"์ค๋ณต 0๊ฑด\": not df.duplicated([\"unit\", \"date\"]).any(), \"ID ํ์\": df[\"unit\"].str.match(r\"^U\\d{2}$\").all(),\n",
" \"๋ ์ง ๋ณํ\": df[\"date\"].notna().all(), \"์จ๋ ๋จ์ C\": (df[\"temp_unit\"] == \"C\").all(), \"์๋ ฅ ๋จ์ kPa\": (df[\"press_unit\"] == \"kPa\").all(),\n",
" \"์จ๋ ๋ฒ์\": df[\"temp\"].between(-30, 150).all(), \"์๋ ฅ 0 ์ด์\": (df[\"press\"] >= 0).all(), \"๊ฒฐ์ธก 0๊ฑด\": df[[\"temp\", \"press\"]].notna().all().all(),\n",
" \"ํํ ์คํ์ดํฌ ์์กด 0๊ฐ\": not (np.abs(Wc - np.median(Wc, axis=1, keepdims=True)).max(axis=1) > 8).any(),\n",
" f\"๊ธด๊ธ ์ ๊ฒ {int(df['emergency'].sum())}๊ฑด ๋ณด์กด\": int(df[\"emergency\"].sum()) == int(snap[\"rul\"].le(3).sum())}\n",
"rate = np.mean(list(checks.values()))\n",
"print(f\"\\n์ ํฉ์ฑ ๊ฒ์ฆ {sum(checks.values())}/{len(checks)} ํต๊ณผ, ํต๊ณผ์จ {rate:.0%} (๋ชฉํ 100%: {'๋ฌ์ฑ' if rate == 1 else '๋ฏธ๋ฌ'})\")\n",
"for k, v in checks.items():\n",
" if not v:\n",
" print(\" ์คํจ:\", k)\n",
"\n",
"# 4) FFT ํน์ง๋: ์ ์ ํจ๊ณผ(๊นจ๋ํ ์๋ณธ ํํ ๋๋น ์ค์ฐจ)\n",
"def feats(Wx):\n",
" x = Wx - Wx.mean(axis=1, keepdims=True)\n",
" return np.sqrt((x ** 2).mean(axis=1)), kurtosis(x, axis=1, fisher=False)\n",
"r0, k0 = feats(W0); r1, k1 = feats(W); r2, k2 = feats(Wc)\n",
"bad = glitch | noisy\n",
"print(\"\\n[ํน์ง๋ ์ ์ ํจ๊ณผ] ์คํ์ดํฌยท๊ฐ์ญ์ด ์๋ ์ฐฝ \" + f\"{int(bad.sum())}๊ฐ์ ๊นจ๋ํ ์๋ณธ ๋๋น ํ๊ท ์ค์ฐจ\")\n",
"print(f\" RMS ์ ์ ์ {np.abs(r1 - r0)[bad].mean():.3f} -> ์ ์ ํ {np.abs(r2 - r0)[bad].mean():.3f}\")\n",
"print(f\" ์ฒจ๋ ์ ์ ์ {np.abs(k1 - k0)[bad].mean():.2f} -> ์ ์ ํ {np.abs(k2 - k0)[bad].mean():.2f}\")\n",
"cmp = pd.DataFrame({\"์ ์ ์ \": [len(raw), pd.to_numeric(raw[\"temp\"], errors=\"coerce\").mean(), raw[\"temp\"].min(), raw[\"press\"].min(), raw[\"press\"].isna().sum()],\n",
" \"์ ์ ํ\": [len(df), df[\"temp\"].mean(), df[\"temp\"].min(), df[\"press\"].min(), df[\"press\"].isna().sum()]},\n",
" index=[\"ํ ์\", \"์จ๋ ํ๊ท \", \"์จ๋ ์ต์\", \"์๋ ฅ ์ต์\", \"์๋ ฅ ๊ฒฐ์ธก\"]).round(1)\n",
"print(\"\\n[์ ์ ์ ํ ๊ธฐ์ ํต๊ณ]\"); print(cmp.to_string())\n",
"\n",
"i = int(np.where(glitch)[0][0])\n",
"fig, ax = plt.subplots(1, 2, figsize=(10, 3.5))\n",
"ax[0].plot(W[i], label=\"raw\", alpha=0.6); ax[0].plot(Wc[i], label=\"cleaned\"); ax[0].set_title(\"Waveform with glitch\"); ax[0].legend()\n",
"f = np.fft.rfftfreq(NS, 1 / FS); ax[1].plot(f, np.abs(np.fft.rfft(Wc[i])) / NS); ax[1].set_title(\"FFT spectrum (cleaned)\"); ax[1].set_xlabel(\"Hz\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
02 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"์์ฒ ์๋ฃ 7813ํ (์ ์ 7733ํ + ์ค๋ณต 80ํ, ์ค๋ฅ ์ฃผ์
1500ํ, ํํ ์คํ์ดํฌ 200๊ฐยท๊ฐ์ญ 250๊ฐ ํฌํจ)\n",
"\n",
"[์ ์ฉ ๊ท์น๊ณผ ์ฒ๋ฆฌ ๊ฑด์]\n",
" R1 ์ค๋ณต ํ ์ ๊ฑฐ 80๊ฑด\n",
" R2 ๊ฐ์ฒด ID ํ๊ธฐ ํต์ผ 150๊ฑด\n",
" R3 ์ธก์ ์ผ ํ์ ํต์ผ 200๊ฑด\n",
" R4 ์จ๋ ๋จ์ ํต์ผ(F -> C) 250๊ฑด\n",
" R5 ์๋ ฅ ๋จ์ ํต์ผ(psi -> kPa) 150๊ฑด\n",
" R6 ๋ถ๊ฐ๋ฅ ๊ฐ ๊ฒฐ์ธก ์ฒ๋ฆฌ 100๊ฑด\n",
" R7 ํํ ๋จ๋ฐ ์คํ์ดํฌ ์ ๊ฑฐ(์ค์๊ฐ ํํฐ) 207๊ฑด\n",
" R8 ๊ฐ์ญ ์ก์ ์ฐฝ ์ ์ญํต๊ณผ(250Hz) ํํฐ 347๊ฑด\n",
" R9 ์ผ์ ๊ฒฐ์ธก KNN ๋ณด์ 500๊ฑด\n",
" R10 ๊ธด๊ธ ์ ๊ฒ ์ธก์ ๋ณด์กด(์ผ๋ฐ ์ด์๊ฐ์ ํ์๋ง) 11๊ฑด\n",
"\n",
"์ ํฉ์ฑ ๊ฒ์ฆ 10/10 ํต๊ณผ, ํต๊ณผ์จ 100% (๋ชฉํ 100%: ๋ฌ์ฑ)\n",
"\n",
"[ํน์ง๋ ์ ์ ํจ๊ณผ] ์คํ์ดํฌยท๊ฐ์ญ์ด ์๋ ์ฐฝ 554๊ฐ์ ๊นจ๋ํ ์๋ณธ ๋๋น ํ๊ท ์ค์ฐจ\n",
" RMS ์ ์ ์ 0.267 -> ์ ์ ํ 0.116\n",
" ์ฒจ๋ ์ ์ ์ 8.31 -> ์ ์ ํ 0.86\n",
"\n",
"[์ ์ ์ ํ ๊ธฐ์ ํต๊ณ]\n",
" ์ ์ ์ ์ ์ ํ\n",
"ํ ์ 7813.0 7733.0\n",
"์จ๋ ํ๊ท 73.2 77.3\n",
"์จ๋ ์ต์ -999.0 61.5\n",
"์๋ ฅ ์ต์ -5.0 342.8\n",
"์๋ ฅ ๊ฒฐ์ธก 201.0 0.0\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
03. ์๊ณ๊ฐ ์ด๊ณผ ๊ธฐ๋ฐ 1์ฐจ ์ผ์ ์ด์ ์งํ ๊ฐ์ง Baseline ์๊ณ ๋ฆฌ์ฆ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์๊ณ๊ฐ ์ด๊ณผ ๊ธฐ๋ฐ 1์ฐจ ์ผ์ ์ด์ ์งํ ๊ฐ์ง Baseline ์๊ณ ๋ฆฌ์ฆ์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์๊ณ๊ฐ ์ด๊ณผ ๊ธฐ๋ฐ 1์ฐจ ์ผ์ ์ด์ ์งํ ๊ฐ์ง Baseline์ ๊ตฌํํ๊ณ , ํ์์ ๋ถ์์ผ๋ก ๊ณ ์ ์๊ณ๊ฐ์ ํ๊ณ์ ์๋ฆฌ๋ถ์ ์๋ชจ์ ํต์ฌ ๊ด๋ฆฌ ๋์์ ์ฐพ๋๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ ์ ์๋ฃ ๋ถํฌ๋ก RMS, ์ฒจ๋, ์จ๋, ์๋ ฅ ์๊ณ๊ฐ์ ์ ํ๊ณ ํ๋๋ผ๋ ๋์ผ๋ฉด ์ด์์ผ๋ก ๋ณด๋ Baseline์ ํ์ง์จ, ์ ๋ฐ๋, ๊ณ ๋ถํ ์ด์ฉ์ผ ์ค๊ฒฝ๋ณด์จ, ๊ณ ์ฅ ์ ํ๋ณด ์๊ฐ์ ์ธก์ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `notebooks/mnt04_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": [
"\"\"\"์๊ณ๊ฐ ์ด๊ณผ(์ง๋ RMSยท์ฒจ๋ยท์จ๋ยท์๋ ฅ) ๊ธฐ๋ฐ 1์ฐจ ์ผ์ ์ด์ ์งํ ๊ฐ์ง Baseline ์๊ณ ๋ฆฌ์ฆ์ ๊ตฌํํด ํ์ง์จยท์ค๊ฒฝ๋ณด์จยท๊ณ ์ฅ ์ ํ๋ณด ์๊ฐ์ ํ์ธํ๊ณ , ํ์์ ๋ถ์(EDA ์ฐจํธ 8์ข
, ํผ์ฒ์ ์์กด์๋ช
์ ์๊ด๊ณ์ยทp-value, ์๋ฆฌ๋ถ์ ์๋ชจ ํ๋ ํ ABC ๋ถ๋ฅ)์ผ๋ก ํต์ฌ ๊ด๋ฆฌ ๋์์ ์๋ณํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy.stats import kurtosis, spearmanr\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๊ธฐ๋์ฒด 48๋์ ์์งยท๊ตฌ๋๊ณ ์ง๋ยท์จ๋ยท์๋ ฅ ์ผ์ ์๋ฃ ์์ฑ๊ธฐ (์ค์ ์ฅ๋นยท๋ถ๋ยท์ ๋น ์ค์ ์์)\n",
"# ๊ฐ์ฒด๋ง๋ค ์ ์ -> ์ดํ -> ๊ณ ์ฅ(์๋ช
์ข
๋ฃ)๊น์ง ํ๋ฃจ 1ํ ์ธก์ (์ง๋ ํํ 256ํ๋ณธ + ์จ๋ + ์๋ ฅ + ๋ถํ)์ ๋ง๋ ๋ค.\n",
"# ์ดํ ์ ํ 4๊ฐ์ง: bearing(๋ฒ ์ด๋ง), gear(๊ธฐ์ด), oil_pump(์ค์ผ ํํ), cooling(๋๊ฐ ํฌ ๋ชจํฐ)\n",
"# ---------------------------------------------------------------------------\n",
"FS, NS = 1024, 256 # ๊ฐ์ ํ๋ณธํ ์ฃผํ์(Hz), ํํ 1๊ฐ๋น ํ๋ณธ ์\n",
"TT = np.arange(NS) / FS\n",
"MODES = { # ์ดํ ์ ํ: (๋ฐ์ ํ๋ฅ , ํ๊ท ์๋ช
(์ผ), ๊ต์ฒด ์๋ฆฌ๋ถ์, ๊ฐ์ ๋จ๊ฐ์ง์)\n",
" \"bearing\": (0.40, 160, \"์ฃผ ๋ฒ ์ด๋ง ์ธํธ\", 6.0), \"gear\": (0.25, 175, \"๊ตฌ๋ ๊ธฐ์ด ์ธํธ\", 9.0),\n",
" \"oil_pump\": (0.20, 150, \"์ค์ผ ํํ\", 4.0), \"cooling\": (0.15, 140, \"๋๊ฐ ํฌ ๋ชจํฐ\", 3.0)}\n",
"KIT_COST = 1.0 # ๋ชจ๋ ์ ๋น์ ํจ๊ป ์๋ชจ๋๋ ์ฐยทํํฐ ํคํธ ๋จ๊ฐ์ง์\n",
"BANDS = {\"b50\": (40, 60), \"b100\": (90, 110), \"b150\": (140, 156), \"b165\": (156, 176), \"b200\": (190, 215)}\n",
"\n",
"\n",
"def make_runs(n=48, seed=1):\n",
" \"\"\"๋ฐํ: units(๊ฐ์ฒด ์ ๋ณด), snap(์ผ๋ณ ์ธก์ ์์ฝ), W(์ง๋ ํํ [์ธก์ ์, 256])\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" names = list(MODES)\n",
" mode = rng.choice(names, n, p=[MODES[m][0] for m in names])\n",
" life = np.clip(np.array([rng.normal(MODES[m][1], 18) for m in mode]), 90, 230).astype(int)\n",
" start = np.sort(rng.integers(0, 240, n))\n",
" units = pd.DataFrame({\"unit\": [f\"U{i:02d}\" for i in range(n)], \"mode\": mode, \"life\": life, \"start\": start, \"base_load\": rng.uniform(0.4, 0.75, n)})\n",
" rows = []\n",
" for u in units.itertuples():\n",
" d = np.arange(u.life + 1)\n",
" onset = 0.45 * u.life\n",
" deg = np.clip((d - onset) / (u.life - onset), 0, 1) ** 1.5\n",
" load = np.clip(u.base_load + rng.normal(0, 0.06, len(d)), 0.2, 1.0)\n",
" hi = rng.random(len(d)) < 0.07 # ๊ณ ๋ถํ ์ด์ฉ์ผ(์ ์์ด์ง๋ง ์ง๋ยท์จ๋ ์์น)\n",
" load = np.where(hi, rng.uniform(0.95, 1.1, len(d)), load)\n",
" rows.append(pd.DataFrame({\"unit\": u.unit, \"day\": d, \"cal\": u.start + d, \"rul\": u.life - d, \"deg\": deg, \"load\": load, \"hi_load\": hi, \"mode\": u.mode}))\n",
" snap = pd.concat(rows, ignore_index=True)\n",
" S = len(snap)\n",
" snap[\"ambient\"] = 15 + 12 * np.sin(2 * np.pi * snap[\"cal\"] / 365) + rng.normal(0, 2, S)\n",
" dg, ld, md = snap[\"deg\"].values, snap[\"load\"].values, snap[\"mode\"].values\n",
" isb, isg, iso, isc = (md == \"bearing\"), (md == \"gear\"), (md == \"oil_pump\"), (md == \"cooling\")\n",
" snap[\"temp\"] = 55 + 22 * ld + 0.5 * snap[\"ambient\"] + 9 * dg * iso + 26 * dg * isc + 3 * dg * (isb | isg) + rng.normal(0, 1.5, S)\n",
" snap[\"press\"] = 450 - 12 * ld - 85 * dg * iso - 15 * dg * isc + rng.normal(0, 6, S)\n",
" ph = rng.uniform(0, 2 * np.pi, (S, 3))\n",
" a1, a2 = 1.0 * (0.5 + ld), 0.4 * (0.5 + ld)\n",
" W = (a1[:, None] * np.sin(2 * np.pi * 50 * TT + ph[:, :1]) + a2[:, None] * np.sin(2 * np.pi * 100 * TT + ph[:, 1:2])\n",
" + rng.normal(0, 0.35, (S, NS)))\n",
" W += (0.9 * dg * isb)[:, None] * np.sin(2 * np.pi * 165 * TT + ph[:, 2:3])\n",
" W += (0.8 * dg * isg)[:, None] * (np.sin(2 * np.pi * 150 * TT + ph[:, 2:3]) + 0.8 * np.sin(2 * np.pi * 100 * TT))\n",
" W += (0.5 * dg * iso)[:, None] * np.sin(2 * np.pi * 200 * TT + ph[:, 2:3])\n",
" k = rng.poisson(10 * dg * isb) # ๋ฒ ์ด๋ง ์ดํ: ์ถฉ๊ฒฉ์ฑ ํ์ค(์ฒจ๋ ์์น)\n",
" pos, amp = rng.integers(0, NS, (S, 30)), rng.normal(0, 3.0, (S, 30)) * dg[:, None]\n",
" amp = amp * (np.arange(30)[None, :] < k[:, None])\n",
" np.add.at(W, (np.repeat(np.arange(S), 30), pos.ravel()), amp.ravel())\n",
" return units, snap, W\n",
"\n",
"\n",
"def extract(W):\n",
" \"\"\"์ง๋ ํํ -> ํน์ง๋: RMS, ์ฒจ๋, ํ๊ณ ์จ, ์ต๋-์ต์, FFT ๋์ญ ์๋์ง(50/100/150/165/200Hz ๋ถ๊ทผ)\"\"\"\n",
" x = W - W.mean(axis=1, keepdims=True)\n",
" rms = np.sqrt((x ** 2).mean(axis=1))\n",
" out = {\"rms\": rms, \"kurt\": kurtosis(x, axis=1, fisher=False), \"crest\": np.abs(x).max(axis=1) / rms, \"p2p\": x.max(axis=1) - x.min(axis=1)}\n",
" A = np.abs(np.fft.rfft(x * np.hanning(NS), axis=1)) / NS\n",
" f = np.fft.rfftfreq(NS, 1 / FS)\n",
" for nm, (lo, hi) in BANDS.items():\n",
" out[nm] = np.sqrt((A[:, (f >= lo) & (f < hi)] ** 2).sum(axis=1))\n",
" return pd.DataFrame(out)\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# ๊ณตํต ๋ถ์ ์กฐ๊ฐ: ์ผ๋ณ ํน์ง๋ ํ(ํํ ํน์ง + ์จ๋ยท์๋ ฅ + ํ์), ๊ฐ์ฒด ๋จ์ ํ์ต/๊ฒ์ฆ ๋ถํ (์์ 24๋ ํ์ต, ๋ค 24๋ ๊ฒ์ฆ)\n",
"# ---------------------------------------------------------------------------\n",
"BASE = [\"rms\", \"kurt\", \"crest\", \"p2p\", \"b50\", \"b100\", \"b150\", \"b165\", \"b200\", \"temp\", \"press\"]\n",
"ROLL = [\"rms\", \"kurt\", \"b150\", \"b165\", \"b200\", \"temp\", \"press\"]\n",
"\n",
"\n",
"def build_features(snap, W):\n",
" D = pd.concat([snap.reset_index(drop=True), extract(W)], axis=1)\n",
" D[\"temp_res\"] = D[\"temp\"] - (55 + 22 * D[\"load\"] + 0.5 * D[\"ambient\"]) # ๋ถํยท์ธ๊ธฐ ๋๋น ์จ๋ ์์ฐจ\n",
" D[\"press_res\"] = D[\"press\"] - (450 - 12 * D[\"load\"]) # ๋ถํ ๋๋น ์๋ ฅ ์์ฐจ\n",
" D[\"rms_n\"] = D[\"rms\"] / (0.5 + D[\"load\"]) # ๋ถํ ์ ๊ทํ RMS\n",
" D[\"hf_ratio\"] = (D[\"b150\"] + D[\"b165\"] + D[\"b200\"]) / (D[\"b50\"] + 1e-6) # ๊ณ ์ฃผํ/๊ธฐ๋ณธ์ฃผํ ๋น\n",
" g = D.groupby(\"unit\")\n",
" for c in ROLL:\n",
" D[c + \"_m3\"] = g[c].transform(lambda s: s.rolling(3, min_periods=1).mean())\n",
" for c in (\"rms_n\", \"temp_res\", \"press_res\"):\n",
" D[c + \"_d\"] = g[c].diff().fillna(0)\n",
" return D\n",
"\n",
"\n",
"DERIVED = [\"temp_res\", \"press_res\", \"rms_n\", \"hf_ratio\"] + [c + \"_m3\" for c in ROLL] + [\"rms_n_d\", \"temp_res_d\", \"press_res_d\"]\n",
"FEATS = BASE + DERIVED\n",
"AE_COLS = [\"rms_n\", \"kurt\", \"b150\", \"b165\", \"b200\", \"hf_ratio\", \"temp_res\", \"press_res\"]\n",
"\n",
"\n",
"def split_units(units, n_train=24):\n",
" return list(units[\"unit\"][:n_train]), list(units[\"unit\"][n_train:])\n",
"\n",
"\n",
"class AE:\n",
" \"\"\"์คํ ์ธ์ฝ๋(์
๋ ฅ 8 -> 10 -> 4 -> 10 -> 8): ์ ์ ์๋ฃ๋ง ์ฌ๊ตฌ์ฑํ๋๋ก ํ์ตํ๊ณ , ์ฌ๊ตฌ์ฑ ์ค์ฐจ๋ฅผ ์ด์ ์ ์๋ก ์ด๋ค.\"\"\"\n",
"\n",
" def __init__(self, cols=AE_COLS, q=0.99, seed=0):\n",
" self.cols, self.q, self.seed = cols, q, seed\n",
"\n",
" def fit(self, healthy):\n",
" self.sc = StandardScaler().fit(healthy[self.cols])\n",
" Z = self.sc.transform(healthy[self.cols])\n",
" self.net = MLPRegressor(hidden_layer_sizes=(10, 4, 10), max_iter=800, random_state=self.seed).fit(Z, Z)\n",
" self.thr = float(np.quantile(self.err(healthy), self.q))\n",
" return self\n",
"\n",
" def contrib(self, d):\n",
" Z = self.sc.transform(d[self.cols])\n",
" return (Z - self.net.predict(Z)) ** 2 # ์
๋ ฅ ํญ๋ชฉ๋ณ ์ฌ๊ตฌ์ฑ ์ค์ฐจ(์ด์ ๊ธฐ์ฌ๋)\n",
"\n",
" def err(self, d):\n",
" return self.contrib(d).mean(axis=1)\n",
"\n",
"\n",
"def latch(d, e, thr, k=2):\n",
" \"\"\"๊ฐ์ ๊ฐ์ฒด์์ ์ด์ ์ ์๊ฐ k์ผ ์ฐ์ ๊ธฐ์ค์ ๋์ผ๋ฉด ๊ฒฝ๋ณด๋ฅผ ์ผ๊ณ ์ ์ง(๋์น)ํ๋ค.\"\"\"\n",
" out, e = np.zeros(len(d), bool), np.asarray(e)\n",
" for idx in d.reset_index(drop=True).groupby(\"unit\").indices.values():\n",
" run, on = 0, False\n",
" for i in idx:\n",
" run = run + 1 if e[i] > thr else 0\n",
" on = on or run >= k\n",
" out[i] = on\n",
" return out\n",
"\n",
"units, snap, W = make_runs()\n",
"D = build_features(snap, W)\n",
"\n",
"# 1) Baseline: ๋จ์ ์๊ณ๊ฐ(ํ๋๋ผ๋ ๋์ผ๋ฉด ์ด์). ์๊ณ๊ฐ์ ์ ์(์ดํ ์ ) ์๋ฃ์ ๋ถํฌ๋ก ์ ํ๋ค.\n",
"healthy = D[D[\"deg\"] == 0]\n",
"TH = {\"rms\": 1.25, \"kurt\": 3.0, \"temp\": 92.0, \"press_low\": 405.0}\n",
"D[\"alarm\"] = (D[\"rms\"] > TH[\"rms\"]) | (D[\"kurt\"] > TH[\"kurt\"]) | (D[\"temp\"] > TH[\"temp\"]) | (D[\"press\"] < TH[\"press_low\"])\n",
"D[\"anomaly\"] = D[\"deg\"] >= 0.1 # ์ ๋ต: ์ดํ๊ฐ ์ผ์ ์์ค ์ด์ ์งํ๋ ์ธก์ \n",
"tp, fp = (D[\"alarm\"] & D[\"anomaly\"]).sum(), (D[\"alarm\"] & ~D[\"anomaly\"]).sum()\n",
"fn = (~D[\"alarm\"] & D[\"anomaly\"]).sum()\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ธก์ {len(D)}๊ฑด, ๊ฐ์ฒด {len(units)}๋, ์ดํ ๊ตฌ๊ฐ {int(D['anomaly'].sum())}๊ฑด\")\n",
"print(f\"Baseline ์๊ณ๊ฐ {TH}\")\n",
"print(f\" ํ์ง์จ {tp / (tp + fn):.1%}, ์ ๋ฐ๋ {tp / (tp + fp):.1%}, ์ ์ ์ธก์ ์ค๊ฒฝ๋ณด์จ {fp / (~D['anomaly']).sum():.1%} (๊ณ ๋ถํ ์ด์ฉ์ผ ์ค๊ฒฝ๋ณด์จ {D.loc[D['hi_load'] & ~D['anomaly'], 'alarm'].mean():.1%})\")\n",
"lead = []\n",
"for u, g in D.groupby(\"unit\"):\n",
" a = g[g[\"alarm\"] & g[\"anomaly\"]]\n",
" lead.append(a[\"rul\"].max() if len(a) else 0)\n",
"units[\"lead\"] = lead\n",
"print(f\" ๊ณ ์ฅ ์ ์ฒซ ๊ฒฝ๋ณด ํ๋ณด ์๊ฐ ์ค์๊ฐ {np.median(lead):.0f}์ผ, ๊ฒฝ๋ณด ๋ชป ๋ธ ๊ฐ์ฒด {int((units['lead'] == 0).sum())}๋/{len(units)}๋\")\n",
"print(\" ์ดํ ์ ํ๋ณ ํ์ง์จ: \" + \", \".join(f\"{m} {D.loc[D['anomaly'] & (D['mode'] == m), 'alarm'].mean():.0%}\" for m in MODES))\n",
"\n",
"# 2) ์๊ด๋ถ์: ํผ์ฒ์ ์์กด์๋ช
(RUL) (์คํผ์ด๋ง)\n",
"rows = []\n",
"for c in [\"rms\", \"kurt\", \"crest\", \"b150\", \"b165\", \"b200\", \"temp\", \"press\", \"hf_ratio\", \"temp_res\", \"press_res\", \"load\", \"ambient\"]:\n",
" r, p = spearmanr(D[c], D[\"rul\"]); rows.append((c, r, p))\n",
"cor = pd.DataFrame(rows, columns=[\"ํผ์ฒ\", \"rho\", \"p\"]); cor[\"์ ์\"] = (cor[\"p\"] < 0.05) & (cor[\"rho\"].abs() >= 0.2)\n",
"print(\"\\n[์๊ด๋ถ์] ํผ์ฒ | ์คํผ์ด๋ง rho | p-value | ์ ์(p<0.05, |rho|>=0.2)\")\n",
"for x in cor.sort_values(\"rho\", key=abs, ascending=False).head(8).itertuples():\n",
" print(f\" {x.ํผ์ฒ:10s} {x.rho:+.2f} {x.p:.4f} {'์ ์' if x.์ ์ else '-'}\")\n",
"nsig = int(cor[\"์ ์\"].sum())\n",
"\n",
"# 3) ํ๋ ํ (ABC): ๊ณ ์ฅ๋ณ ์๋ฆฌ๋ถ์ ์๋ชจ ๋น์ฉ(๊ฐ์ ๋จ๊ฐ์ง์)\n",
"cons = {}\n",
"for m in units[\"mode\"]:\n",
" cons[MODES[m][2]] = cons.get(MODES[m][2], 0) + MODES[m][3]\n",
" cons[\"์ฐยทํํฐ ํคํธ\"] = cons.get(\"์ฐยทํํฐ ํคํธ\", 0) + KIT_COST\n",
"par = pd.Series(cons).sort_values(ascending=False).to_frame(\"์๋ชจ ๋น์ฉ์ง์\")\n",
"par[\"๋์ ๋น์จ\"] = par[\"์๋ชจ ๋น์ฉ์ง์\"].cumsum() / par[\"์๋ชจ ๋น์ฉ์ง์\"].sum()\n",
"par[\"๋ฑ๊ธ\"] = np.where(par[\"๋์ ๋น์จ\"] <= 0.8, \"A\", np.where(par[\"๋์ ๋น์จ\"] <= 0.95, \"B\", \"C\")); par.iloc[0, 2] = \"A\"\n",
"print(\"\\n[ํ๋ ํ ] ์๋ฆฌ๋ถ์ ์๋ชจ ๋น์ฉ(๊ณ ์ฅ 1ํ ๊ต์ฒด ๊ธฐ์ค)\")\n",
"print(par.assign(๋์ ๋น์จ=lambda d: d[\"๋์ ๋น์จ\"].map(\"{:.0%}\".format)).round(0).to_string())\n",
"top20 = par.head(int(np.ceil(len(par) * 0.2)))\n",
"print(f\"์์ 20% ํ๋ชฉ({', '.join(top20.index)})์ด ์ ์ฒด ์๋ชจ ๋น์ฉ์ {top20['์๋ชจ ๋น์ฉ์ง์'].sum() / par['์๋ชจ ๋น์ฉ์ง์'].sum():.0%}\")\n",
"\n",
"# 4) EDA ์ฐจํธ 8์ข
\n",
"fig, ax = plt.subplots(2, 4, figsize=(16, 7))\n",
"ax[0, 0].scatter(D[\"rul\"], D[\"rms\"], s=3); ax[0, 0].set_title(\"1 RMS vs RUL\"); ax[0, 0].invert_xaxis()\n",
"for m in MODES:\n",
" g = D[D[\"mode\"] == m]; ax[0, 1].scatter(g[\"deg\"], g[\"kurt\"], s=2, label=m)\n",
"ax[0, 1].set_title(\"2 Kurtosis vs degradation\"); ax[0, 1].legend(fontsize=6)\n",
"for m in MODES:\n",
" g = D[D[\"mode\"] == m]; ax[0, 2].scatter(g[\"deg\"], g[\"temp_res\"], s=2)\n",
"ax[0, 2].set_title(\"3 Temp residual vs degradation\")\n",
"for m in MODES:\n",
" g = D[D[\"mode\"] == m]; ax[0, 3].scatter(g[\"deg\"], g[\"press_res\"], s=2)\n",
"ax[0, 3].set_title(\"4 Pressure residual vs degradation\")\n",
"f = np.fft.rfftfreq(NS, 1 / FS); bm = D[\"mode\"] == \"bearing\"\n",
"ax[1, 0].plot(f, np.abs(np.fft.rfft(W[(bm & (D[\"deg\"] == 0)).values], axis=1)).mean(axis=0) / NS, label=\"healthy\")\n",
"ax[1, 0].plot(f, np.abs(np.fft.rfft(W[(bm & (D[\"deg\"] > 0.6)).values], axis=1)).mean(axis=0) / NS, label=\"degraded\"); ax[1, 0].legend(); ax[1, 0].set_title(\"5 Mean spectrum (bearing)\")\n",
"ax[1, 1].hist([units.loc[units[\"mode\"] == m, \"life\"] for m in MODES], bins=8, stacked=True, label=list(MODES)); ax[1, 1].set_title(\"6 Life by failure mode\")\n",
"ax[1, 2].scatter(healthy[\"load\"], healthy[\"rms\"], s=2); ax[1, 2].set_title(\"7 Load vs RMS (healthy)\")\n",
"ax[1, 3].bar(range(len(par)), par[\"์๋ชจ ๋น์ฉ์ง์\"]); ax[1, 3].set_title(\"8 Spare-part consumption (Pareto)\")\n",
"plt.tight_layout(); plt.show()\n",
"\n",
"print(f\"\\nEDA ์ฐจํธ 8์ข
์์ฑ(๋ชฉํ 8์ข
์ด์: ๋ฌ์ฑ), ์ ์ํ ํผ์ฒ {nsig}๊ฐ (๋ชฉํ 5๊ฐ ์ด์: {'๋ฌ์ฑ' if nsig >= 5 else '๋ฏธ๋ฌ'})\")\n",
"print(\"ํต์ฌ ๋ฐ๊ฒฌ: 1) ๊ณ ์ ์๊ณ๊ฐ์ ๊ณ ๋ถํ์ผ ์ค๊ฒฝ๋ณด์ ์จ๋ยท์๋ ฅ ์ ํ ๋์นจ์ด ์์ 2) ๋ถํยท์ธ๊ธฐ๋ฅผ ๋ณด์ ํ ์์ฐจ ํผ์ฒ๊ฐ ์ดํ์ ๋ ๋๋ ทํจ 3) ๋ฒ ์ด๋งยท๊ธฐ์ด ๋ถํ์ด ์๋ชจ ๋น์ฉ ๋๋ถ๋ถ\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
03 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ธก์ 7733๊ฑด, ๊ฐ์ฒด 48๋, ์ดํ ๊ตฌ๊ฐ 3336๊ฑด\n",
"Baseline ์๊ณ๊ฐ {'rms': 1.25, 'kurt': 3.0, 'temp': 92.0, 'press_low': 405.0}\n",
" ํ์ง์จ 25.3%, ์ ๋ฐ๋ 90.3%, ์ ์ ์ธก์ ์ค๊ฒฝ๋ณด์จ 2.1% (๊ณ ๋ถํ ์ด์ฉ์ผ ์ค๊ฒฝ๋ณด์จ 28.6%)\n",
" ๊ณ ์ฅ ์ ์ฒซ ๊ฒฝ๋ณด ํ๋ณด ์๊ฐ ์ค์๊ฐ 40์ผ, ๊ฒฝ๋ณด ๋ชป ๋ธ ๊ฐ์ฒด 0๋/48๋\n",
" ์ดํ ์ ํ๋ณ ํ์ง์จ: bearing 29%, gear 9%, oil_pump 54%, cooling 31%\n",
"\n",
"[์๊ด๋ถ์] ํผ์ฒ | ์คํผ์ด๋ง rho | p-value | ์ ์(p<0.05, |rho|>=0.2)\n",
" hf_ratio -0.63 0.0000 ์ ์\n",
" temp_res -0.53 0.0000 ์ ์\n",
" b165 -0.38 0.0000 ์ ์\n",
" b150 -0.37 0.0000 ์ ์\n",
" rms -0.32 0.0000 ์ ์\n",
" ambient +0.31 0.0000 ์ ์\n",
" kurt -0.29 0.0000 ์ ์\n",
" b200 -0.21 0.0000 ์ ์\n",
"\n",
"[ํ๋ ํ ] ์๋ฆฌ๋ถ์ ์๋ชจ ๋น์ฉ(๊ณ ์ฅ 1ํ ๊ต์ฒด ๊ธฐ์ค)\n",
" ์๋ชจ ๋น์ฉ์ง์ ๋์ ๋น์จ ๋ฑ๊ธ\n",
"๊ตฌ๋ ๊ธฐ์ด ์ธํธ 162.0 46% A\n",
"์ฃผ ๋ฒ ์ด๋ง ์ธํธ 90.0 71% A\n",
"์ฐยทํํฐ ํคํธ 48.0 85% B\n",
"์ค์ผ ํํ 32.0 94% B\n",
"๋๊ฐ ํฌ ๋ชจํฐ 21.0 100% C\n",
"์์ 20% ํ๋ชฉ(๊ตฌ๋ ๊ธฐ์ด ์ธํธ)์ด ์ ์ฒด ์๋ชจ ๋น์ฉ์ 46%\n",
"\n",
"EDA ์ฐจํธ 8์ข
์์ฑ(๋ชฉํ 8์ข
์ด์: ๋ฌ์ฑ), ์ ์ํ ํผ์ฒ 9๊ฐ (๋ชฉํ 5๊ฐ ์ด์: ๋ฌ์ฑ)\n",
"ํต์ฌ ๋ฐ๊ฒฌ: 1) ๊ณ ์ ์๊ณ๊ฐ์ ๊ณ ๋ถํ์ผ ์ค๊ฒฝ๋ณด์ ์จ๋ยท์๋ ฅ ์ ํ ๋์นจ์ด ์์ 2) ๋ถํยท์ธ๊ธฐ๋ฅผ ๋ณด์ ํ ์์ฐจ ํผ์ฒ๊ฐ ์ดํ์ ๋ ๋๋ ทํจ 3) ๋ฒ ์ด๋งยท๊ธฐ์ด ๋ถํ์ด ์๋ชจ ๋น์ฉ ๋๋ถ๋ถ\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
04. ์คํ ์ธ์ฝ๋(Autoencoder)/LSTM ๊ธฐ๋ฐ ๋ฏธ์ธ ์ง๋ ๋ณํ ๊ฐ์ง ์์ง์ ๋น(PdM) ๋ชจ๋ธ ๊ณ ๋ํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์คํ ์ธ์ฝ๋ ๊ธฐ๋ฐ ๋ฏธ์ธ ์ง๋ ๋ณํ ๊ฐ์ง ์์ง์ ๋น(PdM) ๋ชจ๋ธ์ ๊ณ ๋ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์คํ ์ธ์ฝ๋ ๊ธฐ๋ฐ ๋ฏธ์ธ ์ง๋ยท์จ๋ ๋ณํ ๊ฐ์ง ์์ง์ ๋น(PdM) ๋ชจ๋ธ์ ๊ณ ๋ํํ๊ณ , ํ์ ํผ์ฒ๋ก ์์กด์๋ช
๊ธฐ์ค ๋ชจ๋ธ์ ๋ง๋ ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ ์ ์๋ฃ๋ง์ผ๋ก ์คํ ์ธ์ฝ๋(MLP)๋ฅผ ํ์ตํด ์ฌ๊ตฌ์ฑ ์ค์ฐจ๋ฅผ ์ด์ ์ ์๋ก ์ฐ๊ณ 2์ผ ์ฐ์ ์ด๊ณผ ์ ๊ฒฝ๋ณด๋ฅผ ์ผ ๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 5๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/features/mnt04_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": [
"\"\"\"์ ์ ์๋ฃ๋ง์ผ๋ก ํ์ตํ ์คํ ์ธ์ฝ๋์ ์ฌ๊ตฌ์ฑ ์ค์ฐจ๋ก ๋ฏธ์ธ ์ง๋ยท์จ๋ ๋ณํ๋ฅผ ๊ฐ์ง(2์ผ ์ฐ์ ์ ๊ฒฝ๋ณด)ํด ์๊ณ๊ฐ Baseline๊ณผ ๋น๊ตํ๊ณ , ํ์ ํผ์ฒ 14๊ฐ๋ก ์์กด์๋ช
(RUL) ๊ธฐ์ค ๋ชจ๋ธ์ ๋ง๋ค์ด ๊ฐ์ฒด ๋จ์ ์๊ณ์ด ๊ต์ฐจ๊ฒ์ฆ MAPE๋ฅผ ํ๊ฐํ๋ฉฐ ์์ด ์ค์๋๋ก ํผ์ฒ๋ฅผ ์ค์ด๋ ์์ง์ ๋น(PdM) ๋ชจ๋ธ์ ๊ตฌํํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"from scipy.stats import kurtosis\n",
"from sklearn.ensemble import GradientBoostingRegressor\n",
"from sklearn.inspection import permutation_importance\n",
"from sklearn.neural_network import MLPRegressor\n",
"from sklearn.preprocessing import StandardScaler\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๊ธฐ๋์ฒด 48๋์ ์์งยท๊ตฌ๋๊ณ ์ง๋ยท์จ๋ยท์๋ ฅ ์ผ์ ์๋ฃ ์์ฑ๊ธฐ (์ค์ ์ฅ๋นยท๋ถ๋ยท์ ๋น ์ค์ ์์)\n",
"# ๊ฐ์ฒด๋ง๋ค ์ ์ -> ์ดํ -> ๊ณ ์ฅ(์๋ช
์ข
๋ฃ)๊น์ง ํ๋ฃจ 1ํ ์ธก์ (์ง๋ ํํ 256ํ๋ณธ + ์จ๋ + ์๋ ฅ + ๋ถํ)์ ๋ง๋ ๋ค.\n",
"# ์ดํ ์ ํ 4๊ฐ์ง: bearing(๋ฒ ์ด๋ง), gear(๊ธฐ์ด), oil_pump(์ค์ผ ํํ), cooling(๋๊ฐ ํฌ ๋ชจํฐ)\n",
"# ---------------------------------------------------------------------------\n",
"FS, NS = 1024, 256 # ๊ฐ์ ํ๋ณธํ ์ฃผํ์(Hz), ํํ 1๊ฐ๋น ํ๋ณธ ์\n",
"TT = np.arange(NS) / FS\n",
"MODES = { # ์ดํ ์ ํ: (๋ฐ์ ํ๋ฅ , ํ๊ท ์๋ช
(์ผ), ๊ต์ฒด ์๋ฆฌ๋ถ์, ๊ฐ์ ๋จ๊ฐ์ง์)\n",
" \"bearing\": (0.40, 160, \"์ฃผ ๋ฒ ์ด๋ง ์ธํธ\", 6.0), \"gear\": (0.25, 175, \"๊ตฌ๋ ๊ธฐ์ด ์ธํธ\", 9.0),\n",
" \"oil_pump\": (0.20, 150, \"์ค์ผ ํํ\", 4.0), \"cooling\": (0.15, 140, \"๋๊ฐ ํฌ ๋ชจํฐ\", 3.0)}\n",
"KIT_COST = 1.0 # ๋ชจ๋ ์ ๋น์ ํจ๊ป ์๋ชจ๋๋ ์ฐยทํํฐ ํคํธ ๋จ๊ฐ์ง์\n",
"BANDS = {\"b50\": (40, 60), \"b100\": (90, 110), \"b150\": (140, 156), \"b165\": (156, 176), \"b200\": (190, 215)}\n",
"\n",
"\n",
"def make_runs(n=48, seed=1):\n",
" \"\"\"๋ฐํ: units(๊ฐ์ฒด ์ ๋ณด), snap(์ผ๋ณ ์ธก์ ์์ฝ), W(์ง๋ ํํ [์ธก์ ์, 256])\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" names = list(MODES)\n",
" mode = rng.choice(names, n, p=[MODES[m][0] for m in names])\n",
" life = np.clip(np.array([rng.normal(MODES[m][1], 18) for m in mode]), 90, 230).astype(int)\n",
" start = np.sort(rng.integers(0, 240, n))\n",
" units = pd.DataFrame({\"unit\": [f\"U{i:02d}\" for i in range(n)], \"mode\": mode, \"life\": life, \"start\": start, \"base_load\": rng.uniform(0.4, 0.75, n)})\n",
" rows = []\n",
" for u in units.itertuples():\n",
" d = np.arange(u.life + 1)\n",
" onset = 0.45 * u.life\n",
" deg = np.clip((d - onset) / (u.life - onset), 0, 1) ** 1.5\n",
" load = np.clip(u.base_load + rng.normal(0, 0.06, len(d)), 0.2, 1.0)\n",
" hi = rng.random(len(d)) < 0.07 # ๊ณ ๋ถํ ์ด์ฉ์ผ(์ ์์ด์ง๋ง ์ง๋ยท์จ๋ ์์น)\n",
" load = np.where(hi, rng.uniform(0.95, 1.1, len(d)), load)\n",
" rows.append(pd.DataFrame({\"unit\": u.unit, \"day\": d, \"cal\": u.start + d, \"rul\": u.life - d, \"deg\": deg, \"load\": load, \"hi_load\": hi, \"mode\": u.mode}))\n",
" snap = pd.concat(rows, ignore_index=True)\n",
" S = len(snap)\n",
" snap[\"ambient\"] = 15 + 12 * np.sin(2 * np.pi * snap[\"cal\"] / 365) + rng.normal(0, 2, S)\n",
" dg, ld, md = snap[\"deg\"].values, snap[\"load\"].values, snap[\"mode\"].values\n",
" isb, isg, iso, isc = (md == \"bearing\"), (md == \"gear\"), (md == \"oil_pump\"), (md == \"cooling\")\n",
" snap[\"temp\"] = 55 + 22 * ld + 0.5 * snap[\"ambient\"] + 9 * dg * iso + 26 * dg * isc + 3 * dg * (isb | isg) + rng.normal(0, 1.5, S)\n",
" snap[\"press\"] = 450 - 12 * ld - 85 * dg * iso - 15 * dg * isc + rng.normal(0, 6, S)\n",
" ph = rng.uniform(0, 2 * np.pi, (S, 3))\n",
" a1, a2 = 1.0 * (0.5 + ld), 0.4 * (0.5 + ld)\n",
" W = (a1[:, None] * np.sin(2 * np.pi * 50 * TT + ph[:, :1]) + a2[:, None] * np.sin(2 * np.pi * 100 * TT + ph[:, 1:2])\n",
" + rng.normal(0, 0.35, (S, NS)))\n",
" W += (0.9 * dg * isb)[:, None] * np.sin(2 * np.pi * 165 * TT + ph[:, 2:3])\n",
" W += (0.8 * dg * isg)[:, None] * (np.sin(2 * np.pi * 150 * TT + ph[:, 2:3]) + 0.8 * np.sin(2 * np.pi * 100 * TT))\n",
" W += (0.5 * dg * iso)[:, None] * np.sin(2 * np.pi * 200 * TT + ph[:, 2:3])\n",
" k = rng.poisson(10 * dg * isb) # ๋ฒ ์ด๋ง ์ดํ: ์ถฉ๊ฒฉ์ฑ ํ์ค(์ฒจ๋ ์์น)\n",
" pos, amp = rng.integers(0, NS, (S, 30)), rng.normal(0, 3.0, (S, 30)) * dg[:, None]\n",
" amp = amp * (np.arange(30)[None, :] < k[:, None])\n",
" np.add.at(W, (np.repeat(np.arange(S), 30), pos.ravel()), amp.ravel())\n",
" return units, snap, W\n",
"\n",
"\n",
"def extract(W):\n",
" \"\"\"์ง๋ ํํ -> ํน์ง๋: RMS, ์ฒจ๋, ํ๊ณ ์จ, ์ต๋-์ต์, FFT ๋์ญ ์๋์ง(50/100/150/165/200Hz ๋ถ๊ทผ)\"\"\"\n",
" x = W - W.mean(axis=1, keepdims=True)\n",
" rms = np.sqrt((x ** 2).mean(axis=1))\n",
" out = {\"rms\": rms, \"kurt\": kurtosis(x, axis=1, fisher=False), \"crest\": np.abs(x).max(axis=1) / rms, \"p2p\": x.max(axis=1) - x.min(axis=1)}\n",
" A = np.abs(np.fft.rfft(x * np.hanning(NS), axis=1)) / NS\n",
" f = np.fft.rfftfreq(NS, 1 / FS)\n",
" for nm, (lo, hi) in BANDS.items():\n",
" out[nm] = np.sqrt((A[:, (f >= lo) & (f < hi)] ** 2).sum(axis=1))\n",
" return pd.DataFrame(out)\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# ๊ณตํต ๋ถ์ ์กฐ๊ฐ: ์ผ๋ณ ํน์ง๋ ํ(ํํ ํน์ง + ์จ๋ยท์๋ ฅ + ํ์), ๊ฐ์ฒด ๋จ์ ํ์ต/๊ฒ์ฆ ๋ถํ (์์ 24๋ ํ์ต, ๋ค 24๋ ๊ฒ์ฆ)\n",
"# ---------------------------------------------------------------------------\n",
"BASE = [\"rms\", \"kurt\", \"crest\", \"p2p\", \"b50\", \"b100\", \"b150\", \"b165\", \"b200\", \"temp\", \"press\"]\n",
"ROLL = [\"rms\", \"kurt\", \"b150\", \"b165\", \"b200\", \"temp\", \"press\"]\n",
"\n",
"\n",
"def build_features(snap, W):\n",
" D = pd.concat([snap.reset_index(drop=True), extract(W)], axis=1)\n",
" D[\"temp_res\"] = D[\"temp\"] - (55 + 22 * D[\"load\"] + 0.5 * D[\"ambient\"]) # ๋ถํยท์ธ๊ธฐ ๋๋น ์จ๋ ์์ฐจ\n",
" D[\"press_res\"] = D[\"press\"] - (450 - 12 * D[\"load\"]) # ๋ถํ ๋๋น ์๋ ฅ ์์ฐจ\n",
" D[\"rms_n\"] = D[\"rms\"] / (0.5 + D[\"load\"]) # ๋ถํ ์ ๊ทํ RMS\n",
" D[\"hf_ratio\"] = (D[\"b150\"] + D[\"b165\"] + D[\"b200\"]) / (D[\"b50\"] + 1e-6) # ๊ณ ์ฃผํ/๊ธฐ๋ณธ์ฃผํ ๋น\n",
" g = D.groupby(\"unit\")\n",
" for c in ROLL:\n",
" D[c + \"_m3\"] = g[c].transform(lambda s: s.rolling(3, min_periods=1).mean())\n",
" for c in (\"rms_n\", \"temp_res\", \"press_res\"):\n",
" D[c + \"_d\"] = g[c].diff().fillna(0)\n",
" return D\n",
"\n",
"\n",
"DERIVED = [\"temp_res\", \"press_res\", \"rms_n\", \"hf_ratio\"] + [c + \"_m3\" for c in ROLL] + [\"rms_n_d\", \"temp_res_d\", \"press_res_d\"]\n",
"FEATS = BASE + DERIVED\n",
"AE_COLS = [\"rms_n\", \"kurt\", \"b150\", \"b165\", \"b200\", \"hf_ratio\", \"temp_res\", \"press_res\"]\n",
"\n",
"\n",
"def split_units(units, n_train=24):\n",
" return list(units[\"unit\"][:n_train]), list(units[\"unit\"][n_train:])\n",
"\n",
"\n",
"class AE:\n",
" \"\"\"์คํ ์ธ์ฝ๋(์
๋ ฅ 8 -> 10 -> 4 -> 10 -> 8): ์ ์ ์๋ฃ๋ง ์ฌ๊ตฌ์ฑํ๋๋ก ํ์ตํ๊ณ , ์ฌ๊ตฌ์ฑ ์ค์ฐจ๋ฅผ ์ด์ ์ ์๋ก ์ด๋ค.\"\"\"\n",
"\n",
" def __init__(self, cols=AE_COLS, q=0.99, seed=0):\n",
" self.cols, self.q, self.seed = cols, q, seed\n",
"\n",
" def fit(self, healthy):\n",
" self.sc = StandardScaler().fit(healthy[self.cols])\n",
" Z = self.sc.transform(healthy[self.cols])\n",
" self.net = MLPRegressor(hidden_layer_sizes=(10, 4, 10), max_iter=800, random_state=self.seed).fit(Z, Z)\n",
" self.thr = float(np.quantile(self.err(healthy), self.q))\n",
" return self\n",
"\n",
" def contrib(self, d):\n",
" Z = self.sc.transform(d[self.cols])\n",
" return (Z - self.net.predict(Z)) ** 2 # ์
๋ ฅ ํญ๋ชฉ๋ณ ์ฌ๊ตฌ์ฑ ์ค์ฐจ(์ด์ ๊ธฐ์ฌ๋)\n",
"\n",
" def err(self, d):\n",
" return self.contrib(d).mean(axis=1)\n",
"\n",
"\n",
"def latch(d, e, thr, k=2):\n",
" \"\"\"๊ฐ์ ๊ฐ์ฒด์์ ์ด์ ์ ์๊ฐ k์ผ ์ฐ์ ๊ธฐ์ค์ ๋์ผ๋ฉด ๊ฒฝ๋ณด๋ฅผ ์ผ๊ณ ์ ์ง(๋์น)ํ๋ค.\"\"\"\n",
" out, e = np.zeros(len(d), bool), np.asarray(e)\n",
" for idx in d.reset_index(drop=True).groupby(\"unit\").indices.values():\n",
" run, on = 0, False\n",
" for i in idx:\n",
" run = run + 1 if e[i] > thr else 0\n",
" on = on or run >= k\n",
" out[i] = on\n",
" return out\n",
"\n",
"units, snap, W = make_runs()\n",
"D = build_features(snap, W)\n",
"tr_u, va_u = split_units(units)\n",
"tr, va = D[D[\"unit\"].isin(tr_u)].reset_index(drop=True), D[D[\"unit\"].isin(va_u)].reset_index(drop=True)\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ํ์ต {len(tr_u)}๋({len(tr)}๊ฑด), ๊ฒ์ฆ {len(va_u)}๋({len(va)}๊ฑด), ํ์ ํผ์ฒ {len(DERIVED)}๊ฐ (๋ชฉํ 10๊ฐ ์ด์: {'๋ฌ์ฑ' if len(DERIVED) >= 10 else '๋ฏธ๋ฌ'})\")\n",
"print(\" ํ์ ํผ์ฒ:\", \", \".join(DERIVED))\n",
"\n",
"# 1) ์คํ ์ธ์ฝ๋ ์ด์ ๊ฐ์ง vs ์๊ณ๊ฐ Baseline (๊ฒ์ฆ ๊ฐ์ฒด)\n",
"ae = AE().fit(tr[tr[\"deg\"] == 0])\n",
"e = ae.err(va)\n",
"va[\"anomaly\"] = va[\"deg\"] >= 0.1\n",
"va[\"ae_alarm\"] = latch(va, e, ae.thr, k=2)\n",
"va[\"th_alarm\"] = (va[\"rms\"] > 1.25) | (va[\"kurt\"] > 3.0) | (va[\"temp\"] > 92.0) | (va[\"press\"] < 405.0)\n",
"va[\"ae_raw\"] = e > ae.thr\n",
"\n",
"\n",
"def report(name, col):\n",
" a = va[col]\n",
" tpr, far = (a & va[\"anomaly\"]).sum() / va[\"anomaly\"].sum(), (a & ~va[\"anomaly\"]).sum() / (~va[\"anomaly\"]).sum()\n",
" hi = a[va[\"hi_load\"] & ~va[\"anomaly\"]].mean()\n",
" lead = [g.loc[g[col] & g[\"anomaly\"], \"rul\"].max() if (g[col] & g[\"anomaly\"]).any() else 0 for _, g in va.groupby(\"unit\")]\n",
" print(f\" {name:22s} ํ์ง์จ {tpr:5.1%} ์ค๊ฒฝ๋ณด์จ {far:5.1%} ๊ณ ๋ถํ์ผ ์ค๊ฒฝ๋ณด {hi:5.1%} ๊ณ ์ฅ ์ ํ๋ณด ์ค์๊ฐ {np.median(lead):.0f}์ผ\")\n",
" return tpr, far\n",
"\n",
"\n",
"print(f\"\\n[์คํ ์ธ์ฝ๋ ์
๋ ฅ {len(AE_COLS)}๊ฐ, ์ ์ ์๋ฃ ์ฌ๊ตฌ์ฑ ์ค์ฐจ 99๋ถ์ {ae.thr:.2f}๋ฅผ ๊ธฐ์ค์ผ๋ก ์ฌ์ฉ]\")\n",
"report(\"์๊ณ๊ฐ Baseline\", \"th_alarm\")\n",
"report(\"์คํ ์ธ์ฝ๋(1ํ ์ด๊ณผ)\", \"ae_raw\")\n",
"tpr, far = report(\"์คํ ์ธ์ฝ๋(2์ผ ์ฐ์)\", \"ae_alarm\")\n",
"print(f\" ๋ฏธ์ธ ๋ณํ ๊ตฌ๊ฐ(์ดํ 0.1~0.3) ํ์ง์จ: Baseline {va.loc[va['deg'].between(0.1, 0.3), 'th_alarm'].mean():.1%} -> ์คํ ์ธ์ฝ๋ {va.loc[va['deg'].between(0.1, 0.3), 'ae_alarm'].mean():.1%}\")\n",
"\n",
"# 2) RUL ๊ธฐ์ค ๋ชจ๋ธ: ํ์ต ๊ฐ์ฒด ์ผ๋ถ๋ก ํ์ต, ๊ฐ์ฒด ๋จ์ ์๊ณ์ด ๊ต์ฐจ๊ฒ์ฆ(์์ ๊ฐ์ฒด๋ก ํ์ต -> ๋ค ๊ฐ์ฒด ๊ฒ์ฆ)\n",
"CAP = 120 # RUL ์ํ(์ผ): ์ดํ ์ด๊ธฐ์๋ 120์ผ ์ด์์ ๊ตฌ๋ถํ์ง ์๋๋ค\n",
"def fit_rul(d, cols):\n",
" return GradientBoostingRegressor(n_estimators=100, max_depth=3, learning_rate=0.1, subsample=0.8, random_state=0).fit(d[cols], np.minimum(d[\"rul\"], CAP))\n",
"\n",
"\n",
"def mape(d, p):\n",
" m = (d[\"deg\"] >= 0.1) & (d[\"rul\"] >= 10) # ์ดํ๊ฐ ์์๋ ๊ตฌ๊ฐ, ๊ณ ์ฅ ์ง์ 10์ผ ๋ฏธ๋ง์ ๋ถ๋ชจ ๋ฌธ์ ๋ก ์ ์ธ\n",
" y = np.minimum(d[\"rul\"], CAP)[m]\n",
" return float(np.mean(np.abs(y - p[m]) / y))\n",
"\n",
"\n",
"order = list(units[\"unit\"])\n",
"folds, cut = [], [12, 24, 36]\n",
"for i, c in enumerate(cut):\n",
" trn, val = D[D[\"unit\"].isin(order[:c])], D[D[\"unit\"].isin(order[c:c + 12])]\n",
" if len(val):\n",
" folds.append(mape(val, fit_rul(trn, FEATS).predict(val[FEATS])))\n",
"base_only = fit_rul(tr, BASE)\n",
"full = fit_rul(tr, FEATS)\n",
"m_base, m_full = mape(va, base_only.predict(va[BASE])), mape(va, full.predict(va[FEATS]))\n",
"print(f\"\\n[RUL ๋ชจ๋ธ] ๊ฐ์ฒด ๋จ์ ์๊ณ์ด ๊ต์ฐจ๊ฒ์ฆ ํด๋๋ณ MAPE: {', '.join(f'{f:.1%}' for f in folds)} -> ํ๊ท {np.mean(folds):.1%} (๋ชฉํ 25% ์ดํ: {'๋ฌ์ฑ' if np.mean(folds) <= 0.25 else '๋ฏธ๋ฌ'})\")\n",
"print(f\" ๊ฒ์ฆ 24๋ MAPE: ์๋ณธ ์ผ์ {len(BASE)}๊ฐ {m_base:.1%} -> ํ์ ํฌํจ {len(FEATS)}๊ฐ {m_full:.1%} (์ดํ ๊ตฌ๊ฐ, ์์กด 10์ผ ์ด์)\")\n",
"\n",
"# 3) ์์ด ์ค์๋๋ก ์ฐจ์ ์ถ์\n",
"pi = permutation_importance(full, va[FEATS], np.minimum(va[\"rul\"], CAP), n_repeats=3, random_state=0, scoring=\"neg_mean_absolute_error\")\n",
"imp = pd.Series(pi.importances_mean, FEATS).sort_values(ascending=False)\n",
"print(\"ํผ์ฒ ์ค์๋ ์์ 5: \" + \", \".join(f\"{k}({v:.1f})\" for k, v in imp.head(5).items()))\n",
"top = list(imp.index[:8])\n",
"m_top = mape(va, fit_rul(tr, top).predict(va[top]))\n",
"print(f\"์ฐจ์ ์ถ์ {len(FEATS)}๊ฐ -> {len(top)}๊ฐ: ๊ฒ์ฆ MAPE {m_full:.1%} -> {m_top:.1%}\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
04 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ํ์ต 24๋(3847๊ฑด), ๊ฒ์ฆ 24๋(3886๊ฑด), ํ์ ํผ์ฒ 14๊ฐ (๋ชฉํ 10๊ฐ ์ด์: ๋ฌ์ฑ)\n",
" ํ์ ํผ์ฒ: temp_res, press_res, rms_n, hf_ratio, rms_m3, kurt_m3, b150_m3, b165_m3, b200_m3, temp_m3, press_m3, rms_n_d, temp_res_d, press_res_d\n",
"\n",
"[์คํ ์ธ์ฝ๋ ์
๋ ฅ 8๊ฐ, ์ ์ ์๋ฃ ์ฌ๊ตฌ์ฑ ์ค์ฐจ 99๋ถ์ 1.27๋ฅผ ๊ธฐ์ค์ผ๋ก ์ฌ์ฉ]\n",
" ์๊ณ๊ฐ Baseline ํ์ง์จ 22.7% ์ค๊ฒฝ๋ณด์จ 1.7% ๊ณ ๋ถํ์ผ ์ค๊ฒฝ๋ณด 22.8% ๊ณ ์ฅ ์ ํ๋ณด ์ค์๊ฐ 40์ผ\n",
" ์คํ ์ธ์ฝ๋(1ํ ์ด๊ณผ) ํ์ง์จ 80.1% ์ค๊ฒฝ๋ณด์จ 1.2% ๊ณ ๋ถํ์ผ ์ค๊ฒฝ๋ณด 0.6% ๊ณ ์ฅ ์ ํ๋ณด ์ค์๊ฐ 66์ผ\n",
" ์คํ ์ธ์ฝ๋(2์ผ ์ฐ์) ํ์ง์จ 79.2% ์ค๊ฒฝ๋ณด์จ 0.0% ๊ณ ๋ถํ์ผ ์ค๊ฒฝ๋ณด 0.0% ๊ณ ์ฅ ์ ํ๋ณด ์ค์๊ฐ 56์ผ\n",
" ๋ฏธ์ธ ๋ณํ ๊ตฌ๊ฐ(์ดํ 0.1~0.3) ํ์ง์จ: Baseline 2.0% -> ์คํ ์ธ์ฝ๋ 32.4%\n",
"\n",
"[RUL ๋ชจ๋ธ] ๊ฐ์ฒด ๋จ์ ์๊ณ์ด ๊ต์ฐจ๊ฒ์ฆ ํด๋๋ณ MAPE: 26.7%, 24.2%, 17.1% -> ํ๊ท 22.7% (๋ชฉํ 25% ์ดํ: ๋ฌ์ฑ)\n",
" ๊ฒ์ฆ 24๋ MAPE: ์๋ณธ ์ผ์ 11๊ฐ 43.3% -> ํ์ ํฌํจ 25๊ฐ 20.4% (์ดํ ๊ตฌ๊ฐ, ์์กด 10์ผ ์ด์)\n",
"ํผ์ฒ ์ค์๋ ์์ 5: hf_ratio(11.8), temp_res(7.0), b150_m3(3.8), b165_m3(2.8), press_m3(2.5)\n",
"์ฐจ์ ์ถ์ 25๊ฐ -> 8๊ฐ: ๊ฒ์ฆ MAPE 20.4% -> 20.8%\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
05. ๋ถํ ๊ณ ์ฅ ์๋ฐ ์์ (RUL) ์์ธก ๋ฐ ์ํ ๊ธฐ๋ฐ ์ด์ ์ ๋ขฐ๋ ์ ์ ์์ง ๊ฐ๋ฐ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋ถํ ๊ณ ์ฅ ์๋ฐ ์์ (RUL) ์์ธก ๋ฐ ์ํ ์ด์ ์ ๋ขฐ๋ ์ ์ ์์ง์ ๊ฐ๋ฐํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋ถํ ๊ณ ์ฅ ์๋ฐ ์์ (RUL)์ ์์ธกํ๋ ์์๋ธ ๋ชจ๋ธ๊ณผ ์ํ ์ด์ ์ ๋ขฐ๋ ์ ์ ์์ง์ ๊ฐ๋ฐํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ทธ๋๋์ธํธ ๋ถ์คํ
, ๋๋ค ํฌ๋ ์คํธ, ๋ฆฟ์ง๋ฅผ ํ๊ท ํ๋ ์์๋ธ์ ํ์ดํผํ๋ผ๋ฏธํฐ ํ์์ผ๋ก ๋ง๋ค์ด RMS์ ์จ๋๋ง ์ฐ๋ ์ ํ ๊ธฐ์ค ๋ชจ๋ธ๊ณผ Hold-out ๊ฒ์ฆ ๊ฐ์ฒด์์ MAE, RMSE, R2๋ฅผ ๋น๊ต ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/mnt04_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": [
"\"\"\"์ง๋ยท์จ๋ยท์๋ ฅ ํน์ง์ผ๋ก ๋ถํ ๊ณ ์ฅ ์๋ฐ ์์ (RUL)์ ์์ธกํ๋ ์์๋ธ(๊ทธ๋๋์ธํธ ๋ถ์คํ
+๋๋ค ํฌ๋ ์คํธ+๋ฆฟ์ง) ๋ชจ๋ธ์ ํ์ดํผํ๋ผ๋ฏธํฐ ํ์์ผ๋ก ๋ง๋ค์ด ๋จ์ ์ ํ ๊ธฐ์ค ๋ชจ๋ธ๊ณผ MAEยทRMSEยทR2๋ฅผ ๋น๊ตํ๊ณ , ์คํ ์ธ์ฝ๋ ์ด์ ์ ์์ ์์ธก ๊ตฌ๊ฐ์ผ๋ก ์ํ ์ด์ ์ ๋ขฐ๋ ์ ์๋ฅผ ์ฐ์ถํด ์ ๋ขฐ๋ ๊ตฌ๊ฐ๋ณ ์ค์ ์ ํ๋์ ์ ๊ฒ ์นด๋๋ฅผ ์ถ๋ ฅํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"from scipy.stats import kurtosis\n",
"from sklearn.ensemble import GradientBoostingRegressor, RandomForestRegressor\n",
"from sklearn.linear_model import LinearRegression, Ridge\n",
"from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score\n",
"from sklearn.neural_network import MLPRegressor\n",
"from sklearn.pipeline import make_pipeline\n",
"from sklearn.preprocessing import StandardScaler\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๊ธฐ๋์ฒด 48๋์ ์์งยท๊ตฌ๋๊ณ ์ง๋ยท์จ๋ยท์๋ ฅ ์ผ์ ์๋ฃ ์์ฑ๊ธฐ (์ค์ ์ฅ๋นยท๋ถ๋ยท์ ๋น ์ค์ ์์)\n",
"# ๊ฐ์ฒด๋ง๋ค ์ ์ -> ์ดํ -> ๊ณ ์ฅ(์๋ช
์ข
๋ฃ)๊น์ง ํ๋ฃจ 1ํ ์ธก์ (์ง๋ ํํ 256ํ๋ณธ + ์จ๋ + ์๋ ฅ + ๋ถํ)์ ๋ง๋ ๋ค.\n",
"# ์ดํ ์ ํ 4๊ฐ์ง: bearing(๋ฒ ์ด๋ง), gear(๊ธฐ์ด), oil_pump(์ค์ผ ํํ), cooling(๋๊ฐ ํฌ ๋ชจํฐ)\n",
"# ---------------------------------------------------------------------------\n",
"FS, NS = 1024, 256 # ๊ฐ์ ํ๋ณธํ ์ฃผํ์(Hz), ํํ 1๊ฐ๋น ํ๋ณธ ์\n",
"TT = np.arange(NS) / FS\n",
"MODES = { # ์ดํ ์ ํ: (๋ฐ์ ํ๋ฅ , ํ๊ท ์๋ช
(์ผ), ๊ต์ฒด ์๋ฆฌ๋ถ์, ๊ฐ์ ๋จ๊ฐ์ง์)\n",
" \"bearing\": (0.40, 160, \"์ฃผ ๋ฒ ์ด๋ง ์ธํธ\", 6.0), \"gear\": (0.25, 175, \"๊ตฌ๋ ๊ธฐ์ด ์ธํธ\", 9.0),\n",
" \"oil_pump\": (0.20, 150, \"์ค์ผ ํํ\", 4.0), \"cooling\": (0.15, 140, \"๋๊ฐ ํฌ ๋ชจํฐ\", 3.0)}\n",
"KIT_COST = 1.0 # ๋ชจ๋ ์ ๋น์ ํจ๊ป ์๋ชจ๋๋ ์ฐยทํํฐ ํคํธ ๋จ๊ฐ์ง์\n",
"BANDS = {\"b50\": (40, 60), \"b100\": (90, 110), \"b150\": (140, 156), \"b165\": (156, 176), \"b200\": (190, 215)}\n",
"\n",
"\n",
"def make_runs(n=48, seed=1):\n",
" \"\"\"๋ฐํ: units(๊ฐ์ฒด ์ ๋ณด), snap(์ผ๋ณ ์ธก์ ์์ฝ), W(์ง๋ ํํ [์ธก์ ์, 256])\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" names = list(MODES)\n",
" mode = rng.choice(names, n, p=[MODES[m][0] for m in names])\n",
" life = np.clip(np.array([rng.normal(MODES[m][1], 18) for m in mode]), 90, 230).astype(int)\n",
" start = np.sort(rng.integers(0, 240, n))\n",
" units = pd.DataFrame({\"unit\": [f\"U{i:02d}\" for i in range(n)], \"mode\": mode, \"life\": life, \"start\": start, \"base_load\": rng.uniform(0.4, 0.75, n)})\n",
" rows = []\n",
" for u in units.itertuples():\n",
" d = np.arange(u.life + 1)\n",
" onset = 0.45 * u.life\n",
" deg = np.clip((d - onset) / (u.life - onset), 0, 1) ** 1.5\n",
" load = np.clip(u.base_load + rng.normal(0, 0.06, len(d)), 0.2, 1.0)\n",
" hi = rng.random(len(d)) < 0.07 # ๊ณ ๋ถํ ์ด์ฉ์ผ(์ ์์ด์ง๋ง ์ง๋ยท์จ๋ ์์น)\n",
" load = np.where(hi, rng.uniform(0.95, 1.1, len(d)), load)\n",
" rows.append(pd.DataFrame({\"unit\": u.unit, \"day\": d, \"cal\": u.start + d, \"rul\": u.life - d, \"deg\": deg, \"load\": load, \"hi_load\": hi, \"mode\": u.mode}))\n",
" snap = pd.concat(rows, ignore_index=True)\n",
" S = len(snap)\n",
" snap[\"ambient\"] = 15 + 12 * np.sin(2 * np.pi * snap[\"cal\"] / 365) + rng.normal(0, 2, S)\n",
" dg, ld, md = snap[\"deg\"].values, snap[\"load\"].values, snap[\"mode\"].values\n",
" isb, isg, iso, isc = (md == \"bearing\"), (md == \"gear\"), (md == \"oil_pump\"), (md == \"cooling\")\n",
" snap[\"temp\"] = 55 + 22 * ld + 0.5 * snap[\"ambient\"] + 9 * dg * iso + 26 * dg * isc + 3 * dg * (isb | isg) + rng.normal(0, 1.5, S)\n",
" snap[\"press\"] = 450 - 12 * ld - 85 * dg * iso - 15 * dg * isc + rng.normal(0, 6, S)\n",
" ph = rng.uniform(0, 2 * np.pi, (S, 3))\n",
" a1, a2 = 1.0 * (0.5 + ld), 0.4 * (0.5 + ld)\n",
" W = (a1[:, None] * np.sin(2 * np.pi * 50 * TT + ph[:, :1]) + a2[:, None] * np.sin(2 * np.pi * 100 * TT + ph[:, 1:2])\n",
" + rng.normal(0, 0.35, (S, NS)))\n",
" W += (0.9 * dg * isb)[:, None] * np.sin(2 * np.pi * 165 * TT + ph[:, 2:3])\n",
" W += (0.8 * dg * isg)[:, None] * (np.sin(2 * np.pi * 150 * TT + ph[:, 2:3]) + 0.8 * np.sin(2 * np.pi * 100 * TT))\n",
" W += (0.5 * dg * iso)[:, None] * np.sin(2 * np.pi * 200 * TT + ph[:, 2:3])\n",
" k = rng.poisson(10 * dg * isb) # ๋ฒ ์ด๋ง ์ดํ: ์ถฉ๊ฒฉ์ฑ ํ์ค(์ฒจ๋ ์์น)\n",
" pos, amp = rng.integers(0, NS, (S, 30)), rng.normal(0, 3.0, (S, 30)) * dg[:, None]\n",
" amp = amp * (np.arange(30)[None, :] < k[:, None])\n",
" np.add.at(W, (np.repeat(np.arange(S), 30), pos.ravel()), amp.ravel())\n",
" return units, snap, W\n",
"\n",
"\n",
"def extract(W):\n",
" \"\"\"์ง๋ ํํ -> ํน์ง๋: RMS, ์ฒจ๋, ํ๊ณ ์จ, ์ต๋-์ต์, FFT ๋์ญ ์๋์ง(50/100/150/165/200Hz ๋ถ๊ทผ)\"\"\"\n",
" x = W - W.mean(axis=1, keepdims=True)\n",
" rms = np.sqrt((x ** 2).mean(axis=1))\n",
" out = {\"rms\": rms, \"kurt\": kurtosis(x, axis=1, fisher=False), \"crest\": np.abs(x).max(axis=1) / rms, \"p2p\": x.max(axis=1) - x.min(axis=1)}\n",
" A = np.abs(np.fft.rfft(x * np.hanning(NS), axis=1)) / NS\n",
" f = np.fft.rfftfreq(NS, 1 / FS)\n",
" for nm, (lo, hi) in BANDS.items():\n",
" out[nm] = np.sqrt((A[:, (f >= lo) & (f < hi)] ** 2).sum(axis=1))\n",
" return pd.DataFrame(out)\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# ๊ณตํต ๋ถ์ ์กฐ๊ฐ: ์ผ๋ณ ํน์ง๋ ํ(ํํ ํน์ง + ์จ๋ยท์๋ ฅ + ํ์), ๊ฐ์ฒด ๋จ์ ํ์ต/๊ฒ์ฆ ๋ถํ (์์ 24๋ ํ์ต, ๋ค 24๋ ๊ฒ์ฆ)\n",
"# ---------------------------------------------------------------------------\n",
"BASE = [\"rms\", \"kurt\", \"crest\", \"p2p\", \"b50\", \"b100\", \"b150\", \"b165\", \"b200\", \"temp\", \"press\"]\n",
"ROLL = [\"rms\", \"kurt\", \"b150\", \"b165\", \"b200\", \"temp\", \"press\"]\n",
"\n",
"\n",
"def build_features(snap, W):\n",
" D = pd.concat([snap.reset_index(drop=True), extract(W)], axis=1)\n",
" D[\"temp_res\"] = D[\"temp\"] - (55 + 22 * D[\"load\"] + 0.5 * D[\"ambient\"]) # ๋ถํยท์ธ๊ธฐ ๋๋น ์จ๋ ์์ฐจ\n",
" D[\"press_res\"] = D[\"press\"] - (450 - 12 * D[\"load\"]) # ๋ถํ ๋๋น ์๋ ฅ ์์ฐจ\n",
" D[\"rms_n\"] = D[\"rms\"] / (0.5 + D[\"load\"]) # ๋ถํ ์ ๊ทํ RMS\n",
" D[\"hf_ratio\"] = (D[\"b150\"] + D[\"b165\"] + D[\"b200\"]) / (D[\"b50\"] + 1e-6) # ๊ณ ์ฃผํ/๊ธฐ๋ณธ์ฃผํ ๋น\n",
" g = D.groupby(\"unit\")\n",
" for c in ROLL:\n",
" D[c + \"_m3\"] = g[c].transform(lambda s: s.rolling(3, min_periods=1).mean())\n",
" for c in (\"rms_n\", \"temp_res\", \"press_res\"):\n",
" D[c + \"_d\"] = g[c].diff().fillna(0)\n",
" return D\n",
"\n",
"\n",
"DERIVED = [\"temp_res\", \"press_res\", \"rms_n\", \"hf_ratio\"] + [c + \"_m3\" for c in ROLL] + [\"rms_n_d\", \"temp_res_d\", \"press_res_d\"]\n",
"FEATS = BASE + DERIVED\n",
"AE_COLS = [\"rms_n\", \"kurt\", \"b150\", \"b165\", \"b200\", \"hf_ratio\", \"temp_res\", \"press_res\"]\n",
"\n",
"\n",
"def split_units(units, n_train=24):\n",
" return list(units[\"unit\"][:n_train]), list(units[\"unit\"][n_train:])\n",
"\n",
"\n",
"class AE:\n",
" \"\"\"์คํ ์ธ์ฝ๋(์
๋ ฅ 8 -> 10 -> 4 -> 10 -> 8): ์ ์ ์๋ฃ๋ง ์ฌ๊ตฌ์ฑํ๋๋ก ํ์ตํ๊ณ , ์ฌ๊ตฌ์ฑ ์ค์ฐจ๋ฅผ ์ด์ ์ ์๋ก ์ด๋ค.\"\"\"\n",
"\n",
" def __init__(self, cols=AE_COLS, q=0.99, seed=0):\n",
" self.cols, self.q, self.seed = cols, q, seed\n",
"\n",
" def fit(self, healthy):\n",
" self.sc = StandardScaler().fit(healthy[self.cols])\n",
" Z = self.sc.transform(healthy[self.cols])\n",
" self.net = MLPRegressor(hidden_layer_sizes=(10, 4, 10), max_iter=800, random_state=self.seed).fit(Z, Z)\n",
" self.thr = float(np.quantile(self.err(healthy), self.q))\n",
" return self\n",
"\n",
" def contrib(self, d):\n",
" Z = self.sc.transform(d[self.cols])\n",
" return (Z - self.net.predict(Z)) ** 2 # ์
๋ ฅ ํญ๋ชฉ๋ณ ์ฌ๊ตฌ์ฑ ์ค์ฐจ(์ด์ ๊ธฐ์ฌ๋)\n",
"\n",
" def err(self, d):\n",
" return self.contrib(d).mean(axis=1)\n",
"\n",
"\n",
"def latch(d, e, thr, k=2):\n",
" \"\"\"๊ฐ์ ๊ฐ์ฒด์์ ์ด์ ์ ์๊ฐ k์ผ ์ฐ์ ๊ธฐ์ค์ ๋์ผ๋ฉด ๊ฒฝ๋ณด๋ฅผ ์ผ๊ณ ์ ์ง(๋์น)ํ๋ค.\"\"\"\n",
" out, e = np.zeros(len(d), bool), np.asarray(e)\n",
" for idx in d.reset_index(drop=True).groupby(\"unit\").indices.values():\n",
" run, on = 0, False\n",
" for i in idx:\n",
" run = run + 1 if e[i] > thr else 0\n",
" on = on or run >= k\n",
" out[i] = on\n",
" return out\n",
"\n",
"CAP = 120\n",
"units, snap, W = make_runs()\n",
"D = build_features(snap, W)\n",
"D[\"hf_max\"] = D.groupby(\"unit\")[\"hf_ratio\"].cummax() # ์ง๊ธ๊น์ง ๊ฐ์ฅ ๋์๋ ๊ณ ์ฃผํ ๋น(์ดํ๋ ๋๋์๊ฐ์ง ์๋๋ค)\n",
"D[\"temp_res_max\"] = D.groupby(\"unit\")[\"temp_res\"].cummax()\n",
"FEATS = FEATS + [\"day\", \"hf_max\", \"temp_res_max\"] # ์ด์ฉ์ผ์(day)๋ ์ ๋น ์ด๋ ฅ์์ ์๋ ๊ฐ\n",
"tr_u, va_u = split_units(units)\n",
"tr, va = D[D[\"unit\"].isin(tr_u)].reset_index(drop=True), D[D[\"unit\"].isin(va_u)].reset_index(drop=True)\n",
"ytr, yva = np.minimum(tr[\"rul\"], CAP), np.minimum(va[\"rul\"], CAP)\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ํ์ต {len(tr_u)}๋ {len(tr)}๊ฑด, Hold-out ๊ฒ์ฆ {len(va_u)}๋ {len(va)}๊ฑด (RUL์ {CAP}์ผ ์ํ)\")\n",
"\n",
"# 1) ๊ธฐ์ค ๋ชจ๋ธ: ์ง๋ RMS์ ์จ๋๋ง ์ฐ๋ ์ ํ ํ๊ท\n",
"base = LinearRegression().fit(tr[[\"rms\", \"temp\"]], ytr)\n",
"pb = base.predict(va[[\"rms\", \"temp\"]])\n",
"\n",
"# 2) ๊ณ ๋ํ: ํ์ดํผํ๋ผ๋ฏธํฐ ํ์(ํ์ต ๊ฐ์ฒด์ ๋ค์ชฝ 6๋๋ฅผ ๋ด๋ถ ๊ฒ์ฆ) ํ ์์๋ธ\n",
"inner_tr, inner_va = tr[tr[\"unit\"].isin(tr_u[:18])], tr[tr[\"unit\"].isin(tr_u[18:])]\n",
"\n",
"\n",
"def ens(d_tr, y, d_va, depth, alpha):\n",
" ms = [GradientBoostingRegressor(n_estimators=100, max_depth=depth, learning_rate=0.1, subsample=0.8, random_state=0),\n",
" RandomForestRegressor(n_estimators=40, max_depth=10, min_samples_leaf=5, n_jobs=-1, random_state=0),\n",
" make_pipeline(StandardScaler(), Ridge(alpha=alpha))]\n",
" for m in ms:\n",
" m.fit(d_tr[FEATS], y)\n",
" return ms, np.mean([m.predict(d_va[FEATS]) for m in ms], axis=0)\n",
"\n",
"\n",
"score = {(dp, al): mean_absolute_error(np.minimum(inner_va[\"rul\"], CAP), ens(inner_tr, np.minimum(inner_tr[\"rul\"], CAP), inner_va, dp, al)[1])\n",
" for dp in (3, 4) for al in (1, 10)}\n",
"dp, al = min(score, key=score.get)\n",
"models, pe = ens(tr, ytr, va, dp, al)\n",
"names = [\"GBR\", \"RF\", \"Ridge\"]\n",
"print(f\"์ ํ ํ์ดํผํ๋ผ๋ฏธํฐ: GBR depth={dp}, ridge alpha={al} (๋ด๋ถ ๊ฒ์ฆ MAE {score[(dp, al)]:.1f}์ผ)\")\n",
"\n",
"\n",
"def metrics(p):\n",
" return mean_absolute_error(yva, p), mean_squared_error(yva, p) ** 0.5, r2_score(yva, p)\n",
"\n",
"\n",
"mb, me = metrics(pb), metrics(pe)\n",
"print(\"\\n[RUL ์์ธก ์ฑ๋ฅ] ๋ชจ๋ธ | MAE(์ผ) | RMSE(์ผ) | R2\")\n",
"print(f\" ๊ธฐ์ค(RMS+์จ๋ ์ ํ) {mb[0]:6.2f} {mb[1]:6.2f} {mb[2]:.3f}\")\n",
"for n, m in zip(names, models):\n",
" x = metrics(m.predict(va[FEATS]))\n",
" print(f\" ๋จ์ผ {n:12s} {x[0]:6.2f} {x[1]:6.2f} {x[2]:.3f}\")\n",
"print(f\" ๊ณ ๋ํ(์์๋ธ) {me[0]:6.2f} {me[1]:6.2f} {me[2]:.3f}\")\n",
"imp_mae, imp_rmse = 1 - me[0] / mb[0], 1 - me[1] / mb[1]\n",
"print(f\"๊ฐ์ ์จ MAE {imp_mae:.1%}, RMSE {imp_rmse:.1%} (๋ชฉํ 20% ์ด์: {'๋ฌ์ฑ' if min(imp_mae, imp_rmse) >= 0.2 else '๋ฏธ๋ฌ'}), R2 {me[2]:.3f} (๋ชฉํ 0.85 ์ด์: {'๋ฌ์ฑ' if me[2] >= 0.85 else '๋ฏธ๋ฌ'})\")\n",
"for lo, hi, nm in [(0, 30, \"๊ณ ์ฅ 30์ผ ์ด๋ด\"), (30, 90, \"30~90์ผ\"), (90, 9999, \"90์ผ ์ด์\")]:\n",
" m = (va[\"rul\"] >= lo) & (va[\"rul\"] < hi)\n",
" print(f\" ๊ตฌ๊ฐ {nm:10s} MAE {mean_absolute_error(yva[m], pe[m]):5.1f}์ผ (๊ธฐ์ค {mean_absolute_error(yva[m], pb[m]):5.1f}์ผ)\")\n",
"\n",
"# 3) ์ํ ์ด์ ์ ๋ขฐ๋ ์ ์ = ์คํ ์ธ์ฝ๋ ์ด์ ์ ์(์ ์ ๋๋น ๋ฐฐ์)์ ์์๋ธ ๊ตฌ์ฑ ๋ชจ๋ธ ๊ฐ ์๊ฒฌ ์ผ์น๋๋ฅผ ๊ฒฐํฉ\n",
"ae = AE().fit(tr[tr[\"deg\"] == 0])\n",
"e = ae.err(va)\n",
"z = np.log(e / ae.thr)\n",
"conf = 1 / (1 + np.exp(-2.5 * z)) # ์ฌ๊ตฌ์ฑ ์ค์ฐจ๊ฐ ๊ธฐ์ค๊ณผ ๊ฐ์ผ๋ฉด 0.5\n",
"spread = np.std([m.predict(va[FEATS]) for m in models], axis=0)\n",
"agree = np.clip(1 - spread / 40, 0, 1) # ๋ชจ๋ธ ๊ฐ RUL ์๊ฒฌ ์ฐจ์ด๊ฐ ์์์๋ก ๋์\n",
"score_c = conf * (0.7 + 0.3 * agree)\n",
"truth = va[\"deg\"] >= 0.1\n",
"print(\"\\n[์ํ ์ด์ ์ ๋ขฐ๋] ๊ตฌ๊ฐ | ๊ฑด์ | ํ๊ท ์ ์ | ์ค์ ์ดํ ๋น์จ\")\n",
"bins = [0, 0.2, 0.5, 0.8, 1.01]\n",
"for a, b in zip(bins[:-1], bins[1:]):\n",
" m = (score_c >= a) & (score_c < b)\n",
" print(f\" {a:.1f}~{min(b, 1):.1f} {m.sum():5d} {score_c[m].mean():.2f} {truth[m].mean():.2f}\")\n",
"ece = sum(((score_c >= a) & (score_c < b)).mean() * abs(score_c[(score_c >= a) & (score_c < b)].mean() - truth[(score_c >= a) & (score_c < b)].mean())\n",
" for a, b in zip(bins[:-1], bins[1:]) if ((score_c >= a) & (score_c < b)).any())\n",
"print(f\"๊ธฐ๋ ๋ณด์ ์ค์ฐจ(ECE) {ece:.3f}\")\n",
"\n",
"# 4) ์ ๊ฒ ์นด๋ ์์(๊ฐ์ฒด ํ๋์ ์ต๊ทผ ์ํ)\n",
"print(\"\\n[์ ๊ฒ ์นด๋ ์์] ๊ฐ์ฒด | ์ธก์ ์ผ | ์ด์ ์ ๋ขฐ๋ | ์์ธก RUL(์ผ) | ์ค์ RUL(์ผ)\")\n",
"va[\"conf\"], va[\"rul_hat\"] = score_c, pe\n",
"for u in va_u[:3]:\n",
" g = va[va[\"unit\"] == u]\n",
" r = g[g[\"rul\"] == 40].iloc[0] if (g[\"rul\"] == 40).any() else g.iloc[-1]\n",
" print(f\" {u} {int(r['day']):3d}์ผ์งธ {r['conf']:.2f} {r['rul_hat']:5.1f} {int(r['rul'])} -> {'์ ๋น ๊ณํ ์๋ฆฝ ๊ถ๊ณ ' if r['conf'] >= 0.8 else '์ถ๊ฐ ๊ด์ฐฐ'}(๊ฒ์ฌ๊ด ํ์ธ)\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
05 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ํ์ต 24๋ 3847๊ฑด, Hold-out ๊ฒ์ฆ 24๋ 3886๊ฑด (RUL์ 120์ผ ์ํ)\n",
"์ ํ ํ์ดํผํ๋ผ๋ฏธํฐ: GBR depth=4, ridge alpha=1 (๋ด๋ถ ๊ฒ์ฆ MAE 6.8์ผ)\n",
"\n",
"[RUL ์์ธก ์ฑ๋ฅ] ๋ชจ๋ธ | MAE(์ผ) | RMSE(์ผ) | R2\n",
" ๊ธฐ์ค(RMS+์จ๋ ์ ํ) 35.67 41.43 -0.078\n",
" ๋จ์ผ GBR 9.45 13.37 0.888\n",
" ๋จ์ผ RF 8.00 12.31 0.905\n",
" ๋จ์ผ Ridge 9.98 12.78 0.897\n",
" ๊ณ ๋ํ(์์๋ธ) 8.56 12.05 0.909\n",
"๊ฐ์ ์จ MAE 76.0%, RMSE 70.9% (๋ชฉํ 20% ์ด์: ๋ฌ์ฑ), R2 0.909 (๋ชฉํ 0.85 ์ด์: ๋ฌ์ฑ)\n",
" ๊ตฌ๊ฐ ๊ณ ์ฅ 30์ผ ์ด๋ด MAE 5.7์ผ (๊ธฐ์ค 44.8์ผ)\n",
" ๊ตฌ๊ฐ 30~90์ผ MAE 10.2์ผ (๊ธฐ์ค 16.9์ผ)\n",
" ๊ตฌ๊ฐ 90์ผ ์ด์ MAE 8.4์ผ (๊ธฐ์ค 47.5์ผ)\n",
"\n",
"[์ํ ์ด์ ์ ๋ขฐ๋] ๊ตฌ๊ฐ | ๊ฑด์ | ํ๊ท ์ ์ | ์ค์ ์ดํ ๋น์จ\n",
" 0.0~0.2 2242 0.04 0.10\n",
" 0.2~0.5 282 0.31 0.41\n",
" 0.5~0.8 161 0.65 0.86\n",
" 0.8~1.0 1201 0.95 1.00\n",
"๊ธฐ๋ ๋ณด์ ์ค์ฐจ(ECE) 0.065\n",
"\n",
"[์ ๊ฒ ์นด๋ ์์] ๊ฐ์ฒด | ์ธก์ ์ผ | ์ด์ ์ ๋ขฐ๋ | ์์ธก RUL(์ผ) | ์ค์ RUL(์ผ)\n",
" U24 94์ผ์งธ 0.01 56.0 40 -> ์ถ๊ฐ ๊ด์ฐฐ(๊ฒ์ฌ๊ด ํ์ธ)\n",
" U25 95์ผ์งธ 0.62 56.6 40 -> ์ถ๊ฐ ๊ด์ฐฐ(๊ฒ์ฌ๊ด ํ์ธ)\n",
" U26 148์ผ์งธ 0.92 28.0 40 -> ์ ๋น ๊ณํ ์๋ฆฝ ๊ถ๊ณ (๊ฒ์ฌ๊ด ํ์ธ)\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
06. ์ฅ๋น ์ผ์ ์ํ ์ค์๊ฐ ๋ชจ๋ํฐ๋ง ๋ฐ ๊ฒฐํจ ์งํ ๊ฒฝ๋ณด ๋์๋ณด๋ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ฅ๋น ์ผ์ ์ํ ์ค์๊ฐ ๋ชจ๋ํฐ๋ง ๋ฐ ๊ฒฐํจ ๊ฒฝ๋ณด ๋์๋ณด๋๋ฅผ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ฅ๋น ์ผ์ ์ํ๋ฅผ ๊ฐ์ฒด๋ณ๋ก ๋ชจ๋ํฐ๋งํด ๊ฒฐํจ ๊ฒฝ๋ณด๋ฅผ ๋์ฐ๋ ์ ๋น๋ณ์ฉ ํ๋ฉด์ ๊ตฌ์ฑํ๊ณ , ๊ฒฝ๋ณด ๊ฑด๋ง๋ค ๊ธฐ์ฌ๋ ์์ 3๊ฐ ์ธ์์ ์์ฐ์ด ์์ฝ์ ์๋ ์ฐ์ถํ๋ ์ค๋ช
๊ฐ๋ฅ์ฑ(XAI) ๋ชจ๋์ ๊ตฌํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์คํ ์ธ์ฝ๋ ์ฌ๊ตฌ์ฑ ์ค์ฐจ๋ฅผ ์
๋ ฅ ์ธ์๋ณ๋ก ๋๋์ด ๊ฒฝ๋ณด ๊ฑด๋ง๋ค ์์ 3๊ฐ ์ธ์๋ฅผ ๋ฝ๊ณ , 1์์ ์ธ์๋ฅผ ์ ์๊ฐ์ผ๋ก ๋๋๋ ธ์ ๋ ์ ์๊ฐ ๋ด๋ ค๊ฐ๋์ง๋ก ๊ทผ๊ฑฐ ์ ํจ์ฑ์ ์ ๊ฒ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/explainability/mnt04_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": [
"\"\"\"์ฅ๋น ์ผ์ ์ํ๋ฅผ ๊ฐ์ฒด๋ณ๋ก ๋ชจ๋ํฐ๋งํด ์ด์ ์ ํธ๋ฅผ ๊ฒฝ๋ณด๋ก ๋์ฐ๋ ์ ๋น๋ณ์ฉ ํ๋ฉด(์ํํยท์ ๊ฒ ์นด๋)์ ๊ตฌ์ฑํ๊ณ , ์คํ ์ธ์ฝ๋ ์ฌ๊ตฌ์ฑ ์ค์ฐจ์์ ๊ฒฝ๋ณด ๊ฑด๋ง๋ค ๊ธฐ์ฌ๋ ์์ 3๊ฐ ์ธ์๋ฅผ ์๋ ์ฐ์ถํ๋ฉฐ(๊ทผ๊ฑฐ ์ ํจ์ฑ ๊ฒ์ฆ), ์์ฌ ๋ถํ๊ณผ ์์ฐ์ด ์์ฝ ๋ฌธ์ฅ์ ์์ฑํ๋ ์ค๋ช
๊ฐ๋ฅ์ฑ(XAI) ๋ชจ๋์ ๊ตฌํํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy.stats import kurtosis\n",
"from sklearn.ensemble import GradientBoostingRegressor\n",
"from sklearn.neural_network import MLPRegressor\n",
"from sklearn.preprocessing import StandardScaler\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๊ธฐ๋์ฒด 48๋์ ์์งยท๊ตฌ๋๊ณ ์ง๋ยท์จ๋ยท์๋ ฅ ์ผ์ ์๋ฃ ์์ฑ๊ธฐ (์ค์ ์ฅ๋นยท๋ถ๋ยท์ ๋น ์ค์ ์์)\n",
"# ๊ฐ์ฒด๋ง๋ค ์ ์ -> ์ดํ -> ๊ณ ์ฅ(์๋ช
์ข
๋ฃ)๊น์ง ํ๋ฃจ 1ํ ์ธก์ (์ง๋ ํํ 256ํ๋ณธ + ์จ๋ + ์๋ ฅ + ๋ถํ)์ ๋ง๋ ๋ค.\n",
"# ์ดํ ์ ํ 4๊ฐ์ง: bearing(๋ฒ ์ด๋ง), gear(๊ธฐ์ด), oil_pump(์ค์ผ ํํ), cooling(๋๊ฐ ํฌ ๋ชจํฐ)\n",
"# ---------------------------------------------------------------------------\n",
"FS, NS = 1024, 256 # ๊ฐ์ ํ๋ณธํ ์ฃผํ์(Hz), ํํ 1๊ฐ๋น ํ๋ณธ ์\n",
"TT = np.arange(NS) / FS\n",
"MODES = { # ์ดํ ์ ํ: (๋ฐ์ ํ๋ฅ , ํ๊ท ์๋ช
(์ผ), ๊ต์ฒด ์๋ฆฌ๋ถ์, ๊ฐ์ ๋จ๊ฐ์ง์)\n",
" \"bearing\": (0.40, 160, \"์ฃผ ๋ฒ ์ด๋ง ์ธํธ\", 6.0), \"gear\": (0.25, 175, \"๊ตฌ๋ ๊ธฐ์ด ์ธํธ\", 9.0),\n",
" \"oil_pump\": (0.20, 150, \"์ค์ผ ํํ\", 4.0), \"cooling\": (0.15, 140, \"๋๊ฐ ํฌ ๋ชจํฐ\", 3.0)}\n",
"KIT_COST = 1.0 # ๋ชจ๋ ์ ๋น์ ํจ๊ป ์๋ชจ๋๋ ์ฐยทํํฐ ํคํธ ๋จ๊ฐ์ง์\n",
"BANDS = {\"b50\": (40, 60), \"b100\": (90, 110), \"b150\": (140, 156), \"b165\": (156, 176), \"b200\": (190, 215)}\n",
"\n",
"\n",
"def make_runs(n=48, seed=1):\n",
" \"\"\"๋ฐํ: units(๊ฐ์ฒด ์ ๋ณด), snap(์ผ๋ณ ์ธก์ ์์ฝ), W(์ง๋ ํํ [์ธก์ ์, 256])\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" names = list(MODES)\n",
" mode = rng.choice(names, n, p=[MODES[m][0] for m in names])\n",
" life = np.clip(np.array([rng.normal(MODES[m][1], 18) for m in mode]), 90, 230).astype(int)\n",
" start = np.sort(rng.integers(0, 240, n))\n",
" units = pd.DataFrame({\"unit\": [f\"U{i:02d}\" for i in range(n)], \"mode\": mode, \"life\": life, \"start\": start, \"base_load\": rng.uniform(0.4, 0.75, n)})\n",
" rows = []\n",
" for u in units.itertuples():\n",
" d = np.arange(u.life + 1)\n",
" onset = 0.45 * u.life\n",
" deg = np.clip((d - onset) / (u.life - onset), 0, 1) ** 1.5\n",
" load = np.clip(u.base_load + rng.normal(0, 0.06, len(d)), 0.2, 1.0)\n",
" hi = rng.random(len(d)) < 0.07 # ๊ณ ๋ถํ ์ด์ฉ์ผ(์ ์์ด์ง๋ง ์ง๋ยท์จ๋ ์์น)\n",
" load = np.where(hi, rng.uniform(0.95, 1.1, len(d)), load)\n",
" rows.append(pd.DataFrame({\"unit\": u.unit, \"day\": d, \"cal\": u.start + d, \"rul\": u.life - d, \"deg\": deg, \"load\": load, \"hi_load\": hi, \"mode\": u.mode}))\n",
" snap = pd.concat(rows, ignore_index=True)\n",
" S = len(snap)\n",
" snap[\"ambient\"] = 15 + 12 * np.sin(2 * np.pi * snap[\"cal\"] / 365) + rng.normal(0, 2, S)\n",
" dg, ld, md = snap[\"deg\"].values, snap[\"load\"].values, snap[\"mode\"].values\n",
" isb, isg, iso, isc = (md == \"bearing\"), (md == \"gear\"), (md == \"oil_pump\"), (md == \"cooling\")\n",
" snap[\"temp\"] = 55 + 22 * ld + 0.5 * snap[\"ambient\"] + 9 * dg * iso + 26 * dg * isc + 3 * dg * (isb | isg) + rng.normal(0, 1.5, S)\n",
" snap[\"press\"] = 450 - 12 * ld - 85 * dg * iso - 15 * dg * isc + rng.normal(0, 6, S)\n",
" ph = rng.uniform(0, 2 * np.pi, (S, 3))\n",
" a1, a2 = 1.0 * (0.5 + ld), 0.4 * (0.5 + ld)\n",
" W = (a1[:, None] * np.sin(2 * np.pi * 50 * TT + ph[:, :1]) + a2[:, None] * np.sin(2 * np.pi * 100 * TT + ph[:, 1:2])\n",
" + rng.normal(0, 0.35, (S, NS)))\n",
" W += (0.9 * dg * isb)[:, None] * np.sin(2 * np.pi * 165 * TT + ph[:, 2:3])\n",
" W += (0.8 * dg * isg)[:, None] * (np.sin(2 * np.pi * 150 * TT + ph[:, 2:3]) + 0.8 * np.sin(2 * np.pi * 100 * TT))\n",
" W += (0.5 * dg * iso)[:, None] * np.sin(2 * np.pi * 200 * TT + ph[:, 2:3])\n",
" k = rng.poisson(10 * dg * isb) # ๋ฒ ์ด๋ง ์ดํ: ์ถฉ๊ฒฉ์ฑ ํ์ค(์ฒจ๋ ์์น)\n",
" pos, amp = rng.integers(0, NS, (S, 30)), rng.normal(0, 3.0, (S, 30)) * dg[:, None]\n",
" amp = amp * (np.arange(30)[None, :] < k[:, None])\n",
" np.add.at(W, (np.repeat(np.arange(S), 30), pos.ravel()), amp.ravel())\n",
" return units, snap, W\n",
"\n",
"\n",
"def extract(W):\n",
" \"\"\"์ง๋ ํํ -> ํน์ง๋: RMS, ์ฒจ๋, ํ๊ณ ์จ, ์ต๋-์ต์, FFT ๋์ญ ์๋์ง(50/100/150/165/200Hz ๋ถ๊ทผ)\"\"\"\n",
" x = W - W.mean(axis=1, keepdims=True)\n",
" rms = np.sqrt((x ** 2).mean(axis=1))\n",
" out = {\"rms\": rms, \"kurt\": kurtosis(x, axis=1, fisher=False), \"crest\": np.abs(x).max(axis=1) / rms, \"p2p\": x.max(axis=1) - x.min(axis=1)}\n",
" A = np.abs(np.fft.rfft(x * np.hanning(NS), axis=1)) / NS\n",
" f = np.fft.rfftfreq(NS, 1 / FS)\n",
" for nm, (lo, hi) in BANDS.items():\n",
" out[nm] = np.sqrt((A[:, (f >= lo) & (f < hi)] ** 2).sum(axis=1))\n",
" return pd.DataFrame(out)\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# ๊ณตํต ๋ถ์ ์กฐ๊ฐ: ์ผ๋ณ ํน์ง๋ ํ(ํํ ํน์ง + ์จ๋ยท์๋ ฅ + ํ์), ๊ฐ์ฒด ๋จ์ ํ์ต/๊ฒ์ฆ ๋ถํ (์์ 24๋ ํ์ต, ๋ค 24๋ ๊ฒ์ฆ)\n",
"# ---------------------------------------------------------------------------\n",
"BASE = [\"rms\", \"kurt\", \"crest\", \"p2p\", \"b50\", \"b100\", \"b150\", \"b165\", \"b200\", \"temp\", \"press\"]\n",
"ROLL = [\"rms\", \"kurt\", \"b150\", \"b165\", \"b200\", \"temp\", \"press\"]\n",
"\n",
"\n",
"def build_features(snap, W):\n",
" D = pd.concat([snap.reset_index(drop=True), extract(W)], axis=1)\n",
" D[\"temp_res\"] = D[\"temp\"] - (55 + 22 * D[\"load\"] + 0.5 * D[\"ambient\"]) # ๋ถํยท์ธ๊ธฐ ๋๋น ์จ๋ ์์ฐจ\n",
" D[\"press_res\"] = D[\"press\"] - (450 - 12 * D[\"load\"]) # ๋ถํ ๋๋น ์๋ ฅ ์์ฐจ\n",
" D[\"rms_n\"] = D[\"rms\"] / (0.5 + D[\"load\"]) # ๋ถํ ์ ๊ทํ RMS\n",
" D[\"hf_ratio\"] = (D[\"b150\"] + D[\"b165\"] + D[\"b200\"]) / (D[\"b50\"] + 1e-6) # ๊ณ ์ฃผํ/๊ธฐ๋ณธ์ฃผํ ๋น\n",
" g = D.groupby(\"unit\")\n",
" for c in ROLL:\n",
" D[c + \"_m3\"] = g[c].transform(lambda s: s.rolling(3, min_periods=1).mean())\n",
" for c in (\"rms_n\", \"temp_res\", \"press_res\"):\n",
" D[c + \"_d\"] = g[c].diff().fillna(0)\n",
" return D\n",
"\n",
"\n",
"DERIVED = [\"temp_res\", \"press_res\", \"rms_n\", \"hf_ratio\"] + [c + \"_m3\" for c in ROLL] + [\"rms_n_d\", \"temp_res_d\", \"press_res_d\"]\n",
"FEATS = BASE + DERIVED\n",
"AE_COLS = [\"rms_n\", \"kurt\", \"b150\", \"b165\", \"b200\", \"hf_ratio\", \"temp_res\", \"press_res\"]\n",
"\n",
"\n",
"def split_units(units, n_train=24):\n",
" return list(units[\"unit\"][:n_train]), list(units[\"unit\"][n_train:])\n",
"\n",
"\n",
"class AE:\n",
" \"\"\"์คํ ์ธ์ฝ๋(์
๋ ฅ 8 -> 10 -> 4 -> 10 -> 8): ์ ์ ์๋ฃ๋ง ์ฌ๊ตฌ์ฑํ๋๋ก ํ์ตํ๊ณ , ์ฌ๊ตฌ์ฑ ์ค์ฐจ๋ฅผ ์ด์ ์ ์๋ก ์ด๋ค.\"\"\"\n",
"\n",
" def __init__(self, cols=AE_COLS, q=0.99, seed=0):\n",
" self.cols, self.q, self.seed = cols, q, seed\n",
"\n",
" def fit(self, healthy):\n",
" self.sc = StandardScaler().fit(healthy[self.cols])\n",
" Z = self.sc.transform(healthy[self.cols])\n",
" self.net = MLPRegressor(hidden_layer_sizes=(10, 4, 10), max_iter=800, random_state=self.seed).fit(Z, Z)\n",
" self.thr = float(np.quantile(self.err(healthy), self.q))\n",
" return self\n",
"\n",
" def contrib(self, d):\n",
" Z = self.sc.transform(d[self.cols])\n",
" return (Z - self.net.predict(Z)) ** 2 # ์
๋ ฅ ํญ๋ชฉ๋ณ ์ฌ๊ตฌ์ฑ ์ค์ฐจ(์ด์ ๊ธฐ์ฌ๋)\n",
"\n",
" def err(self, d):\n",
" return self.contrib(d).mean(axis=1)\n",
"\n",
"\n",
"def latch(d, e, thr, k=2):\n",
" \"\"\"๊ฐ์ ๊ฐ์ฒด์์ ์ด์ ์ ์๊ฐ k์ผ ์ฐ์ ๊ธฐ์ค์ ๋์ผ๋ฉด ๊ฒฝ๋ณด๋ฅผ ์ผ๊ณ ์ ์ง(๋์น)ํ๋ค.\"\"\"\n",
" out, e = np.zeros(len(d), bool), np.asarray(e)\n",
" for idx in d.reset_index(drop=True).groupby(\"unit\").indices.values():\n",
" run, on = 0, False\n",
" for i in idx:\n",
" run = run + 1 if e[i] > thr else 0\n",
" on = on or run >= k\n",
" out[i] = on\n",
" return out\n",
"\n",
"units, snap, W = make_runs()\n",
"D = build_features(snap, W)\n",
"tr_u, va_u = split_units(units)\n",
"tr, va = D[D[\"unit\"].isin(tr_u)].reset_index(drop=True), D[D[\"unit\"].isin(va_u)].reset_index(drop=True)\n",
"ae = AE().fit(tr[tr[\"deg\"] == 0])\n",
"C = ae.contrib(va) # ๊ฑด x ์
๋ ฅํญ๋ชฉ ๊ธฐ์ฌ๋\n",
"e = C.mean(axis=1)\n",
"va[\"alarm\"] = latch(va, e, ae.thr, k=2)\n",
"va[\"score\"] = e / ae.thr\n",
"\n",
"LABEL = {\"rms_n\": \"๋ถํ ๋ณด์ ์ง๋ ํฌ๊ธฐ(RMS)\", \"kurt\": \"์ง๋ ์ถฉ๊ฒฉ์ฑ(์ฒจ๋)\", \"b150\": \"150Hz ๋ถ๊ทผ ์ง๋(๊ธฐ์ด ๋ง๋ฌผ๋ฆผ ๋์ญ)\", \"b165\": \"165Hz ๋ถ๊ทผ ์ง๋(๋ฒ ์ด๋ง ๊ฒฐํจ ๋์ญ)\",\n",
" \"b200\": \"200Hz ๋ถ๊ทผ ์ง๋(ํํ ๋์ญ)\", \"hf_ratio\": \"๊ณ ์ฃผํ/๊ธฐ๋ณธ์ฃผํ ์๋์ง ๋น\", \"temp_res\": \"๋ถํ ๋๋น ์จ๋ ์์น\", \"press_res\": \"๋ถํ ๋๋น ์๋ ฅ ์ ํ\"}\n",
"PART = {\"b165\": \"์ฃผ ๋ฒ ์ด๋ง ์ธํธ\", \"kurt\": \"์ฃผ ๋ฒ ์ด๋ง ์ธํธ\", \"b150\": \"๊ตฌ๋ ๊ธฐ์ด ์ธํธ\", \"b200\": \"์ค์ผ ํํ\", \"press_res\": \"์ค์ผ ํํ\", \"temp_res\": \"๋๊ฐ ํฌ ๋ชจํฐ\"}\n",
"TRUE_PART = {m: MODES[m][2] for m in MODES}\n",
"cols = ae.cols\n",
"\n",
"# 1) ๊ฒฝ๋ณด ๊ฑด๋ง๋ค ๊ธฐ์ฌ๋ ์์ 3๊ฐ ์ธ์ ์๋ ์ฐ์ถ\n",
"idx = np.where(va[\"alarm\"].values)[0]\n",
"top3 = np.argsort(-C[idx], axis=1)[:, :3]\n",
"ok = sum(len(set(t)) == 3 for t in top3)\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฒ์ฆ {len(va_u)}๋ {len(va)}๊ฑด ์ค ๊ฒฝ๋ณด {len(idx)}๊ฑด: ๊ธฐ์ฌ๋ ์์ 3๊ฐ ์ธ์ ์ฐ์ถ {ok}๊ฑด (์ฐ์ถ๋ฅ {ok / len(idx):.0%}, ๋ชฉํ 100%: {'๋ฌ์ฑ' if ok == len(idx) else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# 2) ๊ทผ๊ฑฐ ์ ํจ์ฑ: 1์์ ์ธ์๋ฅผ ์ ์ ํ๊ท (ํ์คํ 0)์ผ๋ก ๋๋๋ฆฌ๋ฉด ์ด์ ์ ์๊ฐ ๋ฎ์์ง๋๊ฐ\n",
"Z = ae.sc.transform(va[cols].iloc[idx])\n",
"Z2 = Z.copy()\n",
"Z2[np.arange(len(idx)), top3[:, 0]] = 0.0\n",
"e1, e2 = ((Z - ae.net.predict(Z)) ** 2).mean(axis=1), ((Z2 - ae.net.predict(Z2)) ** 2).mean(axis=1)\n",
"print(f\"๊ทผ๊ฑฐ ์ ํจ์ฑ: 1์์ ์ธ์๋ฅผ ์ ์๊ฐ์ผ๋ก ๋๋๋ฆฌ๋ฉด ์ด์ ์ ์๊ฐ ๋ฎ์์ง๋ ๋น์จ {(e2 < e1).mean():.1%} (ํ๊ท {e1.mean():.2f} -> {e2.mean():.2f})\")\n",
"\n",
"# 3) ์์ฌ ๋ถํ: 1์์ ์ธ์ -> ๋ถํ ๋งคํ (์ดํ๊ฐ ๋๋ ทํ ๊ฒฝ๋ณด์์ ์ ๋ต ๋น๊ต)\n",
"sub = va.iloc[idx].copy()\n",
"sub[\"top1\"] = [cols[t[0]] for t in top3]\n",
"sub[\"hint\"] = sub[\"top1\"].map(PART).fillna(\"๋ฏธ๋ถ๋ฅ(์ ๊ฒ ํ์)\")\n",
"sub[\"true_part\"] = sub[\"mode\"].map(TRUE_PART)\n",
"strong = sub[sub[\"deg\"] >= 0.3]\n",
"print(f\"์์ฌ ๋ถํ ์ถ์ ์ ํ๋(์ดํ 0.3 ์ด์ ๊ฒฝ๋ณด {len(strong)}๊ฑด): {(strong['hint'] == strong['true_part']).mean():.1%}\")\n",
"print(\" 1์์ ์ธ์ ๋ถํฌ: \" + \", \".join(f\"{LABEL[k].split('(')[0]} {v}\" for k, v in sub[\"top1\"].value_counts().head(4).items()))\n",
"\n",
"\n",
"def summarize(r, t):\n",
" f = [LABEL[cols[i]] for i in t]\n",
" part = PART.get(cols[t[0]], \"์์ธ ๋ถ๋ช
\")\n",
" return f\"{part} ์ด์ ๊ฐ๋ฅ์ฑ(์ด์ ์ ์ {r['score']:.1f}๋ฐฐ). ์ฃผ์ ๊ทผ๊ฑฐ: {f[0]}, {f[1]}, {f[2]}.\"\n",
"\n",
"\n",
"# 4) ์ ๋น๋ณ์ฉ ํ๋ฉด: ์ ๊ฒ ์นด๋์ ์ํํ\n",
"print(\"\\n[์ ๊ฒ ์นด๋] ๊ฐ์ฒด | ์ธก์ ์ผ | ์ค์ ์ดํ ์ ํ\")\n",
"for m in MODES:\n",
" k = sub[(sub[\"mode\"] == m) & (sub[\"deg\"] > 0.5)].index[0]\n",
" j = list(sub.index).index(k)\n",
" r = sub.loc[k]\n",
" print(f\" {r['unit']} {int(r['day'])}์ผ์งธ ({m})\")\n",
" print(\" \" + summarize(r, top3[j]))\n",
" print(\" ๊ทผ๊ฑฐ ๊ธฐ์ฌ๋: \" + \" / \".join(f\"{LABEL[cols[i]].split('(')[0]}({C[idx[j], i]:.1f})\" for i in top3[j]))\n",
"\n",
"CAL = int(va.groupby('cal')['alarm'].sum().idxmax()) # ๊ฒฝ๋ณด๊ฐ ๊ฐ์ฅ ๋ง์ ๋ ์ ๊ฐ์ ๊ธฐ์ค์ผ๋ก ์ด๋ค\n",
"act = va[(va[\"cal\"] == CAL)]\n",
"reg = {\"์ ์\": 0, \"์ฃผ์\": 0, \"๊ฒฝ๋ณด\": 0}\n",
"rows = []\n",
"for r in act.itertuples():\n",
" st = \"๊ฒฝ๋ณด\" if r.alarm else (\"์ฃผ์\" if r.score > 0.7 else \"์ ์\")\n",
" reg[st] += 1\n",
"for st in (\"๊ฒฝ๋ณด\", \"์ฃผ์\"):\n",
" for r in act[(act[\"alarm\"] if st == \"๊ฒฝ๋ณด\" else (~act[\"alarm\"] & (act[\"score\"] > 0.7)))].itertuples():\n",
" rows.append((r.unit, st, f\"{r.score:.1f}\", int(r.rul)))\n",
"print(f\"\\n[์ด์ฉ ์ํํ] ๊ฐ์ ๊ธฐ์ค์ผ {CAL}: ๊ฐ๋ ์ค {len(act)}๋ -> \" + \", \".join(f\"{k} {v}๋\" for k, v in reg.items()))\n",
"for r in rows[:5]:\n",
" print(f\" {r[0]} {r[1]} ์ด์ ์ ์ {r[2]}๋ฐฐ (์ค์ ์์กด {r[3]}์ผ)\")\n",
"\n",
"plt.figure(figsize=(8, 4))\n",
"for u in sub[\"unit\"].unique()[:3]:\n",
" g = va[va[\"unit\"] == u]\n",
" plt.plot(g[\"day\"], g[\"score\"], label=u)\n",
"plt.axhline(1.0, ls=\"--\", color=\"k\"); plt.yscale(\"log\"); plt.xlabel(\"day\"); plt.ylabel(\"anomaly score (x threshold)\"); plt.legend(); plt.title(\"Monitoring view: anomaly score\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
06 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฒ์ฆ 24๋ 3886๊ฑด ์ค ๊ฒฝ๋ณด 1327๊ฑด: ๊ธฐ์ฌ๋ ์์ 3๊ฐ ์ธ์ ์ฐ์ถ 1327๊ฑด (์ฐ์ถ๋ฅ 100%, ๋ชฉํ 100%: ๋ฌ์ฑ)\n",
"๊ทผ๊ฑฐ ์ ํจ์ฑ: 1์์ ์ธ์๋ฅผ ์ ์๊ฐ์ผ๋ก ๋๋๋ฆฌ๋ฉด ์ด์ ์ ์๊ฐ ๋ฎ์์ง๋ ๋น์จ 98.9% (ํ๊ท 21.19 -> 6.57)\n",
"์์ฌ ๋ถํ ์ถ์ ์ ํ๋(์ดํ 0.3 ์ด์ ๊ฒฝ๋ณด 1167๊ฑด): 99.3%\n",
" 1์์ ์ธ์ ๋ถํฌ: 150Hz ๋ถ๊ทผ ์ง๋ 603, 165Hz ๋ถ๊ทผ ์ง๋ 338, ๋ถํ ๋๋น ์จ๋ ์์น 146, ๋ถํ ๋๋น ์๋ ฅ ์ ํ 129\n",
"\n",
"[์ ๊ฒ ์นด๋] ๊ฐ์ฒด | ์ธก์ ์ผ | ์ค์ ์ดํ ์ ํ\n",
" U27 131์ผ์งธ (bearing)\n",
" ์ฃผ ๋ฒ ์ด๋ง ์ธํธ ์ด์ ๊ฐ๋ฅ์ฑ(์ด์ ์ ์ 7.1๋ฐฐ). ์ฃผ์ ๊ทผ๊ฑฐ: 165Hz ๋ถ๊ทผ ์ง๋(๋ฒ ์ด๋ง ๊ฒฐํจ ๋์ญ), 150Hz ๋ถ๊ทผ ์ง๋(๊ธฐ์ด ๋ง๋ฌผ๋ฆผ ๋์ญ), ๋ถํ ๋๋น ์๋ ฅ ์ ํ.\n",
" ๊ทผ๊ฑฐ ๊ธฐ์ฌ๋: 165Hz ๋ถ๊ทผ ์ง๋(36.6) / 150Hz ๋ถ๊ทผ ์ง๋(22.9) / ๋ถํ ๋๋น ์๋ ฅ ์ ํ(5.4)\n",
" U26 150์ผ์งธ (gear)\n",
" ๊ตฌ๋ ๊ธฐ์ด ์ธํธ ์ด์ ๊ฐ๋ฅ์ฑ(์ด์ ์ ์ 10.4๋ฐฐ). ์ฃผ์ ๊ทผ๊ฑฐ: 150Hz ๋ถ๊ทผ ์ง๋(๊ธฐ์ด ๋ง๋ฌผ๋ฆผ ๋์ญ), 165Hz ๋ถ๊ทผ ์ง๋(๋ฒ ์ด๋ง ๊ฒฐํจ ๋์ญ), ์ง๋ ์ถฉ๊ฒฉ์ฑ(์ฒจ๋).\n",
" ๊ทผ๊ฑฐ ๊ธฐ์ฌ๋: 150Hz ๋ถ๊ทผ ์ง๋(60.3) / 165Hz ๋ถ๊ทผ ์ง๋(19.9) / ์ง๋ ์ถฉ๊ฒฉ์ฑ(10.9)\n",
" U25 108์ผ์งธ (oil_pump)\n",
" ์ค์ผ ํํ ์ด์ ๊ฐ๋ฅ์ฑ(์ด์ ์ ์ 6.0๋ฐฐ). ์ฃผ์ ๊ทผ๊ฑฐ: ๋ถํ ๋๋น ์๋ ฅ ์ ํ, 200Hz ๋ถ๊ทผ ์ง๋(ํํ ๋์ญ), 165Hz ๋ถ๊ทผ ์ง๋(๋ฒ ์ด๋ง ๊ฒฐํจ ๋์ญ).\n",
" ๊ทผ๊ฑฐ ๊ธฐ์ฌ๋: ๋ถํ ๋๋น ์๋ ฅ ์ ํ(29.3) / 200Hz ๋ถ๊ทผ ์ง๋(22.0) / 165Hz ๋ถ๊ทผ ์ง๋(4.7)\n",
" U24 107์ผ์งธ (cooling)\n",
" ๋๊ฐ ํฌ ๋ชจํฐ ์ด์ ๊ฐ๋ฅ์ฑ(์ด์ ์ ์ 4.4๋ฐฐ). ์ฃผ์ ๊ทผ๊ฑฐ: ๋ถํ ๋๋น ์จ๋ ์์น, 200Hz ๋ถ๊ทผ ์ง๋(ํํ ๋์ญ), ์ง๋ ์ถฉ๊ฒฉ์ฑ(์ฒจ๋).\n",
" ๊ทผ๊ฑฐ ๊ธฐ์ฌ๋: ๋ถํ ๋๋น ์จ๋ ์์น(34.5) / 200Hz ๋ถ๊ทผ ์ง๋(3.9) / ์ง๋ ์ถฉ๊ฒฉ์ฑ(2.4)\n",
"\n",
"[์ด์ฉ ์ํํ] ๊ฐ์ ๊ธฐ์ค์ผ 308: ๊ฐ๋ ์ค 18๋ -> ์ ์ 6๋, ์ฃผ์ 1๋, ๊ฒฝ๋ณด 11๋\n",
" U26 ๊ฒฝ๋ณด ์ด์ ์ ์ 30.5๋ฐฐ (์ค์ ์์กด 1์ผ)\n",
" U28 ๊ฒฝ๋ณด ์ด์ ์ ์ 55.0๋ฐฐ (์ค์ ์์กด 5์ผ)\n",
" U31 ๊ฒฝ๋ณด ์ด์ ์ ์ 85.1๋ฐฐ (์ค์ ์์กด 1์ผ)\n",
" U32 ๊ฒฝ๋ณด ์ด์ ์ ์ 12.9๋ฐฐ (์ค์ ์์กด 14์ผ)\n",
" U33 ๊ฒฝ๋ณด ์ด์ ์ ์ 27.4๋ฐฐ (์ค์ ์์กด 14์ผ)\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
07. ์๋ฐฉ ์ ๋น ์กฐ๊ธฐ ์
๊ณ ๊ฒฐ์ ์ ์ํ ํญ๊ณต/๊ธฐ๊ฐ ๊ฒ์ฌ๊ด HITL ์น์ธ ๋ชจ๋ ์ค๊ณ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์๋ฐฉ ์ ๋น ์กฐ๊ธฐ ์
๊ณ ๊ฒฐ์ ์ ์ํ ๊ฒ์ฌ๊ด HITL ์น์ธ ๋ชจ๋์ ์ค๊ณํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ผ์ ๊ฒฝ๋ณด ์ ์๋ฐฉ ์ ๋น ์กฐ๊ธฐ ์
๊ณ ๋ฅผ ์ ์ํ๋ ํญ๊ณต/๊ธฐ๊ฐ ๊ฒ์ฌ๊ด์ด ์น์ธํด์ผ ํ์ ๋๋ HITL ์น์ธ ๋ชจ๋์ ์ค๊ณํ๊ณ ํจ๊ณผ๋ฅผ ์๋ฎฌ๋ ์ด์
์ผ๋ก ๊ฒ์ฆํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ฒฝ๋ณด์ RUL ์์ธก์ผ๋ก ์
๊ณ ์ผ์ ์ ์ํ๊ณ ์น์ธ ๋๊ธฐ์ด์ ์ฌ๋ ค ๊ฒ์ฌ๊ด์ ์น์ธยท๋ฐ๋ คยท๋ฌด์๋ต(๊ธฐ๋ณธ ๊ณํ์ธ ์ ๊ธฐ ๊ต์ฒด ์ ์ง)์ ์ฌ์ ์ ํจ๊ป ํด์ ์ฒด์ธ ๊ฐ์ฌ ๋ก๊ทธ๋ก ๋จ๊ธด๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/optimization/mnt04_simulator.py` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ํ์ค์ ๊ตฐ์ ์ ์ฝ์กฐ๊ฑด ๋ฐ์๋ ๋ฐ ์๋๋ฆฌ์ค๋ณ ๋์ ์ ์ ์ ์ ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์ต์ ํ ์ ์ฉ ์ ๋น์ฉ 15% ์ ๊ฐ ๋๋ ์ฌ๊ณ ํ์ ์จ 20% ํฅ์\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์ ํ/์ ์ ๊ณํ๋ฒ(LP/MIP) ์ต์ ํ ํด ๋์ถ ๋ฐ ์๋ฎฌ๋ ์ด์
์๋ฃ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ๊ณผ๊ฑฐ ์ค์ ๋ฐ์ดํฐ ๋๋น ์ต์ ํ ๊ฒฐ๊ณผ ๋น๊ต ๋ฐฑํ
์คํ
\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์ผ์ ์ด์ ๊ฒฝ๋ณด๊ฐ ๋๋ฉด ์๋ฐฉ ์ ๋น ์กฐ๊ธฐ ์
๊ณ ์ผ์ ์ ์ํ๋ ์๋ ํ์ ํ์ง ์๊ณ ํญ๊ณต/๊ธฐ๊ฐ ๊ฒ์ฌ๊ด ์น์ธ ๋๊ธฐ์ด์ ์ฌ๋ ค ์น์ธยท๋ฐ๋ คยท๋ฌด์๋ต(๊ธฐ๋ณธ ๊ณํ ์ ์ง)์ ์ฌ์ ์ ํจ๊ป ํด์ ์ฒด์ธ ๊ฐ์ฌ ๋ก๊ทธ์ ๋จ๊ธฐ๋ HITL ์น์ธ ๋ชจ๋์ ์๋ฎฌ๋ ์ด์
ํ๊ณ , ์ ๊ธฐ ๊ต์ฒด ๋ฐฉ์ ๋๋น ์ ๋นยท๊ณ ์ฅ ๋น์ฉ ์ ๊ฐ๋ฅ ๊ณผ ์น์ธ ์ง์ฐยท์ ์ ์ ํ(๊ฐ๋ ๊ฐ๋ ๊ธ์ฆ) ๋ฏผ๊ฐ๋๋ฅผ ๋ถ์ํ๋ค.\"\"\"\n",
"import hashlib\n",
"import json\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy.stats import kurtosis\n",
"from sklearn.ensemble import GradientBoostingRegressor\n",
"from sklearn.neural_network import MLPRegressor\n",
"from sklearn.preprocessing import StandardScaler\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๊ธฐ๋์ฒด 48๋์ ์์งยท๊ตฌ๋๊ณ ์ง๋ยท์จ๋ยท์๋ ฅ ์ผ์ ์๋ฃ ์์ฑ๊ธฐ (์ค์ ์ฅ๋นยท๋ถ๋ยท์ ๋น ์ค์ ์์)\n",
"# ๊ฐ์ฒด๋ง๋ค ์ ์ -> ์ดํ -> ๊ณ ์ฅ(์๋ช
์ข
๋ฃ)๊น์ง ํ๋ฃจ 1ํ ์ธก์ (์ง๋ ํํ 256ํ๋ณธ + ์จ๋ + ์๋ ฅ + ๋ถํ)์ ๋ง๋ ๋ค.\n",
"# ์ดํ ์ ํ 4๊ฐ์ง: bearing(๋ฒ ์ด๋ง), gear(๊ธฐ์ด), oil_pump(์ค์ผ ํํ), cooling(๋๊ฐ ํฌ ๋ชจํฐ)\n",
"# ---------------------------------------------------------------------------\n",
"FS, NS = 1024, 256 # ๊ฐ์ ํ๋ณธํ ์ฃผํ์(Hz), ํํ 1๊ฐ๋น ํ๋ณธ ์\n",
"TT = np.arange(NS) / FS\n",
"MODES = { # ์ดํ ์ ํ: (๋ฐ์ ํ๋ฅ , ํ๊ท ์๋ช
(์ผ), ๊ต์ฒด ์๋ฆฌ๋ถ์, ๊ฐ์ ๋จ๊ฐ์ง์)\n",
" \"bearing\": (0.40, 160, \"์ฃผ ๋ฒ ์ด๋ง ์ธํธ\", 6.0), \"gear\": (0.25, 175, \"๊ตฌ๋ ๊ธฐ์ด ์ธํธ\", 9.0),\n",
" \"oil_pump\": (0.20, 150, \"์ค์ผ ํํ\", 4.0), \"cooling\": (0.15, 140, \"๋๊ฐ ํฌ ๋ชจํฐ\", 3.0)}\n",
"KIT_COST = 1.0 # ๋ชจ๋ ์ ๋น์ ํจ๊ป ์๋ชจ๋๋ ์ฐยทํํฐ ํคํธ ๋จ๊ฐ์ง์\n",
"BANDS = {\"b50\": (40, 60), \"b100\": (90, 110), \"b150\": (140, 156), \"b165\": (156, 176), \"b200\": (190, 215)}\n",
"\n",
"\n",
"def make_runs(n=48, seed=1):\n",
" \"\"\"๋ฐํ: units(๊ฐ์ฒด ์ ๋ณด), snap(์ผ๋ณ ์ธก์ ์์ฝ), W(์ง๋ ํํ [์ธก์ ์, 256])\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" names = list(MODES)\n",
" mode = rng.choice(names, n, p=[MODES[m][0] for m in names])\n",
" life = np.clip(np.array([rng.normal(MODES[m][1], 18) for m in mode]), 90, 230).astype(int)\n",
" start = np.sort(rng.integers(0, 240, n))\n",
" units = pd.DataFrame({\"unit\": [f\"U{i:02d}\" for i in range(n)], \"mode\": mode, \"life\": life, \"start\": start, \"base_load\": rng.uniform(0.4, 0.75, n)})\n",
" rows = []\n",
" for u in units.itertuples():\n",
" d = np.arange(u.life + 1)\n",
" onset = 0.45 * u.life\n",
" deg = np.clip((d - onset) / (u.life - onset), 0, 1) ** 1.5\n",
" load = np.clip(u.base_load + rng.normal(0, 0.06, len(d)), 0.2, 1.0)\n",
" hi = rng.random(len(d)) < 0.07 # ๊ณ ๋ถํ ์ด์ฉ์ผ(์ ์์ด์ง๋ง ์ง๋ยท์จ๋ ์์น)\n",
" load = np.where(hi, rng.uniform(0.95, 1.1, len(d)), load)\n",
" rows.append(pd.DataFrame({\"unit\": u.unit, \"day\": d, \"cal\": u.start + d, \"rul\": u.life - d, \"deg\": deg, \"load\": load, \"hi_load\": hi, \"mode\": u.mode}))\n",
" snap = pd.concat(rows, ignore_index=True)\n",
" S = len(snap)\n",
" snap[\"ambient\"] = 15 + 12 * np.sin(2 * np.pi * snap[\"cal\"] / 365) + rng.normal(0, 2, S)\n",
" dg, ld, md = snap[\"deg\"].values, snap[\"load\"].values, snap[\"mode\"].values\n",
" isb, isg, iso, isc = (md == \"bearing\"), (md == \"gear\"), (md == \"oil_pump\"), (md == \"cooling\")\n",
" snap[\"temp\"] = 55 + 22 * ld + 0.5 * snap[\"ambient\"] + 9 * dg * iso + 26 * dg * isc + 3 * dg * (isb | isg) + rng.normal(0, 1.5, S)\n",
" snap[\"press\"] = 450 - 12 * ld - 85 * dg * iso - 15 * dg * isc + rng.normal(0, 6, S)\n",
" ph = rng.uniform(0, 2 * np.pi, (S, 3))\n",
" a1, a2 = 1.0 * (0.5 + ld), 0.4 * (0.5 + ld)\n",
" W = (a1[:, None] * np.sin(2 * np.pi * 50 * TT + ph[:, :1]) + a2[:, None] * np.sin(2 * np.pi * 100 * TT + ph[:, 1:2])\n",
" + rng.normal(0, 0.35, (S, NS)))\n",
" W += (0.9 * dg * isb)[:, None] * np.sin(2 * np.pi * 165 * TT + ph[:, 2:3])\n",
" W += (0.8 * dg * isg)[:, None] * (np.sin(2 * np.pi * 150 * TT + ph[:, 2:3]) + 0.8 * np.sin(2 * np.pi * 100 * TT))\n",
" W += (0.5 * dg * iso)[:, None] * np.sin(2 * np.pi * 200 * TT + ph[:, 2:3])\n",
" k = rng.poisson(10 * dg * isb) # ๋ฒ ์ด๋ง ์ดํ: ์ถฉ๊ฒฉ์ฑ ํ์ค(์ฒจ๋ ์์น)\n",
" pos, amp = rng.integers(0, NS, (S, 30)), rng.normal(0, 3.0, (S, 30)) * dg[:, None]\n",
" amp = amp * (np.arange(30)[None, :] < k[:, None])\n",
" np.add.at(W, (np.repeat(np.arange(S), 30), pos.ravel()), amp.ravel())\n",
" return units, snap, W\n",
"\n",
"\n",
"def extract(W):\n",
" \"\"\"์ง๋ ํํ -> ํน์ง๋: RMS, ์ฒจ๋, ํ๊ณ ์จ, ์ต๋-์ต์, FFT ๋์ญ ์๋์ง(50/100/150/165/200Hz ๋ถ๊ทผ)\"\"\"\n",
" x = W - W.mean(axis=1, keepdims=True)\n",
" rms = np.sqrt((x ** 2).mean(axis=1))\n",
" out = {\"rms\": rms, \"kurt\": kurtosis(x, axis=1, fisher=False), \"crest\": np.abs(x).max(axis=1) / rms, \"p2p\": x.max(axis=1) - x.min(axis=1)}\n",
" A = np.abs(np.fft.rfft(x * np.hanning(NS), axis=1)) / NS\n",
" f = np.fft.rfftfreq(NS, 1 / FS)\n",
" for nm, (lo, hi) in BANDS.items():\n",
" out[nm] = np.sqrt((A[:, (f >= lo) & (f < hi)] ** 2).sum(axis=1))\n",
" return pd.DataFrame(out)\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# ๊ณตํต ๋ถ์ ์กฐ๊ฐ: ์ผ๋ณ ํน์ง๋ ํ(ํํ ํน์ง + ์จ๋ยท์๋ ฅ + ํ์), ๊ฐ์ฒด ๋จ์ ํ์ต/๊ฒ์ฆ ๋ถํ (์์ 24๋ ํ์ต, ๋ค 24๋ ๊ฒ์ฆ)\n",
"# ---------------------------------------------------------------------------\n",
"BASE = [\"rms\", \"kurt\", \"crest\", \"p2p\", \"b50\", \"b100\", \"b150\", \"b165\", \"b200\", \"temp\", \"press\"]\n",
"ROLL = [\"rms\", \"kurt\", \"b150\", \"b165\", \"b200\", \"temp\", \"press\"]\n",
"\n",
"\n",
"def build_features(snap, W):\n",
" D = pd.concat([snap.reset_index(drop=True), extract(W)], axis=1)\n",
" D[\"temp_res\"] = D[\"temp\"] - (55 + 22 * D[\"load\"] + 0.5 * D[\"ambient\"]) # ๋ถํยท์ธ๊ธฐ ๋๋น ์จ๋ ์์ฐจ\n",
" D[\"press_res\"] = D[\"press\"] - (450 - 12 * D[\"load\"]) # ๋ถํ ๋๋น ์๋ ฅ ์์ฐจ\n",
" D[\"rms_n\"] = D[\"rms\"] / (0.5 + D[\"load\"]) # ๋ถํ ์ ๊ทํ RMS\n",
" D[\"hf_ratio\"] = (D[\"b150\"] + D[\"b165\"] + D[\"b200\"]) / (D[\"b50\"] + 1e-6) # ๊ณ ์ฃผํ/๊ธฐ๋ณธ์ฃผํ ๋น\n",
" g = D.groupby(\"unit\")\n",
" for c in ROLL:\n",
" D[c + \"_m3\"] = g[c].transform(lambda s: s.rolling(3, min_periods=1).mean())\n",
" for c in (\"rms_n\", \"temp_res\", \"press_res\"):\n",
" D[c + \"_d\"] = g[c].diff().fillna(0)\n",
" return D\n",
"\n",
"\n",
"DERIVED = [\"temp_res\", \"press_res\", \"rms_n\", \"hf_ratio\"] + [c + \"_m3\" for c in ROLL] + [\"rms_n_d\", \"temp_res_d\", \"press_res_d\"]\n",
"FEATS = BASE + DERIVED\n",
"AE_COLS = [\"rms_n\", \"kurt\", \"b150\", \"b165\", \"b200\", \"hf_ratio\", \"temp_res\", \"press_res\"]\n",
"\n",
"\n",
"def split_units(units, n_train=24):\n",
" return list(units[\"unit\"][:n_train]), list(units[\"unit\"][n_train:])\n",
"\n",
"\n",
"class AE:\n",
" \"\"\"์คํ ์ธ์ฝ๋(์
๋ ฅ 8 -> 10 -> 4 -> 10 -> 8): ์ ์ ์๋ฃ๋ง ์ฌ๊ตฌ์ฑํ๋๋ก ํ์ตํ๊ณ , ์ฌ๊ตฌ์ฑ ์ค์ฐจ๋ฅผ ์ด์ ์ ์๋ก ์ด๋ค.\"\"\"\n",
"\n",
" def __init__(self, cols=AE_COLS, q=0.99, seed=0):\n",
" self.cols, self.q, self.seed = cols, q, seed\n",
"\n",
" def fit(self, healthy):\n",
" self.sc = StandardScaler().fit(healthy[self.cols])\n",
" Z = self.sc.transform(healthy[self.cols])\n",
" self.net = MLPRegressor(hidden_layer_sizes=(10, 4, 10), max_iter=800, random_state=self.seed).fit(Z, Z)\n",
" self.thr = float(np.quantile(self.err(healthy), self.q))\n",
" return self\n",
"\n",
" def contrib(self, d):\n",
" Z = self.sc.transform(d[self.cols])\n",
" return (Z - self.net.predict(Z)) ** 2 # ์
๋ ฅ ํญ๋ชฉ๋ณ ์ฌ๊ตฌ์ฑ ์ค์ฐจ(์ด์ ๊ธฐ์ฌ๋)\n",
"\n",
" def err(self, d):\n",
" return self.contrib(d).mean(axis=1)\n",
"\n",
"\n",
"def latch(d, e, thr, k=2):\n",
" \"\"\"๊ฐ์ ๊ฐ์ฒด์์ ์ด์ ์ ์๊ฐ k์ผ ์ฐ์ ๊ธฐ์ค์ ๋์ผ๋ฉด ๊ฒฝ๋ณด๋ฅผ ์ผ๊ณ ์ ์ง(๋์น)ํ๋ค.\"\"\"\n",
" out, e = np.zeros(len(d), bool), np.asarray(e)\n",
" for idx in d.reset_index(drop=True).groupby(\"unit\").indices.values():\n",
" run, on = 0, False\n",
" for i in idx:\n",
" run = run + 1 if e[i] > thr else 0\n",
" on = on or run >= k\n",
" out[i] = on\n",
" return out\n",
"\n",
"PERIOD = 90 # ๊ธฐ๋ณธ ๊ณํ: ์ ๊ธฐ ๊ต์ฒด ์ฃผ๊ธฐ(์ด์ฉ์ผ). ๊ฐ์ฅ ๋น ๋ฅธ ๊ณ ์ฅ ์ฌ๋ก๋ณด๋ค ์งง๊ฒ ์ ํด ์ฌํ ๊ณ ์ฅ์ ๋ง๋ ๋ณด์์ ์ฃผ๊ธฐ\n",
"UNPLANNED = (2.0, 12.0) # [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ณ ์ฅ ํ ์ ๋น ๋น์ฉ = ๋ถํ ๋จ๊ฐ x 2.0 + ์ฐ์ ์์ยท๊ฐ๋์ค๋จ 12.0\n",
"PLANNED_FIXED = 1.0 # ์
๊ณ ์ ๋น ์์
๊ณ ์ ๋น์ฉ(์ง์)\n",
"INSPECT = {\"base\": 0.3, \"pdm\": 0.05} # 30์ผ๋น ์ ๊ฒ ๋น์ฉ: ์ ๊ธฐ ๋ฐฉ์์ ๋ถํด ์ ๊ฒ, ์ผ์ ๊ฐ์ ๋ฐฉ์์ ๊ฒฝ๋ณด ์ ํ์ธ ์ ๊ฒ ์์ฃผ(๊ฐ์ )\n",
"\n",
"\n",
"class Audit:\n",
" def __init__(self):\n",
" self.log = []\n",
"\n",
" def add(self, **body):\n",
" prev = self.log[-1][\"hash\"] if self.log else \"GENESIS\"\n",
" body = {\"seq\": len(self.log), **body}\n",
" self.log.append({**body, \"prev\": prev, \"hash\": hashlib.sha256((prev + json.dumps(body, sort_keys=True, ensure_ascii=False)).encode()).hexdigest()})\n",
"\n",
" def verify(self):\n",
" prev = \"GENESIS\"\n",
" for e in self.log:\n",
" b = {k: v for k, v in e.items() if k not in (\"prev\", \"hash\")}\n",
" if e[\"prev\"] != prev or hashlib.sha256((prev + json.dumps(b, sort_keys=True, ensure_ascii=False)).encode()).hexdigest() != e[\"hash\"]:\n",
" return e[\"seq\"]\n",
" prev = e[\"hash\"]\n",
" return None\n",
"\n",
"\n",
"def add_feat(D):\n",
" D[\"hf_max\"] = D.groupby(\"unit\")[\"hf_ratio\"].cummax(); D[\"temp_res_max\"] = D.groupby(\"unit\")[\"temp_res\"].cummax()\n",
" return D\n",
"\n",
"\n",
"F7 = FEATS + [\"day\", \"hf_max\", \"temp_res_max\"]\n",
"u1, s1, w1 = make_runs(seed=1)\n",
"D1 = add_feat(build_features(s1, w1))\n",
"tr_u, _ = split_units(u1)\n",
"tr = D1[D1[\"unit\"].isin(tr_u)]\n",
"ae = AE().fit(tr[tr[\"deg\"] == 0])\n",
"gbr = GradientBoostingRegressor(n_estimators=100, max_depth=3, learning_rate=0.1, subsample=0.8, random_state=0).fit(tr[F7], np.minimum(tr[\"rul\"], 120))\n",
"\n",
"# ์ํ ๊ฐ์ฒด: ๋ค๋ฅธ ์๋๋ก ๋ง๋ ๊ฐ์ ๊ฐ์ฒด 3๋ฌถ์(144๋)์์ ๊ฐ์ฒด๋ณ ์ฒซ ๊ฒฝ๋ณด์ผ๊ณผ ๊ทธ๋์ RUL ์์ธก์ ๋ชจ์๋ค\n",
"cases = []\n",
"for sd in (2, 3, 4):\n",
" un, sn, ww = make_runs(seed=sd)\n",
" D = add_feat(build_features(sn, ww))\n",
" D[\"alarm\"] = latch(D, ae.err(D), ae.thr, k=2)\n",
" D[\"rul_hat\"] = gbr.predict(D[F7])\n",
" for u in un.itertuples():\n",
" g = D[D[\"unit\"] == u.unit]\n",
" a = g[g[\"alarm\"]]\n",
" cases.append({\"id\": f\"S{sd}-{u.unit}\", \"mode\": u.mode, \"life\": int(u.life), \"a\": int(a[\"day\"].iloc[0]) if len(a) else None,\n",
" \"rul_hat\": float(a[\"rul_hat\"].iloc[0]) if len(a) else None, \"load\": float(a[\"load\"].iloc[0]) if len(a) else None})\n",
"C = pd.DataFrame(cases)\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ํ ๊ฐ์ฒด {len(C)}๋(์๋ช
{C['life'].min()}~{C['life'].max()}์ผ), ๊ฒฝ๋ณด ๋ฐ์ {int(C['a'].notna().sum())}๋\")\n",
"\n",
"\n",
"def cost(mode, life, maint, kind=\"pdm\"):\n",
" p = MODES[mode][3] + KIT_COST + INSPECT[kind] * min(maint if maint is not None else life, life) / 30\n",
" if maint is None or maint >= life: # ์ ๋น ์ ์ ๊ณ ์ฅ\n",
" return MODES[mode][3] * UNPLANNED[0] + KIT_COST + UNPLANNED[1] + INSPECT[kind] * life / 30, True\n",
" return p + PLANNED_FIXED + MODES[mode][3] * (life - maint) / life, False # ๋จ์ ์๋ช
๋ญ๋น๋ถ ํฌํจ\n",
"\n",
"\n",
"def commander(row, rng):\n",
" \"\"\"[๋ชจ์ ๊ฒ์ฌ๊ด] ์์กด ์์ธก์ด ๊ธธ๋ฉด ๋ฐ๋ ค, 10%๋ ๋ฌด์๋ต.\"\"\"\n",
" if rng.random() < 0.10:\n",
" return \"๋ฌด์๋ต\", \"๊ธฐํ ๋ด ์๋ต ์์ -> ๊ธฐ๋ณธ ๊ณํ(์ ๊ธฐ ๊ต์ฒด) ์ ์ง\"\n",
" if row[\"rul_hat\"] > 90:\n",
" return \"๋ฐ๋ ค\", f\"์์ธก ์์กด {row['rul_hat']:.0f}์ผ๋ก ์ถฉ๋ถ -> ํ ๊ณํ ์ ์ง\"\n",
" if row[\"load\"] > 1.0:\n",
" return \"๋ฐ๋ ค\", \"๊ณ ๋ถํ ์ด์ฉ์ผ ์ ํธ๋ก ํ๋จ -> ์ถ๊ฐ ๊ด์ฐฐ\"\n",
" return \"์น์ธ\", f\"์ด์ ์ง์, ์์ธก ์์กด {row['rul_hat']:.0f}์ผ -> ์กฐ๊ธฐ ์
๊ณ \"\n",
"\n",
"\n",
"def run(mode, delay=2, scale=1.0, seed=0, audit=None):\n",
" \"\"\"mode: base(์ ๊ธฐ ๊ต์ฒด) / hitl(์ ์ -> ๊ฒ์ฌ๊ด ๊ฒฐ์ ) / auto(๋ชจ๋ ์ ์ ์ ์ฉ: ๋น๊ต์ฉ ์ํ, ์ค์ ์ด์ฉ ๊ธ์ง)\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" tot, fails, dec = 0.0, 0, {\"์น์ธ\": 0, \"๋ฐ๋ ค\": 0, \"๋ฌด์๋ต\": 0}\n",
" for _, r in C.iterrows():\n",
" life = r[\"life\"] / scale\n",
" maint, kind = PERIOD, \"base\"\n",
" if mode != \"base\" and r[\"a\"] is not None:\n",
" a, rh = r[\"a\"] / scale, r[\"rul_hat\"] / scale\n",
" inbound = a + max(int(0.4 * rh), 3)\n",
" row = {\"rul_hat\": rh, \"load\": r[\"load\"]}\n",
" if mode == \"auto\":\n",
" d, why = \"์น์ธ\", \"๋น๊ต์ฉ ์ ๋ถ ์ ์ฉ\"\n",
" else:\n",
" d, why = commander(row, rng)\n",
" audit.add(event=\"PROPOSE\", id=r[\"id\"], day=round(a), inbound=int(inbound), status=\"PENDING\")\n",
" audit.add(event=\"DECIDE\", id=r[\"id\"], decision=d, reason=why, by=\"๊ฒ์ฌ๊ด(๋ชจ์)\")\n",
" dec[d] += 1\n",
" if d == \"์น์ธ\":\n",
" maint, kind = inbound + (delay if mode == \"hitl\" else 0), \"pdm\"\n",
" c, f = cost(r[\"mode\"], life, maint, kind)\n",
" tot += c; fails += f\n",
" return tot, fails, dec\n",
"\n",
"\n",
"b_cost, b_fail, _ = run(\"base\")\n",
"audit = Audit()\n",
"h_cost, h_fail, dec = run(\"hitl\", seed=3, audit=audit)\n",
"a_cost, a_fail, _ = run(\"auto\")\n",
"print(\"\\n[๋น์ฉ ๋น๊ต] ๋ฐฉ์ | ๋น์ฉ์ง์ | ์ฌํ ๊ณ ์ฅ ๊ฑด์\")\n",
"for nm, c, f in [(\"์ ๊ธฐ ๊ต์ฒด(๊ธฐ๋ณธ ๊ณํ)\", b_cost, b_fail), (\"๊ฒฝ๋ณด ์ ์ + ๊ฒ์ฌ๊ด ์น์ธ\", h_cost, h_fail), (\"์ ์ ์ ๋ถ ์ ์ฉ(๋น๊ต์ฉ)\", a_cost, a_fail)]:\n",
" print(f\" {nm:22s} {c:8.0f} {f:3d}\")\n",
"cut = 1 - h_cost / b_cost\n",
"print(f\"HITL ์ ์ฉ ๋น์ฉ ์ ๊ฐ๋ฅ {cut:.1%} (๋ชฉํ 15% ์ด์: {'๋ฌ์ฑ' if cut >= 0.15 else '๋ฏธ๋ฌ'}), ๊ณ ์ฅ {b_fail} -> {h_fail}๊ฑด\")\n",
"print(f\"๊ฒฐ์ {sum(dec.values())}๊ฑด: \" + \", \".join(f\"{k} {v}๊ฑด\" for k, v in dec.items()))\n",
"n_prop = sum(e[\"event\"] == \"PROPOSE\" for e in audit.log); n_dec = sum(e[\"event\"] == \"DECIDE\" for e in audit.log)\n",
"print(f\"์ ์ {n_prop}๊ฑด ์ค ๊ฒฐ์ ๊ธฐ๋ก {n_dec}๊ฑด(๋ก๊น
๋ฅ {n_dec / n_prop:.0%}), ์๋ ์น์ธ 0๊ฑด, ๋ฌด์๋ต ์ ๊ธฐ๋ณธ ๊ณํ ์ ์ง\")\n",
"rej = [e[\"reason\"] for e in audit.log if e.get(\"decision\") == \"๋ฐ๋ ค\"]\n",
"print(\"๋ฐ๋ ค ์ฌ์ ์:\", rej[0] if rej else \"-\")\n",
"print(f\"๊ฐ์ฌ ๋ก๊ทธ ๋ฌด๊ฒฐ์ฑ: {'์ ์' if audit.verify() is None else '์ด์'}\", end=\", \")\n",
"audit.log[5][\"reason\"] = \"์ฌํ ์์ \"\n",
"print(f\"์ฌํ ๋ณ์กฐ ์ seq {audit.verify()}์์ ํ์ง\")\n",
"\n",
"print(\"\\n[๋ฏผ๊ฐ๋ 1] ์น์ธ ํ ์
๊ณ ๊น์ง ์ง์ฐ(์ผ) -> ๋น์ฉ ์ ๊ฐ๋ฅ \")\n",
"for d in (0, 2, 7, 14, 21):\n",
" c, f, _ = run(\"hitl\", delay=d, seed=3, audit=Audit())\n",
" print(f\" {d:2d}์ผ: {1 - c / b_cost:.1%} (๊ณ ์ฅ {f}๊ฑด)\")\n",
"print(\"[๋ฏผ๊ฐ๋ 2] ๊ฐ๋ ๊ฐ๋ ๊ธ์ฆ(์ ์ ์ ํ ๊ฐ์ , ์ดํ๊ฐ ๋นจ๋ผ์ ธ ์๋ช
์ด 1/๋ฐฐ์๋ก ๋จ์ถ) -> ์ ๊ฐ๋ฅ \")\n",
"for s in (1.0, 1.3, 1.6, 2.0):\n",
" bc, bf, _ = run(\"base\", scale=s); hc, hf, _ = run(\"hitl\", scale=s, seed=3, audit=Audit())\n",
" print(f\" x{s}: ์ ๊ธฐ ๊ต์ฒด ๋น์ฉ {bc:.0f}(๊ณ ์ฅ {bf}) -> HITL {hc:.0f}(๊ณ ์ฅ {hf}), ์ ๊ฐ {1 - hc / bc:.1%}\")\n",
"\n",
"plt.figure(figsize=(6, 4))\n",
"plt.bar([\"periodic\", \"HITL\", \"auto(ref)\"], [b_cost, h_cost, a_cost]); plt.ylabel(\"cost index\"); plt.title(\"HITL early-inbound simulator\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
07 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ํ ๊ฐ์ฒด 144๋(์๋ช
101~212์ผ), ๊ฒฝ๋ณด ๋ฐ์ 144๋\n",
"\n",
"[๋น์ฉ ๋น๊ต] ๋ฐฉ์ | ๋น์ฉ์ง์ | ์ฌํ ๊ณ ์ฅ ๊ฑด์\n",
" ์ ๊ธฐ ๊ต์ฒด(๊ธฐ๋ณธ ๊ณํ) 1640 0\n",
" ๊ฒฝ๋ณด ์ ์ + ๊ฒ์ฌ๊ด ์น์ธ 1389 1\n",
" ์ ์ ์ ๋ถ ์ ์ฉ(๋น๊ต์ฉ) 1364 1\n",
"HITL ์ ์ฉ ๋น์ฉ ์ ๊ฐ๋ฅ 15.3% (๋ชฉํ 15% ์ด์: ๋ฌ์ฑ), ๊ณ ์ฅ 0 -> 1๊ฑด\n",
"๊ฒฐ์ 144๊ฑด: ์น์ธ 126๊ฑด, ๋ฐ๋ ค 7๊ฑด, ๋ฌด์๋ต 11๊ฑด\n",
"์ ์ 144๊ฑด ์ค ๊ฒฐ์ ๊ธฐ๋ก 144๊ฑด(๋ก๊น
๋ฅ 100%), ์๋ ์น์ธ 0๊ฑด, ๋ฌด์๋ต ์ ๊ธฐ๋ณธ ๊ณํ ์ ์ง\n",
"๋ฐ๋ ค ์ฌ์ ์: ๊ณ ๋ถํ ์ด์ฉ์ผ ์ ํธ๋ก ํ๋จ -> ์ถ๊ฐ ๊ด์ฐฐ\n",
"๊ฐ์ฌ ๋ก๊ทธ ๋ฌด๊ฒฐ์ฑ: ์ ์, ์ฌํ ๋ณ์กฐ ์ seq 5์์ ํ์ง\n",
"\n",
"[๋ฏผ๊ฐ๋ 1] ์น์ธ ํ ์
๊ณ ๊น์ง ์ง์ฐ(์ผ) -> ๋น์ฉ ์ ๊ฐ๋ฅ \n",
" 0์ผ: 14.8% (๊ณ ์ฅ 1๊ฑด)\n",
" 2์ผ: 15.3% (๊ณ ์ฅ 1๊ฑด)\n",
" 7์ผ: 14.1% (๊ณ ์ฅ 4๊ฑด)\n",
" 14์ผ: 10.5% (๊ณ ์ฅ 10๊ฑด)\n",
" 21์ผ: 1.8% (๊ณ ์ฅ 21๊ฑด)\n",
"[๋ฏผ๊ฐ๋ 2] ๊ฐ๋ ๊ฐ๋ ๊ธ์ฆ(์ ์ ์ ํ ๊ฐ์ , ์ดํ๊ฐ ๋นจ๋ผ์ ธ ์๋ช
์ด 1/๋ฐฐ์๋ก ๋จ์ถ) -> ์ ๊ฐ๋ฅ \n",
" x1.0: ์ ๊ธฐ ๊ต์ฒด ๋น์ฉ 1640(๊ณ ์ฅ 0) -> HITL 1389(๊ณ ์ฅ 1), ์ ๊ฐ 15.3%\n",
" x1.3: ์ ๊ธฐ ๊ต์ฒด ๋น์ฉ 1571(๊ณ ์ฅ 5) -> HITL 1363(๊ณ ์ฅ 1), ์ ๊ฐ 13.2%\n",
" x1.6: ์ ๊ธฐ ๊ต์ฒด ๋น์ฉ 2005(๊ณ ์ฅ 41) -> HITL 1451(๊ณ ์ฅ 8), ์ ๊ฐ 27.7%\n",
" x2.0: ์ ๊ธฐ ๊ต์ฒด ๋น์ฉ 3394(๊ณ ์ฅ 128) -> HITL 1572(๊ณ ์ฅ 16), ์ ๊ฐ 53.7%\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
08. ์ฅ์๊ฐ ๋นํ/๊ธฐ๋ ์ผ์ ๋ฐ์ดํฐ ๋์ฉ๋ ์คํธ๋ฆผ ์ฃผ์
์คํธ๋ ์ค ํ
์คํธ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ฅ์๊ฐ ๊ธฐ๋ ์ผ์ ๋ฐ์ดํฐ ๋์ฉ๋ ์คํธ๋ฆผ ์ฃผ์
์คํธ๋ ์ค ํ
์คํธ๋ฅผ ์ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ฅ์๊ฐ ๊ธฐ๋ ์ผ์ ๋ฐ์ดํฐ๋ฅผ ๋์ฉ๋ ์คํธ๋ฆผ์ผ๋ก ์ฃผ์
ํด ์ฒ๋ฆฌ ์๊ฐ, ๊ฒฝ๋ณด ์ ํ๋, ๋ถ๋ ํจํท ๊ฒฉ๋ฆฌ๋ฅผ ํ์ธํ๋ ์คํธ๋ ์ค ์ํ์ ์ํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ฐ์ ์ผ์ ์ธก์ 1,000๊ฑด์ ๊ฒฐ์ธก ํจํท, ์ผ์ ๊ณ ์ , ํฌํ, ๋ฒ์ ์ดํ์ ์์ด ํ๊บผ๋ฒ์ ์ฃผ์
ํ๊ณ ์ ์ ์ฒ๋ฆฌยท๊ฒฉ๋ฆฌยท์ ์ค ๊ฑด์์ ์ฒ๋ฆฌ ์๊ฐ์ ์ผ๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/dashboard/mnt04_app.py (Streamlit/Dash ๋ฑ)` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์์ฌ๊ฒฐ์ ์ง์์ ์ต์ ํ๋ ์๊ฐ์ ์๊ณ ๋ฐ ์ฌ์ฉ์ ๊ฒฝํ(UX)\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๋์๋ณด๋ ๋ก๋ฉ ์๊ฐ 2์ด ์ด๋ด, ๊ฒฝ๋ณด ๋ฐ๋ น ์ ํ๋ 95% ์ด์\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์น ๊ธฐ๋ฐ ๋ถ์ ๋์๋ณด๋ ๊ตฌ๋ ๋ฐ ์ค์๊ฐ ํํฐ๋ง ๊ธฐ๋ฅ ๊ฒ์ฆ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ์ค์ ๊ตฐ์ ๋ฐ์ดํฐ ์ฐ๋ ํ๋ฉด ํ
์คํธ ๋ฐ UI ๋์ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์ฅ์๊ฐ ๊ธฐ๋ ์ผ์ ๋ฐ์ดํฐ๋ฅผ 1,000~100,000๊ฑด ๊ท๋ชจ๋ก ํ๊บผ๋ฒ์ ์ฃผ์
ํด ๊ฒ์ฆยทํน์ง ์ถ์ถยท์คํ ์ธ์ฝ๋ ์ ์ยท๊ฒฝ๋ณด ํ์ ์ ๋ฐฐ์น๋ก ์ฒ๋ฆฌํ๋ ์คํธ๋ฆผ ์ํ์ ์ํํ๊ณ , ์ฒ๋ฆฌ ์๊ฐ๊ณผ ์ํ ํ๋ฉด ๊ตฌ์ฑ ์๊ฐ, ๊ฒฝ๋ณด ์ ํ๋, ๋ถ๋ ํจํท(๊ฒฐ์ธกยท๊ณ ์ ๊ฐยทํฌํยท์ค๋ณต) ๊ฒฉ๋ฆฌ์ ์ ์ค 0๊ฑด ์ฌ๋ถ๋ฅผ ํ์ธํ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy.stats import kurtosis\n",
"from sklearn.neural_network import MLPRegressor\n",
"from sklearn.preprocessing import StandardScaler\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๊ธฐ๋์ฒด 48๋์ ์์งยท๊ตฌ๋๊ณ ์ง๋ยท์จ๋ยท์๋ ฅ ์ผ์ ์๋ฃ ์์ฑ๊ธฐ (์ค์ ์ฅ๋นยท๋ถ๋ยท์ ๋น ์ค์ ์์)\n",
"# ๊ฐ์ฒด๋ง๋ค ์ ์ -> ์ดํ -> ๊ณ ์ฅ(์๋ช
์ข
๋ฃ)๊น์ง ํ๋ฃจ 1ํ ์ธก์ (์ง๋ ํํ 256ํ๋ณธ + ์จ๋ + ์๋ ฅ + ๋ถํ)์ ๋ง๋ ๋ค.\n",
"# ์ดํ ์ ํ 4๊ฐ์ง: bearing(๋ฒ ์ด๋ง), gear(๊ธฐ์ด), oil_pump(์ค์ผ ํํ), cooling(๋๊ฐ ํฌ ๋ชจํฐ)\n",
"# ---------------------------------------------------------------------------\n",
"FS, NS = 1024, 256 # ๊ฐ์ ํ๋ณธํ ์ฃผํ์(Hz), ํํ 1๊ฐ๋น ํ๋ณธ ์\n",
"TT = np.arange(NS) / FS\n",
"MODES = { # ์ดํ ์ ํ: (๋ฐ์ ํ๋ฅ , ํ๊ท ์๋ช
(์ผ), ๊ต์ฒด ์๋ฆฌ๋ถ์, ๊ฐ์ ๋จ๊ฐ์ง์)\n",
" \"bearing\": (0.40, 160, \"์ฃผ ๋ฒ ์ด๋ง ์ธํธ\", 6.0), \"gear\": (0.25, 175, \"๊ตฌ๋ ๊ธฐ์ด ์ธํธ\", 9.0),\n",
" \"oil_pump\": (0.20, 150, \"์ค์ผ ํํ\", 4.0), \"cooling\": (0.15, 140, \"๋๊ฐ ํฌ ๋ชจํฐ\", 3.0)}\n",
"KIT_COST = 1.0 # ๋ชจ๋ ์ ๋น์ ํจ๊ป ์๋ชจ๋๋ ์ฐยทํํฐ ํคํธ ๋จ๊ฐ์ง์\n",
"BANDS = {\"b50\": (40, 60), \"b100\": (90, 110), \"b150\": (140, 156), \"b165\": (156, 176), \"b200\": (190, 215)}\n",
"\n",
"\n",
"def make_runs(n=48, seed=1):\n",
" \"\"\"๋ฐํ: units(๊ฐ์ฒด ์ ๋ณด), snap(์ผ๋ณ ์ธก์ ์์ฝ), W(์ง๋ ํํ [์ธก์ ์, 256])\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" names = list(MODES)\n",
" mode = rng.choice(names, n, p=[MODES[m][0] for m in names])\n",
" life = np.clip(np.array([rng.normal(MODES[m][1], 18) for m in mode]), 90, 230).astype(int)\n",
" start = np.sort(rng.integers(0, 240, n))\n",
" units = pd.DataFrame({\"unit\": [f\"U{i:02d}\" for i in range(n)], \"mode\": mode, \"life\": life, \"start\": start, \"base_load\": rng.uniform(0.4, 0.75, n)})\n",
" rows = []\n",
" for u in units.itertuples():\n",
" d = np.arange(u.life + 1)\n",
" onset = 0.45 * u.life\n",
" deg = np.clip((d - onset) / (u.life - onset), 0, 1) ** 1.5\n",
" load = np.clip(u.base_load + rng.normal(0, 0.06, len(d)), 0.2, 1.0)\n",
" hi = rng.random(len(d)) < 0.07 # ๊ณ ๋ถํ ์ด์ฉ์ผ(์ ์์ด์ง๋ง ์ง๋ยท์จ๋ ์์น)\n",
" load = np.where(hi, rng.uniform(0.95, 1.1, len(d)), load)\n",
" rows.append(pd.DataFrame({\"unit\": u.unit, \"day\": d, \"cal\": u.start + d, \"rul\": u.life - d, \"deg\": deg, \"load\": load, \"hi_load\": hi, \"mode\": u.mode}))\n",
" snap = pd.concat(rows, ignore_index=True)\n",
" S = len(snap)\n",
" snap[\"ambient\"] = 15 + 12 * np.sin(2 * np.pi * snap[\"cal\"] / 365) + rng.normal(0, 2, S)\n",
" dg, ld, md = snap[\"deg\"].values, snap[\"load\"].values, snap[\"mode\"].values\n",
" isb, isg, iso, isc = (md == \"bearing\"), (md == \"gear\"), (md == \"oil_pump\"), (md == \"cooling\")\n",
" snap[\"temp\"] = 55 + 22 * ld + 0.5 * snap[\"ambient\"] + 9 * dg * iso + 26 * dg * isc + 3 * dg * (isb | isg) + rng.normal(0, 1.5, S)\n",
" snap[\"press\"] = 450 - 12 * ld - 85 * dg * iso - 15 * dg * isc + rng.normal(0, 6, S)\n",
" ph = rng.uniform(0, 2 * np.pi, (S, 3))\n",
" a1, a2 = 1.0 * (0.5 + ld), 0.4 * (0.5 + ld)\n",
" W = (a1[:, None] * np.sin(2 * np.pi * 50 * TT + ph[:, :1]) + a2[:, None] * np.sin(2 * np.pi * 100 * TT + ph[:, 1:2])\n",
" + rng.normal(0, 0.35, (S, NS)))\n",
" W += (0.9 * dg * isb)[:, None] * np.sin(2 * np.pi * 165 * TT + ph[:, 2:3])\n",
" W += (0.8 * dg * isg)[:, None] * (np.sin(2 * np.pi * 150 * TT + ph[:, 2:3]) + 0.8 * np.sin(2 * np.pi * 100 * TT))\n",
" W += (0.5 * dg * iso)[:, None] * np.sin(2 * np.pi * 200 * TT + ph[:, 2:3])\n",
" k = rng.poisson(10 * dg * isb) # ๋ฒ ์ด๋ง ์ดํ: ์ถฉ๊ฒฉ์ฑ ํ์ค(์ฒจ๋ ์์น)\n",
" pos, amp = rng.integers(0, NS, (S, 30)), rng.normal(0, 3.0, (S, 30)) * dg[:, None]\n",
" amp = amp * (np.arange(30)[None, :] < k[:, None])\n",
" np.add.at(W, (np.repeat(np.arange(S), 30), pos.ravel()), amp.ravel())\n",
" return units, snap, W\n",
"\n",
"\n",
"def extract(W):\n",
" \"\"\"์ง๋ ํํ -> ํน์ง๋: RMS, ์ฒจ๋, ํ๊ณ ์จ, ์ต๋-์ต์, FFT ๋์ญ ์๋์ง(50/100/150/165/200Hz ๋ถ๊ทผ)\"\"\"\n",
" x = W - W.mean(axis=1, keepdims=True)\n",
" rms = np.sqrt((x ** 2).mean(axis=1))\n",
" out = {\"rms\": rms, \"kurt\": kurtosis(x, axis=1, fisher=False), \"crest\": np.abs(x).max(axis=1) / rms, \"p2p\": x.max(axis=1) - x.min(axis=1)}\n",
" A = np.abs(np.fft.rfft(x * np.hanning(NS), axis=1)) / NS\n",
" f = np.fft.rfftfreq(NS, 1 / FS)\n",
" for nm, (lo, hi) in BANDS.items():\n",
" out[nm] = np.sqrt((A[:, (f >= lo) & (f < hi)] ** 2).sum(axis=1))\n",
" return pd.DataFrame(out)\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# ๊ณตํต ๋ถ์ ์กฐ๊ฐ: ์ผ๋ณ ํน์ง๋ ํ(ํํ ํน์ง + ์จ๋ยท์๋ ฅ + ํ์), ๊ฐ์ฒด ๋จ์ ํ์ต/๊ฒ์ฆ ๋ถํ (์์ 24๋ ํ์ต, ๋ค 24๋ ๊ฒ์ฆ)\n",
"# ---------------------------------------------------------------------------\n",
"BASE = [\"rms\", \"kurt\", \"crest\", \"p2p\", \"b50\", \"b100\", \"b150\", \"b165\", \"b200\", \"temp\", \"press\"]\n",
"ROLL = [\"rms\", \"kurt\", \"b150\", \"b165\", \"b200\", \"temp\", \"press\"]\n",
"\n",
"\n",
"def build_features(snap, W):\n",
" D = pd.concat([snap.reset_index(drop=True), extract(W)], axis=1)\n",
" D[\"temp_res\"] = D[\"temp\"] - (55 + 22 * D[\"load\"] + 0.5 * D[\"ambient\"]) # ๋ถํยท์ธ๊ธฐ ๋๋น ์จ๋ ์์ฐจ\n",
" D[\"press_res\"] = D[\"press\"] - (450 - 12 * D[\"load\"]) # ๋ถํ ๋๋น ์๋ ฅ ์์ฐจ\n",
" D[\"rms_n\"] = D[\"rms\"] / (0.5 + D[\"load\"]) # ๋ถํ ์ ๊ทํ RMS\n",
" D[\"hf_ratio\"] = (D[\"b150\"] + D[\"b165\"] + D[\"b200\"]) / (D[\"b50\"] + 1e-6) # ๊ณ ์ฃผํ/๊ธฐ๋ณธ์ฃผํ ๋น\n",
" g = D.groupby(\"unit\")\n",
" for c in ROLL:\n",
" D[c + \"_m3\"] = g[c].transform(lambda s: s.rolling(3, min_periods=1).mean())\n",
" for c in (\"rms_n\", \"temp_res\", \"press_res\"):\n",
" D[c + \"_d\"] = g[c].diff().fillna(0)\n",
" return D\n",
"\n",
"\n",
"DERIVED = [\"temp_res\", \"press_res\", \"rms_n\", \"hf_ratio\"] + [c + \"_m3\" for c in ROLL] + [\"rms_n_d\", \"temp_res_d\", \"press_res_d\"]\n",
"FEATS = BASE + DERIVED\n",
"AE_COLS = [\"rms_n\", \"kurt\", \"b150\", \"b165\", \"b200\", \"hf_ratio\", \"temp_res\", \"press_res\"]\n",
"\n",
"\n",
"def split_units(units, n_train=24):\n",
" return list(units[\"unit\"][:n_train]), list(units[\"unit\"][n_train:])\n",
"\n",
"\n",
"class AE:\n",
" \"\"\"์คํ ์ธ์ฝ๋(์
๋ ฅ 8 -> 10 -> 4 -> 10 -> 8): ์ ์ ์๋ฃ๋ง ์ฌ๊ตฌ์ฑํ๋๋ก ํ์ตํ๊ณ , ์ฌ๊ตฌ์ฑ ์ค์ฐจ๋ฅผ ์ด์ ์ ์๋ก ์ด๋ค.\"\"\"\n",
"\n",
" def __init__(self, cols=AE_COLS, q=0.99, seed=0):\n",
" self.cols, self.q, self.seed = cols, q, seed\n",
"\n",
" def fit(self, healthy):\n",
" self.sc = StandardScaler().fit(healthy[self.cols])\n",
" Z = self.sc.transform(healthy[self.cols])\n",
" self.net = MLPRegressor(hidden_layer_sizes=(10, 4, 10), max_iter=800, random_state=self.seed).fit(Z, Z)\n",
" self.thr = float(np.quantile(self.err(healthy), self.q))\n",
" return self\n",
"\n",
" def contrib(self, d):\n",
" Z = self.sc.transform(d[self.cols])\n",
" return (Z - self.net.predict(Z)) ** 2 # ์
๋ ฅ ํญ๋ชฉ๋ณ ์ฌ๊ตฌ์ฑ ์ค์ฐจ(์ด์ ๊ธฐ์ฌ๋)\n",
"\n",
" def err(self, d):\n",
" return self.contrib(d).mean(axis=1)\n",
"\n",
"\n",
"def latch(d, e, thr, k=2):\n",
" \"\"\"๊ฐ์ ๊ฐ์ฒด์์ ์ด์ ์ ์๊ฐ k์ผ ์ฐ์ ๊ธฐ์ค์ ๋์ผ๋ฉด ๊ฒฝ๋ณด๋ฅผ ์ผ๊ณ ์ ์ง(๋์น)ํ๋ค.\"\"\"\n",
" out, e = np.zeros(len(d), bool), np.asarray(e)\n",
" for idx in d.reset_index(drop=True).groupby(\"unit\").indices.values():\n",
" run, on = 0, False\n",
" for i in idx:\n",
" run = run + 1 if e[i] > thr else 0\n",
" on = on or run >= k\n",
" out[i] = on\n",
" return out\n",
"\n",
"units, snap, W = make_runs()\n",
"D = build_features(snap, W)\n",
"tr_u, va_u = split_units(units)\n",
"tr, va = D[D[\"unit\"].isin(tr_u)].reset_index(drop=True), D[D[\"unit\"].isin(va_u)].reset_index(drop=True)\n",
"Wva = W[D[\"unit\"].isin(va_u).values]\n",
"ae = AE().fit(tr[tr[\"deg\"] == 0])\n",
"KEEP = [\"unit\", \"day\", \"load\", \"ambient\", \"temp\", \"press\"]\n",
"\n",
"\n",
"def score_batch(w, meta):\n",
" \"\"\"์คํธ๋ฆผ ํ ๋ฌถ์: ํํ -> ํน์ง -> ์ด์ ์ ์(๋ฐฐ์). ์ ์๋ ๊ฐ์ฒด ์ฐ์์ฑ(๋์น)๊ณผ ๋ฌด๊ดํ ๋จ๊ฑด ๊ฐ.\"\"\"\n",
" f = extract(w)\n",
" d = pd.DataFrame({\"rms_n\": f[\"rms\"] / (0.5 + meta[\"load\"]), \"kurt\": f[\"kurt\"], \"b150\": f[\"b150\"], \"b165\": f[\"b165\"], \"b200\": f[\"b200\"],\n",
" \"hf_ratio\": (f[\"b150\"] + f[\"b165\"] + f[\"b200\"]) / (f[\"b50\"] + 1e-6),\n",
" \"temp_res\": meta[\"temp\"] - (55 + 22 * meta[\"load\"] + 0.5 * meta[\"ambient\"]), \"press_res\": meta[\"press\"] - (450 - 12 * meta[\"load\"])})\n",
" return ae.err(d) / ae.thr\n",
"\n",
"\n",
"def valid(w, meta):\n",
" \"\"\"๋ถ๋ ํจํท ํ์ : ๊ฒฐ์ธก(NaN), ์ผ์ ๊ณ ์ (๋ถ์ฐ 0), ํฌํ(|๊ฐ|>12), ๊ฐ ๋ฒ์ ์ดํ.\"\"\"\n",
" bad = np.isnan(w).any(axis=1) | (np.nanstd(w, axis=1) < 1e-6) | (np.abs(np.nan_to_num(w)).max(axis=1) > 12)\n",
" bad |= ~meta[\"temp\"].between(20, 160).values | ~meta[\"press\"].between(100, 600).values\n",
" return ~bad\n",
"\n",
"\n",
"def make_stream(n, bad_rate, seed=0):\n",
" rng = np.random.default_rng(seed)\n",
" pick = rng.integers(0, len(va), n)\n",
" w = Wva[pick] + rng.normal(0, 0.02, (n, NS))\n",
" meta = va.iloc[pick][KEEP + [\"deg\"]].reset_index(drop=True)\n",
" kind = np.array([\"\"] * n, dtype=object)\n",
" bad = rng.random(n) < bad_rate\n",
" k = rng.integers(0, 4, n)\n",
" for t, nm in enumerate([\"๊ฒฐ์ธก ํจํท\", \"์ผ์ ๊ณ ์ \", \"ํฌํ\", \"๋ฒ์ ์ดํ\"]):\n",
" m = bad & (k == t)\n",
" kind[m] = nm\n",
" if t == 0: w[m, 10:40] = np.nan\n",
" if t == 1: w[m] = 0.5\n",
" if t == 2: w[m, ::7] = 50.0\n",
" if t == 3: meta.loc[m, \"temp\"] = 400.0\n",
" return w, meta, kind\n",
"\n",
"\n",
"# 1) 1,000๊ฑด ๋์ ์ฃผ์
\n",
"t0 = time.perf_counter()\n",
"w, meta, kind = make_stream(1000, 0.06)\n",
"ok = valid(w, meta)\n",
"s = np.full(1000, np.nan)\n",
"s[ok] = score_batch(w[ok], meta[ok].reset_index(drop=True))\n",
"sec = time.perf_counter() - t0\n",
"inj = int((kind != \"\").sum())\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] 1,000๊ฑด ๋์ ์ฃผ์
: ์ ์ ์ฒ๋ฆฌ {int(ok.sum())}๊ฑด, ๊ฒฉ๋ฆฌ {int((~ok).sum())}๊ฑด, ์ ์ค {1000 - int(ok.sum()) - int((~ok).sum())}๊ฑด, ์ฒ๋ฆฌ ์๊ฐ {sec:.2f}์ด (๋ชฉํ 2์ด ์ด๋ด: {'๋ฌ์ฑ' if sec <= 2 else '๋ฏธ๋ฌ'})\")\n",
"print(f\" ๋ถ๋ ์ฃผ์
{inj}๊ฑด ์ค ๊ฒฉ๋ฆฌ {int(((kind != '') & ~ok).sum())}๊ฑด (\" + \", \".join(f\"{k} {int((kind == k).sum())}\" for k in [\"๊ฒฐ์ธก ํจํท\", \"์ผ์ ๊ณ ์ \", \"ํฌํ\", \"๋ฒ์ ์ดํ\"]) + \")\")\n",
"\n",
"# 2) ๊ฒฝ๋ณด ์ ํ๋: ์ ์ ๊ตฌ๊ฐ๊ณผ ์ดํ๊ฐ ๋๋ ทํ ๊ตฌ๊ฐ(deg>=0.3)์ ํ์ ๋์์ผ๋ก ํ๊ณ , ์ดํ ์ด๊ธฐ ํ์ ๊ตฌ๊ฐ์ ์ ํ๋์์ ์ ์ธํ๊ณ ๋ฐ๋ก ํ์\n",
"al = s > 1.0\n",
"y = (meta[\"deg\"] >= 0.3).values\n",
"eval_m = ok & ((meta[\"deg\"] == 0).values | y)\n",
"acc = (al[eval_m] == y[eval_m]).mean()\n",
"tp, fp, fn = (al & y & ok).sum(), (al & ~y & eval_m).sum(), (~al & y & ok).sum()\n",
"print(f\"๊ฒฝ๋ณด ์ ํ๋ {acc:.1%} (๋ชฉํ 95% ์ด์: {'๋ฌ์ฑ' if acc >= 0.95 else '๋ฏธ๋ฌ'}), ์ฌํ์จ {tp / (tp + fn):.1%}, ์ ๋ฐ๋ {tp / (tp + fp):.1%}, ๊ฒฝ๋ณด {int((al & ok).sum())}๊ฑด\")\n",
"gray = ok & (meta[\"deg\"] > 0).values & (meta[\"deg\"] < 0.3).values\n",
"print(f\" ์ฐธ๊ณ : ์ดํ ์ด๊ธฐ(0~0.3) ๊ตฌ๊ฐ {int(gray.sum())}๊ฑด ์ค ๊ฒฝ๋ณด {al[gray].mean():.0%}(ํ์ ๊ตฌ๊ฐ์ ์ ํ๋์์ ์ ์ธ)\")\n",
"\n",
"# 3) ์ํ ํ๋ฉด ๊ตฌ์ฑ: ์ต๊ทผ 30์ผ x 24๋ ์์ฝ(๊ฐ์ฒด๋ณ ์ต๋๊ฐยท๊ฒฝ๋ณด ์ฌ๋ถ) ์ง๊ณ ์๊ฐ\n",
"t0 = time.perf_counter()\n",
"last = va[va[\"day\"] >= va.groupby(\"unit\")[\"day\"].transform(\"max\") - 30]\n",
"sc = score_batch(Wva[last.index.values], last.reset_index(drop=True))\n",
"view = last.assign(score=sc).groupby(\"unit\").agg(max_score=(\"score\", \"max\"), last_score=(\"score\", \"last\"))\n",
"view[\"์ํ\"] = np.where(view[\"last_score\"] > 1, \"๊ฒฝ๋ณด\", np.where(view[\"max_score\"] > 1, \"์ฃผ์\", \"์ ์\"))\n",
"tv = time.perf_counter() - t0\n",
"print(f\"์ํ ํ๋ฉด ๊ตฌ์ฑ(24๋ x ์ต๊ทผ 30์ผ {len(last)}๊ฑด) {tv:.2f}์ด (๋ชฉํ 2์ด ์ด๋ด: {'๋ฌ์ฑ' if tv <= 2 else '๋ฏธ๋ฌ'}), \" + \", \".join(f\"{k} {v}๋\" for k, v in view[\"์ํ\"].value_counts().items()))\n",
"\n",
"# 4) ๊ท๋ชจ ํ๋: ๋ฐฐ์น ์ฒ๋ฆฌ๋๊ณผ ๋ฉ๋ชจ๋ฆฌ ํ๊ณ ๋์(๋ฐฐ์น ํฌ๊ธฐ 5,000)\n",
"print(\"\\n[์ฃผ์
๊ท๋ชจ๋ณ ์ฒ๋ฆฌ] ๊ฑด์ | ์๊ฐ | ์ฒ๋ฆฌ๋ | ๊ฒฉ๋ฆฌ | ์ ์ค\")\n",
"sizes = (1000, 10000, 50000, 100000)\n",
"ts = []\n",
"for n in sizes:\n",
" w, meta, kind = make_stream(n, 0.06, seed=n)\n",
" t0 = time.perf_counter(); done = quarantine = 0\n",
" for i in range(0, n, 5000):\n",
" wb, mb = w[i:i + 5000], meta.iloc[i:i + 5000].reset_index(drop=True)\n",
" okb = valid(wb, mb)\n",
" if okb.any():\n",
" score_batch(wb[okb], mb[okb].reset_index(drop=True))\n",
" done += int(okb.sum()); quarantine += int((~okb).sum())\n",
" t = time.perf_counter() - t0; ts.append(t)\n",
" print(f\" {n:7d} {t:5.2f}์ด {n / t:8.0f}๊ฑด/์ด {quarantine:5d} {n - done - quarantine}\")\n",
"print(\"์ด์ฉ ๊ธฐ์ค: ์ ์ฒด ๊ฒฉ๋ฆฌ์จ์ด 20%๋ฅผ ๋์ผ๋ฉด ์ผ์ ๊ณํต ์ ๊ฒ ์๋ฆผ, ์ฒ๋ฆฌ ์ง์ฐ ์ FFT ๋์ญ 5๊ฐ๋ง ๊ณ์ฐํ๋ ์ถ์ ๋ชจ๋๋ก ์ ํ\")\n",
"\n",
"plt.figure(figsize=(6, 4))\n",
"plt.plot(sizes, ts, marker=\"o\"); plt.xscale(\"log\"); plt.xlabel(\"stream size\"); plt.ylabel(\"seconds\"); plt.title(\"Stream stress test\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
08 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] 1,000๊ฑด ๋์ ์ฃผ์
: ์ ์ ์ฒ๋ฆฌ 945๊ฑด, ๊ฒฉ๋ฆฌ 55๊ฑด, ์ ์ค 0๊ฑด, ์ฒ๋ฆฌ ์๊ฐ 0.03์ด (๋ชฉํ 2์ด ์ด๋ด: ๋ฌ์ฑ)\n",
" ๋ถ๋ ์ฃผ์
55๊ฑด ์ค ๊ฒฉ๋ฆฌ 55๊ฑด (๊ฒฐ์ธก ํจํท 15, ์ผ์ ๊ณ ์ 16, ํฌํ 15, ๋ฒ์ ์ดํ 9)\n",
"๊ฒฝ๋ณด ์ ํ๋ 98.0% (๋ชฉํ 95% ์ด์: ๋ฌ์ฑ), ์ฌํ์จ 98.0%, ์ ๋ฐ๋ 97.3%, ๊ฒฝ๋ณด 330๊ฑด\n",
" ์ฐธ๊ณ : ์ดํ ์ด๊ธฐ(0~0.3) ๊ตฌ๊ฐ 231๊ฑด ์ค ๊ฒฝ๋ณด 15%(ํ์ ๊ตฌ๊ฐ์ ์ ํ๋์์ ์ ์ธ)\n",
"์ํ ํ๋ฉด ๊ตฌ์ฑ(24๋ x ์ต๊ทผ 30์ผ 744๊ฑด) 0.02์ด (๋ชฉํ 2์ด ์ด๋ด: ๋ฌ์ฑ), ๊ฒฝ๋ณด 24๋\n",
"\n",
"[์ฃผ์
๊ท๋ชจ๋ณ ์ฒ๋ฆฌ] ๊ฑด์ | ์๊ฐ | ์ฒ๋ฆฌ๋ | ๊ฒฉ๋ฆฌ | ์ ์ค\n",
" 1000 0.02์ด 48180๊ฑด/์ด 68 0\n",
" 10000 0.21์ด 48054๊ฑด/์ด 618 0\n",
" 50000 1.23์ด 40710๊ฑด/์ด 2951 0\n",
" 100000 3.51์ด 28472๊ฑด/์ด 5940 0\n",
"์ด์ฉ ๊ธฐ์ค: ์ ์ฒด ๊ฒฉ๋ฆฌ์จ์ด 20%๋ฅผ ๋์ผ๋ฉด ์ผ์ ๊ณํต ์ ๊ฒ ์๋ฆผ, ์ฒ๋ฆฌ ์ง์ฐ ์ FFT ๋์ญ 5๊ฐ๋ง ๊ณ์ฐํ๋ ์ถ์ ๋ชจ๋๋ก ์ ํ\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
09. AAR ๊ธฐ๋ฐ ์์ง์ ๋น ์ค๊ฒฝ๋ณด ๋ถ์ ๋ฐ ์ด์ ๊ฐ์ง ์๊ณ ํ๋ผ๋ฏธํฐ ์ฌ๋ณด์ \n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"AAR ๊ธฐ๋ฐ ์์ง์ ๋น ์ค๊ฒฝ๋ณด๋ฅผ ๋ถ์ํ๊ณ ์๊ณ ํ๋ผ๋ฏธํฐ๋ฅผ ์ฌ๋ณด์ ํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **AAR ๊ธฐ๋ฐ์ผ๋ก ์์ง์ ๋น ์ค๊ฒฝ๋ณด๋ฅผ ์์ธ๋ณ๋ก ๋ถ์ํ๊ณ ์ด์ ๊ฐ์ง ์๊ณ ํ๋ผ๋ฏธํฐ๋ฅผ ์ฌ๋ณด์ ํด ํจ๊ณผ๋ฅผ ์งํ๋ก ์ฐ์ถํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **AAR ๊ธฐ๊ฐ ์ค๊ฒฝ๋ณด๋ฅผ ๊ณ ๋ถํ ์ด์ฉ์ผ, ์ธ๊ธฐ ๊ธ๋ณ, ์ผ์ ์คํ์ดํฌ, ์ดํ ์ด๊ธฐ ๊ฒฝ๊ณ ๋ฑ์ผ๋ก ๋ถ๋ฅํ๊ณ , ์ ์ ๋ถ์์ยท์ฐ์ ์ผ์ยท๊ณ ๋ถํ์ผ ์๊ณ ์ํ๋ฅผ AAR ๊ธฐ๊ฐ ์๋ฃ๋ง์ผ๋ก ๊ฒฉ์ ํ์ํด ์ฌ๋ณด์ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `reports/mnt04_roi_analysis.md` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ถ์ ์ฐ์์ ๊ฐ๊ด์ฑ ๋ฐ ๊ตฐ ์ฌ์ /๊ตฐ์ ๊ธฐํ ๋ถ์ ์ค๋๋ ฅ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๋น์ฉ ์ ๊ฐ์ก, ์๊ฐ ๋จ์ถ๋ฅ ๋ฑ 3๋ ์ ๋ ํจ๊ณผ ์งํ ์ฐ์ถ\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ROI ๋ถ์ ๋ณด๊ณ ์ ์์ฑ ๋ฐ ์ฃผ์ ์งํ ๋ฏผ๊ฐ๋ ๋ถ์ ์๋ก\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ํ๊ณ/๊ธฐํ ๊ธฐ์ค ๋ชจ๋ธ๊ณผ์ ๋น๊ต ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"AAR ๊ธฐ๊ฐ์ ์์ง์ ๋น ์ค๊ฒฝ๋ณด๋ฅผ ์์ธ(๊ณ ๋ถํ ์ด์ฉ์ผยท์ธ๊ธฐ ๊ธ๋ณยท์ผ์ ์คํ์ดํฌยท๊ธฐํ)๋ณ๋ก ๋ถ์ํ๊ณ , ์ ์ ๋ถ์์ยท์ฐ์ ์ผ์ยท๊ณ ๋ถํ์ผ ์๊ณ ์ํ ํ๋ผ๋ฏธํฐ๋ฅผ AAR ๊ธฐ๊ฐ ์๋ฃ๋ก๋ง ๊ฒฉ์ ํ์ํด ์ฌ๋ณด์ ํ ๋ค ๊ฒ์ฆ ๊ธฐ๊ฐ์์ ์ค๊ฒฝ๋ณดยท๊ณ ์ฅ ์ ํ๋ณด ์๊ฐ ๋ณํ์ ๋น์ฉ ์ ๊ฐยท์ ๊ฒ ์๊ฐ ๋จ์ถยทํ๋ณด ์๊ฐ ์ฆ๊ฐ์ 3๋ ์ ๋ ํจ๊ณผ ์งํ๋ฅผ ๊ฐ์ ๋จ๊ฐ๋ก ์ฐ์ถํ๋ค.\"\"\"\n",
"import itertools\n",
"import numpy as np\n",
"import pandas as pd\n",
"from scipy.stats import kurtosis\n",
"from sklearn.neural_network import MLPRegressor\n",
"from sklearn.preprocessing import StandardScaler\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๊ธฐ๋์ฒด 48๋์ ์์งยท๊ตฌ๋๊ณ ์ง๋ยท์จ๋ยท์๋ ฅ ์ผ์ ์๋ฃ ์์ฑ๊ธฐ (์ค์ ์ฅ๋นยท๋ถ๋ยท์ ๋น ์ค์ ์์)\n",
"# ๊ฐ์ฒด๋ง๋ค ์ ์ -> ์ดํ -> ๊ณ ์ฅ(์๋ช
์ข
๋ฃ)๊น์ง ํ๋ฃจ 1ํ ์ธก์ (์ง๋ ํํ 256ํ๋ณธ + ์จ๋ + ์๋ ฅ + ๋ถํ)์ ๋ง๋ ๋ค.\n",
"# ์ดํ ์ ํ 4๊ฐ์ง: bearing(๋ฒ ์ด๋ง), gear(๊ธฐ์ด), oil_pump(์ค์ผ ํํ), cooling(๋๊ฐ ํฌ ๋ชจํฐ)\n",
"# ---------------------------------------------------------------------------\n",
"FS, NS = 1024, 256 # ๊ฐ์ ํ๋ณธํ ์ฃผํ์(Hz), ํํ 1๊ฐ๋น ํ๋ณธ ์\n",
"TT = np.arange(NS) / FS\n",
"MODES = { # ์ดํ ์ ํ: (๋ฐ์ ํ๋ฅ , ํ๊ท ์๋ช
(์ผ), ๊ต์ฒด ์๋ฆฌ๋ถ์, ๊ฐ์ ๋จ๊ฐ์ง์)\n",
" \"bearing\": (0.40, 160, \"์ฃผ ๋ฒ ์ด๋ง ์ธํธ\", 6.0), \"gear\": (0.25, 175, \"๊ตฌ๋ ๊ธฐ์ด ์ธํธ\", 9.0),\n",
" \"oil_pump\": (0.20, 150, \"์ค์ผ ํํ\", 4.0), \"cooling\": (0.15, 140, \"๋๊ฐ ํฌ ๋ชจํฐ\", 3.0)}\n",
"KIT_COST = 1.0 # ๋ชจ๋ ์ ๋น์ ํจ๊ป ์๋ชจ๋๋ ์ฐยทํํฐ ํคํธ ๋จ๊ฐ์ง์\n",
"BANDS = {\"b50\": (40, 60), \"b100\": (90, 110), \"b150\": (140, 156), \"b165\": (156, 176), \"b200\": (190, 215)}\n",
"\n",
"\n",
"def make_runs(n=48, seed=1):\n",
" \"\"\"๋ฐํ: units(๊ฐ์ฒด ์ ๋ณด), snap(์ผ๋ณ ์ธก์ ์์ฝ), W(์ง๋ ํํ [์ธก์ ์, 256])\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" names = list(MODES)\n",
" mode = rng.choice(names, n, p=[MODES[m][0] for m in names])\n",
" life = np.clip(np.array([rng.normal(MODES[m][1], 18) for m in mode]), 90, 230).astype(int)\n",
" start = np.sort(rng.integers(0, 240, n))\n",
" units = pd.DataFrame({\"unit\": [f\"U{i:02d}\" for i in range(n)], \"mode\": mode, \"life\": life, \"start\": start, \"base_load\": rng.uniform(0.4, 0.75, n)})\n",
" rows = []\n",
" for u in units.itertuples():\n",
" d = np.arange(u.life + 1)\n",
" onset = 0.45 * u.life\n",
" deg = np.clip((d - onset) / (u.life - onset), 0, 1) ** 1.5\n",
" load = np.clip(u.base_load + rng.normal(0, 0.06, len(d)), 0.2, 1.0)\n",
" hi = rng.random(len(d)) < 0.07 # ๊ณ ๋ถํ ์ด์ฉ์ผ(์ ์์ด์ง๋ง ์ง๋ยท์จ๋ ์์น)\n",
" load = np.where(hi, rng.uniform(0.95, 1.1, len(d)), load)\n",
" rows.append(pd.DataFrame({\"unit\": u.unit, \"day\": d, \"cal\": u.start + d, \"rul\": u.life - d, \"deg\": deg, \"load\": load, \"hi_load\": hi, \"mode\": u.mode}))\n",
" snap = pd.concat(rows, ignore_index=True)\n",
" S = len(snap)\n",
" snap[\"ambient\"] = 15 + 12 * np.sin(2 * np.pi * snap[\"cal\"] / 365) + rng.normal(0, 2, S)\n",
" dg, ld, md = snap[\"deg\"].values, snap[\"load\"].values, snap[\"mode\"].values\n",
" isb, isg, iso, isc = (md == \"bearing\"), (md == \"gear\"), (md == \"oil_pump\"), (md == \"cooling\")\n",
" snap[\"temp\"] = 55 + 22 * ld + 0.5 * snap[\"ambient\"] + 9 * dg * iso + 26 * dg * isc + 3 * dg * (isb | isg) + rng.normal(0, 1.5, S)\n",
" snap[\"press\"] = 450 - 12 * ld - 85 * dg * iso - 15 * dg * isc + rng.normal(0, 6, S)\n",
" ph = rng.uniform(0, 2 * np.pi, (S, 3))\n",
" a1, a2 = 1.0 * (0.5 + ld), 0.4 * (0.5 + ld)\n",
" W = (a1[:, None] * np.sin(2 * np.pi * 50 * TT + ph[:, :1]) + a2[:, None] * np.sin(2 * np.pi * 100 * TT + ph[:, 1:2])\n",
" + rng.normal(0, 0.35, (S, NS)))\n",
" W += (0.9 * dg * isb)[:, None] * np.sin(2 * np.pi * 165 * TT + ph[:, 2:3])\n",
" W += (0.8 * dg * isg)[:, None] * (np.sin(2 * np.pi * 150 * TT + ph[:, 2:3]) + 0.8 * np.sin(2 * np.pi * 100 * TT))\n",
" W += (0.5 * dg * iso)[:, None] * np.sin(2 * np.pi * 200 * TT + ph[:, 2:3])\n",
" k = rng.poisson(10 * dg * isb) # ๋ฒ ์ด๋ง ์ดํ: ์ถฉ๊ฒฉ์ฑ ํ์ค(์ฒจ๋ ์์น)\n",
" pos, amp = rng.integers(0, NS, (S, 30)), rng.normal(0, 3.0, (S, 30)) * dg[:, None]\n",
" amp = amp * (np.arange(30)[None, :] < k[:, None])\n",
" np.add.at(W, (np.repeat(np.arange(S), 30), pos.ravel()), amp.ravel())\n",
" return units, snap, W\n",
"\n",
"\n",
"def extract(W):\n",
" \"\"\"์ง๋ ํํ -> ํน์ง๋: RMS, ์ฒจ๋, ํ๊ณ ์จ, ์ต๋-์ต์, FFT ๋์ญ ์๋์ง(50/100/150/165/200Hz ๋ถ๊ทผ)\"\"\"\n",
" x = W - W.mean(axis=1, keepdims=True)\n",
" rms = np.sqrt((x ** 2).mean(axis=1))\n",
" out = {\"rms\": rms, \"kurt\": kurtosis(x, axis=1, fisher=False), \"crest\": np.abs(x).max(axis=1) / rms, \"p2p\": x.max(axis=1) - x.min(axis=1)}\n",
" A = np.abs(np.fft.rfft(x * np.hanning(NS), axis=1)) / NS\n",
" f = np.fft.rfftfreq(NS, 1 / FS)\n",
" for nm, (lo, hi) in BANDS.items():\n",
" out[nm] = np.sqrt((A[:, (f >= lo) & (f < hi)] ** 2).sum(axis=1))\n",
" return pd.DataFrame(out)\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# ๊ณตํต ๋ถ์ ์กฐ๊ฐ: ์ผ๋ณ ํน์ง๋ ํ(ํํ ํน์ง + ์จ๋ยท์๋ ฅ + ํ์), ๊ฐ์ฒด ๋จ์ ํ์ต/๊ฒ์ฆ ๋ถํ (์์ 24๋ ํ์ต, ๋ค 24๋ ๊ฒ์ฆ)\n",
"# ---------------------------------------------------------------------------\n",
"BASE = [\"rms\", \"kurt\", \"crest\", \"p2p\", \"b50\", \"b100\", \"b150\", \"b165\", \"b200\", \"temp\", \"press\"]\n",
"ROLL = [\"rms\", \"kurt\", \"b150\", \"b165\", \"b200\", \"temp\", \"press\"]\n",
"\n",
"\n",
"def build_features(snap, W):\n",
" D = pd.concat([snap.reset_index(drop=True), extract(W)], axis=1)\n",
" D[\"temp_res\"] = D[\"temp\"] - (55 + 22 * D[\"load\"] + 0.5 * D[\"ambient\"]) # ๋ถํยท์ธ๊ธฐ ๋๋น ์จ๋ ์์ฐจ\n",
" D[\"press_res\"] = D[\"press\"] - (450 - 12 * D[\"load\"]) # ๋ถํ ๋๋น ์๋ ฅ ์์ฐจ\n",
" D[\"rms_n\"] = D[\"rms\"] / (0.5 + D[\"load\"]) # ๋ถํ ์ ๊ทํ RMS\n",
" D[\"hf_ratio\"] = (D[\"b150\"] + D[\"b165\"] + D[\"b200\"]) / (D[\"b50\"] + 1e-6) # ๊ณ ์ฃผํ/๊ธฐ๋ณธ์ฃผํ ๋น\n",
" g = D.groupby(\"unit\")\n",
" for c in ROLL:\n",
" D[c + \"_m3\"] = g[c].transform(lambda s: s.rolling(3, min_periods=1).mean())\n",
" for c in (\"rms_n\", \"temp_res\", \"press_res\"):\n",
" D[c + \"_d\"] = g[c].diff().fillna(0)\n",
" return D\n",
"\n",
"\n",
"DERIVED = [\"temp_res\", \"press_res\", \"rms_n\", \"hf_ratio\"] + [c + \"_m3\" for c in ROLL] + [\"rms_n_d\", \"temp_res_d\", \"press_res_d\"]\n",
"FEATS = BASE + DERIVED\n",
"AE_COLS = [\"rms_n\", \"kurt\", \"b150\", \"b165\", \"b200\", \"hf_ratio\", \"temp_res\", \"press_res\"]\n",
"\n",
"\n",
"def split_units(units, n_train=24):\n",
" return list(units[\"unit\"][:n_train]), list(units[\"unit\"][n_train:])\n",
"\n",
"\n",
"class AE:\n",
" \"\"\"์คํ ์ธ์ฝ๋(์
๋ ฅ 8 -> 10 -> 4 -> 10 -> 8): ์ ์ ์๋ฃ๋ง ์ฌ๊ตฌ์ฑํ๋๋ก ํ์ตํ๊ณ , ์ฌ๊ตฌ์ฑ ์ค์ฐจ๋ฅผ ์ด์ ์ ์๋ก ์ด๋ค.\"\"\"\n",
"\n",
" def __init__(self, cols=AE_COLS, q=0.99, seed=0):\n",
" self.cols, self.q, self.seed = cols, q, seed\n",
"\n",
" def fit(self, healthy):\n",
" self.sc = StandardScaler().fit(healthy[self.cols])\n",
" Z = self.sc.transform(healthy[self.cols])\n",
" self.net = MLPRegressor(hidden_layer_sizes=(10, 4, 10), max_iter=800, random_state=self.seed).fit(Z, Z)\n",
" self.thr = float(np.quantile(self.err(healthy), self.q))\n",
" return self\n",
"\n",
" def contrib(self, d):\n",
" Z = self.sc.transform(d[self.cols])\n",
" return (Z - self.net.predict(Z)) ** 2 # ์
๋ ฅ ํญ๋ชฉ๋ณ ์ฌ๊ตฌ์ฑ ์ค์ฐจ(์ด์ ๊ธฐ์ฌ๋)\n",
"\n",
" def err(self, d):\n",
" return self.contrib(d).mean(axis=1)\n",
"\n",
"\n",
"def latch(d, e, thr, k=2):\n",
" \"\"\"๊ฐ์ ๊ฐ์ฒด์์ ์ด์ ์ ์๊ฐ k์ผ ์ฐ์ ๊ธฐ์ค์ ๋์ผ๋ฉด ๊ฒฝ๋ณด๋ฅผ ์ผ๊ณ ์ ์ง(๋์น)ํ๋ค.\"\"\"\n",
" out, e = np.zeros(len(d), bool), np.asarray(e)\n",
" for idx in d.reset_index(drop=True).groupby(\"unit\").indices.values():\n",
" run, on = 0, False\n",
" for i in idx:\n",
" run = run + 1 if e[i] > thr else 0\n",
" on = on or run >= k\n",
" out[i] = on\n",
" return out\n",
"\n",
"FA_COST, FA_HOURS, LATE_COST = 1.0, 3.0, 0.15 # [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ค๊ฒฝ๋ณด 1ํ ์ ๊ฒ ๋น์ฉ์ง์, ์ ๊ฒ ์์์๊ฐ(h), ํ๋ณด ์๊ฐ 1์ผ ์์ค ๋น์ฉ์ง์\n",
"units, snap, W = make_runs(seed=5)\n",
"D = build_features(snap, W)\n",
"order = list(units[\"unit\"])\n",
"tr_u, aar_u, val_u = order[:24], order[24:36], order[36:]\n",
"tr = D[D[\"unit\"].isin(tr_u)]\n",
"ae = AE().fit(tr[tr[\"deg\"] == 0])\n",
"healthy_err = ae.err(tr[tr[\"deg\"] == 0])\n",
"E = {u: (D[D[\"unit\"] == u].reset_index(drop=True), ae.err(D[D[\"unit\"] == u])) for u in order}\n",
"\n",
"\n",
"def evaluate(us, q, k, relax):\n",
" \"\"\"๋ฐํ: ์ค๊ฒฝ๋ณด ์ํผ์๋ ์, ์์ธ ๋ถ๋ฅ ๊ฑด์, ๊ณ ์ฅ ์ ํ๋ณด ์๊ฐ(์ผ) ๋ชฉ๋ก, ๋ฏธํ ๊ฐ์ฒด ์, ์์ค ํ๋ณด ์ผ์\"\"\"\n",
" base_thr = float(np.quantile(healthy_err, q))\n",
" fa, causes, leads, miss, lost = 0, {\"๊ณ ๋ถํ ์ด์ฉ์ผ\": 0, \"์ธ๊ธฐ ๊ธ๋ณยท๊ณ ์จ\": 0, \"์ผ์ ์คํ์ดํฌ\": 0, \"์ดํ ์ด๊ธฐ ๊ฒฝ๊ณ\": 0, \"๊ธฐํ\": 0}, [], 0, 0.0\n",
" for u in us:\n",
" g, e = E[u]\n",
" thr = base_thr * np.where(g[\"load\"].values > 0.95, 1 + relax, 1.0)\n",
" over = e > thr\n",
" run = np.zeros(len(g), int)\n",
" for i in range(len(g)):\n",
" run[i] = run[i - 1] + 1 if over[i] and i else int(over[i])\n",
" al = run >= k\n",
" an = (g[\"deg\"] >= 0.1).values\n",
" start = al & ~np.r_[False, al[:-1]]\n",
" for i in np.where(start & ~an)[0]:\n",
" fa += 1\n",
" r = g.iloc[i]\n",
" c = \"๊ณ ๋ถํ ์ด์ฉ์ผ\" if r[\"hi_load\"] else (\"์ธ๊ธฐ ๊ธ๋ณยท๊ณ ์จ\" if r[\"ambient\"] > 24 else (\"์ผ์ ์คํ์ดํฌ\" if r[\"kurt\"] > 3.2 else (\"์ดํ ์ด๊ธฐ ๊ฒฝ๊ณ\" if r[\"deg\"] > 0 else \"๊ธฐํ\")))\n",
" causes[c] += 1\n",
" first_an = np.argmax(an)\n",
" hit = np.where(al & an)[0]\n",
" ideal = g[\"rul\"].iloc[first_an]\n",
" if len(hit):\n",
" leads.append(int(g[\"rul\"].iloc[hit[0]])); lost += ideal - g[\"rul\"].iloc[hit[0]]\n",
" else:\n",
" miss += 1; lost += ideal\n",
" return fa, causes, leads, miss, lost\n",
"\n",
"\n",
"def J(res):\n",
" return FA_COST * res[0] + LATE_COST * res[4]\n",
"\n",
"\n",
"cur = dict(q=0.99, k=1, relax=0.0) # ํํ(AAR ์ด์ ) ํ๋ผ๋ฏธํฐ\n",
"r_aar = evaluate(aar_u, **cur)\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] AAR ๊ธฐ๊ฐ {len(aar_u)}๋: ํํ ํ๋ผ๋ฏธํฐ(์ ์ {cur['q']:.0%} ๋ถ์, {cur['k']}์ผ, ๊ณ ๋ถํ ์ํ {cur['relax']:.0%}) ์ค๊ฒฝ๋ณด {r_aar[0]}๊ฑด\")\n",
"tot = sum(r_aar[1].values())\n",
"for c, v in sorted(r_aar[1].items(), key=lambda x: -x[1]):\n",
" print(f\" {c:10s} {v:3d}๊ฑด ({v / max(tot, 1):.0%})\")\n",
"\n",
"# ์ฌ๋ณด์ : AAR ๊ธฐ๊ฐ ์๋ฃ๋ง์ผ๋ก ๊ฒฉ์ ํ์ (๋ฏธํ ๊ฐ์ฒด๊ฐ ์๋ ์กฐํฉ ์ค ๋น์ฉ ์ต์)\n",
"grid = []\n",
"for q, k, rl in itertools.product((0.98, 0.99, 0.995, 0.999), (1, 2, 3), (0.0, 0.3, 0.6)):\n",
" r = evaluate(aar_u, q, k, rl)\n",
" if r[3] == 0:\n",
" grid.append((J(r), q, k, rl))\n",
"grid.sort()\n",
"best = dict(q=grid[0][1], k=grid[0][2], relax=grid[0][3])\n",
"print(f\"\\n์ฌ๋ณด์ ๊ฒฐ๊ณผ(๊ฒฉ์ {4 * 3 * 3}๊ฐ ์ค ๋ฏธํ 0๊ฐ ์กฐํฉ {len(grid)}๊ฐ ํ์): ์ ์ {best['q']:.1%} ๋ถ์, ์ฐ์ {best['k']}์ผ, ๊ณ ๋ถํ์ผ ์๊ณ {best['relax']:.0%} ์ํ\")\n",
"\n",
"rows = []\n",
"for nm, us in ((\"AAR ๊ธฐ๊ฐ(ํ๋)\", aar_u), (\"๊ฒ์ฆ ๊ธฐ๊ฐ(๋ฏธ์ฌ์ฉ)\", val_u)):\n",
" a, b = evaluate(us, **cur), evaluate(us, **best)\n",
" rows.append((nm, a, b))\n",
" print(f\" {nm:14s} ์ค๊ฒฝ๋ณด {a[0]:3d} -> {b[0]:3d}๊ฑด, ๊ณ ์ฅ ์ ํ๋ณด ์ค์๊ฐ {np.median(a[2]):.0f} -> {np.median(b[2]):.0f}์ผ, ๋ฏธํ {a[3]} -> {b[3]}๋\")\n",
"\n",
"# 3๋ ์ ๋ ํจ๊ณผ ์งํ(๊ฒ์ฆ 3๊ฐ์ ๋์ ๊ฒ์ฆ 12๋ ๊ธฐ์ค, ์ฐํ์ฐ์ ๊ฐ์ฒด ์ ๋น๋ก)\n",
"a, b = rows[1][1], rows[1][2]\n",
"jc, jn = J(a), J(b)\n",
"print(\"\\n[3๋ ์ ๋ ํจ๊ณผ ์งํ] (๊ฒ์ฆ 12๋, ๊ฐ์ ๋จ๊ฐ)\")\n",
"print(f\" 1) ๋น์ฉ ์ ๊ฐ: ๋น์ฉ์ง์ {jc:.1f} -> {jn:.1f} ({1 - jn / jc:.1%} ์ ๊ฐ), 48๋ ์ฐํ์ฐ ์ฝ {(jc - jn) * 4:.0f}\")\n",
"print(f\" 2) ์๊ฐ ๋จ์ถ: ๋ถํ์ ์ ๊ฒ {a[0] * FA_HOURS:.0f}h -> {b[0] * FA_HOURS:.0f}h ({1 - b[0] / max(a[0], 1):.1%} ๋จ์ถ)\")\n",
"print(f\" 3) ํ๋ณด ์๊ฐ: ๊ณ ์ฅ ์ ์ฒซ ๊ฒฝ๋ณด ํ๋ณด ์ค์๊ฐ {np.median(a[2]):.0f}์ผ -> {np.median(b[2]):.0f}์ผ, ์ ์ ์ค๊ฒฝ๋ณด {a[0]}๊ฑด -> {b[0]}๊ฑด\")\n",
"print(\"[๋์
๋ก๋๋งต] ๋จ๊ธฐ(1~3์): ์ฌ๋ณด์ ์๊ณ ์ ์ฉ + ๊ฒ์ฌ๊ด ํ์ธ ๋ณํ / ์ค๊ธฐ(3~12์): ๋ถ๊ธฐ AAR๋ก ์๊ณ ์ฌ๋ณด์ , ๋ถํ ๋ณด์ ํญ ์ถ๊ฐ / ์ฅ๊ธฐ(1๋
~): ๊ตฐ์ ์ ๋ณด์ฒด๊ณ ์ ๋น ์ด๋ ฅ๊ณผ ์ฐ๋ํด ์๊ณ ์๋ ๊ฐฑ์ \")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
09 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] AAR ๊ธฐ๊ฐ 12๋: ํํ ํ๋ผ๋ฏธํฐ(์ ์ 99% ๋ถ์, 1์ผ, ๊ณ ๋ถํ ์ํ 0%) ์ค๊ฒฝ๋ณด 9๊ฑด\n",
" ๊ธฐํ 5๊ฑด (56%)\n",
" ๊ณ ๋ถํ ์ด์ฉ์ผ 2๊ฑด (22%)\n",
" ์ดํ ์ด๊ธฐ ๊ฒฝ๊ณ 2๊ฑด (22%)\n",
" ์ธ๊ธฐ ๊ธ๋ณยท๊ณ ์จ 0๊ฑด (0%)\n",
" ์ผ์ ์คํ์ดํฌ 0๊ฑด (0%)\n",
"\n",
"์ฌ๋ณด์ ๊ฒฐ๊ณผ(๊ฒฉ์ 36๊ฐ ์ค ๋ฏธํ 0๊ฐ ์กฐํฉ 36๊ฐ ํ์): ์ ์ 99.5% ๋ถ์, ์ฐ์ 1์ผ, ๊ณ ๋ถํ์ผ ์๊ณ 30% ์ํ\n",
" AAR ๊ธฐ๊ฐ(ํ๋) ์ค๊ฒฝ๋ณด 9 -> 4๊ฑด, ๊ณ ์ฅ ์ ํ๋ณด ์ค์๊ฐ 58 -> 56์ผ, ๋ฏธํ 0 -> 0๋\n",
" ๊ฒ์ฆ ๊ธฐ๊ฐ(๋ฏธ์ฌ์ฉ) ์ค๊ฒฝ๋ณด 16 -> 10๊ฑด, ๊ณ ์ฅ ์ ํ๋ณด ์ค์๊ฐ 60 -> 57์ผ, ๋ฏธํ 0 -> 0๋\n",
"\n",
"[3๋ ์ ๋ ํจ๊ณผ ์งํ] (๊ฒ์ฆ 12๋, ๊ฐ์ ๋จ๊ฐ)\n",
" 1) ๋น์ฉ ์ ๊ฐ: ๋น์ฉ์ง์ 24.7 -> 22.0 (10.9% ์ ๊ฐ), 48๋ ์ฐํ์ฐ ์ฝ 11\n",
" 2) ์๊ฐ ๋จ์ถ: ๋ถํ์ ์ ๊ฒ 48h -> 30h (37.5% ๋จ์ถ)\n",
" 3) ํ๋ณด ์๊ฐ: ๊ณ ์ฅ ์ ์ฒซ ๊ฒฝ๋ณด ํ๋ณด ์ค์๊ฐ 60์ผ -> 57์ผ, ์ ์ ์ค๊ฒฝ๋ณด 16๊ฑด -> 10๊ฑด\n",
"[๋์
๋ก๋๋งต] ๋จ๊ธฐ(1~3์): ์ฌ๋ณด์ ์๊ณ ์ ์ฉ + ๊ฒ์ฌ๊ด ํ์ธ ๋ณํ / ์ค๊ธฐ(3~12์): ๋ถ๊ธฐ AAR๋ก ์๊ณ ์ฌ๋ณด์ , ๋ถํ ๋ณด์ ํญ ์ถ๊ฐ / ์ฅ๊ธฐ(1๋
~): ๊ตฐ์ ์ ๋ณด์ฒด๊ณ ์ ๋น ์ด๋ ฅ๊ณผ ์ฐ๋ํด ์๊ณ ์๋ ๊ฐฑ์ \n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
10. ํญ๊ณต/๊ธฐ๊ฐ ์ฅ๋น ์์ง์ ๋น(PdM) ์ผ์ ๋ฐ์ดํฐ ์ด์ ์งํ ๊ฐ์ง ์์คํ
ํตํฉ ์์ฑ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"ํญ๊ณต/๊ธฐ๊ฐ ์ฅ๋น ์์ง์ ๋น(PdM) ์ผ์ ๋ฐ์ดํฐ ์ด์ ์งํ ๊ฐ์ง ์์คํ
์ ์์ฑํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ผ์ ๊ฒ์ฆ๋ถํฐ ์ด์ ๊ฐ์ง, RUL ์์ธก, ์น์ธ ๋๊ธฐ ์
๊ณ ์ ์, ์๋ฆฌ๋ถ์ ์์ ์๊ณ , ์ฌํ์ต๊น์ง ํ ํ์ผ๋ก ํตํฉํ๊ณ ์์ฒด ์ ๊ฒ๊ณผ ๋ธ๋ฆฌํ ์์ฝ์ ์ถ๋ ฅํ๋ ๋๊ตฌ๋ฅผ ์์ฑํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ง๋๋ฌยท์ด๋ฒ ๋ฌ ์ค์ ์ผ๋ก ์ฑํผ์ธ ๋ชจ๋ธ๊ณผ ๋์ ์ ๋ชจ๋ธ์ ํ์ตํด ๊ฒ์ฆ F1์ ๋น๊ตํ๊ณ ๋น์ทํ๊ฑฐ๋ ๋์ ๋๋ง ๊ต์ฒด ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `docs/mnt04_operations_guide.md ๋ฐ pipeline/mnt04_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": [
"\"\"\"์ผ์ ํํ ๊ฒ์ฆยท์ ์ , ์คํ ์ธ์ฝ๋ ์ด์ ๊ฐ์ง ๋ชจ๋ธ์ ์๋ ์ฌํ์ต(์ฑํผ์ธยท๋์ ์ ๋น๊ต ํ ๊ต์ฒด), ์ฌ๋ณด์ ์๊ณ ์ ์ฉ ๊ฒฝ๋ณด, ์์กด์๋ช
(RUL) ์์ธก, ๊ฒ์ฌ๊ด ์น์ธ ๋๊ธฐ ์
๊ณ ์ ์, ์๋ฆฌ๋ถ์ ์์ ์๊ณ ๊น์ง ์์ง์ ๋น(PdM) ์ด์ ๊ฐ์ง ์์คํ
์ ํ ํ์ผ๋ก ํตํฉ ์คํํ๊ณ ์์ฒด ์ ๊ฒยท์ด์ ๋ฌธ์ ์๋น ์ ๊ฒยท์ ๋น ๋ด๋น ์ฅ๊ต ๋ธ๋ฆฌํ ์์ฝ์ ์ถ๋ ฅํ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy.stats import kurtosis\n",
"from sklearn.ensemble import GradientBoostingRegressor\n",
"from sklearn.neural_network import MLPRegressor\n",
"from sklearn.preprocessing import StandardScaler\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๊ธฐ๋์ฒด 48๋์ ์์งยท๊ตฌ๋๊ณ ์ง๋ยท์จ๋ยท์๋ ฅ ์ผ์ ์๋ฃ ์์ฑ๊ธฐ (์ค์ ์ฅ๋นยท๋ถ๋ยท์ ๋น ์ค์ ์์)\n",
"# ๊ฐ์ฒด๋ง๋ค ์ ์ -> ์ดํ -> ๊ณ ์ฅ(์๋ช
์ข
๋ฃ)๊น์ง ํ๋ฃจ 1ํ ์ธก์ (์ง๋ ํํ 256ํ๋ณธ + ์จ๋ + ์๋ ฅ + ๋ถํ)์ ๋ง๋ ๋ค.\n",
"# ์ดํ ์ ํ 4๊ฐ์ง: bearing(๋ฒ ์ด๋ง), gear(๊ธฐ์ด), oil_pump(์ค์ผ ํํ), cooling(๋๊ฐ ํฌ ๋ชจํฐ)\n",
"# ---------------------------------------------------------------------------\n",
"FS, NS = 1024, 256 # ๊ฐ์ ํ๋ณธํ ์ฃผํ์(Hz), ํํ 1๊ฐ๋น ํ๋ณธ ์\n",
"TT = np.arange(NS) / FS\n",
"MODES = { # ์ดํ ์ ํ: (๋ฐ์ ํ๋ฅ , ํ๊ท ์๋ช
(์ผ), ๊ต์ฒด ์๋ฆฌ๋ถ์, ๊ฐ์ ๋จ๊ฐ์ง์)\n",
" \"bearing\": (0.40, 160, \"์ฃผ ๋ฒ ์ด๋ง ์ธํธ\", 6.0), \"gear\": (0.25, 175, \"๊ตฌ๋ ๊ธฐ์ด ์ธํธ\", 9.0),\n",
" \"oil_pump\": (0.20, 150, \"์ค์ผ ํํ\", 4.0), \"cooling\": (0.15, 140, \"๋๊ฐ ํฌ ๋ชจํฐ\", 3.0)}\n",
"KIT_COST = 1.0 # ๋ชจ๋ ์ ๋น์ ํจ๊ป ์๋ชจ๋๋ ์ฐยทํํฐ ํคํธ ๋จ๊ฐ์ง์\n",
"BANDS = {\"b50\": (40, 60), \"b100\": (90, 110), \"b150\": (140, 156), \"b165\": (156, 176), \"b200\": (190, 215)}\n",
"\n",
"\n",
"def make_runs(n=48, seed=1):\n",
" \"\"\"๋ฐํ: units(๊ฐ์ฒด ์ ๋ณด), snap(์ผ๋ณ ์ธก์ ์์ฝ), W(์ง๋ ํํ [์ธก์ ์, 256])\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" names = list(MODES)\n",
" mode = rng.choice(names, n, p=[MODES[m][0] for m in names])\n",
" life = np.clip(np.array([rng.normal(MODES[m][1], 18) for m in mode]), 90, 230).astype(int)\n",
" start = np.sort(rng.integers(0, 240, n))\n",
" units = pd.DataFrame({\"unit\": [f\"U{i:02d}\" for i in range(n)], \"mode\": mode, \"life\": life, \"start\": start, \"base_load\": rng.uniform(0.4, 0.75, n)})\n",
" rows = []\n",
" for u in units.itertuples():\n",
" d = np.arange(u.life + 1)\n",
" onset = 0.45 * u.life\n",
" deg = np.clip((d - onset) / (u.life - onset), 0, 1) ** 1.5\n",
" load = np.clip(u.base_load + rng.normal(0, 0.06, len(d)), 0.2, 1.0)\n",
" hi = rng.random(len(d)) < 0.07 # ๊ณ ๋ถํ ์ด์ฉ์ผ(์ ์์ด์ง๋ง ์ง๋ยท์จ๋ ์์น)\n",
" load = np.where(hi, rng.uniform(0.95, 1.1, len(d)), load)\n",
" rows.append(pd.DataFrame({\"unit\": u.unit, \"day\": d, \"cal\": u.start + d, \"rul\": u.life - d, \"deg\": deg, \"load\": load, \"hi_load\": hi, \"mode\": u.mode}))\n",
" snap = pd.concat(rows, ignore_index=True)\n",
" S = len(snap)\n",
" snap[\"ambient\"] = 15 + 12 * np.sin(2 * np.pi * snap[\"cal\"] / 365) + rng.normal(0, 2, S)\n",
" dg, ld, md = snap[\"deg\"].values, snap[\"load\"].values, snap[\"mode\"].values\n",
" isb, isg, iso, isc = (md == \"bearing\"), (md == \"gear\"), (md == \"oil_pump\"), (md == \"cooling\")\n",
" snap[\"temp\"] = 55 + 22 * ld + 0.5 * snap[\"ambient\"] + 9 * dg * iso + 26 * dg * isc + 3 * dg * (isb | isg) + rng.normal(0, 1.5, S)\n",
" snap[\"press\"] = 450 - 12 * ld - 85 * dg * iso - 15 * dg * isc + rng.normal(0, 6, S)\n",
" ph = rng.uniform(0, 2 * np.pi, (S, 3))\n",
" a1, a2 = 1.0 * (0.5 + ld), 0.4 * (0.5 + ld)\n",
" W = (a1[:, None] * np.sin(2 * np.pi * 50 * TT + ph[:, :1]) + a2[:, None] * np.sin(2 * np.pi * 100 * TT + ph[:, 1:2])\n",
" + rng.normal(0, 0.35, (S, NS)))\n",
" W += (0.9 * dg * isb)[:, None] * np.sin(2 * np.pi * 165 * TT + ph[:, 2:3])\n",
" W += (0.8 * dg * isg)[:, None] * (np.sin(2 * np.pi * 150 * TT + ph[:, 2:3]) + 0.8 * np.sin(2 * np.pi * 100 * TT))\n",
" W += (0.5 * dg * iso)[:, None] * np.sin(2 * np.pi * 200 * TT + ph[:, 2:3])\n",
" k = rng.poisson(10 * dg * isb) # ๋ฒ ์ด๋ง ์ดํ: ์ถฉ๊ฒฉ์ฑ ํ์ค(์ฒจ๋ ์์น)\n",
" pos, amp = rng.integers(0, NS, (S, 30)), rng.normal(0, 3.0, (S, 30)) * dg[:, None]\n",
" amp = amp * (np.arange(30)[None, :] < k[:, None])\n",
" np.add.at(W, (np.repeat(np.arange(S), 30), pos.ravel()), amp.ravel())\n",
" return units, snap, W\n",
"\n",
"\n",
"def extract(W):\n",
" \"\"\"์ง๋ ํํ -> ํน์ง๋: RMS, ์ฒจ๋, ํ๊ณ ์จ, ์ต๋-์ต์, FFT ๋์ญ ์๋์ง(50/100/150/165/200Hz ๋ถ๊ทผ)\"\"\"\n",
" x = W - W.mean(axis=1, keepdims=True)\n",
" rms = np.sqrt((x ** 2).mean(axis=1))\n",
" out = {\"rms\": rms, \"kurt\": kurtosis(x, axis=1, fisher=False), \"crest\": np.abs(x).max(axis=1) / rms, \"p2p\": x.max(axis=1) - x.min(axis=1)}\n",
" A = np.abs(np.fft.rfft(x * np.hanning(NS), axis=1)) / NS\n",
" f = np.fft.rfftfreq(NS, 1 / FS)\n",
" for nm, (lo, hi) in BANDS.items():\n",
" out[nm] = np.sqrt((A[:, (f >= lo) & (f < hi)] ** 2).sum(axis=1))\n",
" return pd.DataFrame(out)\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# ๊ณตํต ๋ถ์ ์กฐ๊ฐ: ์ผ๋ณ ํน์ง๋ ํ(ํํ ํน์ง + ์จ๋ยท์๋ ฅ + ํ์), ๊ฐ์ฒด ๋จ์ ํ์ต/๊ฒ์ฆ ๋ถํ (์์ 24๋ ํ์ต, ๋ค 24๋ ๊ฒ์ฆ)\n",
"# ---------------------------------------------------------------------------\n",
"BASE = [\"rms\", \"kurt\", \"crest\", \"p2p\", \"b50\", \"b100\", \"b150\", \"b165\", \"b200\", \"temp\", \"press\"]\n",
"ROLL = [\"rms\", \"kurt\", \"b150\", \"b165\", \"b200\", \"temp\", \"press\"]\n",
"\n",
"\n",
"def build_features(snap, W):\n",
" D = pd.concat([snap.reset_index(drop=True), extract(W)], axis=1)\n",
" D[\"temp_res\"] = D[\"temp\"] - (55 + 22 * D[\"load\"] + 0.5 * D[\"ambient\"]) # ๋ถํยท์ธ๊ธฐ ๋๋น ์จ๋ ์์ฐจ\n",
" D[\"press_res\"] = D[\"press\"] - (450 - 12 * D[\"load\"]) # ๋ถํ ๋๋น ์๋ ฅ ์์ฐจ\n",
" D[\"rms_n\"] = D[\"rms\"] / (0.5 + D[\"load\"]) # ๋ถํ ์ ๊ทํ RMS\n",
" D[\"hf_ratio\"] = (D[\"b150\"] + D[\"b165\"] + D[\"b200\"]) / (D[\"b50\"] + 1e-6) # ๊ณ ์ฃผํ/๊ธฐ๋ณธ์ฃผํ ๋น\n",
" g = D.groupby(\"unit\")\n",
" for c in ROLL:\n",
" D[c + \"_m3\"] = g[c].transform(lambda s: s.rolling(3, min_periods=1).mean())\n",
" for c in (\"rms_n\", \"temp_res\", \"press_res\"):\n",
" D[c + \"_d\"] = g[c].diff().fillna(0)\n",
" return D\n",
"\n",
"\n",
"DERIVED = [\"temp_res\", \"press_res\", \"rms_n\", \"hf_ratio\"] + [c + \"_m3\" for c in ROLL] + [\"rms_n_d\", \"temp_res_d\", \"press_res_d\"]\n",
"FEATS = BASE + DERIVED\n",
"AE_COLS = [\"rms_n\", \"kurt\", \"b150\", \"b165\", \"b200\", \"hf_ratio\", \"temp_res\", \"press_res\"]\n",
"\n",
"\n",
"def split_units(units, n_train=24):\n",
" return list(units[\"unit\"][:n_train]), list(units[\"unit\"][n_train:])\n",
"\n",
"\n",
"class AE:\n",
" \"\"\"์คํ ์ธ์ฝ๋(์
๋ ฅ 8 -> 10 -> 4 -> 10 -> 8): ์ ์ ์๋ฃ๋ง ์ฌ๊ตฌ์ฑํ๋๋ก ํ์ตํ๊ณ , ์ฌ๊ตฌ์ฑ ์ค์ฐจ๋ฅผ ์ด์ ์ ์๋ก ์ด๋ค.\"\"\"\n",
"\n",
" def __init__(self, cols=AE_COLS, q=0.99, seed=0):\n",
" self.cols, self.q, self.seed = cols, q, seed\n",
"\n",
" def fit(self, healthy):\n",
" self.sc = StandardScaler().fit(healthy[self.cols])\n",
" Z = self.sc.transform(healthy[self.cols])\n",
" self.net = MLPRegressor(hidden_layer_sizes=(10, 4, 10), max_iter=800, random_state=self.seed).fit(Z, Z)\n",
" self.thr = float(np.quantile(self.err(healthy), self.q))\n",
" return self\n",
"\n",
" def contrib(self, d):\n",
" Z = self.sc.transform(d[self.cols])\n",
" return (Z - self.net.predict(Z)) ** 2 # ์
๋ ฅ ํญ๋ชฉ๋ณ ์ฌ๊ตฌ์ฑ ์ค์ฐจ(์ด์ ๊ธฐ์ฌ๋)\n",
"\n",
" def err(self, d):\n",
" return self.contrib(d).mean(axis=1)\n",
"\n",
"\n",
"def latch(d, e, thr, k=2):\n",
" \"\"\"๊ฐ์ ๊ฐ์ฒด์์ ์ด์ ์ ์๊ฐ k์ผ ์ฐ์ ๊ธฐ์ค์ ๋์ผ๋ฉด ๊ฒฝ๋ณด๋ฅผ ์ผ๊ณ ์ ์ง(๋์น)ํ๋ค.\"\"\"\n",
" out, e = np.zeros(len(d), bool), np.asarray(e)\n",
" for idx in d.reset_index(drop=True).groupby(\"unit\").indices.values():\n",
" run, on = 0, False\n",
" for i in idx:\n",
" run = run + 1 if e[i] > thr else 0\n",
" on = on or run >= k\n",
" out[i] = on\n",
" return out\n",
"\n",
"CONFIG = {\"q\": 0.995, \"k\": 1, \"relax\": 0.3, \"retrain_tolerance\": 0.02, \"approve_rul\": 90, \"spare_horizon\": 60} # ๋ฏธ์
9 ์ฌ๋ณด์ ๊ฒฐ๊ณผ\n",
"OPS_GUIDE = { # docs/mnt04_operations_guide.md ์์ฝ\n",
" \"1. ๋ชฉ์ ๊ณผ ๋ฒ์\": \"์์งยท๊ตฌ๋๊ณ ์ผ์๋ก ์ด์ ์งํ์ ์์กด์๋ช
์ ๋ณด์กฐ ํ๋จ. ์
๊ณ ยท์ ๋น ๊ฒฐ์ ์ ๊ฒ์ฌ๊ด.\",\n",
" \"2. ์
๋ ฅ ๋ฐ์ดํฐ\": \"์ผ 1ํ ์ง๋ ํํยท์จ๋ยท์๋ ฅยท๋ถํ. ์ค์ ์ฅ๋น ์๋ฃ ๋์ ๊ฐ์ ์๋ณ์ฝ๋ ์ฌ์ฉ.\",\n",
" \"3. ์ผ์ผ ์ด์ฉ ์ ์ฐจ\": \"์์ง -> ๊ฒ์ฆยท๊ฒฉ๋ฆฌ -> ํน์ง ์ถ์ถ -> ์ด์ ์ ์ -> ๊ฒฝ๋ณด -> ์
๊ณ ์ ์(๋๊ธฐ) -> ๊ฒ์ฌ๊ด ๊ฒฐ์ -> ๊ฒฐ๊ณผ ๊ธฐ๋ก.\",\n",
" \"4. ์น์ธ(HITL) ๊ธฐ์ค\": \"์กฐ๊ธฐ ์
๊ณ ์ ์์ ๊ฒ์ฌ๊ด ์น์ธ ํ ํ์ , ๋ฌด์๋ต ์ ๊ธฐ๋ณธ ๊ณํ(์ ๊ธฐ ๊ต์ฒด) ์ ์ง, ์ฌ์ ๊ธฐ๋ก.\",\n",
" \"5. ์ฌํ์ต ์ ์ฐจ\": \"๋ถ๊ธฐ 1ํ ์ ๊ท ์ ์ยท๊ณ ์ฅ ์ค์ ์ผ๋ก ๋์ ์ ํ์ต, ๊ฒ์ฆ F1์ด ๋๋น ์ง์ง ์์ ๋๋ง ๊ต์ฒด.\",\n",
" \"6. ์ฅ์ ๋์\": \"๋ชจ๋ธ ์ค๋ฅ ์ ์ ๊ธฐ ๊ต์ฒด ์ฃผ๊ธฐ ๊ธฐ์ค์ผ๋ก ๋์ฒด, ์์ ์ผ์ ์๋ฃ๋ ์๋ ํ์ธ ๋ชฉ๋ก์ผ๋ก.\",\n",
" \"7. ์ฐ๋ ์ธํฐํ์ด์ค\": \"๊ตญ๋ฐฉ ๊ตฐ์ ์ ๋ณด์ฒด๊ณ์ ์ ๋น ์ด๋ ฅยท์๋ฆฌ๋ถ์ ์๋ชจ ํ์ค ํ๋(์ฅ๋น์ฝ๋ยท๋ถ๋ฅ๊ตฐยท์ผ์)๋ฅผ CSV/API๋ก ๊ตํํ๋ ์์ ์ฐจ๊ธฐ ๊ณผ์ ๋ก ๋๋ค.\",\n",
"}\n",
"F10 = FEATS + [\"day\", \"hf_max\", \"temp_res_max\"]\n",
"\n",
"\n",
"def prep(units, snap, W):\n",
" \"\"\"๊ฒ์ฆ: ํํ ๊ฒฐ์ธกยท๊ณ ์ ยทํฌํ, ๊ฐ ๋ฒ์ ์ดํ ์ธก์ ์ ๊ฒฉ๋ฆฌํ๊ณ ๋๋จธ์ง๋ก ํน์งํ๋ฅผ ๋ง๋ ๋ค.\"\"\"\n",
" bad = np.isnan(W).any(axis=1) | (np.nanstd(W, axis=1) < 1e-6) | (np.abs(np.nan_to_num(W)).max(axis=1) > 12) | ~snap[\"temp\"].between(20, 160).values\n",
" ok = ~bad\n",
" D = build_features(snap[ok].reset_index(drop=True), W[ok])\n",
" D[\"hf_max\"] = D.groupby(\"unit\")[\"hf_ratio\"].cummax(); D[\"temp_res_max\"] = D.groupby(\"unit\")[\"temp_res\"].cummax()\n",
" return D, int(bad.sum())\n",
"\n",
"\n",
"def alarms(D, e, thr, cfg=CONFIG):\n",
" over = np.asarray(e) > thr * np.where(D[\"load\"].values > 0.95, 1 + cfg[\"relax\"], 1.0)\n",
" out = np.zeros(len(D), bool)\n",
" for idx in D.reset_index(drop=True).groupby(\"unit\").indices.values():\n",
" run = 0\n",
" for i in idx:\n",
" run = run + 1 if over[i] else 0\n",
" out[i] = run >= cfg[\"k\"]\n",
" return out\n",
"\n",
"\n",
"def f1(D, al):\n",
" y = (D[\"deg\"] >= 0.1).values\n",
" tp, fp, fn = (al & y).sum(), (al & ~y).sum(), (~al & y).sum()\n",
" return float(2 * tp / (2 * tp + fp + fn))\n",
"\n",
"\n",
"def retrain(champion, D_old, D_new):\n",
" \"\"\"์ฑํผ์ธ AE(์ง๋๋ฌ ์๋ฃ) vs ๋์ ์ AE(์ง๋๋ฌ+์ด๋ฒ ๋ฌ ํ์ต ๊ฐ์ฒด) -> ์ด๋ฒ ๋ฌ ๊ฒ์ฆ ๊ฐ์ฒด F1 ๋น๊ต.\"\"\"\n",
" new_tr, new_va = D_new[D_new[\"unit\"].isin(list(D_new[\"unit\"].unique())[:24])], D_new[D_new[\"unit\"].isin(list(D_new[\"unit\"].unique())[24:])]\n",
" ch = AE(seed=1).fit(pd.concat([D_old[D_old[\"deg\"] == 0], new_tr[new_tr[\"deg\"] == 0]]))\n",
" thr_c = float(np.quantile(champion.err(D_old[D_old[\"deg\"] == 0]), CONFIG[\"q\"]))\n",
" thr_n = float(np.quantile(ch.err(pd.concat([D_old[D_old[\"deg\"] == 0], new_tr[new_tr[\"deg\"] == 0]])), CONFIG[\"q\"]))\n",
" f_old, f_new = f1(new_va, alarms(new_va, champion.err(new_va), thr_c)), f1(new_va, alarms(new_va, ch.err(new_va), thr_n))\n",
" swap = f_new >= f_old - CONFIG[\"retrain_tolerance\"]\n",
" return (ch if swap else champion), (thr_n if swap else thr_c), {\"์ฑํผ์ธ F1\": round(f_old, 3), \"๋์ ์ F1\": round(f_new, 3), \"๊ต์ฒด\": bool(swap)}\n",
"\n",
"\n",
"def predict_safe(model, D):\n",
" try:\n",
" return np.maximum(model.predict(D[F10]), 0), \"๋ชจ๋ธ\"\n",
" except Exception:\n",
" return np.maximum(90 - D[\"day\"].values, 0).astype(float), \"์ ๊ธฐ ์ฃผ๊ธฐ ๊ธฐ์ค ๋์ฒด\"\n",
"\n",
"\n",
"# ---------------- 1. ํตํฉ ์คํ ----------------\n",
"t_start = time.perf_counter()\n",
"Dold, _ = prep(*make_runs(seed=11)) # ์ง๋๋ฌ ์ค์ \n",
"Dnew, _ = prep(*make_runs(seed=1)) # ์ด๋ฒ ๋ฌ ์ค์ \n",
"u3, s3, w3 = make_runs(seed=21) # ๋ค์ ๋ฌ ์ด์ฉ(์ ๊ท ์ธก์ )\n",
"w3[[100, 2000], 20:50] = np.nan # ์์ ์ธก์ 2๊ฑด ์ฃผ์
(๊ฒฐ์ธก ํจํท)\n",
"Dop, n_bad = prep(u3, s3, w3)\n",
"champion = AE().fit(Dold[Dold[\"deg\"] == 0])\n",
"champ_thr = float(np.quantile(champion.err(Dold[Dold[\"deg\"] == 0]), CONFIG[\"q\"]))\n",
"model_ae, thr, rt = retrain(champion, Dold, Dnew)\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ฌํ์ต: {rt} -> {'๋์ ์ ๊ต์ฒด' if rt['๊ต์ฒด'] else '์ฑํผ์ธ ์ ์ง'}\")\n",
"gbr = GradientBoostingRegressor(n_estimators=100, max_depth=3, learning_rate=0.1, subsample=0.8, random_state=0).fit(Dnew[F10], np.minimum(Dnew[\"rul\"], 120))\n",
"e = model_ae.err(Dop)\n",
"Dop[\"alarm\"] = alarms(Dop, e, thr)\n",
"Dop[\"rul_hat\"], src = predict_safe(gbr, Dop)\n",
"Dop[\"score\"] = e / thr\n",
"sec = time.perf_counter() - t_start\n",
"print(f\"์ด์ฉ ์ธก์ {len(s3)}๊ฑด: ์ฒ๋ฆฌ {len(Dop)}๊ฑด, ๊ฒฉ๋ฆฌ {n_bad}๊ฑด, ๊ฒฝ๋ณด ์ธก์ {int(Dop['alarm'].sum())}๊ฑด, RUL ์ถ์ฒ '{src}', ์ ์ฒด ์คํ {sec:.1f}์ด\")\n",
"\n",
"# ์
๊ณ ์ ์(๋๊ธฐ)๊ณผ ์๋ฆฌ๋ถ์ ์์ ์๊ณ : ๊ฒฝ๋ณด ๊ฐ์ฒด์ ๋ง์ง๋ง ์ธก์ ๊ธฐ์ค\n",
"PART = {\"b165\": \"์ฃผ ๋ฒ ์ด๋ง ์ธํธ\", \"kurt\": \"์ฃผ ๋ฒ ์ด๋ง ์ธํธ\", \"b150\": \"๊ตฌ๋ ๊ธฐ์ด ์ธํธ\", \"b200\": \"์ค์ผ ํํ\", \"press_res\": \"์ค์ผ ํํ\", \"temp_res\": \"๋๊ฐ ํฌ ๋ชจํฐ\"}\n",
"first = Dop[Dop[\"alarm\"]].groupby(\"unit\").first()\n",
"cols = model_ae.cols\n",
"C = model_ae.contrib(Dop)\n",
"prop = []\n",
"for u, r in first.iterrows():\n",
" i = Dop.index[(Dop[\"unit\"] == u) & (Dop[\"day\"] == r[\"day\"])][0]\n",
" prop.append({\"unit\": u, \"day\": int(r[\"day\"]), \"rul_hat\": float(r[\"rul_hat\"]), \"rul\": int(r[\"rul\"]), \"part\": PART.get(cols[int(np.argmax(C[i]))], \"๋ฏธ๋ถ๋ฅ\"),\n",
" \"mode\": r[\"mode\"], \"status\": \"PENDING\" if r[\"rul_hat\"] <= CONFIG[\"approve_rul\"] else \"๊ด์ฐฐ\"})\n",
"P = pd.DataFrame(prop)\n",
"P[\"true_part\"] = P[\"mode\"].map({m: MODES[m][2] for m in MODES})\n",
"# ์๋ฆฌ๋ถ์ ์์ ์๊ณ : ์์ธก ์์กด 30์ผ ์ด๋ด๋ก ๋ด๋ ค์จ ๊ฐ์ฒด์ ์์ฌ ๋ถํ\n",
"need = P[P[\"rul_hat\"] <= CONFIG[\"spare_horizon\"]]\n",
"\n",
"# ---------------- 2. ์์ฒด ์ ๊ฒ ----------------\n",
"_, bad_src = predict_safe(None, Dop.head(3))\n",
"y = (Dop[\"deg\"] >= 0.1).values\n",
"checks = {\n",
" \"์ธก์ ์ ์ค 0๊ฑด\": len(Dop) + n_bad == len(s3),\n",
" \"์์ ์ธก์ 2๊ฑด ๊ฒฉ๋ฆฌ\": n_bad == 2,\n",
" \"์ฌํ์ต ํ์ดํ๋ผ์ธ ์คํ\": \"๊ต์ฒด\" in rt,\n",
" \"RUL ์์ธก๊ฐ ์์ ์์\": bool((Dop[\"rul_hat\"] >= 0).all()),\n",
" \"๋ชจ๋ธ ์ฅ์ ์ ์ ๊ธฐ ์ฃผ๊ธฐ ๋์ฒด\": bad_src == \"์ ๊ธฐ ์ฃผ๊ธฐ ๊ธฐ์ค ๋์ฒด\",\n",
" \"์ดํ ๊ฐ์ฒด ๋ฏธํ 0๋\": bool(Dop.groupby(\"unit\").apply(lambda g: (g[\"alarm\"] & (g[\"deg\"] >= 0.1)).any() or not (g[\"deg\"] >= 0.1).any()).all()),\n",
" \"๊ฒฝ๋ณด ๊ฐ์ฒด ๋ชจ๋ ์ ์ ๋๋ ๊ด์ฐฐ ๋ถ๋ฅ\": len(P) == int(Dop[Dop[\"alarm\"]][\"unit\"].nunique()),\n",
" \"์๋ ์น์ธ 0๊ฑด(์น์ธ ๋์์ ๋๊ธฐ)\": bool(P[\"status\"].isin([\"PENDING\", \"๊ด์ฐฐ\"]).all()),\n",
"}\n",
"print(f\"\\n์์ฒด ์ ๊ฒ {sum(checks.values())}/{len(checks)} ํต๊ณผ, ์๋ ์ฌํ์ต ํ์ดํ๋ผ์ธ {'์ ์ ์คํ' if checks['์ฌํ์ต ํ์ดํ๋ผ์ธ ์คํ'] else '์คํจ'}\")\n",
"for k, v in checks.items():\n",
" if not v:\n",
" print(\" ์คํจ:\", k)\n",
"doc = sum(bool(v.strip()) for v in OPS_GUIDE.values()) / len(OPS_GUIDE)\n",
"print(f\"์ด์ ๋ฌธ์ {len(OPS_GUIDE)}๊ฐ ์ฅ ์์ฑ๋ฅ {doc:.0%} (๋ชฉํ 100%: {'๋ฌ์ฑ' if doc == 1 else '๋ฏธ๋ฌ'}) - \" + \", \".join(k.split(\". \")[1] for k in OPS_GUIDE))\n",
"\n",
"# ---------------- 3. ํ๋ฉด๊ณผ ๋ธ๋ฆฌํ ----------------\n",
"fig, ax = plt.subplots(1, 2, figsize=(11, 4))\n",
"for u in list(P[\"unit\"])[:3]:\n",
" g = Dop[Dop[\"unit\"] == u]\n",
" ax[0].plot(g[\"day\"], g[\"score\"], label=u)\n",
"ax[0].axhline(1.0, ls=\"--\", color=\"k\"); ax[0].set_yscale(\"log\"); ax[0].set_title(\"Anomaly score (x threshold)\"); ax[0].set_xlabel(\"day\"); ax[0].legend()\n",
"ax[1].scatter(P[\"rul\"], P[\"rul_hat\"]); ax[1].plot([0, 120], [0, 120], \"k--\"); ax[1].set_title(\"RUL at first alarm: actual vs predicted\"); ax[1].set_xlabel(\"actual (days)\")\n",
"plt.tight_layout(); plt.show()\n",
"\n",
"lead = P[\"rul\"]\n",
"print(\"\\n[์ ๋น ๋ด๋น ์ฅ๊ต ๋ธ๋ฆฌํ ์์ฝ] [๊ต์ก์ฉ๊ฐ์์๋ฃ]\")\n",
"print(f\"1. ์ด์ฉ ๊ฐ์ฒด {len(u3)}๋ ์ธก์ {len(s3)}๊ฑด ์ค ์์ {n_bad}๊ฑด ๊ฒฉ๋ฆฌ, ์ดํ ๊ฐ์ฒด {len(P)}๋์์ ๊ฒฝ๋ณด ๋ฐ๋ น, ๊ณ ์ฅ ์ ํ๋ณด ์๊ฐ ์ค์๊ฐ {lead.median():.0f}์ผ(์ต์ {lead.min()}์ผ)\")\n",
"print(f\"2. ์ด์ ๊ฐ์ง ๋ชจ๋ธ: {'์ ๊ท ์ค์ ์ผ๋ก ๊ต์ฒด' if rt['๊ต์ฒด'] else '๊ธฐ์กด ์ ์ง'}(๊ฒ์ฆ F1 {max(rt['์ฑํผ์ธ F1'], rt['๋์ ์ F1']):.2f}), ์ฌ๋ณด์ ์๊ณ ์ ์ฉ์ผ๋ก ์ ์ ๊ฐ์ฒด ์ค๊ฒฝ๋ณด {int((Dop['alarm'] & (Dop['deg'] == 0)).sum())}๊ฑด\")\n",
"print(f\"3. ์กฐ๊ธฐ ์
๊ณ ์ ์ {int((P['status'] == 'PENDING').sum())}๊ฑด์ ๊ฒ์ฌ๊ด ์น์ธ ๋๊ธฐ(์๋ ์น์ธ 0๊ฑด), ์์ธก ์์กด {CONFIG['approve_rul']}์ผ ์ด๊ณผ {int((P['status'] == '๊ด์ฐฐ').sum())}๊ฑด์ ๊ด์ฐฐ ์ ์ง\")\n",
"print(\"4. ์๋ฆฌ๋ถ์ ์์ ์๊ณ (์์ธก ์์กด \" + str(CONFIG[\"spare_horizon\"]) + \"์ผ ์ด๋ด ๊ฐ์ฒด์ ์์ฌ ๋ถํ): \" + \", \".join(f\"{k} {v}๊ฑด\" for k, v in need[\"part\"].value_counts().items())\n",
" + f\" (์ฒซ ๊ฒฝ๋ณด ์์ ์์ฌ ๋ถํ ์ ํ๋ {(P['part'] == P['true_part']).mean():.0%})\")\n",
"print(f\"5. RUL ์์ธก: ๊ฒฝ๋ณด ์์ ํ๊ท ์ ๋์ค์ฐจ {np.mean(np.abs(P['rul_hat'] - np.minimum(P['rul'], 120))):.1f}์ผ\")\n",
"print(\"6. ๊ฑด์: ๊ตฌ๋ ๊ธฐ์ดยท์ฃผ ๋ฒ ์ด๋ง ์ธํธ ์ฐ์ ํ๋ณด(์๋ชจ ๋น์ฉ ์์), ๊ตญ๋ฐฉ ๊ตฐ์ ์ ๋ณด์ฒด๊ณ์ ์ ๋น ์ด๋ ฅยท์๋ฆฌ๋ถ์ ์๋ชจ ํ์ค ํ๋ ์ฐ๋ ์ธํฐํ์ด์ค ์ ์\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
10 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ฌํ์ต: {'์ฑํผ์ธ F1': 0.868, '๋์ ์ F1': 0.837, '๊ต์ฒด': False} -> ์ฑํผ์ธ ์ ์ง\n",
"์ด์ฉ ์ธก์ 7695๊ฑด: ์ฒ๋ฆฌ 7693๊ฑด, ๊ฒฉ๋ฆฌ 2๊ฑด, ๊ฒฝ๋ณด ์ธก์ 2562๊ฑด, RUL ์ถ์ฒ '๋ชจ๋ธ', ์ ์ฒด ์คํ 20.6์ด\n",
"\n",
"์์ฒด ์ ๊ฒ 8/8 ํต๊ณผ, ์๋ ์ฌํ์ต ํ์ดํ๋ผ์ธ ์ ์ ์คํ\n",
"์ด์ ๋ฌธ์ 7๊ฐ ์ฅ ์์ฑ๋ฅ 100% (๋ชฉํ 100%: ๋ฌ์ฑ) - ๋ชฉ์ ๊ณผ ๋ฒ์, ์
๋ ฅ ๋ฐ์ดํฐ, ์ผ์ผ ์ด์ฉ ์ ์ฐจ, ์น์ธ(HITL) ๊ธฐ์ค, ์ฌํ์ต ์ ์ฐจ, ์ฅ์ ๋์, ์ฐ๋ ์ธํฐํ์ด์ค\n",
"\n",
"[์ ๋น ๋ด๋น ์ฅ๊ต ๋ธ๋ฆฌํ ์์ฝ] [๊ต์ก์ฉ๊ฐ์์๋ฃ]\n",
"1. ์ด์ฉ ๊ฐ์ฒด 48๋ ์ธก์ 7695๊ฑด ์ค ์์ 2๊ฑด ๊ฒฉ๋ฆฌ, ์ดํ ๊ฐ์ฒด 48๋์์ ๊ฒฝ๋ณด ๋ฐ๋ น, ๊ณ ์ฅ ์ ํ๋ณด ์๊ฐ ์ค์๊ฐ 66์ผ(์ต์ 37์ผ)\n",
"2. ์ด์ ๊ฐ์ง ๋ชจ๋ธ: ๊ธฐ์กด ์ ์ง(๊ฒ์ฆ F1 0.87), ์ฌ๋ณด์ ์๊ณ ์ ์ฉ์ผ๋ก ์ ์ ๊ฐ์ฒด ์ค๊ฒฝ๋ณด 23๊ฑด\n",
"3. ์กฐ๊ธฐ ์
๊ณ ์ ์ 31๊ฑด์ ๊ฒ์ฌ๊ด ์น์ธ ๋๊ธฐ(์๋ ์น์ธ 0๊ฑด), ์์ธก ์์กด 90์ผ ์ด๊ณผ 17๊ฑด์ ๊ด์ฐฐ ์ ์ง\n",
"4. ์๋ฆฌ๋ถ์ ์์ ์๊ณ (์์ธก ์์กด 60์ผ ์ด๋ด ๊ฐ์ฒด์ ์์ฌ ๋ถํ): ๊ตฌ๋ ๊ธฐ์ด ์ธํธ 6๊ฑด, ์ฃผ ๋ฒ ์ด๋ง ์ธํธ 3๊ฑด, ๋๊ฐ ํฌ ๋ชจํฐ 3๊ฑด, ์ค์ผ ํํ 2๊ฑด (์ฒซ ๊ฒฝ๋ณด ์์ ์์ฌ ๋ถํ ์ ํ๋ 52%)\n",
"5. RUL ์์ธก: ๊ฒฝ๋ณด ์์ ํ๊ท ์ ๋์ค์ฐจ 10.0์ผ\n",
"6. ๊ฑด์: ๊ตฌ๋ ๊ธฐ์ดยท์ฃผ ๋ฒ ์ด๋ง ์ธํธ ์ฐ์ ํ๋ณด(์๋ชจ ๋น์ฉ ์์), ๊ตญ๋ฐฉ ๊ตฐ์ ์ ๋ณด์ฒด๊ณ์ ์ ๋น ์ด๋ ฅยท์๋ฆฌ๋ถ์ ์๋ชจ ํ์ค ํ๋ ์ฐ๋ ์ธํฐํ์ด์ค ์ ์\n",
"```\n",
" \n"
]
}
],
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}