{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# ๐ฏ [WEL03] ๊ฐ์ ์์ง๊ฒ์ฌ ๋ฐ์ดํฐ ์ํ ์ ํธ ๋์๋ณด๋\n",
"### Google Colab ์ค์ต ํ๊ฒฝ (์ฅ๋ณ ๋ณต์งยท์ง์ / Level 2)\n",
"\n",
"[](https://colab.research.google.com/github/edunlab/milicontent2/blob/main/uploads/WEL03/WEL03_solution.ipynb)\n",
"\n",
"---\n",
"\n",
"## ๐ 1. ํ๋ก์ ํธ ๊ฐ์ ๋ฐ ๋ฐฐ๊ฒฝ\n",
"\n",
"| ํญ๋ชฉ | ๋ด์ฉ |\n",
"| :--- | :--- |\n",
"| **๊ณผ์ ๋ถ๋ฅ** | ์ฅ๋ณ ๋ณต์งยท์ง์ (Level 2) |\n",
"| **ํ์ต ์์ ์๊ฐ** | 10๊ฐ ๋ฏธ์
(5์ผ / ์ด 27.5์๊ฐ) |\n",
"| **ํ๊ฒฝ ์ฌ์** | Python 3.10+, numpy, pandas, matplotlib, scipy, scikit-learn |\n",
"\n",
"### ๐ ํ๋ก์ ํธ ๋ชฉํ\n",
"์ ์ฅ์์๋ ๊ธ์์ฐจ, ์ ์ ์ฅ๋น, ์ฐ๋ฌผ, ํ์ฒ ์ทจ์์ ๋ฑ ๊ธ์์์ ์์ง ์ด์์ ๋นจ๋ฆฌ ์์์ฑ์ง ๋ชปํ๋ฉด ์ฅ๋ณ์ ๊ฑด๊ฐ๊ณผ ๋ถ๋ ํ๋์ ๋ฐ๋ก ์ํฅ์ ์ฃผ์ง๋ง, ์ผ์ ๊ฐ์ ์ก์๊ณผ ๊ณ ์ฅ์ด ๋ง์ ์ค๊ฒฝ๋ณด๋ก ๋ถํ์ํ๊ฒ ๊ธ์๋ฅผ ๋๋ ์ผ๋ ์๊ธด๋ค. ๋ฌ์์-์ฐํฌ๋ผ์ด๋ ์ ์ ์ฌ๋ก์์ ํ์ต ์ด์ ์ผ๋ก ์ผ์ ์ ์ฅ ์์ยท๊ฑด๊ฐ ์ํ ๊ด๋ฆฌ๋ฅผ ๋ฐ์ํด, ๋ณธ ํ๋ก์ ํธ๋ ๊ฐ์ ์์ง๊ฒ์ฌ ๋ฐ์ดํฐ๋ฅผ ๋ถ์ํด ์ํ ์ ํธ๋ฅผ ๋ฐ์ดํฐ๋ก ํ๋จํ๋ ๊ณผ์ ๋ฅผ ๋ค๋ฃฌ๋ค. ๊ต์ก์ฉ ๊ฐ์ ๊ธ์์ 8๊ณณ์ pH, ํ๋, ์๋ฅ์ผ์, ์ค๊ธ์, ๋์ฅ๊ท ์ธก์ ์๋ฃ๋ฅผ ์ฝ๋๋ก ๋ง๋ค์ด ์คํค๋ง ์ ์, ์ผ์ ๊ฒฐ์ธกยท์ด์์น ์ ์ , ๊ธฐ์ค์น Baseline ํ์ , ๋ณตํฉ ์ค์ผ ์งํ ๋ถ๋ฅ ๋ชจ๋ธ, ์ ๋ขฐ๋ ์ง์์ ์ค๊ฒฝ๋ณด ๊ฐ์ถ, ์ํฉํ ๋์๋ณด๋, ๊ธ์ ์ค๋จ 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. ์ผ์ ๊ธ์์ ๊ฐ์ ์์ง๊ฒ์ฌ ํญ๋ชฉ(pH, ํ๋, ์๋ฅ์ผ์, ์ค๊ธ์, ๋์ฅ๊ท ) ๋ฐ์ดํฐ์
์ ์\n",
"**์์์๊ฐ**: ์ฝ 1.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ผ์ ๊ธ์์ ๊ฐ์ ์์ง๊ฒ์ฌ ํญ๋ชฉ(pH, ํ๋, ์๋ฅ์ผ์, ์ค๊ธ์) ๋ฐ์ดํฐ์
์ ์ ์ํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ผ์ ๊ธ์์ ๊ฐ์ ์์ง๊ฒ์ฌ ํญ๋ชฉ(pH, ํ๋, ์๋ฅ์ผ์, ์ค๊ธ์, ๋์ฅ๊ท )๊ณผ ์ผ์ยท์ ์ ํ๊ฒฝ ์ ๋ณด์ ์คํค๋ง์ ๋ฐ์ดํฐ ๋ช
์ธ๋ฅผ ์ ์ํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ธก์ ๋ ์ฝ๋๋ฅผ ๊ธ์์ ์ ๋ณด, ์์ง 5ํญ๋ชฉ, ์ผ์ ์ํ, ์ ์ ํ๋ผ๋ฏธํฐ(๊ธฐ์, ์์น ์ค์ฐจ, ํต์ ์ง์ฐ) ๋ค ๋ฌถ์ 15๊ฐ ํ๋๋ก ์ ์ํ๊ณ ์๋ฃํยท๋จ์ยทํ์ฉ ๋ฒ์ยทํ์ ๊ท์น์ ๋๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `data/schema/wel03_input_schema.json ๋ฐ docs/wel03_data_spec.md` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ฅ๋ณ ๋ณต์งยท์ง์ ์ค๋ฌด ๋ฐ ๋ฌ์์-์ฐํฌ๋ผ์ด๋ ์ ์ ์ ์ฅ ํ๊ฒฝ๊ณผ์ ๊ตฐ์ฌ์ ๋ถํฉ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์คํค๋ง ํ๋ ์ ์ ์์ฑ๋ 100%, ์ํ ๋ฐ์ดํฐ ๊ฒ์ฆ ์ค๋ฅ 0๊ฑด\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์
๋ ฅ ๋ฐ์ดํฐ ์ ํจ์ฑ ๊ฒ์ฆ ์คํฌ๋ฆฝํธ ํต๊ณผ ๋ฐ ๋ช
์ธ์ ๋ฌธ์ํ ์๋ฃ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: JSON Schema ๊ฒ์ฆ ๋๊ตฌ ๋ฐ ์ํ ๋ฐ์ดํฐ ํ์ฑ ๋จ์ ํ
์คํธ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์ผ์ ๊ธ์์ ๊ฐ์ ์์ง๊ฒ์ฌ ํญ๋ชฉ(pH, ํ๋, ์๋ฅ์ผ์, ์ค๊ธ์, ๋์ฅ๊ท )๊ณผ ๊ธ์์ยท์ผ์ยท์ ์ ํ๋ผ๋ฏธํฐ๋ฅผ ๋ด์ ์
๋ ฅ ๋ฐ์ดํฐ ์คํค๋ง๋ฅผ ์ ์ํ๊ณ , ๊ฐ์ ์ํ 20๊ฑด๊ณผ ์ค๋ฅ ์ฃผ์
๋ฐ์ดํฐ๋ก ๊ฒ์ฆ๊ธฐ๋ฅผ ์ํํ๋ค.\"\"\"\n",
"import json\n",
"import re\n",
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ผ์ ๊ธ์์ ๊ฐ์ ์์ง ์ผ์ ๋ฐ์ดํฐ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์์นยท๊ฒ์ฌ ๊ฒฐ๊ณผ ์์)\n",
"# ๊ฐ์ ๊ธ์์ 8๊ณณ x 14์ผ x 1์๊ฐ ๊ฐ๊ฒฉ. ์ค์ ์์ง(true_*)ยท์ผ์ ์ธก์ ๊ฐยท์ค์ผ ์ฌ๊ฑดยท์ผ์ ๊ณ ์ฅ ์ ๋ต์ ํจ๊ป ๋ง๋ ๋ค.\n",
"# ๊ธฐ์ค์น๋ ๊ต์ก์ฉ ๊ฐ์ ๊ฐ์ด๋ฉฐ, ์ค์ ์์ฉ ํ์ ์ ๊ณต์ธ ๊ฒ์ฌ ๊ฒฐ๊ณผ์ ์๋ฌด(์๋ฐฉ์ํ) ๋ด๋น ๊ธฐ์ค์ ๋ฐ๋ฅธ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"PARAMS = [\"ph\", \"turbidity\", \"chlorine\", \"metal\", \"ecoli\"]\n",
"SOURCES = [(\"W01\", \"water_truck\", 1.0, 2.0), (\"W02\", \"water_truck\", 3.5, 1.2), (\"W03\", \"purification_unit\", 2.2, 4.1),\n",
" (\"W04\", \"purification_unit\", 5.0, 3.3), (\"W05\", \"well\", 0.8, 5.6), (\"W06\", \"well\", 4.4, 6.0),\n",
" (\"W07\", \"river_intake\", 6.3, 1.8), (\"W08\", \"river_intake\", 6.9, 4.9)]\n",
"BASE = {\"water_truck\": (7.2, 0.25, 0.75, 1.5), \"purification_unit\": (7.0, 0.20, 0.85, 1.2),\n",
" \"well\": (6.8, 0.45, 0.50, 3.0), \"river_intake\": (7.3, 0.55, 0.60, 2.0)} # pH, ํ๋(NTU), ์๋ฅ์ผ์(mg/L), ์ค๊ธ์(ug/L)\n",
"LIMITS = {\"ph\": (5.8, 8.5), \"turbidity\": (1.0, 2.0), \"chlorine\": (0.2, 0.1), \"metal\": (5.0, 10.0), \"ecoli\": (1, 1)} # (์ฃผ์, ์ํ) ๊ฐ์ ๊ธฐ์ค\n",
"EVENT_TYPES = { # ์ค์ผ ์ฌ๊ฑด: ๋ณ์๋ณ ์ต๋ ๋ณํ๋, ์ต๋์น ๋๋ฌ ์๊ฐ(h)\n",
" \"sewage\": ({\"turbidity\": 2.8, \"chlorine\": -0.5, \"ecoli\": 30, \"ph\": -0.3}, 4),\n",
" \"metal\": ({\"metal\": 22.0, \"ph\": -0.5}, 4),\n",
" \"disinfect_fail\": ({\"chlorine\": -0.8, \"ecoli\": 6}, 6),\n",
" \"chemical\": ({\"ph\": 1.7, \"turbidity\": 1.0}, 3),\n",
" \"creeping_metal\": ({\"metal\": 13.0}, 18),\n",
"}\n",
"FAULT_TYPES = [\"stuck\", \"spike\", \"drift\", \"dropout\"]\n",
"NOISE = {\"ph\": 0.04, \"turbidity\": 0.06, \"chlorine\": 0.03, \"metal\": 0.35}\n",
"DIGITS = {\"ph\": 2, \"turbidity\": 2, \"chlorine\": 2, \"metal\": 1}\n",
"\n",
"\n",
"def make_dataset(days=14, seed=7, n_events=6, n_faults=10, events=None, faults=None, sources=None):\n",
" \"\"\"events: [(๊ธ์์ ๋ฒํธ, ์ฌ๊ฑด ์ ํ, ์์ ์๊ฐ h, ์ง์ h, ๊ฐ๋)], faults: [(๊ธ์์ ๋ฒํธ, ๊ณ ์ฅ ์ ํ, ํญ๋ชฉ, ์์ h, ์ง์ h)]\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" sources = sources or SOURCES\n",
" T = days * 24\n",
" tt = np.arange(T)\n",
" hour = tt % 24\n",
" night = (hour < 6) | (hour >= 20)\n",
" rain = np.zeros(T)\n",
" for _ in range(max(1, days // 4)):\n",
" s0, d = int(rng.integers(0, T - 14)), int(rng.integers(6, 14))\n",
" rain[s0:s0 + d] = np.maximum(rain[s0:s0 + d], rng.uniform(0.6, 1.0))\n",
" if events is None:\n",
" events = [(int(rng.integers(len(sources))), str(rng.choice(list(EVENT_TYPES))), int(rng.integers(min(24, T // 4), max(T // 4 + 1, T - 48))),\n",
" int(rng.integers(10, 30)), 1.0) for _ in range(n_events)]\n",
" if faults is None:\n",
" faults = [(int(rng.integers(len(sources))), str(rng.choice(FAULT_TYPES)), str(rng.choice([\"ph\", \"turbidity\", \"chlorine\", \"metal\"])),\n",
" int(rng.integers(12, max(13, T - 30))), int(rng.integers(6, 24))) for _ in range(n_faults)]\n",
" out = []\n",
" for si, (sid, stype, x, y) in enumerate(sources):\n",
" ph0, tb0, cl0, mt0 = BASE[stype]\n",
" tr = {\"ph\": ph0 + 0.08 * np.sin(2 * np.pi * hour / 24) + rng.normal(0, 0.03, T),\n",
" \"turbidity\": tb0 * (1 + 0.1 * rng.normal(0, 1, T)),\n",
" \"chlorine\": (cl0 * np.exp(-0.022 * hour) if stype == \"water_truck\" else cl0 + rng.normal(0, 0.03, T)),\n",
" \"metal\": mt0 + rng.normal(0, 0.2, T), \"ecoli\": np.zeros(T)}\n",
" if stype in (\"river_intake\", \"well\"):\n",
" tr[\"turbidity\"] = tr[\"turbidity\"] + rain * (0.9 if stype == \"river_intake\" else 0.45)\n",
" f_all, etype = np.zeros(T), np.array([\"none\"] * T, dtype=object)\n",
" for (s_i, et, onset, dur, scale) in events:\n",
" if s_i != si:\n",
" continue\n",
" delta, ramp = EVENT_TYPES[et]\n",
" f = np.clip((tt - onset + 1) / ramp, 0, 1) * ((tt >= onset) & (tt < onset + dur + ramp))\n",
" for k, v in delta.items():\n",
" tr[k] = tr[k] + v * f * scale\n",
" etype[f > 0] = et\n",
" f_all = np.maximum(f_all, f)\n",
" tr[\"chlorine\"] = np.clip(tr[\"chlorine\"], 0.02, None)\n",
" tr[\"turbidity\"] = np.clip(tr[\"turbidity\"], 0.05, None)\n",
" tr[\"metal\"] = np.clip(tr[\"metal\"], 0.1, None)\n",
" grade = np.where(f_all >= 0.2, 2, 0)\n",
" mild = (tr[\"turbidity\"] > LIMITS[\"turbidity\"][0]) | (tr[\"chlorine\"] < LIMITS[\"chlorine\"][0]) | (tr[\"metal\"] > LIMITS[\"metal\"][0])\n",
" grade = np.where((grade == 0) & mild, 1, grade)\n",
" ms = {}\n",
" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n",
" sd = NOISE[p] * np.where(night, 1.5, 1.0) * (tr[p] if p == \"turbidity\" else 1)\n",
" ms[p] = np.round(np.clip(tr[p] + rng.normal(0, 1, T) * sd, 0, None), DIGITS[p])\n",
" ms[\"ecoli\"] = (rng.poisson(tr[\"ecoli\"]) + (rng.random(T) < 0.004)).astype(float)\n",
" fault, fparam = np.array([\"none\"] * T, dtype=object), np.array([\"-\"] * T, dtype=object)\n",
" for (s_i, ft, p, s0, d) in faults:\n",
" if s_i != si:\n",
" continue\n",
" seg = slice(s0, min(T, s0 + d))\n",
" if ft == \"stuck\":\n",
" ms[p][seg] = ms[p][s0]\n",
" elif ft == \"spike\":\n",
" seg = slice(s0, s0 + 1)\n",
" ms[p][s0] = {\"ph\": 11.8, \"turbidity\": 38.0, \"chlorine\": 0.0, \"metal\": 75.0}[p]\n",
" elif ft == \"drift\":\n",
" ramp = np.linspace(0, 1, seg.stop - s0)\n",
" ms[p][seg] = np.clip(ms[p][seg] + ramp * {\"ph\": -1.6, \"turbidity\": 2.6, \"chlorine\": -0.75, \"metal\": 12.0}[p], 0, None).round(DIGITS[p])\n",
" elif ft == \"dropout\":\n",
" for q in PARAMS:\n",
" ms[q][seg] = np.nan\n",
" p = \"all\"\n",
" fault[seg], fparam[seg] = ft, p\n",
" df = pd.DataFrame({\"source_id\": sid, \"source_type\": stype, \"x_km\": x, \"y_km\": y, \"t\": tt,\n",
" \"cond\": np.where(rain > 0, \"storm\", np.where(night, \"night\", \"day\")),\n",
" **{p: ms[p] for p in PARAMS}, **{f\"true_{p}\": tr[p] for p in PARAMS},\n",
" \"event\": etype, \"fault\": fault, \"fault_param\": fparam, \"grade\": grade})\n",
" out.append(df)\n",
" return pd.concat(out, ignore_index=True)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# 1) ์คํค๋ง ์ ์: ๊ธ์์ ์ ๋ณด, ์์ง ์ธก์ ๊ฐ(5ํญ๋ชฉ), ์ผ์ ์ํ, ์ ์ ํ๋ผ๋ฏธํฐ(๊ธฐ์ยท์์น์ค์ฐจยทํต์ ์ง์ฐ)\n",
"SCHEMA = {\n",
" \"required\": [\"record_id\", \"source\", \"measured_at\", \"quality\", \"sensor\", \"tactical\"],\n",
" \"fields\": {\n",
" \"record_id\": {\"type\": str, \"pattern\": r\"^WQ-W\\d{2}-\\d{4}$\"},\n",
" \"measured_at\": {\"type\": str, \"pattern\": r\"^D\\+\\d{2} \\d{2}:00$\"},\n",
" },\n",
" \"source\": {\n",
" \"source_id\": {\"type\": str, \"pattern\": r\"^W\\d{2}$\"},\n",
" \"source_type\": {\"type\": str, \"enum\": [\"water_truck\", \"purification_unit\", \"well\", \"river_intake\"]},\n",
" \"grid_km\": {\"type\": list, \"length\": 2},\n",
" },\n",
" \"quality\": { # ๋จ์์ ๋ฌผ๋ฆฌ์ ์ผ๋ก ๊ฐ๋ฅํ ๋ฒ์(๊ธฐ์ค์น๊ฐ ์๋๋ผ ์ธก์ ๊ฐ๋ฅ ๋ฒ์)\n",
" \"ph\": {\"type\": (int, float), \"min\": 0, \"max\": 14, \"unit\": \"-\"},\n",
" \"turbidity_ntu\": {\"type\": (int, float), \"min\": 0, \"max\": 1000, \"unit\": \"NTU\"},\n",
" \"chlorine_mgL\": {\"type\": (int, float), \"min\": 0, \"max\": 10, \"unit\": \"mg/L\"},\n",
" \"metal_ugL\": {\"type\": (int, float), \"min\": 0, \"max\": 1000, \"unit\": \"ug/L\"},\n",
" \"ecoli_cfu\": {\"type\": int, \"min\": 0, \"max\": 100000, \"unit\": \"CFU/100mL\"},\n",
" },\n",
" \"sensor\": {\n",
" \"status\": {\"type\": str, \"enum\": [\"ok\", \"calibration_due\", \"fault\", \"offline\"]},\n",
" \"last_calibrated_h\": {\"type\": int, \"min\": 0, \"max\": 720},\n",
" },\n",
" \"tactical\": {\n",
" \"weather\": {\"type\": str, \"enum\": [\"clear\", \"rain\", \"night\", \"snow\", \"dust\"]},\n",
" \"position_error_m\": {\"type\": (int, float), \"min\": 0, \"max\": 500},\n",
" \"comm_delay_ms\": {\"type\": (int, float), \"min\": 0, \"max\": 10000},\n",
" },\n",
"}\n",
"\n",
"\n",
"def check(name, value, rule):\n",
" \"\"\"๊ฐ ํ๋๋ฅผ ๊ท์น(type, enum, min, max, length, pattern)์ผ๋ก ๊ฒ์ฌํด ์ค๋ฅ ๋ชฉ๋ก์ ๋๋ ค์ค๋ค.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 1] ์ผ์ ๊ธ์์ ๊ฐ์ ์์ง๊ฒ์ฌ ํญ๋ชฉ(pH, ํ๋, ์๋ฅ์ผ์, ์ค๊ธ์, ๋์ฅ๊ท ) ๋ฐ์ดํฐ์
์ ์ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def validate(rec):\n",
" # [TODO : ํ์ ์ค์ต 1] ์ผ์ ๊ธ์์ ๊ฐ์ ์์ง๊ฒ์ฌ ํญ๋ชฉ(pH, ํ๋, ์๋ฅ์ผ์, ์ค๊ธ์, ๋์ฅ๊ท ) ๋ฐ์ดํฐ์
์ ์ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"data = make_dataset(days=3, seed=1, n_events=1, n_faults=0)\n",
"rng = np.random.default_rng(1)\n",
"rows = data.sample(20, random_state=1).sort_values([\"source_id\", \"t\"])\n",
"records = []\n",
"for _, r in rows.iterrows():\n",
" records.append({\n",
" \"record_id\": f\"WQ-{r['source_id']}-{int(r['t']):04d}\", \"measured_at\": f\"D+{int(r['t']) // 24:02d} {int(r['t']) % 24:02d}:00\",\n",
" \"source\": {\"source_id\": r[\"source_id\"], \"source_type\": r[\"source_type\"], \"grid_km\": [r[\"x_km\"], r[\"y_km\"]]},\n",
" \"quality\": {\"ph\": float(r[\"ph\"]), \"turbidity_ntu\": float(r[\"turbidity\"]), \"chlorine_mgL\": float(r[\"chlorine\"]),\n",
" \"metal_ugL\": float(r[\"metal\"]), \"ecoli_cfu\": int(r[\"ecoli\"])},\n",
" \"sensor\": {\"status\": \"ok\", \"last_calibrated_h\": int(rng.integers(1, 200))},\n",
" \"tactical\": {\"weather\": {\"storm\": \"rain\", \"night\": \"night\", \"day\": \"clear\"}[r[\"cond\"]],\n",
" \"position_error_m\": float(round(rng.uniform(3, 40), 1)), \"comm_delay_ms\": int(rng.integers(30, 900))}})\n",
"\n",
"ok = sum(1 for r in records if not validate(r))\n",
"print(f\"์ ์ ์ํ {len(records)}๊ฑด ์ค ๊ฒ์ฆ ํต๊ณผ {ok}๊ฑด, ์ค๋ฅ {len(records) - ok}๊ฑด\")\n",
"parsed = json.loads(json.dumps(records, ensure_ascii=False)) # ์ง๋ ฌํ -> ํ์ฑ ์๋ณต ์ํ\n",
"print(\"JSON ์ง๋ ฌํยทํ์ฑ ์๋ณต ํ ๊ฐ ์ผ์น:\", parsed == records)\n",
"\n",
"# 3) ์ค๋ฅ ์ฃผ์
์ํ: ๊ฒ์ฆ๊ธฐ๊ฐ ์๋ชป๋ ๋ฐ์ดํฐ๋ฅผ ์ก์๋ด๋์ง ํ์ธ\n",
"bad = json.loads(json.dumps(records[0]))\n",
"bad[\"record_id\"] = \"WQ-1\" # ํ์ ์ค๋ฅ\n",
"bad[\"quality\"][\"ph\"] = 15.2 # ๋ฌผ๋ฆฌ ๋ฒ์ ์ด๊ณผ\n",
"bad[\"quality\"][\"ecoli_cfu\"] = \"๊ฒ์ถ\" # ์๋ฃํ ์ค๋ฅ(์ซ์์ฌ์ผ ํจ)\n",
"bad[\"source\"][\"source_type\"] = \"bottle\" # ํ์ฉ๋์ง ์์ ๊ธ์์ ์ ํ\n",
"del bad[\"tactical\"][\"comm_delay_ms\"] # ์ ์ ํ๋ผ๋ฏธํฐ ๋๋ฝ\n",
"print(\"\\n[์ค๋ฅ ์ฃผ์
๊ฒฐ๊ณผ]\")\n",
"errs = validate(bad)\n",
"for e in errs:\n",
" print(\" -\", e)\n",
"print(f\"์ฃผ์
์ค๋ฅ 5์ข
์ค ๊ฒ์ถ {len(errs)}์ข
-> {'๋ชจ๋ ๊ฒ์ถ' if len(errs) >= 5 else '๋๋ฝ ์์'}\")\n",
"\n",
"# 4) ๋ฐ์ดํฐ ๋ช
์ธ ์์ฝ(๋ช
์ธ์ ์ด์)\n",
"spec = []\n",
"for g in (\"fields\", \"source\", \"quality\", \"sensor\", \"tactical\"):\n",
" for k, r in SCHEMA[g].items():\n",
" t = r[\"type\"].__name__ if isinstance(r[\"type\"], type) else \"number\"\n",
" spec.append((g, k, t, r.get(\"unit\", \"\"), str({x: y for x, y in r.items() if x not in (\"type\", \"unit\")})))\n",
"print(\"\\n[๋ฐ์ดํฐ ๋ช
์ธ ์์ฝ]\")\n",
"print(pd.DataFrame(spec, columns=[\"๊ทธ๋ฃน\", \"ํ๋\", \"์๋ฃํ\", \"๋จ์\", \"๊ท์น\"]).to_string(index=False))\n",
"done = sum(1 for s in spec if s[2] and s[4] != \"{}\")\n",
"print(f\"์คํค๋ง ํ๋ ์ ์ ์์ฑ๋ {done}/{len(spec)} ({done / len(spec):.0%}): ๋ชจ๋ ํ๋์ ์๋ฃํยท๊ฒ์ฆ ๊ท์น ์ ์\")\n",
"print(\"\\n[๊ฐ์ ํ์ ๊ธฐ์ค(๊ต์ก์ฉ)] (์ฃผ์, ์ํ):\", {k: v for k, v in LIMITS.items()})\n",
"print(\"๊ธ์์ ์ ํ๋ณ ์ํ ์:\", rows[\"source_type\"].value_counts().to_dict())\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
01 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"์ ์ ์ํ 20๊ฑด ์ค ๊ฒ์ฆ ํต๊ณผ 20๊ฑด, ์ค๋ฅ 0๊ฑด\n",
"JSON ์ง๋ ฌํยทํ์ฑ ์๋ณต ํ ๊ฐ ์ผ์น: True\n",
"\n",
"[์ค๋ฅ ์ฃผ์
๊ฒฐ๊ณผ]\n",
" - record_id: ํ์ ๋ถ์ผ์น 'WQ-1'\n",
" - source.source_type: ํ์ฉ๋์ง ์์ ๊ฐ 'bottle'\n",
" - quality.ph: ์ต๋๊ฐ 14 ์ด๊ณผ\n",
" - quality.ecoli_cfu: ์๋ฃํ ์ค๋ฅ(str)\n",
" - tactical.comm_delay_ms: ํ์ ํ๋ ๋๋ฝ\n",
"์ฃผ์
์ค๋ฅ 5์ข
์ค ๊ฒ์ถ 5์ข
-> ๋ชจ๋ ๊ฒ์ถ\n",
"\n",
"[๋ฐ์ดํฐ ๋ช
์ธ ์์ฝ]\n",
" ๊ทธ๋ฃน ํ๋ ์๋ฃํ ๋จ์ ๊ท์น\n",
" fields record_id str {'pattern': '^WQ-W\\\\d{2}-\\\\d{4}$'}\n",
" fields measured_at str {'pattern': '^D\\\\+\\\\d{2} \\\\d{2}:00$'}\n",
" source source_id str {'pattern': '^W\\\\d{2}$'}\n",
" source source_type str {'enum': ['water_truck', 'purification_unit', 'well', 'river_intake']}\n",
" source grid_km list {'length': 2}\n",
" quality ph number - {'min': 0, 'max': 14}\n",
" quality turbidity_ntu number NTU {'min': 0, 'max': 1000}\n",
" quality chlorine_mgL number mg/L {'min': 0, 'max': 10}\n",
" quality metal_ugL number ug/L {'min': 0, 'max': 1000}\n",
" quality ecoli_cfu int CFU/100mL {'min': 0, 'max': 100000}\n",
" sensor status str {'enum': ['ok', 'calibration_due', 'fault', 'offline']}\n",
" sensor last_calibrated_h int {'min': 0, 'max': 720}\n",
"tactical weather str {'enum': ['clear', 'rain', 'night', 'snow', 'dust']}\n",
"tactical position_error_m number {'min': 0, 'max': 500}\n",
"tactical comm_delay_ms number {'min': 0, 'max': 10000}\n",
"์คํค๋ง ํ๋ ์ ์ ์์ฑ๋ 15/15 (100%): ๋ชจ๋ ํ๋์ ์๋ฃํยท๊ฒ์ฆ ๊ท์น ์ ์\n",
"\n",
"[๊ฐ์ ํ์ ๊ธฐ์ค(๊ต์ก์ฉ)] (์ฃผ์, ์ํ): {'ph': (5.8, 8.5), 'turbidity': (1.0, 2.0), 'chlorine': (0.2, 0.1), 'metal': (5.0, 10.0), 'ecoli': (1, 1)}\n",
"๊ธ์์ ์ ํ๋ณ ์ํ ์: {'purification_unit': 7, 'river_intake': 7, 'water_truck': 3, 'well': 3}\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
02. ์์ง ์ผ์ ๊ฒฐ์ธก์น ๋์ฒด ๋ฐ ์๊ณ์ด ์ผ์ ์ด์์น ์ ์ ์ ์ฒ๋ฆฌ ๋ชจ๋ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์์ง ์ผ์ ๊ฒฐ์ธก์น ๋์ฒด ๋ฐ ์๊ณ์ด ์ด์์น ์ ์ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์์ง ์ผ์์ ๊ฒฐ์ธก์น๋ฅผ ๋ณด์ ํ๊ณ ์๊ณ์ด ์ด์์น๋ฅผ ์ ์ ํ๋ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํด ์๋์ ์ ํ๋๋ฅผ ํ์ธํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ธ์์๋ณ ์๊ณ์ด์์ ๋ฌผ๋ฆฌ ๋ฒ์ ๋ฐ ๊ฐ, ๊ฐ์ ๊ฐ์ด ๋ฐ๋ณต๋๋ ๊ณ ์ ๊ฐ(stuck), ํ์๋ค๊ฐ ๋ฐ๋ก ๋์์ค๋ ๋จ๋ฐ ์คํ์ดํฌ๋ฅผ ํ์ยท์ ๊ฑฐํ๊ณ ์งง์ ๊ฒฐ์ธก์ ์ ํ ๋ณด๊ฐ, ๊ธด ๊ฒฐ์ธก์ ํ์ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/preprocess/wel03_preprocessor.py ๋ฐ ์ ์ฒ๋ฆฌ ์ ํ ๋ฐ์ดํฐ์
` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ ์ ํ๊ฒฝ ํน์ ์ ๋
ธ์ด์ฆ(์คํธํ, ๋ฐ์ฌํ, ์ผ์ ๋จ์ ๋ฑ) ์ ๊ฑฐ ํ๋น์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๋ฐ์ดํฐ ์ ์ ์ฒ๋ฆฌ ์๋ 1,000๊ฑด/์ด ์ด์, ์ ์ฒ๋ฆฌ ์ค๋ฅ์จ 0%\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์ ์ ํ์ดํ๋ผ์ธ ์๋ํ ๋ฐ ๋จ์ ํ
์คํธ ์ผ์ด์ค 5์ข
ํต๊ณผ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: pytest ๊ธฐ๋ฐ ์ ์ฒ๋ฆฌ ์ ํฉ์ฑ ํ
์คํธ ๋ฐ ๊ฒฐ์ธก์น ๋ณด์ ์จ ํ์ธ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์์ง ์ผ์ ์๊ณ์ด์ ๋ฌผ๋ฆฌ ๋ฒ์ ๋ฐ ๊ฐยท๊ณ ์ ๊ฐ(stuck)ยท๋จ๋ฐ ์คํ์ดํฌ๋ฅผ ์ ์ ํ๊ณ ๊ฒฐ์ธก์ ๋ณด์ ํ๋ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ ๋ค, ์ฒ๋ฆฌ ์๋ยท์ค๋ฅ์จยท๋ณด์ ์ ํ๋์ ๋จ์ ํ
์คํธ 5์ข
์ ํ์ธํ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ผ์ ๊ธ์์ ๊ฐ์ ์์ง ์ผ์ ๋ฐ์ดํฐ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์์นยท๊ฒ์ฌ ๊ฒฐ๊ณผ ์์)\n",
"# ๊ฐ์ ๊ธ์์ 8๊ณณ x 14์ผ x 1์๊ฐ ๊ฐ๊ฒฉ. ์ค์ ์์ง(true_*)ยท์ผ์ ์ธก์ ๊ฐยท์ค์ผ ์ฌ๊ฑดยท์ผ์ ๊ณ ์ฅ ์ ๋ต์ ํจ๊ป ๋ง๋ ๋ค.\n",
"# ๊ธฐ์ค์น๋ ๊ต์ก์ฉ ๊ฐ์ ๊ฐ์ด๋ฉฐ, ์ค์ ์์ฉ ํ์ ์ ๊ณต์ธ ๊ฒ์ฌ ๊ฒฐ๊ณผ์ ์๋ฌด(์๋ฐฉ์ํ) ๋ด๋น ๊ธฐ์ค์ ๋ฐ๋ฅธ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"PARAMS = [\"ph\", \"turbidity\", \"chlorine\", \"metal\", \"ecoli\"]\n",
"SOURCES = [(\"W01\", \"water_truck\", 1.0, 2.0), (\"W02\", \"water_truck\", 3.5, 1.2), (\"W03\", \"purification_unit\", 2.2, 4.1),\n",
" (\"W04\", \"purification_unit\", 5.0, 3.3), (\"W05\", \"well\", 0.8, 5.6), (\"W06\", \"well\", 4.4, 6.0),\n",
" (\"W07\", \"river_intake\", 6.3, 1.8), (\"W08\", \"river_intake\", 6.9, 4.9)]\n",
"BASE = {\"water_truck\": (7.2, 0.25, 0.75, 1.5), \"purification_unit\": (7.0, 0.20, 0.85, 1.2),\n",
" \"well\": (6.8, 0.45, 0.50, 3.0), \"river_intake\": (7.3, 0.55, 0.60, 2.0)} # pH, ํ๋(NTU), ์๋ฅ์ผ์(mg/L), ์ค๊ธ์(ug/L)\n",
"LIMITS = {\"ph\": (5.8, 8.5), \"turbidity\": (1.0, 2.0), \"chlorine\": (0.2, 0.1), \"metal\": (5.0, 10.0), \"ecoli\": (1, 1)} # (์ฃผ์, ์ํ) ๊ฐ์ ๊ธฐ์ค\n",
"EVENT_TYPES = { # ์ค์ผ ์ฌ๊ฑด: ๋ณ์๋ณ ์ต๋ ๋ณํ๋, ์ต๋์น ๋๋ฌ ์๊ฐ(h)\n",
" \"sewage\": ({\"turbidity\": 2.8, \"chlorine\": -0.5, \"ecoli\": 30, \"ph\": -0.3}, 4),\n",
" \"metal\": ({\"metal\": 22.0, \"ph\": -0.5}, 4),\n",
" \"disinfect_fail\": ({\"chlorine\": -0.8, \"ecoli\": 6}, 6),\n",
" \"chemical\": ({\"ph\": 1.7, \"turbidity\": 1.0}, 3),\n",
" \"creeping_metal\": ({\"metal\": 13.0}, 18),\n",
"}\n",
"FAULT_TYPES = [\"stuck\", \"spike\", \"drift\", \"dropout\"]\n",
"NOISE = {\"ph\": 0.04, \"turbidity\": 0.06, \"chlorine\": 0.03, \"metal\": 0.35}\n",
"DIGITS = {\"ph\": 2, \"turbidity\": 2, \"chlorine\": 2, \"metal\": 1}\n",
"\n",
"\n",
"def make_dataset(days=14, seed=7, n_events=6, n_faults=10, events=None, faults=None, sources=None):\n",
" \"\"\"events: [(๊ธ์์ ๋ฒํธ, ์ฌ๊ฑด ์ ํ, ์์ ์๊ฐ h, ์ง์ h, ๊ฐ๋)], faults: [(๊ธ์์ ๋ฒํธ, ๊ณ ์ฅ ์ ํ, ํญ๋ชฉ, ์์ h, ์ง์ h)]\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" sources = sources or SOURCES\n",
" T = days * 24\n",
" tt = np.arange(T)\n",
" hour = tt % 24\n",
" night = (hour < 6) | (hour >= 20)\n",
" rain = np.zeros(T)\n",
" for _ in range(max(1, days // 4)):\n",
" s0, d = int(rng.integers(0, T - 14)), int(rng.integers(6, 14))\n",
" rain[s0:s0 + d] = np.maximum(rain[s0:s0 + d], rng.uniform(0.6, 1.0))\n",
" if events is None:\n",
" events = [(int(rng.integers(len(sources))), str(rng.choice(list(EVENT_TYPES))), int(rng.integers(min(24, T // 4), max(T // 4 + 1, T - 48))),\n",
" int(rng.integers(10, 30)), 1.0) for _ in range(n_events)]\n",
" if faults is None:\n",
" faults = [(int(rng.integers(len(sources))), str(rng.choice(FAULT_TYPES)), str(rng.choice([\"ph\", \"turbidity\", \"chlorine\", \"metal\"])),\n",
" int(rng.integers(12, max(13, T - 30))), int(rng.integers(6, 24))) for _ in range(n_faults)]\n",
" out = []\n",
" for si, (sid, stype, x, y) in enumerate(sources):\n",
" ph0, tb0, cl0, mt0 = BASE[stype]\n",
" tr = {\"ph\": ph0 + 0.08 * np.sin(2 * np.pi * hour / 24) + rng.normal(0, 0.03, T),\n",
" \"turbidity\": tb0 * (1 + 0.1 * rng.normal(0, 1, T)),\n",
" \"chlorine\": (cl0 * np.exp(-0.022 * hour) if stype == \"water_truck\" else cl0 + rng.normal(0, 0.03, T)),\n",
" \"metal\": mt0 + rng.normal(0, 0.2, T), \"ecoli\": np.zeros(T)}\n",
" if stype in (\"river_intake\", \"well\"):\n",
" tr[\"turbidity\"] = tr[\"turbidity\"] + rain * (0.9 if stype == \"river_intake\" else 0.45)\n",
" f_all, etype = np.zeros(T), np.array([\"none\"] * T, dtype=object)\n",
" for (s_i, et, onset, dur, scale) in events:\n",
" if s_i != si:\n",
" continue\n",
" delta, ramp = EVENT_TYPES[et]\n",
" f = np.clip((tt - onset + 1) / ramp, 0, 1) * ((tt >= onset) & (tt < onset + dur + ramp))\n",
" for k, v in delta.items():\n",
" tr[k] = tr[k] + v * f * scale\n",
" etype[f > 0] = et\n",
" f_all = np.maximum(f_all, f)\n",
" tr[\"chlorine\"] = np.clip(tr[\"chlorine\"], 0.02, None)\n",
" tr[\"turbidity\"] = np.clip(tr[\"turbidity\"], 0.05, None)\n",
" tr[\"metal\"] = np.clip(tr[\"metal\"], 0.1, None)\n",
" grade = np.where(f_all >= 0.2, 2, 0)\n",
" mild = (tr[\"turbidity\"] > LIMITS[\"turbidity\"][0]) | (tr[\"chlorine\"] < LIMITS[\"chlorine\"][0]) | (tr[\"metal\"] > LIMITS[\"metal\"][0])\n",
" grade = np.where((grade == 0) & mild, 1, grade)\n",
" ms = {}\n",
" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n",
" sd = NOISE[p] * np.where(night, 1.5, 1.0) * (tr[p] if p == \"turbidity\" else 1)\n",
" ms[p] = np.round(np.clip(tr[p] + rng.normal(0, 1, T) * sd, 0, None), DIGITS[p])\n",
" ms[\"ecoli\"] = (rng.poisson(tr[\"ecoli\"]) + (rng.random(T) < 0.004)).astype(float)\n",
" fault, fparam = np.array([\"none\"] * T, dtype=object), np.array([\"-\"] * T, dtype=object)\n",
" for (s_i, ft, p, s0, d) in faults:\n",
" if s_i != si:\n",
" continue\n",
" seg = slice(s0, min(T, s0 + d))\n",
" if ft == \"stuck\":\n",
" ms[p][seg] = ms[p][s0]\n",
" elif ft == \"spike\":\n",
" seg = slice(s0, s0 + 1)\n",
" ms[p][s0] = {\"ph\": 11.8, \"turbidity\": 38.0, \"chlorine\": 0.0, \"metal\": 75.0}[p]\n",
" elif ft == \"drift\":\n",
" ramp = np.linspace(0, 1, seg.stop - s0)\n",
" ms[p][seg] = np.clip(ms[p][seg] + ramp * {\"ph\": -1.6, \"turbidity\": 2.6, \"chlorine\": -0.75, \"metal\": 12.0}[p], 0, None).round(DIGITS[p])\n",
" elif ft == \"dropout\":\n",
" for q in PARAMS:\n",
" ms[q][seg] = np.nan\n",
" p = \"all\"\n",
" fault[seg], fparam[seg] = ft, p\n",
" df = pd.DataFrame({\"source_id\": sid, \"source_type\": stype, \"x_km\": x, \"y_km\": y, \"t\": tt,\n",
" \"cond\": np.where(rain > 0, \"storm\", np.where(night, \"night\", \"day\")),\n",
" **{p: ms[p] for p in PARAMS}, **{f\"true_{p}\": tr[p] for p in PARAMS},\n",
" \"event\": etype, \"fault\": fault, \"fault_param\": fparam, \"grade\": grade})\n",
" out.append(df)\n",
" return pd.concat(out, ignore_index=True)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌ ๊ณตํต ํจ์: ๋ฌผ๋ฆฌ ๋ฒ์ ์ ๊ฒ -> ๊ณ ์ ๊ฐ(stuck) ํ์ -> ๋จ๋ฐ ์คํ์ดํฌ ์ ๊ฑฐ -> ๊ฒฐ์ธก ๋ณด์ ----\n",
"PHYS = {\"ph\": (0, 14), \"turbidity\": (0, 1000), \"chlorine\": (0, 10), \"metal\": (0, 1000), \"ecoli\": (0, 100000)}\n",
"SPIKE_K = {\"day\": 6.0, \"night\": 7.0, \"storm\": 9.0} # ์ํฉ๋ณ(์ฃผ๊ฐ/์ผ๊ฐ/์
์ฒํ) ์คํ์ดํฌ ํ์ ๋ฐฐ์\n",
"MIN_JUMP = {\"ph\": 0.8, \"turbidity\": 1.5, \"chlorine\": 0.3, \"metal\": 6.0}\n",
"STUCK_RUN, SHORT_GAP = 6, 3\n",
"\n",
"\n",
"def clean(df, spike_k=None, min_jump=None):\n",
" \"\"\"๊ธ์์๋ณ ์๊ณ์ด์ ์ ์ ํ๊ณ ํญ๋ชฉ๋ณ ์ฒ๋ฆฌ ๋ด์ญ(<ํญ๋ชฉ>_flag: range/stuck/spike/interp/long_gap)์ ๋จ๊ธด๋ค.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 2] ์์ง ์ผ์ ๊ฒฐ์ธก์น ๋์ฒด ๋ฐ ์๊ณ์ด ์ผ์ ์ด์์น ์ ์ ์ ์ฒ๋ฆฌ ๋ชจ๋ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"data = make_dataset(days=14, seed=7, n_events=8, n_faults=16)\n",
"\n",
"# 1) ์ฒ๋ฆฌ ์๋์ ์ค๋ฅ์จ\n",
"t0 = time.perf_counter()\n",
"c = clean(data)\n",
"dt = time.perf_counter() - t0\n",
"remain = int(c[PARAMS].isna().sum().sum())\n",
"print(f\"์ ์ฒ๋ฆฌ {len(data):,}๊ฑด, {len(data) / dt:,.0f}๊ฑด/์ด, ์ฒ๋ฆฌ ํ ๋จ์ ๊ฒฐ์ธก {remain}๊ฑด, ์ค๋ฅ์จ {remain / len(data):.1%}\")\n",
"\n",
"# 2) ์ ์ฒ๋ฆฌ ์ ํ ๋น๊ต: ์ผ์ ๊ณ ์ฅ ์ ํ๋ณ๋ก ๋ฌด์์ด ์ฒ๋ฆฌ๋์๋๊ฐ\n",
"flag_cols = [f\"{p}_flag\" for p in PARAMS]\n",
"c[\"์ฒ๋ฆฌ๋ด์ญ\"] = c[flag_cols].apply(lambda r: \",\".join(sorted({x for x in r if x})) or \"-\", axis=1)\n",
"print(\"\\n[์ค์ ๊ณ ์ฅ ์ ํ x ์ ์ฒ๋ฆฌ ์ฒ๋ฆฌ ๋ด์ญ] (ํ ์)\")\n",
"print(pd.crosstab(c[\"fault\"], c[\"์ฒ๋ฆฌ๋ด์ญ\"]).to_string())\n",
"print(f\"๊ฒฐ์ธก ์
์: ์ ์ฒ๋ฆฌ ์ {int(data[PARAMS].isna().sum().sum())} -> ํ {remain}\")\n",
"\n",
"# 3) ๊ฒฐ์ธก ๋ณด์ ์ ํ๋: ์ ์ ๊ตฌ๊ฐ์์ ๊ฐ์ ์ผ๋ถ ์ง์ ๋ณด๊ณ , ์ค์ ๊ฐ๊ณผ ๋น๊ต\n",
"rng = np.random.default_rng(3)\n",
"test = data.copy()\n",
"normal_idx = test.index[(test[\"fault\"] == \"none\") & (test[\"event\"] == \"none\")]\n",
"holes = rng.choice(normal_idx, 120, replace=False)\n",
"test.loc[holes, \"chlorine\"] = np.nan\n",
"filled = clean(test).set_index([\"source_id\", \"t\"])\n",
"truth = data.loc[holes].set_index([\"source_id\", \"t\"])[\"true_chlorine\"]\n",
"mae = (filled.loc[truth.index, \"chlorine\"] - truth).abs().mean()\n",
"mean_fill = (data[\"chlorine\"].mean() - truth).abs().mean()\n",
"print(f\"\\n์๋ฅ์ผ์ ๊ฒฐ์ธก 120๊ฑด ๋ณด์ ์ค์ฐจ(MAE): ์ ํ ๋ณด๊ฐ {mae:.3f} mg/L vs ์ ์ฒด ํ๊ท ๋์ฒด {mean_fill:.3f} mg/L\")\n",
"\n",
"# 4) ๋จ์ ํ
์คํธ 5์ข
\n",
"def unit_tests():\n",
" # [TODO : ํ์ ์ค์ต 2] ์์ง ์ผ์ ๊ฒฐ์ธก์น ๋์ฒด ๋ฐ ์๊ณ์ด ์ผ์ ์ด์์น ์ ์ ์ ์ฒ๋ฆฌ ๋ชจ๋ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"res = unit_tests()\n",
"print(\"\\n[๋จ์ ํ
์คํธ]\")\n",
"for k, v in res.items():\n",
" print(f\" [{'PASS' if v else 'FAIL'}] {k}\")\n",
"print(f\"๋จ์ ํ
์คํธ {sum(res.values())}/5 ํต๊ณผ\")\n",
"\n",
"# 5) ์ํฉ๋ณ(์ฃผ๊ฐ/์ผ๊ฐ/์
์ฒํ) ์๊ณ๊ฐ ๋์ ์กฐ์ ํจ๊ณผ: ์ต์ ๋ณํํญ ํํ์ ๋๊ณ ๋ฐฐ์๋ง ๋น๊ต\n",
"# (๊ณ ์ฅ์ด ์๋ ํ์ด ์คํ์ดํฌ๋ก ์๋ชป ์ ๊ฑฐ๋ ๊ฑด์ / ์ค์ ์คํ์ดํฌ ๊ณ ์ฅ ๊ฒ์ถ ๊ฑด์)\n",
"no_floor = {p: 0.0 for p in MIN_JUMP}\n",
"for name, k in [(\"๊ณ ์ ์๊ณ(๋ชจ๋ ์ํฉ 3๋ฐฐ)\", {\"day\": 3.0, \"night\": 3.0, \"storm\": 3.0}), (\"์ํฉ๋ณ ๋์ ์๊ณ\", SPIKE_K)]:\n",
" cc = clean(data, spike_k=k, min_jump=no_floor)\n",
" sp = (cc[flag_cols] == \"spike\").any(axis=1)\n",
" wrong, caught = int((sp & (cc[\"fault\"] != \"spike\")).sum()), int((sp & (cc[\"fault\"] == \"spike\")).sum())\n",
" print(f\"{name}: ์ ์ ๋ณ๋ ์ค์ ๊ฑฐ {wrong}๊ฑด(์ผ๊ฐยท์
์ฒํ {int((sp & (cc['fault'] != 'spike') & (cc['cond'] != 'day')).sum())}๊ฑด), ์คํ์ดํฌ ๊ฒ์ถ {caught}๊ฑด\")\n",
"print(\"์ด์ฉ ์ค์ ์ ๋์ ์๊ณ + ํญ๋ชฉ๋ณ ์ต์ ๋ณํํญ ํํ(MIN_JUMP)์ ํจ๊ป ์ด๋ค.\")\n",
"\n",
"# 6) ์ ํ ๋น๊ต ๊ทธ๋ฆผ\n",
"sid = data.loc[data[\"fault\"].isin([\"spike\", \"dropout\"]), \"source_id\"].iloc[0]\n",
"raw, cl = data[data.source_id == sid], c[c.source_id == sid]\n",
"fig, ax = plt.subplots(2, 1, figsize=(10, 6), sharex=True)\n",
"for a, p in zip(ax, [\"turbidity\", \"chlorine\"]):\n",
" a.plot(raw[\"t\"], raw[p], color=\"lightgray\", lw=3, label=\"raw sensor\")\n",
" a.plot(cl[\"t\"], cl[p], color=\"tab:blue\", lw=1, label=\"cleaned\")\n",
" a.set_ylabel(p)\n",
" a.legend(loc=\"upper right\")\n",
"ax[1].set_xlabel(\"hour\")\n",
"plt.suptitle(f\"{sid} before / after preprocessing (synthetic)\")\n",
"plt.tight_layout()\n",
"plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
02 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"์ ์ฒ๋ฆฌ 2,688๊ฑด, 22,643๊ฑด/์ด, ์ฒ๋ฆฌ ํ ๋จ์ ๊ฒฐ์ธก 0๊ฑด, ์ค๋ฅ์จ 0.0%\n",
"\n",
"[์ค์ ๊ณ ์ฅ ์ ํ x ์ ์ฒ๋ฆฌ ์ฒ๋ฆฌ ๋ด์ญ] (ํ ์)\n",
"์ฒ๋ฆฌ๋ด์ญ - long_gap spike stuck\n",
"fault \n",
"drift 78 8 0 0\n",
"dropout 0 17 0 0\n",
"none 2485 0 0 0\n",
"spike 1 0 2 0\n",
"stuck 6 0 0 91\n",
"๊ฒฐ์ธก ์
์: ์ ์ฒ๋ฆฌ ์ 125 -> ํ 0\n",
"\n",
"์๋ฅ์ผ์ ๊ฒฐ์ธก 120๊ฑด ๋ณด์ ์ค์ฐจ(MAE): ์ ํ ๋ณด๊ฐ 0.042 mg/L vs ์ ์ฒด ํ๊ท ๋์ฒด 0.122 mg/L\n",
"\n",
"[๋จ์ ํ
์คํธ]\n",
" [PASS] T1 ๋ฌผ๋ฆฌ ๋ฒ์ ๋ฐ pH 15.0 ์ ๊ฑฐ ํ ๋ณด์ \n",
" [PASS] T2 ๋จ๋ฐ ์คํ์ดํฌ(ํ๋ 38) ์ ๊ฑฐ\n",
" [PASS] T3 ์ง์ ์์น(ํ์ ์ ์
)์ ๋ณด์กด\n",
" [PASS] T4 10์๊ฐ ๊ณ ์ ๊ฐ -> stuck ํ์\n",
" [PASS] T5 2์๊ฐ ๊ฒฐ์ธก ๋ณด๊ฐ / 8์๊ฐ ๊ฒฐ์ธก ํ์\n",
"๋จ์ ํ
์คํธ 5/5 ํต๊ณผ\n",
"๊ณ ์ ์๊ณ(๋ชจ๋ ์ํฉ 3๋ฐฐ): ์ ์ ๋ณ๋ ์ค์ ๊ฑฐ 7๊ฑด(์ผ๊ฐยท์
์ฒํ 6๊ฑด), ์คํ์ดํฌ ๊ฒ์ถ 3๊ฑด\n",
"์ํฉ๋ณ ๋์ ์๊ณ: ์ ์ ๋ณ๋ ์ค์ ๊ฑฐ 1๊ฑด(์ผ๊ฐยท์
์ฒํ 0๊ฑด), ์คํ์ดํฌ ๊ฒ์ถ 2๊ฑด\n",
"์ด์ฉ ์ค์ ์ ๋์ ์๊ณ + ํญ๋ชฉ๋ณ ์ต์ ๋ณํํญ ํํ(MIN_JUMP)์ ํจ๊ป ์ด๋ค.\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",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ ์ฒ๋ฆฌํ ์๋ฃ์ pH, ํ๋, ์๋ฅ์ผ์, ์ค๊ธ์, ๋์ฅ๊ท ์ ์ฃผ์ยท์ํ ๊ฐ์ ๊ธฐ์ค์ ์ ์ฉํด ์์ ยท์ฃผ์ยท์ํ 3๋ฑ๊ธ ํ์ ํ๊ณ ์ ํ๋๋ฅผ ์ธก์ ํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/wel03_baseline_model.py ๋ฐ ๊ธฐ๋ณธ ์ถ๋ก ๊ฒฐ๊ณผ ๋ณด๊ณ ์` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์๊ณ ๋ฆฌ์ฆ์ ๋
ผ๋ฆฌ์ ํ๋ฆ ๋ฐ ๊ตฐ์ฌ์ ํ๋จ ๊ธฐ์ค์ ํฌ๋ช
์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๊ธฐ๋ณธ ๋ฐ์ดํฐ์
๋์ ํ์ง/์ฒ๋ฆฌ ์ ํ๋ 75% ์ด์ ๋ฌ์ฑ\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ๋จ์ ๊ธฐ๋ฅ ๋์ ํ์ธ ๋ฐ ๊ธฐ๋ณธ ์๋๋ฆฌ์ค ์คํ ๋ก๊ทธ ์์ฑ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ๊ธฐ์ค ๋ฒค์น๋งํฌ ๋ฐ์ดํฐ์
๋์ ์ถ๋ก ๊ฒฐ๊ณผ ์ค์ฐจ์จ ๋น๊ต\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"๊ฐ์ ๊ธฐ์ค์น(์ฃผ์ยท์ํ)๋ฅผ ๋๋ ํญ๋ชฉ์ด ์์ผ๋ฉด ์์ฉ ๋ถ์ ํฉ ์์ฌ์ผ๋ก ํ์ ํ๋ 1์ฐจ ํํฐ๋ง Baseline ์๊ณ ๋ฆฌ์ฆ์ ๊ตฌํํ๊ณ , ๊ธฐ์ค ๋ฒค์น๋งํฌ ๋ฐ์ดํฐ์์ ์ ํ๋ยท์ค์ฐจ ์ ํยทํ์ ๊ทผ๊ฑฐยท์ฒ๋ฆฌ ์ง์ฐ์ ํ์ธํ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ผ์ ๊ธ์์ ๊ฐ์ ์์ง ์ผ์ ๋ฐ์ดํฐ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์์นยท๊ฒ์ฌ ๊ฒฐ๊ณผ ์์)\n",
"# ๊ฐ์ ๊ธ์์ 8๊ณณ x 14์ผ x 1์๊ฐ ๊ฐ๊ฒฉ. ์ค์ ์์ง(true_*)ยท์ผ์ ์ธก์ ๊ฐยท์ค์ผ ์ฌ๊ฑดยท์ผ์ ๊ณ ์ฅ ์ ๋ต์ ํจ๊ป ๋ง๋ ๋ค.\n",
"# ๊ธฐ์ค์น๋ ๊ต์ก์ฉ ๊ฐ์ ๊ฐ์ด๋ฉฐ, ์ค์ ์์ฉ ํ์ ์ ๊ณต์ธ ๊ฒ์ฌ ๊ฒฐ๊ณผ์ ์๋ฌด(์๋ฐฉ์ํ) ๋ด๋น ๊ธฐ์ค์ ๋ฐ๋ฅธ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"PARAMS = [\"ph\", \"turbidity\", \"chlorine\", \"metal\", \"ecoli\"]\n",
"SOURCES = [(\"W01\", \"water_truck\", 1.0, 2.0), (\"W02\", \"water_truck\", 3.5, 1.2), (\"W03\", \"purification_unit\", 2.2, 4.1),\n",
" (\"W04\", \"purification_unit\", 5.0, 3.3), (\"W05\", \"well\", 0.8, 5.6), (\"W06\", \"well\", 4.4, 6.0),\n",
" (\"W07\", \"river_intake\", 6.3, 1.8), (\"W08\", \"river_intake\", 6.9, 4.9)]\n",
"BASE = {\"water_truck\": (7.2, 0.25, 0.75, 1.5), \"purification_unit\": (7.0, 0.20, 0.85, 1.2),\n",
" \"well\": (6.8, 0.45, 0.50, 3.0), \"river_intake\": (7.3, 0.55, 0.60, 2.0)} # pH, ํ๋(NTU), ์๋ฅ์ผ์(mg/L), ์ค๊ธ์(ug/L)\n",
"LIMITS = {\"ph\": (5.8, 8.5), \"turbidity\": (1.0, 2.0), \"chlorine\": (0.2, 0.1), \"metal\": (5.0, 10.0), \"ecoli\": (1, 1)} # (์ฃผ์, ์ํ) ๊ฐ์ ๊ธฐ์ค\n",
"EVENT_TYPES = { # ์ค์ผ ์ฌ๊ฑด: ๋ณ์๋ณ ์ต๋ ๋ณํ๋, ์ต๋์น ๋๋ฌ ์๊ฐ(h)\n",
" \"sewage\": ({\"turbidity\": 2.8, \"chlorine\": -0.5, \"ecoli\": 30, \"ph\": -0.3}, 4),\n",
" \"metal\": ({\"metal\": 22.0, \"ph\": -0.5}, 4),\n",
" \"disinfect_fail\": ({\"chlorine\": -0.8, \"ecoli\": 6}, 6),\n",
" \"chemical\": ({\"ph\": 1.7, \"turbidity\": 1.0}, 3),\n",
" \"creeping_metal\": ({\"metal\": 13.0}, 18),\n",
"}\n",
"FAULT_TYPES = [\"stuck\", \"spike\", \"drift\", \"dropout\"]\n",
"NOISE = {\"ph\": 0.04, \"turbidity\": 0.06, \"chlorine\": 0.03, \"metal\": 0.35}\n",
"DIGITS = {\"ph\": 2, \"turbidity\": 2, \"chlorine\": 2, \"metal\": 1}\n",
"\n",
"\n",
"def make_dataset(days=14, seed=7, n_events=6, n_faults=10, events=None, faults=None, sources=None):\n",
" \"\"\"events: [(๊ธ์์ ๋ฒํธ, ์ฌ๊ฑด ์ ํ, ์์ ์๊ฐ h, ์ง์ h, ๊ฐ๋)], faults: [(๊ธ์์ ๋ฒํธ, ๊ณ ์ฅ ์ ํ, ํญ๋ชฉ, ์์ h, ์ง์ h)]\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" sources = sources or SOURCES\n",
" T = days * 24\n",
" tt = np.arange(T)\n",
" hour = tt % 24\n",
" night = (hour < 6) | (hour >= 20)\n",
" rain = np.zeros(T)\n",
" for _ in range(max(1, days // 4)):\n",
" s0, d = int(rng.integers(0, T - 14)), int(rng.integers(6, 14))\n",
" rain[s0:s0 + d] = np.maximum(rain[s0:s0 + d], rng.uniform(0.6, 1.0))\n",
" if events is None:\n",
" events = [(int(rng.integers(len(sources))), str(rng.choice(list(EVENT_TYPES))), int(rng.integers(min(24, T // 4), max(T // 4 + 1, T - 48))),\n",
" int(rng.integers(10, 30)), 1.0) for _ in range(n_events)]\n",
" if faults is None:\n",
" faults = [(int(rng.integers(len(sources))), str(rng.choice(FAULT_TYPES)), str(rng.choice([\"ph\", \"turbidity\", \"chlorine\", \"metal\"])),\n",
" int(rng.integers(12, max(13, T - 30))), int(rng.integers(6, 24))) for _ in range(n_faults)]\n",
" out = []\n",
" for si, (sid, stype, x, y) in enumerate(sources):\n",
" ph0, tb0, cl0, mt0 = BASE[stype]\n",
" tr = {\"ph\": ph0 + 0.08 * np.sin(2 * np.pi * hour / 24) + rng.normal(0, 0.03, T),\n",
" \"turbidity\": tb0 * (1 + 0.1 * rng.normal(0, 1, T)),\n",
" \"chlorine\": (cl0 * np.exp(-0.022 * hour) if stype == \"water_truck\" else cl0 + rng.normal(0, 0.03, T)),\n",
" \"metal\": mt0 + rng.normal(0, 0.2, T), \"ecoli\": np.zeros(T)}\n",
" if stype in (\"river_intake\", \"well\"):\n",
" tr[\"turbidity\"] = tr[\"turbidity\"] + rain * (0.9 if stype == \"river_intake\" else 0.45)\n",
" f_all, etype = np.zeros(T), np.array([\"none\"] * T, dtype=object)\n",
" for (s_i, et, onset, dur, scale) in events:\n",
" if s_i != si:\n",
" continue\n",
" delta, ramp = EVENT_TYPES[et]\n",
" f = np.clip((tt - onset + 1) / ramp, 0, 1) * ((tt >= onset) & (tt < onset + dur + ramp))\n",
" for k, v in delta.items():\n",
" tr[k] = tr[k] + v * f * scale\n",
" etype[f > 0] = et\n",
" f_all = np.maximum(f_all, f)\n",
" tr[\"chlorine\"] = np.clip(tr[\"chlorine\"], 0.02, None)\n",
" tr[\"turbidity\"] = np.clip(tr[\"turbidity\"], 0.05, None)\n",
" tr[\"metal\"] = np.clip(tr[\"metal\"], 0.1, None)\n",
" grade = np.where(f_all >= 0.2, 2, 0)\n",
" mild = (tr[\"turbidity\"] > LIMITS[\"turbidity\"][0]) | (tr[\"chlorine\"] < LIMITS[\"chlorine\"][0]) | (tr[\"metal\"] > LIMITS[\"metal\"][0])\n",
" grade = np.where((grade == 0) & mild, 1, grade)\n",
" ms = {}\n",
" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n",
" sd = NOISE[p] * np.where(night, 1.5, 1.0) * (tr[p] if p == \"turbidity\" else 1)\n",
" ms[p] = np.round(np.clip(tr[p] + rng.normal(0, 1, T) * sd, 0, None), DIGITS[p])\n",
" ms[\"ecoli\"] = (rng.poisson(tr[\"ecoli\"]) + (rng.random(T) < 0.004)).astype(float)\n",
" fault, fparam = np.array([\"none\"] * T, dtype=object), np.array([\"-\"] * T, dtype=object)\n",
" for (s_i, ft, p, s0, d) in faults:\n",
" if s_i != si:\n",
" continue\n",
" seg = slice(s0, min(T, s0 + d))\n",
" if ft == \"stuck\":\n",
" ms[p][seg] = ms[p][s0]\n",
" elif ft == \"spike\":\n",
" seg = slice(s0, s0 + 1)\n",
" ms[p][s0] = {\"ph\": 11.8, \"turbidity\": 38.0, \"chlorine\": 0.0, \"metal\": 75.0}[p]\n",
" elif ft == \"drift\":\n",
" ramp = np.linspace(0, 1, seg.stop - s0)\n",
" ms[p][seg] = np.clip(ms[p][seg] + ramp * {\"ph\": -1.6, \"turbidity\": 2.6, \"chlorine\": -0.75, \"metal\": 12.0}[p], 0, None).round(DIGITS[p])\n",
" elif ft == \"dropout\":\n",
" for q in PARAMS:\n",
" ms[q][seg] = np.nan\n",
" p = \"all\"\n",
" fault[seg], fparam[seg] = ft, p\n",
" df = pd.DataFrame({\"source_id\": sid, \"source_type\": stype, \"x_km\": x, \"y_km\": y, \"t\": tt,\n",
" \"cond\": np.where(rain > 0, \"storm\", np.where(night, \"night\", \"day\")),\n",
" **{p: ms[p] for p in PARAMS}, **{f\"true_{p}\": tr[p] for p in PARAMS},\n",
" \"event\": etype, \"fault\": fault, \"fault_param\": fparam, \"grade\": grade})\n",
" out.append(df)\n",
" return pd.concat(out, ignore_index=True)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌ ๊ณตํต ํจ์: ๋ฌผ๋ฆฌ ๋ฒ์ ์ ๊ฒ -> ๊ณ ์ ๊ฐ(stuck) ํ์ -> ๋จ๋ฐ ์คํ์ดํฌ ์ ๊ฑฐ -> ๊ฒฐ์ธก ๋ณด์ ----\n",
"PHYS = {\"ph\": (0, 14), \"turbidity\": (0, 1000), \"chlorine\": (0, 10), \"metal\": (0, 1000), \"ecoli\": (0, 100000)}\n",
"SPIKE_K = {\"day\": 6.0, \"night\": 7.0, \"storm\": 9.0} # ์ํฉ๋ณ(์ฃผ๊ฐ/์ผ๊ฐ/์
์ฒํ) ์คํ์ดํฌ ํ์ ๋ฐฐ์\n",
"MIN_JUMP = {\"ph\": 0.8, \"turbidity\": 1.5, \"chlorine\": 0.3, \"metal\": 6.0}\n",
"STUCK_RUN, SHORT_GAP = 6, 3\n",
"\n",
"\n",
"def clean(df, spike_k=None, min_jump=None):\n",
" \"\"\"๊ธ์์๋ณ ์๊ณ์ด์ ์ ์ ํ๊ณ ํญ๋ชฉ๋ณ ์ฒ๋ฆฌ ๋ด์ญ(<ํญ๋ชฉ>_flag: range/stuck/spike/interp/long_gap)์ ๋จ๊ธด๋ค.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 3] ์์ฉ ์ ํฉ/๋ถ์ ํฉ ํ์ 1์ฐจ ๊ธฐ์ค์น ํํฐ๋ง Baseline ์๊ณ ๋ฆฌ์ฆ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"GRADE_NAME = {0: \"์์ \", 1: \"์ฃผ์\", 2: \"์ํ\"}\n",
"\n",
"\n",
"def rule_grade(df):\n",
" \"\"\"๊ธฐ์ค์น 1์ฐจ ํํฐ: ์ํ(์์ฉ ๋ถ์ ํฉ ์์ฌ) = ํ ํญ๋ชฉ์ด๋ผ๋ ์ํ ๊ธฐ์ค ์ด๊ณผ, ์ฃผ์ = ์ฃผ์ ๊ธฐ์ค ์ด๊ณผ.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 3] ์์ฉ ์ ํฉ/๋ถ์ ํฉ ํ์ 1์ฐจ ๊ธฐ์ค์น ํํฐ๋ง Baseline ์๊ณ ๋ฆฌ์ฆ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def reasons(row):\n",
" r = []\n",
" if not LIMITS[\"ph\"][0] <= row[\"ph\"] <= LIMITS[\"ph\"][1]:\n",
" r.append(f\"pH {row['ph']:.2f}\")\n",
" if row[\"turbidity\"] > LIMITS[\"turbidity\"][0]:\n",
" r.append(f\"ํ๋ {row['turbidity']:.2f}NTU\")\n",
" if row[\"chlorine\"] < LIMITS[\"chlorine\"][0]:\n",
" r.append(f\"์๋ฅ์ผ์ {row['chlorine']:.2f}mg/L\")\n",
" if row[\"metal\"] > LIMITS[\"metal\"][0]:\n",
" r.append(f\"์ค๊ธ์ {row['metal']:.1f}ug/L\")\n",
" if row[\"ecoli\"] >= 1:\n",
" r.append(f\"๋์ฅ๊ท {row['ecoli']:.0f}\")\n",
" return \", \".join(r) or \"-\"\n",
"\n",
"\n",
"FEATS = [f\"{p}{s}\" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"] for s in [\"\", \"_d1\", \"_d3\", \"_dev\", \"_sd6\"]] + \\\n",
" [\"ecoli\", \"ecoli_3h\", \"storm\", \"night\", \"n_flag\", \"truck\", \"purif\", \"well\", \"river\"]\n",
"\n",
"\n",
"def add_features(c):\n",
" \"\"\"์๊ฐ ์์๋ฅผ ์งํค๋(๊ณผ๊ฑฐ ๊ฐ๋ง ์ฐ๋) ๊ธ์์๋ณ ํน์ง: ๋ณํ๋, 24์๊ฐ ๊ธฐ์ค์ ๋๋น ํธ์ฐจ, 6์๊ฐ ๋ณ๋์ฑ, ๋์ฅ๊ท ๋์ ๋ฑ.\"\"\"\n",
" c = c.sort_values([\"source_id\", \"t\"]).reset_index(drop=True)\n",
" g = c.groupby(\"source_id\")\n",
" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n",
" c[f\"{p}_d1\"] = g[p].diff().fillna(0)\n",
" c[f\"{p}_d3\"] = g[p].diff(3).fillna(0)\n",
" base = g[p].transform(lambda s: s.shift(1).rolling(24, min_periods=4).median())\n",
" c[f\"{p}_dev\"] = (c[p] - base).fillna(0)\n",
" c[f\"{p}_sd6\"] = g[p].transform(lambda s: s.rolling(6, min_periods=2).std()).fillna(0)\n",
" c[\"ecoli_3h\"] = g[\"ecoli\"].transform(lambda s: s.rolling(3, min_periods=1).sum())\n",
" c[\"storm\"], c[\"night\"] = (c[\"cond\"] == \"storm\").astype(int), (c[\"cond\"] == \"night\").astype(int)\n",
" flags = [f\"{p}_flag\" for p in PARAMS if f\"{p}_flag\" in c]\n",
" c[\"n_flag\"] = sum(c[f].isin([\"stuck\", \"spike\", \"long_gap\", \"range\"]).astype(int) for f in flags) if flags else 0\n",
" for k, name in [(\"truck\", \"water_truck\"), (\"purif\", \"purification_unit\"), (\"well\", \"well\"), (\"river\", \"river_intake\")]:\n",
" c[k] = (c[\"source_type\"] == name).astype(int)\n",
" return c\n",
"\n",
"\n",
"# ๊ธฐ์ค ๋ฒค์น๋งํฌ ๋ฐ์ดํฐ์
: ๊ฐ์ ๊ธ์์ 8๊ณณ x 14์ผ, ์ค์ผ ์ฌ๊ฑด 10๊ฑด, ์ผ์ ๊ณ ์ฅ 14๊ฑด (์ ๋ต ๋ฑ๊ธ grade ํฌํจ)\n",
"bench = make_dataset(days=14, seed=11, n_events=10, n_faults=14)\n",
"truth_raw = bench.sort_values([\"source_id\", \"t\"])[\"grade\"].to_numpy()\n",
"\n",
"# 1) ์ ์ฒ๋ฆฌ ์์ด(๊ฒฐ์ธก์ 0์ผ๋ก) vs ์ ์ฒ๋ฆฌ ํ Baseline ํ์ \n",
"t0 = time.perf_counter()\n",
"c = clean(bench)\n",
"c[\"pred\"] = rule_grade(c)\n",
"dt = time.perf_counter() - t0\n",
"raw = bench.sort_values([\"source_id\", \"t\"]).fillna({p: 0 for p in PARAMS})\n",
"acc_raw = (rule_grade(raw) == truth_raw).mean()\n",
"acc = (c[\"pred\"] == c[\"grade\"]).mean()\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ฒค์น๋งํฌ {len(c):,}๊ฑด(๊ธ์์ 8๊ณณ x 336์๊ฐ)\")\n",
"print(f\"์ ํ๋: ์ ์ฒ๋ฆฌ ์์ {acc_raw:.1%} -> ์ ์ฒ๋ฆฌ + ๊ธฐ์ค์น ํํฐ {acc:.1%} (๋ชฉํ 75% ์ด์: {'๋ฌ์ฑ' if acc >= 0.75 else '๋ฏธ๋ฌ'})\")\n",
"print(f\"์ฒ๋ฆฌ ์ง์ฐ: ์ ์ฒด {dt * 1000:.0f}ms, 1๊ฑด๋น {dt / len(c) * 1000:.3f}ms\")\n",
"\n",
"# 2) ํผ๋ํ๋ ฌ๊ณผ ์ค์ฐจ ์ ํ\n",
"cm = pd.crosstab(c[\"grade\"].map(GRADE_NAME), c[\"pred\"].map(GRADE_NAME), rownames=[\"์ค์ \"], colnames=[\"ํ์ \"])\n",
"cm = cm.reindex(index=[\"์์ \", \"์ฃผ์\", \"์ํ\"], columns=[\"์์ \", \"์ฃผ์\", \"์ํ\"], fill_value=0)\n",
"print(\"\\n[ํผ๋ํ๋ ฌ]\")\n",
"print(cm.to_string())\n",
"danger = c[\"grade\"] == 2\n",
"rec = ((c[\"pred\"] == 2) & danger).sum() / danger.sum()\n",
"far = ((c[\"pred\"] == 2) & ~danger).sum() / max((c[\"pred\"] == 2).sum(), 1)\n",
"print(f\"์ํ ์ฌํ์จ {rec:.1%}, ์ํ ๊ฒฝ๋ณด ์ค ์ค๊ฒฝ๋ณด ๋น์จ {far:.1%}\")\n",
"miss = c[danger & (c[\"pred\"] != 2)]\n",
"fa = c[~danger & (c[\"pred\"] == 2)]\n",
"print(\"๋์น ์ํ(์ฌ๊ฑด ์ ํ๋ณ):\", miss[\"event\"].value_counts().to_dict())\n",
"print(\"์ค๊ฒฝ๋ณด(์ผ์ ๊ณ ์ฅ ์ ํ๋ณ):\", fa[\"fault\"].value_counts().to_dict())\n",
"\n",
"# 3) ํ์ ๊ทผ๊ฑฐ(ํฌ๋ช
์ฑ): ์ํ ํ์ ์ฌ๋ก ์ผ๋ถ์ ์ด๊ณผ ํญ๋ชฉ\n",
"print(\"\\n[์ํ ํ์ ๊ทผ๊ฑฐ ์์]\")\n",
"show = c[c[\"pred\"] == 2].groupby(\"source_id\").head(1).head(5)\n",
"for _, r in show.iterrows():\n",
" print(f\" {r['source_id']} D+{int(r['t']) // 24:02d} {int(r['t']) % 24:02d}์: {reasons(r)} -> ์ค์ {GRADE_NAME[r['grade']]}({r['event']})\")\n",
"\n",
"# 4) ํด๋ฆฌ์คํฑ 1์ฐจ ํ๋: ์ํ ํ์ ์ '2์๊ฐ ์ฐ์ ์ด๊ณผ'๋ก ๋ฐ๊พธ๋ฉด ์ค๊ฒฝ๋ณด๋ ์ค์ง๋ง ๋์นจ์ด ๋์ด๋๋ค\n",
"c[\"pred2\"] = c[\"pred\"]\n",
"prev = c.groupby(\"source_id\")[\"pred\"].shift(1).fillna(0)\n",
"c.loc[(c[\"pred\"] == 2) & (prev < 2), \"pred2\"] = 1\n",
"for name, col in [(\"1์๊ฐ ์ฆ์ ํ์ \", \"pred\"), (\"2์๊ฐ ์ฐ์ ํ์ธ\", \"pred2\")]:\n",
" p = c[col]\n",
" print(f\"{name}: ์ ํ๋ {(p == c['grade']).mean():.1%}, ์ํ ์ฌํ์จ {((p == 2) & danger).sum() / danger.sum():.1%}, \"\n",
" f\"์ค๊ฒฝ๋ณด {int(((p == 2) & ~danger).sum())}๊ฑด\")\n",
"print(\"-> ์์ฉ์๋ ๋์นจ์ด ๋ ์ํํ๋ฏ๋ก Baseline์ ์ฆ์ ํ์ ์ ์ ์งํ๊ณ , ์ค๊ฒฝ๋ณด๋ ๋ฏธ์
5 ์ ๋ขฐ๋ ์ง์๋ก ๋ค๋ฃฌ๋ค.\")\n",
"print(\"โป ๊ธฐ์ค์น๋ ๊ต์ก์ฉ ๊ฐ์ ๊ฐ์ด๋ค. ์ค์ ์์ฉ ์ ํฉ ํ์ ์ ๊ณต์ ์์ง ๊ธฐ์ค๊ณผ ์๋ฌด(์๋ฐฉ์ํ)ยทํ๊ฒฝ ๋ด๋น ํ์ธ์ ๋ฐ๋ฅธ๋ค.\")\n",
"\n",
"sid = miss[\"source_id\"].mode().iloc[0] if len(miss) else \"W01\"\n",
"s = c[c[\"source_id\"] == sid]\n",
"fig, ax = plt.subplots(figsize=(10, 3.5))\n",
"ax.plot(s[\"t\"], s[\"grade\"], color=\"black\", lw=2, label=\"true grade\")\n",
"ax.plot(s[\"t\"], s[\"pred\"] + 0.05, color=\"tab:red\", lw=1, label=\"baseline grade\")\n",
"ax.set_yticks([0, 1, 2], [\"safe\", \"caution\", \"danger\"]); ax.set_xlabel(\"hour\"); ax.legend()\n",
"ax.set_title(f\"{sid} baseline vs true grade (synthetic)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
03 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ฒค์น๋งํฌ 2,688๊ฑด(๊ธ์์ 8๊ณณ x 336์๊ฐ)\n",
"์ ํ๋: ์ ์ฒ๋ฆฌ ์์ 96.8% -> ์ ์ฒ๋ฆฌ + ๊ธฐ์ค์น ํํฐ 97.9% (๋ชฉํ 75% ์ด์: ๋ฌ์ฑ)\n",
"์ฒ๋ฆฌ ์ง์ฐ: ์ ์ฒด 158ms, 1๊ฑด๋น 0.059ms\n",
"\n",
"[ํผ๋ํ๋ ฌ]\n",
"ํ์ ์์ ์ฃผ์ ์ํ\n",
"์ค์ \n",
"์์ 2369 8 17\n",
"์ฃผ์ 3 45 0\n",
"์ํ 9 19 218\n",
"์ํ ์ฌํ์จ 88.6%, ์ํ ๊ฒฝ๋ณด ์ค ์ค๊ฒฝ๋ณด ๋น์จ 7.2%\n",
"๋์น ์ํ(์ฌ๊ฑด ์ ํ๋ณ): {'creeping_metal': 22, 'chemical': 4, 'disinfect_fail': 1, 'metal': 1}\n",
"์ค๊ฒฝ๋ณด(์ผ์ ๊ณ ์ฅ ์ ํ๋ณ): {'none': 11, 'drift': 6}\n",
"\n",
"[์ํ ํ์ ๊ทผ๊ฑฐ ์์]\n",
" W01 D+13 20์: ๋์ฅ๊ท 1 -> ์ค์ ์์ (none)\n",
" W02 D+06 03์: pH 9.04, ํ๋ 1.24NTU -> ์ค์ ์ํ(chemical)\n",
" W03 D+02 03์: ๋์ฅ๊ท 1 -> ์ค์ ์์ (none)\n",
" W04 D+12 00์: pH 8.65, ํ๋ 1.16NTU -> ์ค์ ์ํ(chemical)\n",
" W05 D+00 09์: ๋์ฅ๊ท 1 -> ์ค์ ์์ (none)\n",
"1์๊ฐ ์ฆ์ ํ์ : ์ ํ๋ 97.9%, ์ํ ์ฌํ์จ 88.6%, ์ค๊ฒฝ๋ณด 17๊ฑด\n",
"2์๊ฐ ์ฐ์ ํ์ธ: ์ ํ๋ 97.6%, ์ํ ์ฌํ์จ 85.0%, ์ค๊ฒฝ๋ณด 5๊ฑด\n",
"-> ์์ฉ์๋ ๋์นจ์ด ๋ ์ํํ๋ฏ๋ก Baseline์ ์ฆ์ ํ์ ์ ์ ์งํ๊ณ , ์ค๊ฒฝ๋ณด๋ ๋ฏธ์
5 ์ ๋ขฐ๋ ์ง์๋ก ๋ค๋ฃฌ๋ค.\n",
"โป ๊ธฐ์ค์น๋ ๊ต์ก์ฉ ๊ฐ์ ๊ฐ์ด๋ค. ์ค์ ์์ฉ ์ ํฉ ํ์ ์ ๊ณต์ ์์ง ๊ธฐ์ค๊ณผ ์๋ฌด(์๋ฐฉ์ํ)ยทํ๊ฒฝ ๋ด๋น ํ์ธ์ ๋ฐ๋ฅธ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
04. ๋ณตํฉ ์ค์ผ์ ์ ์
์งํ ๋ฐ ๊ธ์ ์์ ๋ ์ํ ๋ฑ๊ธ ๋ถ๋ฅ ๋จธ์ ๋ฌ๋ ๋ชจ๋ธ ๊ณ ๋ํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋ณตํฉ ์ค์ผ์ ์ ์
์งํ ๋ฐ ๊ธ์ ์ํ ๋ฑ๊ธ ๋ถ๋ฅ ๋จธ์ ๋ฌ๋ ๋ชจ๋ธ์ ๊ณ ๋ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋ณตํฉ ์ค์ผ์ ์ ์
์งํ์ ๊ธ์ ์์ ๋ ์ํ ๋ฑ๊ธ์ ๋ถ๋ฅํ๋ ๋จธ์ ๋ฌ๋ ๋ชจ๋ธ์ ๊ณ ๋ํํ๊ณ ๋ฐฉ์ด ๋ก์ง์ ๊ฒ์ฆํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๋ณํ๋, 24์๊ฐ ๊ธฐ์ค์ ๋๋น ํธ์ฐจ, 6์๊ฐ ๋ณ๋์ฑ, ๋์ฅ๊ท ๋์ , ์ผ์ ์ฒ๋ฆฌ ํ์ ๋ฑ 29๊ฐ ํน์ง์ผ๋ก ๋๋ค ํฌ๋ ์คํธ๋ฅผ ํ์ตํ๊ณ Baseline๊ณผ ๋น๊ต ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/wel03_advanced_detector.py ๋ฐ ์์ธ ํ์ง ํ
์คํธ ๊ฒฐ๊ณผ` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ธฐ๋ง/๊ต๋ ๊ณต๊ฒฉ์ ๋ํ ๋ฐฉ์ด ๋ก์ง์ ํ์ค์ฑ ๋ฐ ์ ๋ขฐ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๋ณตํฉ ์ํ ํ์ง ์ฌํ์จ(Recall) 85% ์ด์, ์ฒ๋ฆฌ ์ง์ฐ 200ms ์ดํ\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ๋ณตํฉ ์ํ ์๋๋ฆฌ์ค 10์ข
์ ๋ํ ํ์ง ํ
์คํธ ํต๊ณผ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ํฉ์ฑ ์ํ ๋ฐ์ดํฐ์
์ฃผ์
์ ์ด์์น ํ์ง ์ ํ๋ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์๊ณ์ด ํน์ง(๋ณํ๋ยท๊ธฐ์ค์ ํธ์ฐจยท๋ณ๋์ฑยท๋์ฅ๊ท ๋์ ยท์ผ์ ์ฒ๋ฆฌ ํ์)์ผ๋ก ๊ธ์ ์์ ๋ ์ํ ๋ฑ๊ธ(์์ ยท์ฃผ์ยท์ํ)์ ๋ถ๋ฅํ๋ ๋๋ค ํฌ๋ ์คํธ ๋ชจ๋ธ์ ํ์ตํ๊ณ , ๋ณตํฉ ์ํ ์๋๋ฆฌ์ค 10์ข
(์ค์ผ ์ฌ๊ฑด 7์ข
, ์ผ์ ๊ต๋ 3์ข
)์ผ๋ก ์ฌํ์จยท์ค๊ฒฝ๋ณดยท์ฒ๋ฆฌ ์ง์ฐ์ ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.ensemble import RandomForestClassifier\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ผ์ ๊ธ์์ ๊ฐ์ ์์ง ์ผ์ ๋ฐ์ดํฐ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์์นยท๊ฒ์ฌ ๊ฒฐ๊ณผ ์์)\n",
"# ๊ฐ์ ๊ธ์์ 8๊ณณ x 14์ผ x 1์๊ฐ ๊ฐ๊ฒฉ. ์ค์ ์์ง(true_*)ยท์ผ์ ์ธก์ ๊ฐยท์ค์ผ ์ฌ๊ฑดยท์ผ์ ๊ณ ์ฅ ์ ๋ต์ ํจ๊ป ๋ง๋ ๋ค.\n",
"# ๊ธฐ์ค์น๋ ๊ต์ก์ฉ ๊ฐ์ ๊ฐ์ด๋ฉฐ, ์ค์ ์์ฉ ํ์ ์ ๊ณต์ธ ๊ฒ์ฌ ๊ฒฐ๊ณผ์ ์๋ฌด(์๋ฐฉ์ํ) ๋ด๋น ๊ธฐ์ค์ ๋ฐ๋ฅธ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"PARAMS = [\"ph\", \"turbidity\", \"chlorine\", \"metal\", \"ecoli\"]\n",
"SOURCES = [(\"W01\", \"water_truck\", 1.0, 2.0), (\"W02\", \"water_truck\", 3.5, 1.2), (\"W03\", \"purification_unit\", 2.2, 4.1),\n",
" (\"W04\", \"purification_unit\", 5.0, 3.3), (\"W05\", \"well\", 0.8, 5.6), (\"W06\", \"well\", 4.4, 6.0),\n",
" (\"W07\", \"river_intake\", 6.3, 1.8), (\"W08\", \"river_intake\", 6.9, 4.9)]\n",
"BASE = {\"water_truck\": (7.2, 0.25, 0.75, 1.5), \"purification_unit\": (7.0, 0.20, 0.85, 1.2),\n",
" \"well\": (6.8, 0.45, 0.50, 3.0), \"river_intake\": (7.3, 0.55, 0.60, 2.0)} # pH, ํ๋(NTU), ์๋ฅ์ผ์(mg/L), ์ค๊ธ์(ug/L)\n",
"LIMITS = {\"ph\": (5.8, 8.5), \"turbidity\": (1.0, 2.0), \"chlorine\": (0.2, 0.1), \"metal\": (5.0, 10.0), \"ecoli\": (1, 1)} # (์ฃผ์, ์ํ) ๊ฐ์ ๊ธฐ์ค\n",
"EVENT_TYPES = { # ์ค์ผ ์ฌ๊ฑด: ๋ณ์๋ณ ์ต๋ ๋ณํ๋, ์ต๋์น ๋๋ฌ ์๊ฐ(h)\n",
" \"sewage\": ({\"turbidity\": 2.8, \"chlorine\": -0.5, \"ecoli\": 30, \"ph\": -0.3}, 4),\n",
" \"metal\": ({\"metal\": 22.0, \"ph\": -0.5}, 4),\n",
" \"disinfect_fail\": ({\"chlorine\": -0.8, \"ecoli\": 6}, 6),\n",
" \"chemical\": ({\"ph\": 1.7, \"turbidity\": 1.0}, 3),\n",
" \"creeping_metal\": ({\"metal\": 13.0}, 18),\n",
"}\n",
"FAULT_TYPES = [\"stuck\", \"spike\", \"drift\", \"dropout\"]\n",
"NOISE = {\"ph\": 0.04, \"turbidity\": 0.06, \"chlorine\": 0.03, \"metal\": 0.35}\n",
"DIGITS = {\"ph\": 2, \"turbidity\": 2, \"chlorine\": 2, \"metal\": 1}\n",
"\n",
"\n",
"def make_dataset(days=14, seed=7, n_events=6, n_faults=10, events=None, faults=None, sources=None):\n",
" \"\"\"events: [(๊ธ์์ ๋ฒํธ, ์ฌ๊ฑด ์ ํ, ์์ ์๊ฐ h, ์ง์ h, ๊ฐ๋)], faults: [(๊ธ์์ ๋ฒํธ, ๊ณ ์ฅ ์ ํ, ํญ๋ชฉ, ์์ h, ์ง์ h)]\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" sources = sources or SOURCES\n",
" T = days * 24\n",
" tt = np.arange(T)\n",
" hour = tt % 24\n",
" night = (hour < 6) | (hour >= 20)\n",
" rain = np.zeros(T)\n",
" for _ in range(max(1, days // 4)):\n",
" s0, d = int(rng.integers(0, T - 14)), int(rng.integers(6, 14))\n",
" rain[s0:s0 + d] = np.maximum(rain[s0:s0 + d], rng.uniform(0.6, 1.0))\n",
" if events is None:\n",
" events = [(int(rng.integers(len(sources))), str(rng.choice(list(EVENT_TYPES))), int(rng.integers(min(24, T // 4), max(T // 4 + 1, T - 48))),\n",
" int(rng.integers(10, 30)), 1.0) for _ in range(n_events)]\n",
" if faults is None:\n",
" faults = [(int(rng.integers(len(sources))), str(rng.choice(FAULT_TYPES)), str(rng.choice([\"ph\", \"turbidity\", \"chlorine\", \"metal\"])),\n",
" int(rng.integers(12, max(13, T - 30))), int(rng.integers(6, 24))) for _ in range(n_faults)]\n",
" out = []\n",
" for si, (sid, stype, x, y) in enumerate(sources):\n",
" ph0, tb0, cl0, mt0 = BASE[stype]\n",
" tr = {\"ph\": ph0 + 0.08 * np.sin(2 * np.pi * hour / 24) + rng.normal(0, 0.03, T),\n",
" \"turbidity\": tb0 * (1 + 0.1 * rng.normal(0, 1, T)),\n",
" \"chlorine\": (cl0 * np.exp(-0.022 * hour) if stype == \"water_truck\" else cl0 + rng.normal(0, 0.03, T)),\n",
" \"metal\": mt0 + rng.normal(0, 0.2, T), \"ecoli\": np.zeros(T)}\n",
" if stype in (\"river_intake\", \"well\"):\n",
" tr[\"turbidity\"] = tr[\"turbidity\"] + rain * (0.9 if stype == \"river_intake\" else 0.45)\n",
" f_all, etype = np.zeros(T), np.array([\"none\"] * T, dtype=object)\n",
" for (s_i, et, onset, dur, scale) in events:\n",
" if s_i != si:\n",
" continue\n",
" delta, ramp = EVENT_TYPES[et]\n",
" f = np.clip((tt - onset + 1) / ramp, 0, 1) * ((tt >= onset) & (tt < onset + dur + ramp))\n",
" for k, v in delta.items():\n",
" tr[k] = tr[k] + v * f * scale\n",
" etype[f > 0] = et\n",
" f_all = np.maximum(f_all, f)\n",
" tr[\"chlorine\"] = np.clip(tr[\"chlorine\"], 0.02, None)\n",
" tr[\"turbidity\"] = np.clip(tr[\"turbidity\"], 0.05, None)\n",
" tr[\"metal\"] = np.clip(tr[\"metal\"], 0.1, None)\n",
" grade = np.where(f_all >= 0.2, 2, 0)\n",
" mild = (tr[\"turbidity\"] > LIMITS[\"turbidity\"][0]) | (tr[\"chlorine\"] < LIMITS[\"chlorine\"][0]) | (tr[\"metal\"] > LIMITS[\"metal\"][0])\n",
" grade = np.where((grade == 0) & mild, 1, grade)\n",
" ms = {}\n",
" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n",
" sd = NOISE[p] * np.where(night, 1.5, 1.0) * (tr[p] if p == \"turbidity\" else 1)\n",
" ms[p] = np.round(np.clip(tr[p] + rng.normal(0, 1, T) * sd, 0, None), DIGITS[p])\n",
" ms[\"ecoli\"] = (rng.poisson(tr[\"ecoli\"]) + (rng.random(T) < 0.004)).astype(float)\n",
" fault, fparam = np.array([\"none\"] * T, dtype=object), np.array([\"-\"] * T, dtype=object)\n",
" for (s_i, ft, p, s0, d) in faults:\n",
" if s_i != si:\n",
" continue\n",
" seg = slice(s0, min(T, s0 + d))\n",
" if ft == \"stuck\":\n",
" ms[p][seg] = ms[p][s0]\n",
" elif ft == \"spike\":\n",
" seg = slice(s0, s0 + 1)\n",
" ms[p][s0] = {\"ph\": 11.8, \"turbidity\": 38.0, \"chlorine\": 0.0, \"metal\": 75.0}[p]\n",
" elif ft == \"drift\":\n",
" ramp = np.linspace(0, 1, seg.stop - s0)\n",
" ms[p][seg] = np.clip(ms[p][seg] + ramp * {\"ph\": -1.6, \"turbidity\": 2.6, \"chlorine\": -0.75, \"metal\": 12.0}[p], 0, None).round(DIGITS[p])\n",
" elif ft == \"dropout\":\n",
" for q in PARAMS:\n",
" ms[q][seg] = np.nan\n",
" p = \"all\"\n",
" fault[seg], fparam[seg] = ft, p\n",
" df = pd.DataFrame({\"source_id\": sid, \"source_type\": stype, \"x_km\": x, \"y_km\": y, \"t\": tt,\n",
" \"cond\": np.where(rain > 0, \"storm\", np.where(night, \"night\", \"day\")),\n",
" **{p: ms[p] for p in PARAMS}, **{f\"true_{p}\": tr[p] for p in PARAMS},\n",
" \"event\": etype, \"fault\": fault, \"fault_param\": fparam, \"grade\": grade})\n",
" out.append(df)\n",
" return pd.concat(out, ignore_index=True)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌ ๊ณตํต ํจ์: ๋ฌผ๋ฆฌ ๋ฒ์ ์ ๊ฒ -> ๊ณ ์ ๊ฐ(stuck) ํ์ -> ๋จ๋ฐ ์คํ์ดํฌ ์ ๊ฑฐ -> ๊ฒฐ์ธก ๋ณด์ ----\n",
"PHYS = {\"ph\": (0, 14), \"turbidity\": (0, 1000), \"chlorine\": (0, 10), \"metal\": (0, 1000), \"ecoli\": (0, 100000)}\n",
"SPIKE_K = {\"day\": 6.0, \"night\": 7.0, \"storm\": 9.0} # ์ํฉ๋ณ(์ฃผ๊ฐ/์ผ๊ฐ/์
์ฒํ) ์คํ์ดํฌ ํ์ ๋ฐฐ์\n",
"MIN_JUMP = {\"ph\": 0.8, \"turbidity\": 1.5, \"chlorine\": 0.3, \"metal\": 6.0}\n",
"STUCK_RUN, SHORT_GAP = 6, 3\n",
"\n",
"\n",
"def clean(df, spike_k=None, min_jump=None):\n",
" \"\"\"๊ธ์์๋ณ ์๊ณ์ด์ ์ ์ ํ๊ณ ํญ๋ชฉ๋ณ ์ฒ๋ฆฌ ๋ด์ญ(<ํญ๋ชฉ>_flag: range/stuck/spike/interp/long_gap)์ ๋จ๊ธด๋ค.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 4] ๋ณตํฉ ์ค์ผ์ ์ ์
์งํ ๋ฐ ๊ธ์ ์์ ๋ ์ํ ๋ฑ๊ธ ๋ถ๋ฅ ๋จธ์ ๋ฌ๋ ๋ชจ๋ธ ๊ณ ๋ํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"GRADE_NAME = {0: \"์์ \", 1: \"์ฃผ์\", 2: \"์ํ\"}\n",
"\n",
"\n",
"def rule_grade(df):\n",
" \"\"\"๊ธฐ์ค์น 1์ฐจ ํํฐ: ์ํ(์์ฉ ๋ถ์ ํฉ ์์ฌ) = ํ ํญ๋ชฉ์ด๋ผ๋ ์ํ ๊ธฐ์ค ์ด๊ณผ, ์ฃผ์ = ์ฃผ์ ๊ธฐ์ค ์ด๊ณผ.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 4] ๋ณตํฉ ์ค์ผ์ ์ ์
์งํ ๋ฐ ๊ธ์ ์์ ๋ ์ํ ๋ฑ๊ธ ๋ถ๋ฅ ๋จธ์ ๋ฌ๋ ๋ชจ๋ธ ๊ณ ๋ํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def reasons(row):\n",
" r = []\n",
" if not LIMITS[\"ph\"][0] <= row[\"ph\"] <= LIMITS[\"ph\"][1]:\n",
" r.append(f\"pH {row['ph']:.2f}\")\n",
" if row[\"turbidity\"] > LIMITS[\"turbidity\"][0]:\n",
" r.append(f\"ํ๋ {row['turbidity']:.2f}NTU\")\n",
" if row[\"chlorine\"] < LIMITS[\"chlorine\"][0]:\n",
" r.append(f\"์๋ฅ์ผ์ {row['chlorine']:.2f}mg/L\")\n",
" if row[\"metal\"] > LIMITS[\"metal\"][0]:\n",
" r.append(f\"์ค๊ธ์ {row['metal']:.1f}ug/L\")\n",
" if row[\"ecoli\"] >= 1:\n",
" r.append(f\"๋์ฅ๊ท {row['ecoli']:.0f}\")\n",
" return \", \".join(r) or \"-\"\n",
"\n",
"\n",
"FEATS = [f\"{p}{s}\" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"] for s in [\"\", \"_d1\", \"_d3\", \"_dev\", \"_sd6\"]] + \\\n",
" [\"ecoli\", \"ecoli_3h\", \"storm\", \"night\", \"n_flag\", \"truck\", \"purif\", \"well\", \"river\"]\n",
"\n",
"\n",
"def add_features(c):\n",
" \"\"\"์๊ฐ ์์๋ฅผ ์งํค๋(๊ณผ๊ฑฐ ๊ฐ๋ง ์ฐ๋) ๊ธ์์๋ณ ํน์ง: ๋ณํ๋, 24์๊ฐ ๊ธฐ์ค์ ๋๋น ํธ์ฐจ, 6์๊ฐ ๋ณ๋์ฑ, ๋์ฅ๊ท ๋์ ๋ฑ.\"\"\"\n",
" c = c.sort_values([\"source_id\", \"t\"]).reset_index(drop=True)\n",
" g = c.groupby(\"source_id\")\n",
" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n",
" c[f\"{p}_d1\"] = g[p].diff().fillna(0)\n",
" c[f\"{p}_d3\"] = g[p].diff(3).fillna(0)\n",
" base = g[p].transform(lambda s: s.shift(1).rolling(24, min_periods=4).median())\n",
" c[f\"{p}_dev\"] = (c[p] - base).fillna(0)\n",
" c[f\"{p}_sd6\"] = g[p].transform(lambda s: s.rolling(6, min_periods=2).std()).fillna(0)\n",
" c[\"ecoli_3h\"] = g[\"ecoli\"].transform(lambda s: s.rolling(3, min_periods=1).sum())\n",
" c[\"storm\"], c[\"night\"] = (c[\"cond\"] == \"storm\").astype(int), (c[\"cond\"] == \"night\").astype(int)\n",
" flags = [f\"{p}_flag\" for p in PARAMS if f\"{p}_flag\" in c]\n",
" c[\"n_flag\"] = sum(c[f].isin([\"stuck\", \"spike\", \"long_gap\", \"range\"]).astype(int) for f in flags) if flags else 0\n",
" for k, name in [(\"truck\", \"water_truck\"), (\"purif\", \"purification_unit\"), (\"well\", \"well\"), (\"river\", \"river_intake\")]:\n",
" c[k] = (c[\"source_type\"] == name).astype(int)\n",
" return c\n",
"\n",
"\n",
"def prepare(df):\n",
" c = add_features(clean(df))\n",
" c[\"rule\"] = rule_grade(c)\n",
" return c\n",
"\n",
"\n",
"# 1) ํ์ต(๊ฐ์ 2์ฃผ x 2์ธํธ)๊ณผ ํ๊ฐ(๋ฒค์น๋งํฌ seed 11)\n",
"train = pd.concat([prepare(make_dataset(seed=s, n_events=14, n_faults=14)) for s in (7, 8)], ignore_index=True)\n",
"test = prepare(make_dataset(seed=11, n_events=10, n_faults=14))\n",
"model = RandomForestClassifier(n_estimators=200, max_depth=10, class_weight=\"balanced_subsample\", random_state=0, n_jobs=1)\n",
"model.fit(train[FEATS], train[\"grade\"])\n",
"t0 = time.perf_counter()\n",
"test[\"ml\"] = model.predict(test[FEATS])\n",
"batch_ms = (time.perf_counter() - t0) * 1000\n",
"t0 = time.perf_counter()\n",
"for i in range(20):\n",
" model.predict(test[FEATS].iloc[[i]])\n",
"single_ms = (time.perf_counter() - t0) / 20 * 1000\n",
"\n",
"d = test[\"grade\"] == 2\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ํ์ต {len(train):,}๊ฑด / ํ๊ฐ {len(test):,}๊ฑด, ํน์ง {len(FEATS)}๊ฐ\")\n",
"for name, col in [(\"Baseline ๊ธฐ์ค์น\", \"rule\"), (\"๋๋ค ํฌ๋ ์คํธ\", \"ml\")]:\n",
" p = test[col]\n",
" print(f\"{name}: ์ ํ๋ {(p == test['grade']).mean():.1%}, ์ํ ์ฌํ์จ {((p == 2) & d).sum() / d.sum():.1%}, \"\n",
" f\"์ํ ์ค๊ฒฝ๋ณด {int(((p == 2) & ~d).sum())}๊ฑด\")\n",
"rec = ((test[\"ml\"] == 2) & d).sum() / d.sum()\n",
"print(f\"๋ณตํฉ ์ํ ์ฌํ์จ {rec:.1%} (๋ชฉํ 85% ์ด์: {'๋ฌ์ฑ' if rec >= 0.85 else '๋ฏธ๋ฌ'}), \"\n",
" f\"์ฒ๋ฆฌ ์ง์ฐ 1๊ฑด {single_ms:.1f}ms / ์ ์ฒด {batch_ms:.0f}ms (๋ชฉํ 200ms ์ดํ: {'๋ฌ์ฑ' if single_ms <= 200 else '๋ฏธ๋ฌ'})\")\n",
"print(\"Baseline์ด ๋์น ์์ํ ์ค๋ฅด๋ ์ค๊ธ์(creeping_metal) ์ค ๋ชจ๋ธ ๊ฒ์ถ:\",\n",
" f\"{int(((test['ml'] == 2) & d & (test['rule'] != 2) & (test['event'] == 'creeping_metal')).sum())}\"\n",
" f\"/{int((d & (test['rule'] != 2) & (test['event'] == 'creeping_metal')).sum())}์๊ฐ\")\n",
"\n",
"# 2) ์ค๊ฒฝ๋ณด ์ ๋ฐ ํจํด: ์ํ ์ค๊ฒฝ๋ณด๊ฐ ์ด๋ค ์ผ์ ๊ณ ์ฅ์์ ๋์๋๊ฐ -> ํน์ง ๊ณต๊ฐ ์ฌ์ค๊ณ ๋จ์\n",
"fa = test[(test[\"ml\"] == 2) & ~d]\n",
"print(\"๋ชจ๋ธ ์ํ ์ค๊ฒฝ๋ณด์ ์ผ์ ๊ณ ์ฅ ์ ํ:\", fa[\"fault\"].value_counts().to_dict(), \"-> drift(์์ํ ํ์ด์ง)๋ ๋ฏธ์
5 ๊ต์ฐจ ๊ฒ์ฆ์ผ๋ก ๋ณด์\")\n",
"imp = pd.Series(model.feature_importances_, FEATS).sort_values(ascending=False).head(6)\n",
"print(\"์ค์ ํน์ง Top6:\", \", \".join(f\"{k} {v:.2f}\" for k, v in imp.items()))\n",
"\n",
"# 3) ๋ณตํฉ ์ํ ์๋๋ฆฌ์ค 10์ข
: (๊ธ์์, ์ฌ๊ฑด) / ์ผ์ ๊ต๋์ ์ค์ผ์ด ์์ผ๋ฏ๋ก ์ํ ๊ฒฝ๋ณด๊ฐ ๋์ค๋ฉด ์ ๋๋ค\n",
"SCEN = [(\"ํ์ ์ ์
\", [(6, \"sewage\", 30, 14, 1.0)], []), (\"์ค๊ธ์ ๊ธ์ฆ\", [(4, \"metal\", 30, 14, 1.0)], []),\n",
" (\"์๋
์คํจ\", [(0, \"disinfect_fail\", 30, 14, 1.0)], []), (\"ํํ ์ค์ผ(pH ๊ธ๋ณ)\", [(2, \"chemical\", 30, 14, 1.0)], []),\n",
" (\"์ค๊ธ์ ์์ํ ์ฆ๊ฐ\", [(5, \"creeping_metal\", 20, 24, 1.0)], []),\n",
" (\"ํ์+์ค๊ธ์ ๋์\", [(7, \"sewage\", 30, 14, 1.0), (7, \"metal\", 34, 10, 1.0)], []),\n",
" (\"์ฝํ ํ์ ์ ์
(๊ฐ๋ 60%)\", [(3, \"sewage\", 30, 14, 0.6)], []),\n",
" (\"๊ต๋: ํ๋ ์ผ์ ๋๋ฆฌํํธ\", [], [(1, \"drift\", \"turbidity\", 24, 18)]),\n",
" (\"๊ต๋: ์๋ฅ์ผ์ ๋จ๋ฐ ์คํ์ดํฌ\", [], [(0, \"spike\", \"chlorine\", 40, 1)]),\n",
" (\"๊ต๋: ํต์ ๋จ์ (์ ํญ๋ชฉ ๊ฒฐ์ธก)\", [], [(6, \"dropout\", \"ph\", 30, 10)])]\n",
"rows = []\n",
"for name, ev, ft in SCEN:\n",
" c = prepare(make_dataset(days=3, seed=21, events=ev, faults=ft))\n",
" c[\"ml\"] = model.predict(c[FEATS])\n",
" if ev:\n",
" sid = SOURCES[ev[0][0]][0]; on = ev[0][2]\n",
" hit = c[(c[\"source_id\"] == sid) & (c[\"ml\"] == 2) & (c[\"t\"] >= on)]\n",
" lag = int(hit[\"t\"].min() - on) if len(hit) else None\n",
" ok = lag is not None and lag <= 8\n",
" rows.append((name, \"์ค์ผ\", f\"{lag}h\" if lag is not None else \"๋ฏธํ\", \"PASS\" if ok else \"FAIL\"))\n",
" else:\n",
" sid = SOURCES[ft[0][0]][0]\n",
" n = int(((c[\"source_id\"] == sid) & (c[\"ml\"] == 2)).sum())\n",
" rows.append((name, \"๊ต๋\", f\"์ํ ์ค๊ฒฝ๋ณด {n}h\", \"PASS\" if n <= 2 else \"FAIL\"))\n",
"res = pd.DataFrame(rows, columns=[\"์๋๋ฆฌ์ค\", \"๊ตฌ๋ถ\", \"ํ์ง ์ง์ฐ/์ค๊ฒฝ๋ณด\", \"ํ์ \"])\n",
"print(\"\\n[๋ณตํฉ ์ํ ์๋๋ฆฌ์ค 10์ข
]\")\n",
"print(res.to_string(index=False))\n",
"n_pass = int((res[\"ํ์ \"] == \"PASS\").sum())\n",
"print(f\"ํต๊ณผ {n_pass}/10 (์ค์ผ: ์์ ํ 8์๊ฐ ๋ด ์ํ ํ์ , ๊ต๋: ์ํ ์ค๊ฒฝ๋ณด 2์๊ฐ ์ดํ)\")\n",
"\n",
"plt.figure(figsize=(8, 4))\n",
"imp.iloc[::-1].plot.barh(color=\"tab:blue\")\n",
"plt.title(\"Random forest feature importance (top 6, synthetic)\"); plt.xlabel(\"importance\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
04 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ํ์ต 5,376๊ฑด / ํ๊ฐ 2,688๊ฑด, ํน์ง 29๊ฐ\n",
"Baseline ๊ธฐ์ค์น: ์ ํ๋ 97.9%, ์ํ ์ฌํ์จ 88.6%, ์ํ ์ค๊ฒฝ๋ณด 17๊ฑด\n",
"๋๋ค ํฌ๋ ์คํธ: ์ ํ๋ 99.3%, ์ํ ์ฌํ์จ 97.2%, ์ํ ์ค๊ฒฝ๋ณด 10๊ฑด\n",
"๋ณตํฉ ์ํ ์ฌํ์จ 97.2% (๋ชฉํ 85% ์ด์: ๋ฌ์ฑ), ์ฒ๋ฆฌ ์ง์ฐ 1๊ฑด 20.7ms / ์ ์ฒด 49ms (๋ชฉํ 200ms ์ดํ: ๋ฌ์ฑ)\n",
"Baseline์ด ๋์น ์์ํ ์ค๋ฅด๋ ์ค๊ธ์(creeping_metal) ์ค ๋ชจ๋ธ ๊ฒ์ถ: 17/22์๊ฐ\n",
"๋ชจ๋ธ ์ํ ์ค๊ฒฝ๋ณด์ ์ผ์ ๊ณ ์ฅ ์ ํ: {'drift': 10} -> drift(์์ํ ํ์ด์ง)๋ ๋ฏธ์
5 ๊ต์ฐจ ๊ฒ์ฆ์ผ๋ก ๋ณด์\n",
"์ค์ ํน์ง Top6: storm 0.17, turbidity 0.15, metal 0.10, turbidity_dev 0.09, ph 0.07, river 0.06\n",
"\n",
"[๋ณตํฉ ์ํ ์๋๋ฆฌ์ค 10์ข
]\n",
" ์๋๋ฆฌ์ค ๊ตฌ๋ถ ํ์ง ์ง์ฐ/์ค๊ฒฝ๋ณด ํ์ \n",
" ํ์ ์ ์
์ค์ผ 0h PASS\n",
" ์ค๊ธ์ ๊ธ์ฆ ์ค์ผ 0h PASS\n",
" ์๋
์คํจ ์ค์ผ 3h PASS\n",
" ํํ ์ค์ผ(pH ๊ธ๋ณ) ์ค์ผ 0h PASS\n",
" ์ค๊ธ์ ์์ํ ์ฆ๊ฐ ์ค์ผ 2h PASS\n",
" ํ์+์ค๊ธ์ ๋์ ์ค์ผ 0h PASS\n",
" ์ฝํ ํ์ ์ ์
(๊ฐ๋ 60%) ์ค์ผ 0h PASS\n",
" ๊ต๋: ํ๋ ์ผ์ ๋๋ฆฌํํธ ๊ต๋ ์ํ ์ค๊ฒฝ๋ณด 13h FAIL\n",
" ๊ต๋: ์๋ฅ์ผ์ ๋จ๋ฐ ์คํ์ดํฌ ๊ต๋ ์ํ ์ค๊ฒฝ๋ณด 0h PASS\n",
"๊ต๋: ํต์ ๋จ์ (์ ํญ๋ชฉ ๊ฒฐ์ธก) ๊ต๋ ์ํ ์ค๊ฒฝ๋ณด 0h PASS\n",
"ํต๊ณผ 9/10 (์ค์ผ: ์์ ํ 8์๊ฐ ๋ด ์ํ ํ์ , ๊ต๋: ์ํ ์ค๊ฒฝ๋ณด 2์๊ฐ ์ดํ)\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
05. ์ผ์ ์ค๋ฅ์ ์ํ ๋จ์ ์ค๊ฒฝ๋ณด ๋ฐฉ์ง ๋ฐ ์์ง ์์ ์ ๋ขฐ๋ ์ง์ ์ฐ์ถ ๋ชจ๋ ๊ฐ๋ฐ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ผ์ ์ค๋ฅ์ ์ํ ๋จ์ ์ค๊ฒฝ๋ณด ๋ฐฉ์ง ์ ๋ขฐ๋ ์ง์ ์ฐ์ถ ๋ชจ๋์ ๊ฐ๋ฐํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ผ์ ์ค๋ฅ๋ก ์ธํ ๊ธ์ ์ค๋จ ์ค๊ฒฝ๋ณด๋ฅผ ์ค์ด๋ ์์ง ์์ ์ ๋ขฐ๋ ์ง์์ HighยทMedยทLow ๋ฑ๊ธ ๋ชจ๋์ ๊ฐ๋ฐํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ํ ๊ฒฝ๋ณด๋ง๋ค ํญ๋ชฉ๋ณ ํธ์ฐจ์ ์ผ์น๋, ๊ฒฝ๋ณด ์ง์ ์๊ฐ, ์ผ์ ์ด์ ํ์, ๋๋ฆฌํํธ ์์ฌ ์ฌ๋ถ๋ฅผ ์ ์ํํด 0~100์ ์ ๋ขฐ๋๋ฅผ ๋ง๋ค๊ณ HighยทMedยทLow๋ก ๋๋๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/verification/wel03_confidence_engine.py` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ ๋ขฐ๋ ๋ฑ๊ธ(High/Med/Low) ๋ถ์ฌ ๊ธฐ์ค์ ๊ตฐ์ฌ์ ํ๋น์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์ค๊ฒฝ๋ณด์จ(FAR) 40% ์ด์ ๊ฐ์ถ, ์ ๋ขฐ๋ ์ค์ฝ์ด ์ฐ์ถ ์ผ๊ด์ฑ 90% ์ด์\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์ค๊ฒฝ๋ณด ํํฐ๋ง ์ ํ ๋น๊ต ์งํ ์ฐ์ถ ๋ฐ ํตํฉ ํ
์คํธ ์๋ฃ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ROC-AUC ๊ณก์ ๋ถ์ ๋ฐ ์๊ณ๊ฐ ๋ฏผ๊ฐ๋ ํ
์คํธ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์ํ ๊ฒฝ๋ณด๋ง๋ค ๋คํญ๋ชฉ ๋์ ๋ณํยท์ฐ์ ์ง์ยท์ผ์ ์ํยท๋จ์ผ ํญ๋ชฉ ๋๋ฆฌํํธ ์ฌ๋ถ๋ก ์์ง ์์ ์ ๋ขฐ๋ ์ง์(0~100, High/Med/Low)๋ฅผ ์ฐ์ถํด, ์ผ์ ์ค๋ฅ์ ์ํ ๊ธ์ ์ค๋จ(๋จ์) ์ค๊ฒฝ๋ณด๋ฅผ ์ค์ด๊ณ ROC-AUCยท์๊ณ๊ฐ ๋ฏผ๊ฐ๋ยท์ฐ์ถ ์ผ๊ด์ฑ์ ํ์ธํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.ensemble import RandomForestClassifier\n",
"from sklearn.metrics import roc_auc_score, roc_curve\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ผ์ ๊ธ์์ ๊ฐ์ ์์ง ์ผ์ ๋ฐ์ดํฐ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์์นยท๊ฒ์ฌ ๊ฒฐ๊ณผ ์์)\n",
"# ๊ฐ์ ๊ธ์์ 8๊ณณ x 14์ผ x 1์๊ฐ ๊ฐ๊ฒฉ. ์ค์ ์์ง(true_*)ยท์ผ์ ์ธก์ ๊ฐยท์ค์ผ ์ฌ๊ฑดยท์ผ์ ๊ณ ์ฅ ์ ๋ต์ ํจ๊ป ๋ง๋ ๋ค.\n",
"# ๊ธฐ์ค์น๋ ๊ต์ก์ฉ ๊ฐ์ ๊ฐ์ด๋ฉฐ, ์ค์ ์์ฉ ํ์ ์ ๊ณต์ธ ๊ฒ์ฌ ๊ฒฐ๊ณผ์ ์๋ฌด(์๋ฐฉ์ํ) ๋ด๋น ๊ธฐ์ค์ ๋ฐ๋ฅธ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"PARAMS = [\"ph\", \"turbidity\", \"chlorine\", \"metal\", \"ecoli\"]\n",
"SOURCES = [(\"W01\", \"water_truck\", 1.0, 2.0), (\"W02\", \"water_truck\", 3.5, 1.2), (\"W03\", \"purification_unit\", 2.2, 4.1),\n",
" (\"W04\", \"purification_unit\", 5.0, 3.3), (\"W05\", \"well\", 0.8, 5.6), (\"W06\", \"well\", 4.4, 6.0),\n",
" (\"W07\", \"river_intake\", 6.3, 1.8), (\"W08\", \"river_intake\", 6.9, 4.9)]\n",
"BASE = {\"water_truck\": (7.2, 0.25, 0.75, 1.5), \"purification_unit\": (7.0, 0.20, 0.85, 1.2),\n",
" \"well\": (6.8, 0.45, 0.50, 3.0), \"river_intake\": (7.3, 0.55, 0.60, 2.0)} # pH, ํ๋(NTU), ์๋ฅ์ผ์(mg/L), ์ค๊ธ์(ug/L)\n",
"LIMITS = {\"ph\": (5.8, 8.5), \"turbidity\": (1.0, 2.0), \"chlorine\": (0.2, 0.1), \"metal\": (5.0, 10.0), \"ecoli\": (1, 1)} # (์ฃผ์, ์ํ) ๊ฐ์ ๊ธฐ์ค\n",
"EVENT_TYPES = { # ์ค์ผ ์ฌ๊ฑด: ๋ณ์๋ณ ์ต๋ ๋ณํ๋, ์ต๋์น ๋๋ฌ ์๊ฐ(h)\n",
" \"sewage\": ({\"turbidity\": 2.8, \"chlorine\": -0.5, \"ecoli\": 30, \"ph\": -0.3}, 4),\n",
" \"metal\": ({\"metal\": 22.0, \"ph\": -0.5}, 4),\n",
" \"disinfect_fail\": ({\"chlorine\": -0.8, \"ecoli\": 6}, 6),\n",
" \"chemical\": ({\"ph\": 1.7, \"turbidity\": 1.0}, 3),\n",
" \"creeping_metal\": ({\"metal\": 13.0}, 18),\n",
"}\n",
"FAULT_TYPES = [\"stuck\", \"spike\", \"drift\", \"dropout\"]\n",
"NOISE = {\"ph\": 0.04, \"turbidity\": 0.06, \"chlorine\": 0.03, \"metal\": 0.35}\n",
"DIGITS = {\"ph\": 2, \"turbidity\": 2, \"chlorine\": 2, \"metal\": 1}\n",
"\n",
"\n",
"def make_dataset(days=14, seed=7, n_events=6, n_faults=10, events=None, faults=None, sources=None):\n",
" \"\"\"events: [(๊ธ์์ ๋ฒํธ, ์ฌ๊ฑด ์ ํ, ์์ ์๊ฐ h, ์ง์ h, ๊ฐ๋)], faults: [(๊ธ์์ ๋ฒํธ, ๊ณ ์ฅ ์ ํ, ํญ๋ชฉ, ์์ h, ์ง์ h)]\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" sources = sources or SOURCES\n",
" T = days * 24\n",
" tt = np.arange(T)\n",
" hour = tt % 24\n",
" night = (hour < 6) | (hour >= 20)\n",
" rain = np.zeros(T)\n",
" for _ in range(max(1, days // 4)):\n",
" s0, d = int(rng.integers(0, T - 14)), int(rng.integers(6, 14))\n",
" rain[s0:s0 + d] = np.maximum(rain[s0:s0 + d], rng.uniform(0.6, 1.0))\n",
" if events is None:\n",
" events = [(int(rng.integers(len(sources))), str(rng.choice(list(EVENT_TYPES))), int(rng.integers(min(24, T // 4), max(T // 4 + 1, T - 48))),\n",
" int(rng.integers(10, 30)), 1.0) for _ in range(n_events)]\n",
" if faults is None:\n",
" faults = [(int(rng.integers(len(sources))), str(rng.choice(FAULT_TYPES)), str(rng.choice([\"ph\", \"turbidity\", \"chlorine\", \"metal\"])),\n",
" int(rng.integers(12, max(13, T - 30))), int(rng.integers(6, 24))) for _ in range(n_faults)]\n",
" out = []\n",
" for si, (sid, stype, x, y) in enumerate(sources):\n",
" ph0, tb0, cl0, mt0 = BASE[stype]\n",
" tr = {\"ph\": ph0 + 0.08 * np.sin(2 * np.pi * hour / 24) + rng.normal(0, 0.03, T),\n",
" \"turbidity\": tb0 * (1 + 0.1 * rng.normal(0, 1, T)),\n",
" \"chlorine\": (cl0 * np.exp(-0.022 * hour) if stype == \"water_truck\" else cl0 + rng.normal(0, 0.03, T)),\n",
" \"metal\": mt0 + rng.normal(0, 0.2, T), \"ecoli\": np.zeros(T)}\n",
" if stype in (\"river_intake\", \"well\"):\n",
" tr[\"turbidity\"] = tr[\"turbidity\"] + rain * (0.9 if stype == \"river_intake\" else 0.45)\n",
" f_all, etype = np.zeros(T), np.array([\"none\"] * T, dtype=object)\n",
" for (s_i, et, onset, dur, scale) in events:\n",
" if s_i != si:\n",
" continue\n",
" delta, ramp = EVENT_TYPES[et]\n",
" f = np.clip((tt - onset + 1) / ramp, 0, 1) * ((tt >= onset) & (tt < onset + dur + ramp))\n",
" for k, v in delta.items():\n",
" tr[k] = tr[k] + v * f * scale\n",
" etype[f > 0] = et\n",
" f_all = np.maximum(f_all, f)\n",
" tr[\"chlorine\"] = np.clip(tr[\"chlorine\"], 0.02, None)\n",
" tr[\"turbidity\"] = np.clip(tr[\"turbidity\"], 0.05, None)\n",
" tr[\"metal\"] = np.clip(tr[\"metal\"], 0.1, None)\n",
" grade = np.where(f_all >= 0.2, 2, 0)\n",
" mild = (tr[\"turbidity\"] > LIMITS[\"turbidity\"][0]) | (tr[\"chlorine\"] < LIMITS[\"chlorine\"][0]) | (tr[\"metal\"] > LIMITS[\"metal\"][0])\n",
" grade = np.where((grade == 0) & mild, 1, grade)\n",
" ms = {}\n",
" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n",
" sd = NOISE[p] * np.where(night, 1.5, 1.0) * (tr[p] if p == \"turbidity\" else 1)\n",
" ms[p] = np.round(np.clip(tr[p] + rng.normal(0, 1, T) * sd, 0, None), DIGITS[p])\n",
" ms[\"ecoli\"] = (rng.poisson(tr[\"ecoli\"]) + (rng.random(T) < 0.004)).astype(float)\n",
" fault, fparam = np.array([\"none\"] * T, dtype=object), np.array([\"-\"] * T, dtype=object)\n",
" for (s_i, ft, p, s0, d) in faults:\n",
" if s_i != si:\n",
" continue\n",
" seg = slice(s0, min(T, s0 + d))\n",
" if ft == \"stuck\":\n",
" ms[p][seg] = ms[p][s0]\n",
" elif ft == \"spike\":\n",
" seg = slice(s0, s0 + 1)\n",
" ms[p][s0] = {\"ph\": 11.8, \"turbidity\": 38.0, \"chlorine\": 0.0, \"metal\": 75.0}[p]\n",
" elif ft == \"drift\":\n",
" ramp = np.linspace(0, 1, seg.stop - s0)\n",
" ms[p][seg] = np.clip(ms[p][seg] + ramp * {\"ph\": -1.6, \"turbidity\": 2.6, \"chlorine\": -0.75, \"metal\": 12.0}[p], 0, None).round(DIGITS[p])\n",
" elif ft == \"dropout\":\n",
" for q in PARAMS:\n",
" ms[q][seg] = np.nan\n",
" p = \"all\"\n",
" fault[seg], fparam[seg] = ft, p\n",
" df = pd.DataFrame({\"source_id\": sid, \"source_type\": stype, \"x_km\": x, \"y_km\": y, \"t\": tt,\n",
" \"cond\": np.where(rain > 0, \"storm\", np.where(night, \"night\", \"day\")),\n",
" **{p: ms[p] for p in PARAMS}, **{f\"true_{p}\": tr[p] for p in PARAMS},\n",
" \"event\": etype, \"fault\": fault, \"fault_param\": fparam, \"grade\": grade})\n",
" out.append(df)\n",
" return pd.concat(out, ignore_index=True)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌ ๊ณตํต ํจ์: ๋ฌผ๋ฆฌ ๋ฒ์ ์ ๊ฒ -> ๊ณ ์ ๊ฐ(stuck) ํ์ -> ๋จ๋ฐ ์คํ์ดํฌ ์ ๊ฑฐ -> ๊ฒฐ์ธก ๋ณด์ ----\n",
"PHYS = {\"ph\": (0, 14), \"turbidity\": (0, 1000), \"chlorine\": (0, 10), \"metal\": (0, 1000), \"ecoli\": (0, 100000)}\n",
"SPIKE_K = {\"day\": 6.0, \"night\": 7.0, \"storm\": 9.0} # ์ํฉ๋ณ(์ฃผ๊ฐ/์ผ๊ฐ/์
์ฒํ) ์คํ์ดํฌ ํ์ ๋ฐฐ์\n",
"MIN_JUMP = {\"ph\": 0.8, \"turbidity\": 1.5, \"chlorine\": 0.3, \"metal\": 6.0}\n",
"STUCK_RUN, SHORT_GAP = 6, 3\n",
"\n",
"\n",
"def clean(df, spike_k=None, min_jump=None):\n",
" \"\"\"๊ธ์์๋ณ ์๊ณ์ด์ ์ ์ ํ๊ณ ํญ๋ชฉ๋ณ ์ฒ๋ฆฌ ๋ด์ญ(<ํญ๋ชฉ>_flag: range/stuck/spike/interp/long_gap)์ ๋จ๊ธด๋ค.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 5] ์ผ์ ์ค๋ฅ์ ์ํ ๋จ์ ์ค๊ฒฝ๋ณด ๋ฐฉ์ง ๋ฐ ์์ง ์์ ์ ๋ขฐ๋ ์ง์ ์ฐ์ถ ๋ชจ๋ ๊ฐ๋ฐ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"GRADE_NAME = {0: \"์์ \", 1: \"์ฃผ์\", 2: \"์ํ\"}\n",
"\n",
"\n",
"def rule_grade(df):\n",
" \"\"\"๊ธฐ์ค์น 1์ฐจ ํํฐ: ์ํ(์์ฉ ๋ถ์ ํฉ ์์ฌ) = ํ ํญ๋ชฉ์ด๋ผ๋ ์ํ ๊ธฐ์ค ์ด๊ณผ, ์ฃผ์ = ์ฃผ์ ๊ธฐ์ค ์ด๊ณผ.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 5] ์ผ์ ์ค๋ฅ์ ์ํ ๋จ์ ์ค๊ฒฝ๋ณด ๋ฐฉ์ง ๋ฐ ์์ง ์์ ์ ๋ขฐ๋ ์ง์ ์ฐ์ถ ๋ชจ๋ ๊ฐ๋ฐ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def reasons(row):\n",
" r = []\n",
" if not LIMITS[\"ph\"][0] <= row[\"ph\"] <= LIMITS[\"ph\"][1]:\n",
" r.append(f\"pH {row['ph']:.2f}\")\n",
" if row[\"turbidity\"] > LIMITS[\"turbidity\"][0]:\n",
" r.append(f\"ํ๋ {row['turbidity']:.2f}NTU\")\n",
" if row[\"chlorine\"] < LIMITS[\"chlorine\"][0]:\n",
" r.append(f\"์๋ฅ์ผ์ {row['chlorine']:.2f}mg/L\")\n",
" if row[\"metal\"] > LIMITS[\"metal\"][0]:\n",
" r.append(f\"์ค๊ธ์ {row['metal']:.1f}ug/L\")\n",
" if row[\"ecoli\"] >= 1:\n",
" r.append(f\"๋์ฅ๊ท {row['ecoli']:.0f}\")\n",
" return \", \".join(r) or \"-\"\n",
"\n",
"\n",
"FEATS = [f\"{p}{s}\" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"] for s in [\"\", \"_d1\", \"_d3\", \"_dev\", \"_sd6\"]] + \\\n",
" [\"ecoli\", \"ecoli_3h\", \"storm\", \"night\", \"n_flag\", \"truck\", \"purif\", \"well\", \"river\"]\n",
"\n",
"\n",
"def add_features(c):\n",
" \"\"\"์๊ฐ ์์๋ฅผ ์งํค๋(๊ณผ๊ฑฐ ๊ฐ๋ง ์ฐ๋) ๊ธ์์๋ณ ํน์ง: ๋ณํ๋, 24์๊ฐ ๊ธฐ์ค์ ๋๋น ํธ์ฐจ, 6์๊ฐ ๋ณ๋์ฑ, ๋์ฅ๊ท ๋์ ๋ฑ.\"\"\"\n",
" c = c.sort_values([\"source_id\", \"t\"]).reset_index(drop=True)\n",
" g = c.groupby(\"source_id\")\n",
" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n",
" c[f\"{p}_d1\"] = g[p].diff().fillna(0)\n",
" c[f\"{p}_d3\"] = g[p].diff(3).fillna(0)\n",
" base = g[p].transform(lambda s: s.shift(1).rolling(24, min_periods=4).median())\n",
" c[f\"{p}_dev\"] = (c[p] - base).fillna(0)\n",
" c[f\"{p}_sd6\"] = g[p].transform(lambda s: s.rolling(6, min_periods=2).std()).fillna(0)\n",
" c[\"ecoli_3h\"] = g[\"ecoli\"].transform(lambda s: s.rolling(3, min_periods=1).sum())\n",
" c[\"storm\"], c[\"night\"] = (c[\"cond\"] == \"storm\").astype(int), (c[\"cond\"] == \"night\").astype(int)\n",
" flags = [f\"{p}_flag\" for p in PARAMS if f\"{p}_flag\" in c]\n",
" c[\"n_flag\"] = sum(c[f].isin([\"stuck\", \"spike\", \"long_gap\", \"range\"]).astype(int) for f in flags) if flags else 0\n",
" for k, name in [(\"truck\", \"water_truck\"), (\"purif\", \"purification_unit\"), (\"well\", \"well\"), (\"river\", \"river_intake\")]:\n",
" c[k] = (c[\"source_type\"] == name).astype(int)\n",
" return c\n",
"\n",
"SCALE = {\"ph\": 0.35, \"turbidity\": 0.6, \"chlorine\": 0.2, \"metal\": 3.0} # ํญ๋ชฉ๋ณ '์๋ฏธ ์๋ ํธ์ฐจ' ํฌ๊ธฐ(๊ฐ์ )\n",
"W = {\"corrob\": 40, \"persist\": 25, \"health\": 20, \"nondrift\": 15} # ์ ๋ขฐ๋ ๊ฐ์ค์น ๋งคํธ๋ฆญ์ค(ํฉ 100)\n",
"CUT = {\"High\": 60, \"Med\": 35} # High๋ง ๊ธ์ ์ค๋จ ๊ฒํ ์์ฒญ์ผ๋ก ์ฌ๋ฆฐ๋ค\n",
"\n",
"\n",
"def prepare(df):\n",
" c = add_features(clean(df))\n",
" c[\"rule\"] = rule_grade(c)\n",
" return c\n",
"\n",
"\n",
"def confidence(c, alert):\n",
" \"\"\"๊ฒฝ๋ณด(alert=True) ํ๋ง๋ค ์ ๋ขฐ๋ ์ ์์ ๊ตฌ์ฑ ์์๋ฅผ ๊ณ์ฐํ๋ค.\"\"\"\n",
" dev = pd.DataFrame({p: (c[f\"{p}_dev\"].abs() > SCALE[p]) | (c[f\"{p}_d3\"].abs() > SCALE[p]) for p in SCALE})\n",
" n_dev = dev.sum(axis=1) + (c[\"ecoli_3h\"] >= 2)\n",
" corrob = np.minimum(n_dev, 3) / 3\n",
" a = pd.Series(alert.astype(int), index=c.index)\n",
" run = a.groupby([c[\"source_id\"], (a != a.groupby(c[\"source_id\"]).shift()).cumsum()]).cumsum()\n",
" persist = np.minimum(run, 3) / 3\n",
" flagged = c[\"n_flag\"].groupby(c[\"source_id\"]).transform(lambda s: s.rolling(6, min_periods=1).max()) > 0\n",
" health = 1 - flagged.astype(float)\n",
" # ๋๋ฆฌํํธ ์์ฌ: ํ ํญ๋ชฉ๋ง 24์๊ฐ ๊ธฐ์ค์ ์์ ๋ฒ์ด๋๊ณ , 6์๊ฐ ๋ณ๋์ฑ์ด ์์ผ๋ฉฐ(๋งค๋๋ฌ์ด ๋ณํ) ๋์ฅ๊ท ์ด ์๋ค\n",
" g = c.groupby(\"source_id\")\n",
" jitter = {p: g[f\"{p}_d1\"].transform(lambda s: s.rolling(6, min_periods=3).std()).fillna(0) for p in SCALE}\n",
" smooth = pd.concat([jitter[p] < SCALE[p] * 0.5 for p in SCALE], axis=1).all(axis=1) # ๋ณํ๋์ด ์ผ์ = ๊ธฐ์ธ๊ธฐ๋ง ์๋ ๋ณํ\n",
" nondrift = 1 - ((n_dev <= 1) & smooth & (c[\"ecoli_3h\"] < 2)).astype(float)\n",
" score = W[\"corrob\"] * corrob + W[\"persist\"] * persist + W[\"health\"] * health + W[\"nondrift\"] * nondrift\n",
" level = np.where(score >= CUT[\"High\"], \"High\", np.where(score >= CUT[\"Med\"], \"Med\", \"Low\"))\n",
" return pd.DataFrame({\"score\": score.round(1), \"level\": level, \"n_dev\": n_dev}, index=c.index)\n",
"\n",
"\n",
"train = pd.concat([prepare(make_dataset(seed=s, n_events=14, n_faults=14)) for s in (7, 8)], ignore_index=True)\n",
"model = RandomForestClassifier(n_estimators=200, max_depth=10, class_weight=\"balanced_subsample\", random_state=0, n_jobs=1)\n",
"model.fit(train[FEATS], train[\"grade\"])\n",
"\n",
"test = prepare(make_dataset(seed=11, n_events=10, n_faults=14))\n",
"alert = ((test[\"rule\"] == 2) | (model.predict(test[FEATS]) == 2)).to_numpy() # Baseline ๋๋ ๋ชจ๋ธ์ด ์ํ ํ์ \n",
"conf = confidence(test, alert)\n",
"A = test[alert].join(conf)\n",
"A[\"real\"] = A[\"grade\"] == 2\n",
"far_before = (~A[\"real\"]).mean()\n",
"stop = A[A[\"level\"] == \"High\"]\n",
"far_after = (~stop[\"real\"]).mean()\n",
"cut = 1 - far_after / far_before\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ํ ๊ฒฝ๋ณด {len(A)}๊ฑด(์ค์ ์ํ {int(A['real'].sum())}๊ฑด, ์ค๊ฒฝ๋ณด {int((~A['real']).sum())}๊ฑด)\")\n",
"print(f\"๊ธ์ ์ค๋จ ๊ฒํ ๊ฒฝ๋ณด ์ค๊ฒฝ๋ณด์จ(FAR): ์ ๋ขฐ๋ ์ ์ฉ ์ {far_before:.1%} -> High๋ง ์์ {far_after:.1%} \"\n",
" f\"({cut:.0%} ๊ฐ์ถ, ๋ชฉํ 40% ์ด์: {'๋ฌ์ฑ' if cut >= 0.4 else '๋ฏธ๋ฌ'})\")\n",
"print(\"\\n[์ ๋ขฐ๋ ๋ฑ๊ธ๋ณ ๊ฒฝ๋ณด ์์ ์กฐ์น]\")\n",
"tab = A.groupby(\"level\").agg(๊ฒฝ๋ณด=(\"real\", \"size\"), ์ค์ ์ํ=(\"real\", \"sum\"), ํ๊ท ์ ์=(\"score\", \"mean\"))\n",
"tab[\"์กฐ์น\"] = tab.index.map({\"High\": \"๊ธ์ ์ค๋จ ๊ฒํ ์์ฒญ(HITL)\", \"Med\": \"์ฆ์ ์ฌ์ฑ์ยทํ์ฅ ํคํธ ๊ฒ์ฌ\", \"Low\": \"์ผ์ ์ ๊ฒยท์ฌ์ธก์ \"})\n",
"print(tab.reindex([\"High\", \"Med\", \"Low\"]).round(1).to_string())\n",
"print(\"์ค๊ฒฝ๋ณด์ ์์ธ(์ผ์ ๊ณ ์ฅ ์ ํ) x ๋ฑ๊ธ:\")\n",
"print(pd.crosstab(A.loc[~A[\"real\"], \"fault\"], A.loc[~A[\"real\"], \"level\"]).to_string())\n",
"miss_real = int((A[\"real\"] & (A[\"level\"] == \"Low\")).sum())\n",
"print(f\"์ค์ ์ํ์ธ๋ฐ Low๋ก ๋ฎ์ถฐ์ง ๊ฒฝ๋ณด {miss_real}๊ฑด -> Low๋ ์ฌ์ธก์ ๋์์ด๋ฏ๋ก ๋ฌด์๋์ง ์๋๋ค\")\n",
"\n",
"auc = roc_auc_score(A[\"real\"], A[\"score\"])\n",
"print(f\"\\nROC-AUC(์ ๋ขฐ๋ ์ ์๋ก ์ค์ ์ํ ๊ตฌ๋ถ) {auc:.3f}\")\n",
"print(\"[์๊ณ๊ฐ ๋ฏผ๊ฐ๋] High ๊ธฐ์ค / ์์ ๊ฒฝ๋ณด / ์ค๊ฒฝ๋ณด์จ / ์ค์ ์ํ ์์ ๋น์จ\")\n",
"for th in [45, 55, 60, 70, 80]:\n",
" s = A[A[\"score\"] >= th]\n",
" print(f\" {th:>3} / {len(s):>4} / {(~s['real']).mean():6.1%} / {s['real'].sum() / A['real'].sum():6.1%}\")\n",
"\n",
"# ์ฐ์ถ ์ผ๊ด์ฑ: ๊ฐ์ ์ฌ๊ฑดยท๊ณ ์ฅ ์กฐ๊ฑด์์ ์ธก์ ์ก์๋ง ๋ฐ๊ฟ 5ํ ์ฌ์ฐ์ถํ์ ๋ ์ค์ ์ํ ๊ฒฝ๋ณด์ ๋ฑ๊ธ ์ผ์น์จ\n",
"EV = [(6, \"sewage\", 30, 14, 1.0), (4, \"metal\", 60, 12, 1.0), (2, \"chemical\", 40, 10, 1.0), (5, \"creeping_metal\", 20, 24, 1.0)]\n",
"FT = [(1, \"drift\", \"turbidity\", 24, 18), (0, \"stuck\", \"chlorine\", 30, 10)]\n",
"levels = []\n",
"for s in range(5):\n",
" c = prepare(make_dataset(days=4, seed=100 + s, events=EV, faults=FT))\n",
" al = ((c[\"rule\"] == 2) | (model.predict(c[FEATS]) == 2)).to_numpy()\n",
" levels.append(confidence(c, al)[\"level\"].where(al, \"-\").to_numpy())\n",
"L = np.array(levels)\n",
"ref = pd.Series(L[0]).mode().iloc[0]\n",
"mask = (L != \"-\").sum(axis=0) >= 3\n",
"agree = np.mean([pd.Series(col[col != \"-\"]).value_counts(normalize=True).iloc[0] for col in L[:, mask].T])\n",
"print(f\"\\n์ก์ ์ฌํ 5ํ ๋ฑ๊ธ ์ผ์น์จ {agree:.1%} (๊ฒฝ๋ณด 3ํ ์ด์ ์์ {int(mask.sum())}๊ฐ, ๋ชฉํ 90% ์ด์: {'๋ฌ์ฑ' if agree >= 0.9 else '๋ฏธ๋ฌ'})\")\n",
"print(\"โป ์ ๋ขฐ๋ ๋ฑ๊ธ์ ํ๋จ ๋ณด์กฐ๋ค. ๊ธ์ ์ค๋จยท์ฌ๊ฐ๋ ์๋ฌด(์๋ฐฉ์ํ)ยทํ๊ฒฝ ๋ด๋น ํ์ธ๊ณผ ์งํ๊ด ์น์ธ์ผ๋ก ๊ฒฐ์ ํ๋ค.\")\n",
"\n",
"fpr, tpr, _ = roc_curve(A[\"real\"], A[\"score\"])\n",
"fig, ax = plt.subplots(1, 2, figsize=(10, 4))\n",
"ax[0].plot(fpr, tpr, color=\"tab:blue\"); ax[0].plot([0, 1], [0, 1], \"k--\", lw=0.8)\n",
"ax[0].set_title(f\"ROC of confidence score (AUC {auc:.2f})\"); ax[0].set_xlabel(\"false positive rate\"); ax[0].set_ylabel(\"true positive rate\")\n",
"ax[1].hist([A.loc[A[\"real\"], \"score\"], A.loc[~A[\"real\"], \"score\"]], bins=12, label=[\"real danger\", \"false alarm\"], color=[\"tab:red\", \"tab:gray\"])\n",
"ax[1].axvline(CUT[\"High\"], color=\"k\", ls=\"--\"); ax[1].set_xlabel(\"confidence score\"); ax[1].legend(); ax[1].set_title(\"Score distribution (synthetic)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
05 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ํ ๊ฒฝ๋ณด 262๊ฑด(์ค์ ์ํ 240๊ฑด, ์ค๊ฒฝ๋ณด 22๊ฑด)\n",
"๊ธ์ ์ค๋จ ๊ฒํ ๊ฒฝ๋ณด ์ค๊ฒฝ๋ณด์จ(FAR): ์ ๋ขฐ๋ ์ ์ฉ ์ 8.4% -> High๋ง ์์ 0.7% (91% ๊ฐ์ถ, ๋ชฉํ 40% ์ด์: ๋ฌ์ฑ)\n",
"\n",
"[์ ๋ขฐ๋ ๋ฑ๊ธ๋ณ ๊ฒฝ๋ณด ์์ ์กฐ์น]\n",
" ๊ฒฝ๋ณด ์ค์ ์ํ ํ๊ท ์ ์ ์กฐ์น\n",
"level \n",
"High 137 136 84.1 ๊ธ์ ์ค๋จ ๊ฒํ ์์ฒญ(HITL)\n",
"Med 112 100 53.5 ์ฆ์ ์ฌ์ฑ์ยทํ์ฅ ํคํธ ๊ฒ์ฌ\n",
"Low 13 4 27.5 ์ผ์ ์ ๊ฒยท์ฌ์ธก์ \n",
"์ค๊ฒฝ๋ณด์ ์์ธ(์ผ์ ๊ณ ์ฅ ์ ํ) x ๋ฑ๊ธ:\n",
"level High Low Med\n",
"fault \n",
"drift 1 0 10\n",
"none 0 9 2\n",
"์ค์ ์ํ์ธ๋ฐ Low๋ก ๋ฎ์ถฐ์ง ๊ฒฝ๋ณด 4๊ฑด -> Low๋ ์ฌ์ธก์ ๋์์ด๋ฏ๋ก ๋ฌด์๋์ง ์๋๋ค\n",
"\n",
"ROC-AUC(์ ๋ขฐ๋ ์ ์๋ก ์ค์ ์ํ ๊ตฌ๋ถ) 0.821\n",
"[์๊ณ๊ฐ ๋ฏผ๊ฐ๋] High ๊ธฐ์ค / ์์ ๊ฒฝ๋ณด / ์ค๊ฒฝ๋ณด์จ / ์ค์ ์ํ ์์ ๋น์จ\n",
" 45 / 243 / 4.9% / 96.2%\n",
" 55 / 210 / 5.2% / 82.9%\n",
" 60 / 137 / 0.7% / 56.7%\n",
" 70 / 134 / 0.7% / 55.4%\n",
" 80 / 88 / 1.1% / 36.2%\n",
"\n",
"์ก์ ์ฌํ 5ํ ๋ฑ๊ธ ์ผ์น์จ 93.6% (๊ฒฝ๋ณด 3ํ ์ด์ ์์ 99๊ฐ, ๋ชฉํ 90% ์ด์: ๋ฌ์ฑ)\n",
"โป ์ ๋ขฐ๋ ๋ฑ๊ธ์ ํ๋จ ๋ณด์กฐ๋ค. ๊ธ์ ์ค๋จยท์ฌ๊ฐ๋ ์๋ฌด(์๋ฐฉ์ํ)ยทํ๊ฒฝ ๋ด๋น ํ์ธ๊ณผ ์งํ๊ด ์น์ธ์ผ๋ก ๊ฒฐ์ ํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
06. ์ผ์ ๊ธ์์ฐจ ๋ฐ ์ ์ ์์ค๋ณ ์์ง ์ํ ์ค์๊ฐ ์๊ฐํ ๋์๋ณด๋ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ผ์ ๊ธ์ ์์ค๋ณ ์์ง ์ํ ์ค์๊ฐ ์๊ฐํ ๋์๋ณด๋๋ฅผ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ผ์ ๊ธ์์ฐจ์ ์ ์ ์์ค๋ณ ์์ง ์ํ๋ฅผ ์ค์๊ฐ์ฒ๋ผ ๊ฐฑ์ ํด ๋ณด์ฌ ์ฃผ๋ ์ํฉํ ๋์๋ณด๋๋ฅผ ๊ตฌํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ฐ์ ๊ธ์์ 8๊ณณ์ ์ธก์ ์คํธ๋ฆผ์ 1์๊ฐ ๋จ์๋ก ๊ฐฑ์ ํ๋ฉฐ ๊ธ์์ ์ํ ์ง๋, ํญ๋ชฉ๋ณ ๊ธฐ์ค ๋๋น ๋น์จ ํํธ๋งต, ๊ฒฝ๋ณด ๊ธ์์ ์ถ์ด, ์ํ ํํฉํ, ๊ธด๊ธ ๊ฒฝ๋ณด ๋ชฉ๋ก 5์ข
์ปดํฌ๋ํธ๋ฅผ ๊ทธ๋ฆฐ๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/ui/wel03_dashboard.py (๋๋ ์น UI ์ปดํฌ๋ํธ)` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ ์ ์์ ์ํฉํ์ผ๋ก์์ ์ง๊ด์ฑ ๋ฐ ์ธ์ง ๋ถํ ๊ฐ์๋\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ํ๋ฉด ๊ฐฑ์ ์ฃผ๊ธฐ 1์ด ์ด๋ด, UI ์๋ต์ฑ 100ms ์ด๋ด\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์ฃผ์ ์งํ ์๊ฐํ ์ปดํฌ๋ํธ 4์ข
์ด์ ๋ ๋๋ง ๋ฐ ์ค์๊ฐ ๋ฐ์ดํฐ ์ฐ๋\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ์ฌ์ฉ์ ์ธํฐ๋์
ํ
์คํธ ๋ฐ ๋ชจ์ ๋ฐ์ดํฐ ์คํธ๋ฆผ ๋ ๋๋ง ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"๊ฐ์ ๊ธ์์ 8๊ณณ์ ์์ง ์ธก์ ์คํธ๋ฆผ์ 1์๊ฐ ๋จ์๋ก ๊ฐฑ์ ํ๋ฉฐ ๊ธ์์ ์ํ ์ง๋, ํญ๋ชฉ๋ณ ๊ธฐ์ค ๋๋น ๋น์จ ํํธ๋งต, ๊ฒฝ๋ณด ๊ธ์์ ์ถ์ด, ์ํ ํํฉํ, ๊ธด๊ธ ๊ฒฝ๋ณด ๋ชฉ๋ก 5์ข
์ ๊ทธ๋ฆฌ๋ ์ํฉํ์ ๊ตฌํํ๊ณ , ๊ฐฑ์ ์ฃผ๊ธฐยท์๋ต ์๊ฐ์ ์ธก์ ํ๋ค(์ผ๊ฐ ๋ชจ๋ ์ง์).\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ผ์ ๊ธ์์ ๊ฐ์ ์์ง ์ผ์ ๋ฐ์ดํฐ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์์นยท๊ฒ์ฌ ๊ฒฐ๊ณผ ์์)\n",
"# ๊ฐ์ ๊ธ์์ 8๊ณณ x 14์ผ x 1์๊ฐ ๊ฐ๊ฒฉ. ์ค์ ์์ง(true_*)ยท์ผ์ ์ธก์ ๊ฐยท์ค์ผ ์ฌ๊ฑดยท์ผ์ ๊ณ ์ฅ ์ ๋ต์ ํจ๊ป ๋ง๋ ๋ค.\n",
"# ๊ธฐ์ค์น๋ ๊ต์ก์ฉ ๊ฐ์ ๊ฐ์ด๋ฉฐ, ์ค์ ์์ฉ ํ์ ์ ๊ณต์ธ ๊ฒ์ฌ ๊ฒฐ๊ณผ์ ์๋ฌด(์๋ฐฉ์ํ) ๋ด๋น ๊ธฐ์ค์ ๋ฐ๋ฅธ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"PARAMS = [\"ph\", \"turbidity\", \"chlorine\", \"metal\", \"ecoli\"]\n",
"SOURCES = [(\"W01\", \"water_truck\", 1.0, 2.0), (\"W02\", \"water_truck\", 3.5, 1.2), (\"W03\", \"purification_unit\", 2.2, 4.1),\n",
" (\"W04\", \"purification_unit\", 5.0, 3.3), (\"W05\", \"well\", 0.8, 5.6), (\"W06\", \"well\", 4.4, 6.0),\n",
" (\"W07\", \"river_intake\", 6.3, 1.8), (\"W08\", \"river_intake\", 6.9, 4.9)]\n",
"BASE = {\"water_truck\": (7.2, 0.25, 0.75, 1.5), \"purification_unit\": (7.0, 0.20, 0.85, 1.2),\n",
" \"well\": (6.8, 0.45, 0.50, 3.0), \"river_intake\": (7.3, 0.55, 0.60, 2.0)} # pH, ํ๋(NTU), ์๋ฅ์ผ์(mg/L), ์ค๊ธ์(ug/L)\n",
"LIMITS = {\"ph\": (5.8, 8.5), \"turbidity\": (1.0, 2.0), \"chlorine\": (0.2, 0.1), \"metal\": (5.0, 10.0), \"ecoli\": (1, 1)} # (์ฃผ์, ์ํ) ๊ฐ์ ๊ธฐ์ค\n",
"EVENT_TYPES = { # ์ค์ผ ์ฌ๊ฑด: ๋ณ์๋ณ ์ต๋ ๋ณํ๋, ์ต๋์น ๋๋ฌ ์๊ฐ(h)\n",
" \"sewage\": ({\"turbidity\": 2.8, \"chlorine\": -0.5, \"ecoli\": 30, \"ph\": -0.3}, 4),\n",
" \"metal\": ({\"metal\": 22.0, \"ph\": -0.5}, 4),\n",
" \"disinfect_fail\": ({\"chlorine\": -0.8, \"ecoli\": 6}, 6),\n",
" \"chemical\": ({\"ph\": 1.7, \"turbidity\": 1.0}, 3),\n",
" \"creeping_metal\": ({\"metal\": 13.0}, 18),\n",
"}\n",
"FAULT_TYPES = [\"stuck\", \"spike\", \"drift\", \"dropout\"]\n",
"NOISE = {\"ph\": 0.04, \"turbidity\": 0.06, \"chlorine\": 0.03, \"metal\": 0.35}\n",
"DIGITS = {\"ph\": 2, \"turbidity\": 2, \"chlorine\": 2, \"metal\": 1}\n",
"\n",
"\n",
"def make_dataset(days=14, seed=7, n_events=6, n_faults=10, events=None, faults=None, sources=None):\n",
" \"\"\"events: [(๊ธ์์ ๋ฒํธ, ์ฌ๊ฑด ์ ํ, ์์ ์๊ฐ h, ์ง์ h, ๊ฐ๋)], faults: [(๊ธ์์ ๋ฒํธ, ๊ณ ์ฅ ์ ํ, ํญ๋ชฉ, ์์ h, ์ง์ h)]\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" sources = sources or SOURCES\n",
" T = days * 24\n",
" tt = np.arange(T)\n",
" hour = tt % 24\n",
" night = (hour < 6) | (hour >= 20)\n",
" rain = np.zeros(T)\n",
" for _ in range(max(1, days // 4)):\n",
" s0, d = int(rng.integers(0, T - 14)), int(rng.integers(6, 14))\n",
" rain[s0:s0 + d] = np.maximum(rain[s0:s0 + d], rng.uniform(0.6, 1.0))\n",
" if events is None:\n",
" events = [(int(rng.integers(len(sources))), str(rng.choice(list(EVENT_TYPES))), int(rng.integers(min(24, T // 4), max(T // 4 + 1, T - 48))),\n",
" int(rng.integers(10, 30)), 1.0) for _ in range(n_events)]\n",
" if faults is None:\n",
" faults = [(int(rng.integers(len(sources))), str(rng.choice(FAULT_TYPES)), str(rng.choice([\"ph\", \"turbidity\", \"chlorine\", \"metal\"])),\n",
" int(rng.integers(12, max(13, T - 30))), int(rng.integers(6, 24))) for _ in range(n_faults)]\n",
" out = []\n",
" for si, (sid, stype, x, y) in enumerate(sources):\n",
" ph0, tb0, cl0, mt0 = BASE[stype]\n",
" tr = {\"ph\": ph0 + 0.08 * np.sin(2 * np.pi * hour / 24) + rng.normal(0, 0.03, T),\n",
" \"turbidity\": tb0 * (1 + 0.1 * rng.normal(0, 1, T)),\n",
" \"chlorine\": (cl0 * np.exp(-0.022 * hour) if stype == \"water_truck\" else cl0 + rng.normal(0, 0.03, T)),\n",
" \"metal\": mt0 + rng.normal(0, 0.2, T), \"ecoli\": np.zeros(T)}\n",
" if stype in (\"river_intake\", \"well\"):\n",
" tr[\"turbidity\"] = tr[\"turbidity\"] + rain * (0.9 if stype == \"river_intake\" else 0.45)\n",
" f_all, etype = np.zeros(T), np.array([\"none\"] * T, dtype=object)\n",
" for (s_i, et, onset, dur, scale) in events:\n",
" if s_i != si:\n",
" continue\n",
" delta, ramp = EVENT_TYPES[et]\n",
" f = np.clip((tt - onset + 1) / ramp, 0, 1) * ((tt >= onset) & (tt < onset + dur + ramp))\n",
" for k, v in delta.items():\n",
" tr[k] = tr[k] + v * f * scale\n",
" etype[f > 0] = et\n",
" f_all = np.maximum(f_all, f)\n",
" tr[\"chlorine\"] = np.clip(tr[\"chlorine\"], 0.02, None)\n",
" tr[\"turbidity\"] = np.clip(tr[\"turbidity\"], 0.05, None)\n",
" tr[\"metal\"] = np.clip(tr[\"metal\"], 0.1, None)\n",
" grade = np.where(f_all >= 0.2, 2, 0)\n",
" mild = (tr[\"turbidity\"] > LIMITS[\"turbidity\"][0]) | (tr[\"chlorine\"] < LIMITS[\"chlorine\"][0]) | (tr[\"metal\"] > LIMITS[\"metal\"][0])\n",
" grade = np.where((grade == 0) & mild, 1, grade)\n",
" ms = {}\n",
" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n",
" sd = NOISE[p] * np.where(night, 1.5, 1.0) * (tr[p] if p == \"turbidity\" else 1)\n",
" ms[p] = np.round(np.clip(tr[p] + rng.normal(0, 1, T) * sd, 0, None), DIGITS[p])\n",
" ms[\"ecoli\"] = (rng.poisson(tr[\"ecoli\"]) + (rng.random(T) < 0.004)).astype(float)\n",
" fault, fparam = np.array([\"none\"] * T, dtype=object), np.array([\"-\"] * T, dtype=object)\n",
" for (s_i, ft, p, s0, d) in faults:\n",
" if s_i != si:\n",
" continue\n",
" seg = slice(s0, min(T, s0 + d))\n",
" if ft == \"stuck\":\n",
" ms[p][seg] = ms[p][s0]\n",
" elif ft == \"spike\":\n",
" seg = slice(s0, s0 + 1)\n",
" ms[p][s0] = {\"ph\": 11.8, \"turbidity\": 38.0, \"chlorine\": 0.0, \"metal\": 75.0}[p]\n",
" elif ft == \"drift\":\n",
" ramp = np.linspace(0, 1, seg.stop - s0)\n",
" ms[p][seg] = np.clip(ms[p][seg] + ramp * {\"ph\": -1.6, \"turbidity\": 2.6, \"chlorine\": -0.75, \"metal\": 12.0}[p], 0, None).round(DIGITS[p])\n",
" elif ft == \"dropout\":\n",
" for q in PARAMS:\n",
" ms[q][seg] = np.nan\n",
" p = \"all\"\n",
" fault[seg], fparam[seg] = ft, p\n",
" df = pd.DataFrame({\"source_id\": sid, \"source_type\": stype, \"x_km\": x, \"y_km\": y, \"t\": tt,\n",
" \"cond\": np.where(rain > 0, \"storm\", np.where(night, \"night\", \"day\")),\n",
" **{p: ms[p] for p in PARAMS}, **{f\"true_{p}\": tr[p] for p in PARAMS},\n",
" \"event\": etype, \"fault\": fault, \"fault_param\": fparam, \"grade\": grade})\n",
" out.append(df)\n",
" return pd.concat(out, ignore_index=True)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌ ๊ณตํต ํจ์: ๋ฌผ๋ฆฌ ๋ฒ์ ์ ๊ฒ -> ๊ณ ์ ๊ฐ(stuck) ํ์ -> ๋จ๋ฐ ์คํ์ดํฌ ์ ๊ฑฐ -> ๊ฒฐ์ธก ๋ณด์ ----\n",
"PHYS = {\"ph\": (0, 14), \"turbidity\": (0, 1000), \"chlorine\": (0, 10), \"metal\": (0, 1000), \"ecoli\": (0, 100000)}\n",
"SPIKE_K = {\"day\": 6.0, \"night\": 7.0, \"storm\": 9.0} # ์ํฉ๋ณ(์ฃผ๊ฐ/์ผ๊ฐ/์
์ฒํ) ์คํ์ดํฌ ํ์ ๋ฐฐ์\n",
"MIN_JUMP = {\"ph\": 0.8, \"turbidity\": 1.5, \"chlorine\": 0.3, \"metal\": 6.0}\n",
"STUCK_RUN, SHORT_GAP = 6, 3\n",
"\n",
"\n",
"def clean(df, spike_k=None, min_jump=None):\n",
" \"\"\"๊ธ์์๋ณ ์๊ณ์ด์ ์ ์ ํ๊ณ ํญ๋ชฉ๋ณ ์ฒ๋ฆฌ ๋ด์ญ(<ํญ๋ชฉ>_flag: range/stuck/spike/interp/long_gap)์ ๋จ๊ธด๋ค.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 6] ์ผ์ ๊ธ์์ฐจ ๋ฐ ์ ์ ์์ค๋ณ ์์ง ์ํ ์ค์๊ฐ ์๊ฐํ ๋์๋ณด๋ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"GRADE_NAME = {0: \"์์ \", 1: \"์ฃผ์\", 2: \"์ํ\"}\n",
"\n",
"\n",
"def rule_grade(df):\n",
" \"\"\"๊ธฐ์ค์น 1์ฐจ ํํฐ: ์ํ(์์ฉ ๋ถ์ ํฉ ์์ฌ) = ํ ํญ๋ชฉ์ด๋ผ๋ ์ํ ๊ธฐ์ค ์ด๊ณผ, ์ฃผ์ = ์ฃผ์ ๊ธฐ์ค ์ด๊ณผ.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 6] ์ผ์ ๊ธ์์ฐจ ๋ฐ ์ ์ ์์ค๋ณ ์์ง ์ํ ์ค์๊ฐ ์๊ฐํ ๋์๋ณด๋ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def reasons(row):\n",
" r = []\n",
" if not LIMITS[\"ph\"][0] <= row[\"ph\"] <= LIMITS[\"ph\"][1]:\n",
" r.append(f\"pH {row['ph']:.2f}\")\n",
" if row[\"turbidity\"] > LIMITS[\"turbidity\"][0]:\n",
" r.append(f\"ํ๋ {row['turbidity']:.2f}NTU\")\n",
" if row[\"chlorine\"] < LIMITS[\"chlorine\"][0]:\n",
" r.append(f\"์๋ฅ์ผ์ {row['chlorine']:.2f}mg/L\")\n",
" if row[\"metal\"] > LIMITS[\"metal\"][0]:\n",
" r.append(f\"์ค๊ธ์ {row['metal']:.1f}ug/L\")\n",
" if row[\"ecoli\"] >= 1:\n",
" r.append(f\"๋์ฅ๊ท {row['ecoli']:.0f}\")\n",
" return \", \".join(r) or \"-\"\n",
"\n",
"\n",
"FEATS = [f\"{p}{s}\" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"] for s in [\"\", \"_d1\", \"_d3\", \"_dev\", \"_sd6\"]] + \\\n",
" [\"ecoli\", \"ecoli_3h\", \"storm\", \"night\", \"n_flag\", \"truck\", \"purif\", \"well\", \"river\"]\n",
"\n",
"\n",
"def add_features(c):\n",
" \"\"\"์๊ฐ ์์๋ฅผ ์งํค๋(๊ณผ๊ฑฐ ๊ฐ๋ง ์ฐ๋) ๊ธ์์๋ณ ํน์ง: ๋ณํ๋, 24์๊ฐ ๊ธฐ์ค์ ๋๋น ํธ์ฐจ, 6์๊ฐ ๋ณ๋์ฑ, ๋์ฅ๊ท ๋์ ๋ฑ.\"\"\"\n",
" c = c.sort_values([\"source_id\", \"t\"]).reset_index(drop=True)\n",
" g = c.groupby(\"source_id\")\n",
" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n",
" c[f\"{p}_d1\"] = g[p].diff().fillna(0)\n",
" c[f\"{p}_d3\"] = g[p].diff(3).fillna(0)\n",
" base = g[p].transform(lambda s: s.shift(1).rolling(24, min_periods=4).median())\n",
" c[f\"{p}_dev\"] = (c[p] - base).fillna(0)\n",
" c[f\"{p}_sd6\"] = g[p].transform(lambda s: s.rolling(6, min_periods=2).std()).fillna(0)\n",
" c[\"ecoli_3h\"] = g[\"ecoli\"].transform(lambda s: s.rolling(3, min_periods=1).sum())\n",
" c[\"storm\"], c[\"night\"] = (c[\"cond\"] == \"storm\").astype(int), (c[\"cond\"] == \"night\").astype(int)\n",
" flags = [f\"{p}_flag\" for p in PARAMS if f\"{p}_flag\" in c]\n",
" c[\"n_flag\"] = sum(c[f].isin([\"stuck\", \"spike\", \"long_gap\", \"range\"]).astype(int) for f in flags) if flags else 0\n",
" for k, name in [(\"truck\", \"water_truck\"), (\"purif\", \"purification_unit\"), (\"well\", \"well\"), (\"river\", \"river_intake\")]:\n",
" c[k] = (c[\"source_type\"] == name).astype(int)\n",
" return c\n",
"\n",
"DARK = True # ์ผ๊ฐ ์์ ๋ชจ๋(์ด๋์ด ๋ฐฐ๊ฒฝ)\n",
"NOW = 290 # ์ํฉํ ๊ธฐ์ค ์๊ฐ(๋ชจ์ ์์ ํ ๊ฒฝ๊ณผ ์๊ฐ h)\n",
"COLOR = {0: \"tab:green\", 1: \"gold\", 2: \"red\"}\n",
"\n",
"data = make_dataset(days=14, seed=11, n_events=10, n_faults=14)\n",
"data = data[data[\"t\"] <= NOW]\n",
"\n",
"\n",
"def ratio(row):\n",
" \"\"\"ํญ๋ชฉ๋ณ '์ํ ๊ธฐ์ค ๋๋น ๋น์จ'(1 ์ด์์ด๋ฉด ์ํ). ์๋ฅ์ผ์ยทpH๋ ๋ฐฉํฅ์ ๋ง์ถฐ ํ์ฐํ๋ค.\"\"\"\n",
" lo, hi = LIMITS[\"ph\"]\n",
" return {\"pH\": max((row[\"ph\"] - 7.0) / (hi - 7.0), (7.0 - row[\"ph\"]) / (7.0 - lo)),\n",
" \"turb\": row[\"turbidity\"] / LIMITS[\"turbidity\"][1], \"Cl\": LIMITS[\"chlorine\"][1] / max(row[\"chlorine\"], 0.01),\n",
" \"metal\": row[\"metal\"] / LIMITS[\"metal\"][1], \"E.coli\": row[\"ecoli\"] / LIMITS[\"ecoli\"][1]}\n",
"\n",
"\n",
"# 1) ์ค์๊ฐ ๊ฐฑ์ ๋ชจ์: ์ต๊ทผ 24์๊ฐ ์ฐฝ์ ์ ์งํ๋ฉฐ 1์๊ฐ์ฉ ์ ์ธก์ ๊ฐ์ด ๋ค์ด์ฌ ๋ ์ํ ๊ณ์ฐ ์๊ฐ\n",
"window = data[data[\"t\"] <= NOW - 24].copy()\n",
"upd = []\n",
"for h in range(NOW - 23, NOW + 1):\n",
" t0 = time.perf_counter()\n",
" window = pd.concat([window[window[\"t\"] > h - 48], data[data[\"t\"] == h]])\n",
" c = clean(window)\n",
" c[\"g\"] = rule_grade(c)\n",
" latest = c[c[\"t\"] == h]\n",
" upd.append((time.perf_counter() - t0) * 1000)\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ค์๊ฐ ๊ฐฑ์ ๋ชจ์ 24ํ: 1ํ ํ๊ท {np.mean(upd):.1f}ms, ์ต๋ {np.max(upd):.1f}ms \"\n",
" f\"(๊ฐฑ์ ์ฃผ๊ธฐ 1์ด ์ด๋ด: {'๋ฌ์ฑ' if max(upd) < 1000 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# 2) ํ์ฌ ์ํํ\n",
"state = latest.set_index(\"source_id\")\n",
"hist = c.groupby(\"source_id\")[\"g\"].apply(lambda s: int((s.tail(6) == 2).sum()))\n",
"board = pd.DataFrame({\"์ ํ\": state[\"source_type\"], \"์ํ\": state[\"g\"].map(GRADE_NAME), \"์ต๊ทผ6h ์ํ\": hist,\n",
" \"๊ทผ๊ฑฐ\": [reasons(r) for _, r in state.iterrows()]})\n",
"print(f\"\\n[D+{NOW // 24:02d} {NOW % 24:02d}์ ๊ธ์์ ์ํํ]\")\n",
"print(board.to_string())\n",
"alerts = board[(board[\"์ํ\"] != \"์์ \") | (board[\"์ต๊ทผ6h ์ํ\"] > 0)]\n",
"print(f\"๊ธด๊ธ ๊ฒฝ๋ณด ๊ฐ์กฐ ๋์ {len(alerts)}๊ณณ: {', '.join(alerts.index)}\")\n",
"\n",
"# 3) ์ํฉํ ๊ทธ๋ฆฌ๊ธฐ(์ปดํฌ๋ํธ 5์ข
) + ๋ ๋๋ง ์๊ฐ\n",
"plt.style.use(\"dark_background\" if DARK else \"default\")\n",
"t0 = time.perf_counter()\n",
"fig = plt.figure(figsize=(13, 7.5))\n",
"gs = fig.add_gridspec(2, 3)\n",
"ax1 = fig.add_subplot(gs[0, 0])\n",
"for sid, r in state.iterrows():\n",
" ax1.scatter(r[\"x_km\"], r[\"y_km\"], s=260, color=COLOR[r[\"g\"]], edgecolor=\"white\" if r[\"g\"] == 2 else \"none\", lw=2)\n",
" ax1.annotate(sid, (r[\"x_km\"], r[\"y_km\"]), xytext=(6, 6), textcoords=\"offset points\", fontsize=8)\n",
"ax1.set_title(\"Water source status map (virtual grid km)\"); ax1.set_xlim(0, 7.5); ax1.set_ylim(0, 7)\n",
"ax2 = fig.add_subplot(gs[0, 1])\n",
"R = pd.DataFrame([ratio(r) for _, r in state.iterrows()], index=state.index)\n",
"im = ax2.imshow(R.clip(0, 2).to_numpy(), cmap=\"RdYlGn_r\", vmin=0, vmax=2, aspect=\"auto\")\n",
"ax2.set_xticks(range(R.shape[1]), R.columns); ax2.set_yticks(range(len(R)), R.index)\n",
"ax2.set_title(\"Ratio to danger limit (>=1 danger)\"); fig.colorbar(im, ax=ax2, fraction=0.046)\n",
"ax3 = fig.add_subplot(gs[0, 2])\n",
"cnt = state[\"g\"].value_counts().reindex([0, 1, 2], fill_value=0)\n",
"ax3.bar([\"safe\", \"caution\", \"danger\"], cnt.values, color=[COLOR[i] for i in range(3)])\n",
"ax3.set_title(f\"Status count at hour {NOW}\")\n",
"ax4 = fig.add_subplot(gs[1, :2])\n",
"focus = alerts.index[0] if len(alerts) else state.index[0]\n",
"s = c[c[\"source_id\"] == focus]\n",
"for p, col in [(\"turbidity\", \"tab:cyan\"), (\"chlorine\", \"tab:orange\"), (\"metal\", \"violet\")]:\n",
" ax4.plot(s[\"t\"], s[p] / LIMITS[p][1] if p != \"chlorine\" else LIMITS[p][1] / s[p].clip(lower=0.01), label=p, color=col)\n",
"ax4.axhline(1, color=\"red\", ls=\"--\", lw=1); ax4.set_ylim(0, 3); ax4.legend(loc=\"upper left\")\n",
"ax4.set_title(f\"{focus} last 48h ratio to danger limit\"); ax4.set_xlabel(\"hour\")\n",
"ax5 = fig.add_subplot(gs[1, 2]); ax5.axis(\"off\")\n",
"lines = [f\"{sid}: {('DANGER' if r['์ํ'] == '์ํ' else 'CHECK')} 6h danger {r['์ต๊ทผ6h ์ํ']}\" for sid, r in alerts.iterrows()]\n",
"ax5.text(0, 1, \"ALERT LIST\\n\" + \"\\n\".join(lines or [\"no alert\"]), va=\"top\", family=\"monospace\", fontsize=10,\n",
" color=\"red\" if len(alerts) else \"white\" if DARK else \"black\")\n",
"fig.suptitle(\"Field water quality risk board (synthetic, decision support only)\")\n",
"fig.tight_layout()\n",
"fig.canvas.draw()\n",
"draw_ms = (time.perf_counter() - t0) * 1000\n",
"\n",
"# 4) UI ์๋ต์ฑ: ๊ธ์์ ํด๋ฆญ(์ ํ) ์ ์์ธ ์กฐํ ์๋ต ์๊ฐ\n",
"resp = []\n",
"for sid in state.index:\n",
" t0 = time.perf_counter()\n",
" d = c[c[\"source_id\"] == sid].tail(6)\n",
" _ = (d[[\"ph\", \"turbidity\", \"chlorine\", \"metal\", \"ecoli\"]].agg([\"min\", \"max\"]), reasons(d.iloc[-1]))\n",
" resp.append((time.perf_counter() - t0) * 1000)\n",
"print(f\"\\n์๊ฐํ ์ปดํฌ๋ํธ 5์ข
๋ ๋๋ง {draw_ms:.0f}ms, ๊ธ์์ ์์ธ ์กฐํ ์๋ต ํ๊ท {np.mean(resp):.1f}ms \"\n",
" f\"(UI ์๋ต์ฑ 100ms ์ด๋ด: {'๋ฌ์ฑ' if max(resp) < 100 else '๋ฏธ๋ฌ'})\")\n",
"print(\"โป ์ํฉํ์ ์ํ ํ์๋ ๊ฐ์ ๊ธฐ์ค์น ๊ธฐ๋ฐ ํ๋จ ๋ณด์กฐ์ด๋ฉฐ, ๊ธ์ ์ค๋จ ์ฌ๋ถ๋ ์๋ฌด(์๋ฐฉ์ํ)ยทํ๊ฒฝ ๋ด๋น ํ์ธ ํ ์งํ๊ด์ด ๊ฒฐ์ ํ๋ค.\")\n",
"plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
06 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ค์๊ฐ ๊ฐฑ์ ๋ชจ์ 24ํ: 1ํ ํ๊ท 158.2ms, ์ต๋ 205.7ms (๊ฐฑ์ ์ฃผ๊ธฐ 1์ด ์ด๋ด: ๋ฌ์ฑ)\n",
"\n",
"[D+12 02์ ๊ธ์์ ์ํํ]\n",
" ์ ํ ์ํ ์ต๊ทผ6h ์ํ ๊ทผ๊ฑฐ\n",
"source_id \n",
"W01 water_truck ์์ 0 -\n",
"W02 water_truck ์์ 0 -\n",
"W03 purification_unit ์์ 0 -\n",
"W04 purification_unit ์ํ 3 pH 8.72, ํ๋ 1.31NTU\n",
"W05 well ์์ 0 -\n",
"W06 well ์์ 0 -\n",
"W07 river_intake ์ฃผ์ 0 ์ค๊ธ์ 8.7ug/L\n",
"W08 river_intake ์ํ 6 ์ค๊ธ์ 24.1ug/L\n",
"๊ธด๊ธ ๊ฒฝ๋ณด ๊ฐ์กฐ ๋์ 3๊ณณ: W04, W07, W08\n",
"\n",
"์๊ฐํ ์ปดํฌ๋ํธ 5์ข
๋ ๋๋ง 581ms, ๊ธ์์ ์์ธ ์กฐํ ์๋ต ํ๊ท 5.7ms (UI ์๋ต์ฑ 100ms ์ด๋ด: ๋ฌ์ฑ)\n",
"โป ์ํฉํ์ ์ํ ํ์๋ ๊ฐ์ ๊ธฐ์ค์น ๊ธฐ๋ฐ ํ๋จ ๋ณด์กฐ์ด๋ฉฐ, ๊ธ์ ์ค๋จ ์ฌ๋ถ๋ ์๋ฌด(์๋ฐฉ์ํ)ยทํ๊ฒฝ ๋ด๋น ํ์ธ ํ ์งํ๊ด์ด ๊ฒฐ์ ํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
07. ์์ง ์ํ ๊ฒฝ๋ณด ๋ฐ์ ์ ๊ธ์๊ด ๊ธด๊ธ ์ฑ์ ๊ฒ์ฌ ๋ฐ ๊ธ์ ์ค๋จ HITL ๊ฒฐ์ฌ์ ์ค๊ณ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์์ง ์ํ ๊ฒฝ๋ณด ๋ฐ์ ์ ๊ธ์ ์ค๋จ ๋ฐ ์ฌ๊ฒ์ฌ HITL ๊ฒฐ์ฌ์ ์ ์ค๊ณํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์์ง ์ํ ๊ฒฝ๋ณด ๋ฐ์ ์ ๊ธด๊ธ ์ฑ์ ๊ฒ์ฌ์ ๊ธ์ ์ค๋จ์ ์ฌ๋์ด ๊ฒฐ์ ํ๋ HITL ๊ฒฐ์ฌ์ ์ ์ค๊ณํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ฒฝ๋ณด ์ฌ๊ฑด์ ๊ฒํ ํ์ ์ฐ์ ์์๋ก ๋ฑ๋กํ๊ณ ์ธก์ ๋ณ ์ฑ์, ์๋ฐฉ์ํ ๋ด๋น ๊ฒํ , ์งํ๊ด ๊ฒฐ์ฌ์ ์์๋ก ๊ธ์ ์ค๋จ ๋๋ ์ ์ง๋ฅผ ๊ฒฐ์ ํ๋ ์ํ ๊ธฐ๊ณ ๊ตฌํํ๊ณ ๋์์ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/hitl/wel03_approval_workflow.py ๋ฐ docs/wel03_hitl_guide.md` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์งํ ์ฑ
์ ๋ถ๊ณ์ ๋ช
ํํ ๋ฐ ์ฑ๊ธํ ์๋ ๋จ์ ๋ฐฉ์ง ์ฒด๊ณ ๋ถํฉ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์ธ๊ฐ ๊ฒํ ํ(Queue) ๋๊ธฐ์๊ฐ ๋ฐ ์น์ธ/๋ฐ๋ ค ์ฒ๋ฆฌ ๋ก๊น
์ฑ๊ณต๋ฅ 100%\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์น์ธ/๋ฐ๋ ค ์ธํฐํ์ด์ค ๋์ ๋ฐ ๊ฐ์ฌ ๋ก๊ทธ(Audit Log) ์์ฑ ๊ฒ์ฆ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ๋ชจ์ ์กฐ์์ ์น์ธ/๋ฐ๋ ค ์๋ฎฌ๋ ์ด์
๋ฐ ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์์ง ์ํ ๊ฒฝ๋ณด๊ฐ ๋๋ฉด ์์คํ
์ ๊ธ์ ์ค๋จ์ ์๋ ํ์ ํ์ง ์๊ณ ์ฐ์ ์์ ๊ฒํ ํ์ ์ฌ๋ฆฌ๋ฉฐ, ์ธก์ ๋ด๋น ๊ธด๊ธ ์ฑ์ โ ์๋ฐฉ์ํ ๋ด๋น ๊ฒํ โ ์งํ๊ด ๊ธ์ ์ค๋จ ์น์ธยท๋ฐ๋ ค(๋ฐ๋ ค ์ฌ์ ํ์) ๊ฒฐ์ฌ์ ๊ณผ ๊ถํ ํต์ , ํด์ ์ฒด์ธ ๊ฐ์ฌ ๋ก๊ทธ, ํ ๋๊ธฐ์๊ฐ์ ์๋ฎฌ๋ ์ด์
ํ๋ค.\"\"\"\n",
"import hashlib\n",
"import json\n",
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ผ์ ๊ธ์์ ๊ฐ์ ์์ง ์ผ์ ๋ฐ์ดํฐ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์์นยท๊ฒ์ฌ ๊ฒฐ๊ณผ ์์)\n",
"# ๊ฐ์ ๊ธ์์ 8๊ณณ x 14์ผ x 1์๊ฐ ๊ฐ๊ฒฉ. ์ค์ ์์ง(true_*)ยท์ผ์ ์ธก์ ๊ฐยท์ค์ผ ์ฌ๊ฑดยท์ผ์ ๊ณ ์ฅ ์ ๋ต์ ํจ๊ป ๋ง๋ ๋ค.\n",
"# ๊ธฐ์ค์น๋ ๊ต์ก์ฉ ๊ฐ์ ๊ฐ์ด๋ฉฐ, ์ค์ ์์ฉ ํ์ ์ ๊ณต์ธ ๊ฒ์ฌ ๊ฒฐ๊ณผ์ ์๋ฌด(์๋ฐฉ์ํ) ๋ด๋น ๊ธฐ์ค์ ๋ฐ๋ฅธ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"PARAMS = [\"ph\", \"turbidity\", \"chlorine\", \"metal\", \"ecoli\"]\n",
"SOURCES = [(\"W01\", \"water_truck\", 1.0, 2.0), (\"W02\", \"water_truck\", 3.5, 1.2), (\"W03\", \"purification_unit\", 2.2, 4.1),\n",
" (\"W04\", \"purification_unit\", 5.0, 3.3), (\"W05\", \"well\", 0.8, 5.6), (\"W06\", \"well\", 4.4, 6.0),\n",
" (\"W07\", \"river_intake\", 6.3, 1.8), (\"W08\", \"river_intake\", 6.9, 4.9)]\n",
"BASE = {\"water_truck\": (7.2, 0.25, 0.75, 1.5), \"purification_unit\": (7.0, 0.20, 0.85, 1.2),\n",
" \"well\": (6.8, 0.45, 0.50, 3.0), \"river_intake\": (7.3, 0.55, 0.60, 2.0)} # pH, ํ๋(NTU), ์๋ฅ์ผ์(mg/L), ์ค๊ธ์(ug/L)\n",
"LIMITS = {\"ph\": (5.8, 8.5), \"turbidity\": (1.0, 2.0), \"chlorine\": (0.2, 0.1), \"metal\": (5.0, 10.0), \"ecoli\": (1, 1)} # (์ฃผ์, ์ํ) ๊ฐ์ ๊ธฐ์ค\n",
"EVENT_TYPES = { # ์ค์ผ ์ฌ๊ฑด: ๋ณ์๋ณ ์ต๋ ๋ณํ๋, ์ต๋์น ๋๋ฌ ์๊ฐ(h)\n",
" \"sewage\": ({\"turbidity\": 2.8, \"chlorine\": -0.5, \"ecoli\": 30, \"ph\": -0.3}, 4),\n",
" \"metal\": ({\"metal\": 22.0, \"ph\": -0.5}, 4),\n",
" \"disinfect_fail\": ({\"chlorine\": -0.8, \"ecoli\": 6}, 6),\n",
" \"chemical\": ({\"ph\": 1.7, \"turbidity\": 1.0}, 3),\n",
" \"creeping_metal\": ({\"metal\": 13.0}, 18),\n",
"}\n",
"FAULT_TYPES = [\"stuck\", \"spike\", \"drift\", \"dropout\"]\n",
"NOISE = {\"ph\": 0.04, \"turbidity\": 0.06, \"chlorine\": 0.03, \"metal\": 0.35}\n",
"DIGITS = {\"ph\": 2, \"turbidity\": 2, \"chlorine\": 2, \"metal\": 1}\n",
"\n",
"\n",
"def make_dataset(days=14, seed=7, n_events=6, n_faults=10, events=None, faults=None, sources=None):\n",
" \"\"\"events: [(๊ธ์์ ๋ฒํธ, ์ฌ๊ฑด ์ ํ, ์์ ์๊ฐ h, ์ง์ h, ๊ฐ๋)], faults: [(๊ธ์์ ๋ฒํธ, ๊ณ ์ฅ ์ ํ, ํญ๋ชฉ, ์์ h, ์ง์ h)]\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" sources = sources or SOURCES\n",
" T = days * 24\n",
" tt = np.arange(T)\n",
" hour = tt % 24\n",
" night = (hour < 6) | (hour >= 20)\n",
" rain = np.zeros(T)\n",
" for _ in range(max(1, days // 4)):\n",
" s0, d = int(rng.integers(0, T - 14)), int(rng.integers(6, 14))\n",
" rain[s0:s0 + d] = np.maximum(rain[s0:s0 + d], rng.uniform(0.6, 1.0))\n",
" if events is None:\n",
" events = [(int(rng.integers(len(sources))), str(rng.choice(list(EVENT_TYPES))), int(rng.integers(min(24, T // 4), max(T // 4 + 1, T - 48))),\n",
" int(rng.integers(10, 30)), 1.0) for _ in range(n_events)]\n",
" if faults is None:\n",
" faults = [(int(rng.integers(len(sources))), str(rng.choice(FAULT_TYPES)), str(rng.choice([\"ph\", \"turbidity\", \"chlorine\", \"metal\"])),\n",
" int(rng.integers(12, max(13, T - 30))), int(rng.integers(6, 24))) for _ in range(n_faults)]\n",
" out = []\n",
" for si, (sid, stype, x, y) in enumerate(sources):\n",
" ph0, tb0, cl0, mt0 = BASE[stype]\n",
" tr = {\"ph\": ph0 + 0.08 * np.sin(2 * np.pi * hour / 24) + rng.normal(0, 0.03, T),\n",
" \"turbidity\": tb0 * (1 + 0.1 * rng.normal(0, 1, T)),\n",
" \"chlorine\": (cl0 * np.exp(-0.022 * hour) if stype == \"water_truck\" else cl0 + rng.normal(0, 0.03, T)),\n",
" \"metal\": mt0 + rng.normal(0, 0.2, T), \"ecoli\": np.zeros(T)}\n",
" if stype in (\"river_intake\", \"well\"):\n",
" tr[\"turbidity\"] = tr[\"turbidity\"] + rain * (0.9 if stype == \"river_intake\" else 0.45)\n",
" f_all, etype = np.zeros(T), np.array([\"none\"] * T, dtype=object)\n",
" for (s_i, et, onset, dur, scale) in events:\n",
" if s_i != si:\n",
" continue\n",
" delta, ramp = EVENT_TYPES[et]\n",
" f = np.clip((tt - onset + 1) / ramp, 0, 1) * ((tt >= onset) & (tt < onset + dur + ramp))\n",
" for k, v in delta.items():\n",
" tr[k] = tr[k] + v * f * scale\n",
" etype[f > 0] = et\n",
" f_all = np.maximum(f_all, f)\n",
" tr[\"chlorine\"] = np.clip(tr[\"chlorine\"], 0.02, None)\n",
" tr[\"turbidity\"] = np.clip(tr[\"turbidity\"], 0.05, None)\n",
" tr[\"metal\"] = np.clip(tr[\"metal\"], 0.1, None)\n",
" grade = np.where(f_all >= 0.2, 2, 0)\n",
" mild = (tr[\"turbidity\"] > LIMITS[\"turbidity\"][0]) | (tr[\"chlorine\"] < LIMITS[\"chlorine\"][0]) | (tr[\"metal\"] > LIMITS[\"metal\"][0])\n",
" grade = np.where((grade == 0) & mild, 1, grade)\n",
" ms = {}\n",
" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n",
" sd = NOISE[p] * np.where(night, 1.5, 1.0) * (tr[p] if p == \"turbidity\" else 1)\n",
" ms[p] = np.round(np.clip(tr[p] + rng.normal(0, 1, T) * sd, 0, None), DIGITS[p])\n",
" ms[\"ecoli\"] = (rng.poisson(tr[\"ecoli\"]) + (rng.random(T) < 0.004)).astype(float)\n",
" fault, fparam = np.array([\"none\"] * T, dtype=object), np.array([\"-\"] * T, dtype=object)\n",
" for (s_i, ft, p, s0, d) in faults:\n",
" if s_i != si:\n",
" continue\n",
" seg = slice(s0, min(T, s0 + d))\n",
" if ft == \"stuck\":\n",
" ms[p][seg] = ms[p][s0]\n",
" elif ft == \"spike\":\n",
" seg = slice(s0, s0 + 1)\n",
" ms[p][s0] = {\"ph\": 11.8, \"turbidity\": 38.0, \"chlorine\": 0.0, \"metal\": 75.0}[p]\n",
" elif ft == \"drift\":\n",
" ramp = np.linspace(0, 1, seg.stop - s0)\n",
" ms[p][seg] = np.clip(ms[p][seg] + ramp * {\"ph\": -1.6, \"turbidity\": 2.6, \"chlorine\": -0.75, \"metal\": 12.0}[p], 0, None).round(DIGITS[p])\n",
" elif ft == \"dropout\":\n",
" for q in PARAMS:\n",
" ms[q][seg] = np.nan\n",
" p = \"all\"\n",
" fault[seg], fparam[seg] = ft, p\n",
" df = pd.DataFrame({\"source_id\": sid, \"source_type\": stype, \"x_km\": x, \"y_km\": y, \"t\": tt,\n",
" \"cond\": np.where(rain > 0, \"storm\", np.where(night, \"night\", \"day\")),\n",
" **{p: ms[p] for p in PARAMS}, **{f\"true_{p}\": tr[p] for p in PARAMS},\n",
" \"event\": etype, \"fault\": fault, \"fault_param\": fparam, \"grade\": grade})\n",
" out.append(df)\n",
" return pd.concat(out, ignore_index=True)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌ ๊ณตํต ํจ์: ๋ฌผ๋ฆฌ ๋ฒ์ ์ ๊ฒ -> ๊ณ ์ ๊ฐ(stuck) ํ์ -> ๋จ๋ฐ ์คํ์ดํฌ ์ ๊ฑฐ -> ๊ฒฐ์ธก ๋ณด์ ----\n",
"PHYS = {\"ph\": (0, 14), \"turbidity\": (0, 1000), \"chlorine\": (0, 10), \"metal\": (0, 1000), \"ecoli\": (0, 100000)}\n",
"SPIKE_K = {\"day\": 6.0, \"night\": 7.0, \"storm\": 9.0} # ์ํฉ๋ณ(์ฃผ๊ฐ/์ผ๊ฐ/์
์ฒํ) ์คํ์ดํฌ ํ์ ๋ฐฐ์\n",
"MIN_JUMP = {\"ph\": 0.8, \"turbidity\": 1.5, \"chlorine\": 0.3, \"metal\": 6.0}\n",
"STUCK_RUN, SHORT_GAP = 6, 3\n",
"\n",
"\n",
"def clean(df, spike_k=None, min_jump=None):\n",
" \"\"\"๊ธ์์๋ณ ์๊ณ์ด์ ์ ์ ํ๊ณ ํญ๋ชฉ๋ณ ์ฒ๋ฆฌ ๋ด์ญ(<ํญ๋ชฉ>_flag: range/stuck/spike/interp/long_gap)์ ๋จ๊ธด๋ค.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 7] ์์ง ์ํ ๊ฒฝ๋ณด ๋ฐ์ ์ ๊ธ์๊ด ๊ธด๊ธ ์ฑ์ ๊ฒ์ฌ ๋ฐ ๊ธ์ ์ค๋จ HITL ๊ฒฐ์ฌ์ ์ค๊ณ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"GRADE_NAME = {0: \"์์ \", 1: \"์ฃผ์\", 2: \"์ํ\"}\n",
"\n",
"\n",
"def rule_grade(df):\n",
" \"\"\"๊ธฐ์ค์น 1์ฐจ ํํฐ: ์ํ(์์ฉ ๋ถ์ ํฉ ์์ฌ) = ํ ํญ๋ชฉ์ด๋ผ๋ ์ํ ๊ธฐ์ค ์ด๊ณผ, ์ฃผ์ = ์ฃผ์ ๊ธฐ์ค ์ด๊ณผ.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 7] ์์ง ์ํ ๊ฒฝ๋ณด ๋ฐ์ ์ ๊ธ์๊ด ๊ธด๊ธ ์ฑ์ ๊ฒ์ฌ ๋ฐ ๊ธ์ ์ค๋จ HITL ๊ฒฐ์ฌ์ ์ค๊ณ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def reasons(row):\n",
" r = []\n",
" if not LIMITS[\"ph\"][0] <= row[\"ph\"] <= LIMITS[\"ph\"][1]:\n",
" r.append(f\"pH {row['ph']:.2f}\")\n",
" if row[\"turbidity\"] > LIMITS[\"turbidity\"][0]:\n",
" r.append(f\"ํ๋ {row['turbidity']:.2f}NTU\")\n",
" if row[\"chlorine\"] < LIMITS[\"chlorine\"][0]:\n",
" r.append(f\"์๋ฅ์ผ์ {row['chlorine']:.2f}mg/L\")\n",
" if row[\"metal\"] > LIMITS[\"metal\"][0]:\n",
" r.append(f\"์ค๊ธ์ {row['metal']:.1f}ug/L\")\n",
" if row[\"ecoli\"] >= 1:\n",
" r.append(f\"๋์ฅ๊ท {row['ecoli']:.0f}\")\n",
" return \", \".join(r) or \"-\"\n",
"\n",
"\n",
"FEATS = [f\"{p}{s}\" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"] for s in [\"\", \"_d1\", \"_d3\", \"_dev\", \"_sd6\"]] + \\\n",
" [\"ecoli\", \"ecoli_3h\", \"storm\", \"night\", \"n_flag\", \"truck\", \"purif\", \"well\", \"river\"]\n",
"\n",
"\n",
"def add_features(c):\n",
" \"\"\"์๊ฐ ์์๋ฅผ ์งํค๋(๊ณผ๊ฑฐ ๊ฐ๋ง ์ฐ๋) ๊ธ์์๋ณ ํน์ง: ๋ณํ๋, 24์๊ฐ ๊ธฐ์ค์ ๋๋น ํธ์ฐจ, 6์๊ฐ ๋ณ๋์ฑ, ๋์ฅ๊ท ๋์ ๋ฑ.\"\"\"\n",
" c = c.sort_values([\"source_id\", \"t\"]).reset_index(drop=True)\n",
" g = c.groupby(\"source_id\")\n",
" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n",
" c[f\"{p}_d1\"] = g[p].diff().fillna(0)\n",
" c[f\"{p}_d3\"] = g[p].diff(3).fillna(0)\n",
" base = g[p].transform(lambda s: s.shift(1).rolling(24, min_periods=4).median())\n",
" c[f\"{p}_dev\"] = (c[p] - base).fillna(0)\n",
" c[f\"{p}_sd6\"] = g[p].transform(lambda s: s.rolling(6, min_periods=2).std()).fillna(0)\n",
" c[\"ecoli_3h\"] = g[\"ecoli\"].transform(lambda s: s.rolling(3, min_periods=1).sum())\n",
" c[\"storm\"], c[\"night\"] = (c[\"cond\"] == \"storm\").astype(int), (c[\"cond\"] == \"night\").astype(int)\n",
" flags = [f\"{p}_flag\" for p in PARAMS if f\"{p}_flag\" in c]\n",
" c[\"n_flag\"] = sum(c[f].isin([\"stuck\", \"spike\", \"long_gap\", \"range\"]).astype(int) for f in flags) if flags else 0\n",
" for k, name in [(\"truck\", \"water_truck\"), (\"purif\", \"purification_unit\"), (\"well\", \"well\"), (\"river\", \"river_intake\")]:\n",
" c[k] = (c[\"source_type\"] == name).astype(int)\n",
" return c\n",
"\n",
"ROLE_ACTIONS = {\"์ธก์ ๋ณ\": {\"sample\"}, \"์๋ฐฉ์ํ๋ด๋น\": {\"review\"}, \"์งํ๊ด\": {\"approve_stop\", \"reject\"}, \"์์คํ
\": {\"enqueue\"}}\n",
"USERS = {\"op01\": \"์ธก์ ๋ณ\", \"med01\": \"์๋ฐฉ์ํ๋ด๋น\", \"cdr01\": \"์งํ๊ด\", \"sys\": \"์์คํ
\"}\n",
"FLOW = {\"enqueue\": (\"-\", \"์ฑ์๋๊ธฐ\"), \"sample\": (\"์ฑ์๋๊ธฐ\", \"๊ฒํ ๋๊ธฐ\"), \"review\": (\"๊ฒํ ๋๊ธฐ\", \"์น์ธ๋๊ธฐ\"),\n",
" \"approve_stop\": (\"์น์ธ๋๊ธฐ\", \"๊ธ์์ค๋จ\"), \"reject\": (\"์น์ธ๋๊ธฐ\", \"๊ธ์์ ์ง\")}\n",
"\n",
"\n",
"class AuditLog:\n",
" def __init__(self):\n",
" self.rows = []\n",
"\n",
" def add(self, **kw):\n",
" prev = self.rows[-1][\"hash\"] if self.rows else \"0\" * 16\n",
" body = json.dumps(kw, ensure_ascii=False, sort_keys=True)\n",
" self.rows.append({**kw, \"prev\": prev, \"hash\": hashlib.sha256((prev + body).encode()).hexdigest()[:16]})\n",
"\n",
" def verify(self):\n",
" prev = \"0\" * 16\n",
" for r in self.rows:\n",
" body = json.dumps({k: v for k, v in r.items() if k not in (\"prev\", \"hash\")}, ensure_ascii=False, sort_keys=True)\n",
" if r[\"prev\"] != prev or hashlib.sha256((prev + body).encode()).hexdigest()[:16] != r[\"hash\"]:\n",
" return False\n",
" prev = r[\"hash\"]\n",
" return True\n",
"\n",
"\n",
"class Workflow:\n",
" def __init__(self):\n",
" self.log, self.state, self.n_try = AuditLog(), {}, 0\n",
"\n",
" def act(self, user, action, case, t, note=\"\"):\n",
" \"\"\"๊ถํยท์์ยท์ฌ์ ๋ฅผ ๊ฒ์ฌํ ๋ค ์ํ๋ฅผ ๋ฐ๊พผ๋ค. ๊ฑฐ๋ถ๋ ๊ฐ์ฌ ๋ก๊ทธ์ ๋จ๊ธด๋ค.\"\"\"\n",
" self.n_try += 1\n",
" role = USERS.get(user, \"๋ฏธ๋ฑ๋ก\")\n",
" cur = self.state.get(case, \"-\")\n",
" err = None\n",
" if action not in ROLE_ACTIONS.get(role, set()):\n",
" err = f\"๊ถํ ์์({role} ์ญํ ์ {action} ๊ถํ ์์)\"\n",
" elif FLOW[action][0] != cur:\n",
" err = f\"์์ ์๋ฐ(ํ์ฌ {cur})\"\n",
" elif action in (\"reject\", \"approve_stop\", \"review\") and len(note.strip()) < 5:\n",
" err = \"์ฌ์ ยท๊ทผ๊ฑฐ ์
๋ ฅ ํ์\"\n",
" if err is None:\n",
" self.state[case] = FLOW[action][1]\n",
" self.log.add(t=t, user=user, role=role, case=case, action=action, result=\"๊ฑฐ๋ถ\" if err else \"์ฒ๋ฆฌ\",\n",
" detail=err or note, state=self.state.get(case, \"-\"))\n",
" return err is None\n",
"\n",
"\n",
"# 1) ๊ฒฝ๋ณด ์์ฑ: ๊ฐ์ ๋ฒค์น๋งํฌ์์ ์ํ ํ์ ์ด 2์๊ฐ ์ด์ ์ด์ด์ง ๊ตฌ๊ฐ์ '๊ฒฝ๋ณด ์ฌ๊ฑด'์ผ๋ก ๋ฌถ๋๋ค\n",
"c = clean(make_dataset(days=14, seed=11, n_events=10, n_faults=14))\n",
"c[\"g\"] = rule_grade(c)\n",
"cases = []\n",
"for sid, s in c.groupby(\"source_id\"):\n",
" d = (s[\"g\"] == 2).astype(int).to_numpy()\n",
" seg = np.split(np.arange(len(d)), np.where(np.diff(d) != 0)[0] + 1)\n",
" for idx in seg:\n",
" if d[idx[0]] == 1 and len(idx) >= 2:\n",
" r = s.iloc[idx[0]]\n",
" cases.append({\"case\": f\"AL-{len(cases) + 1:02d}\", \"source\": sid, \"type\": r[\"source_type\"], \"t\": int(r[\"t\"]),\n",
" \"hours\": len(idx), \"real\": bool((s.iloc[idx][\"grade\"] == 2).mean() >= 0.5), \"basis\": reasons(s.iloc[idx[-1]])})\n",
"Q = pd.DataFrame(cases)\n",
"# ์ฐ์ ์์: ์ง์์๊ฐ + ๊ธ์์ฐจยท์ ์์ฅ(๊ณต๊ธ ์ธ์ ๋ง์) ๊ฐ์ค + ๋์ฅ๊ท ยท์ค๊ธ์ ํฌํจ ๊ฐ์ค\n",
"Q[\"priority\"] = Q[\"hours\"].clip(upper=12) + Q[\"type\"].isin([\"water_truck\", \"purification_unit\"]) * 4 \\\n",
" + Q[\"basis\"].str.contains(\"๋์ฅ๊ท |์ค๊ธ์\") * 3\n",
"Q = Q.sort_values([\"priority\", \"t\"], ascending=[False, True]).reset_index(drop=True)\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฒฝ๋ณด ์ฌ๊ฑด {len(Q)}๊ฑด์ด ๊ฒํ ํ์ ๋ฑ๋ก๋จ(์๋ ๊ธ์ ์ค๋จ 0๊ฑด)\")\n",
"\n",
"# 2) ๊ฒฐ์ฌ ์๋ฎฌ๋ ์ด์
: ์ฑ์ ๋ด๋น 30๋ถ ๋ด ์ฑ์, ์๋ฐฉ์ํ ๋ด๋น ์ฌ๊ฒ์ฌ ๊ฒฐ๊ณผ๋ก ์๊ฒฌ, ์งํ๊ด์ด ์น์ธยท๋ฐ๋ ค\n",
"wf = Workflow()\n",
"rng = np.random.default_rng(5)\n",
"wait = []\n",
"for _, q in Q.iterrows():\n",
" t = q[\"t\"] * 60\n",
" wf.act(\"sys\", \"enqueue\", q[\"case\"], t, \"๊ธฐ์ค์น ์ด๊ณผ ๊ฒฝ๋ณด ์๋ ๋ฑ๋ก\")\n",
" t += int(rng.integers(10, 30)); wf.act(\"op01\", \"sample\", q[\"case\"], t, \"ํ์ฅ ํคํธ ์ฌ์ฑ์\")\n",
" t += int(rng.integers(20, 60))\n",
" opinion = \"์ฌ๊ฒ์ฌ ์ํ ํ์ธ, ๊ธ์ ์ค๋จ ๊ถ๊ณ \" if q[\"real\"] else \"์ฌ๊ฒ์ฌ ์ ์, ์ผ์ ์ ๊ฒ ๊ถ๊ณ \"\n",
" wf.act(\"med01\", \"review\", q[\"case\"], t, opinion)\n",
" t += int(rng.integers(5, 25))\n",
" if q[\"real\"]:\n",
" wf.act(\"cdr01\", \"approve_stop\", q[\"case\"], t, f\"{q['basis']} ํ์ธ, ๋์ฒด ๊ธ์์ ์ ํ\")\n",
" else:\n",
" wf.act(\"cdr01\", \"reject\", q[\"case\"], t, \"์ฌ๊ฒ์ฌ ์ ์, ๊ธ์ ์ ์งํ๊ณ ์ผ์ ๊ต์ \")\n",
" wait.append(t - q[\"t\"] * 60)\n",
"st = pd.Series(wf.state).value_counts()\n",
"print(\"๊ฒฐ์ฌ ๊ฒฐ๊ณผ:\", st.to_dict(), f\"/ ๊ฒฝ๋ณด๋ถํฐ ๊ฒฐ์ ๊น์ง ํ๊ท {np.mean(wait):.0f}๋ถ, ์ต๋ {max(wait)}๋ถ\")\n",
"print(\"\\n[๊ฒํ ํ ์์ 5๊ฑด]\")\n",
"print(Q.head(5)[[\"case\", \"source\", \"type\", \"t\", \"hours\", \"priority\", \"basis\"]].to_string(index=False))\n",
"\n",
"# 3) ๊ถํ ์ฐํยท์ ์ฐจ ์๋ฐ ์๋ 6์ข
(์ฌ์ด์ฌ์ด ์ ์ ์ ์ฐจ๋ฅผ ์งํ)\n",
"wf.act(\"sys\", \"enqueue\", \"AL-X\", 99999, \"์ํ ๊ฒฝ๋ณด\")\n",
"STEPS = [(\"์๋ฐ\", \"op01\", \"approve_stop\", \"์ธก์ ๋ณ์ด ์ง์ ๊ธ์ ์ค๋จ\", \"์ค๋จ\"), (\"์๋ฐ\", \"sys\", \"approve_stop\", \"์์คํ
์๋ ํ์ \", \"์๋\"),\n",
" (\"์๋ฐ\", \"cdr01\", \"approve_stop\", \"์ฑ์ยท๊ฒํ ์์ด ์งํ๊ด ์น์ธ\", \"์ฆ์ ์ค๋จ\"), (\"์๋ฐ\", \"hacker\", \"review\", \"๋ฏธ๋ฑ๋ก ์ฌ์ฉ์ ๊ฒํ \", \"์ํ ํ์ธ๋จ\"),\n",
" (\"์ ์\", \"op01\", \"sample\", \"์ ์ ์ฑ์\", \"์ฌ์ฑ์\"), (\"์๋ฐ\", \"med01\", \"review\", \"๊ทผ๊ฑฐ ์๋ ๊ฒํ ์๊ฒฌ\", \"\"),\n",
" (\"์ ์\", \"med01\", \"review\", \"์ ์ ๊ฒํ \", \"์ฌ๊ฒ์ฌ ์ ์ ํ์ธ\"), (\"์๋ฐ\", \"cdr01\", \"reject\", \"์ฌ์ ์๋ ๋ฐ๋ ค\", \"\")]\n",
"blocked = n_v = 0\n",
"print(\"\\n[๊ถํ ์ฐํยท์ ์ฐจ ์๋ฐ ์๋]\")\n",
"for kind, u, a, desc, note in STEPS:\n",
" ok = wf.act(u, a, \"AL-X\", 99999, note)\n",
" if kind == \"์๋ฐ\":\n",
" n_v += 1; blocked += not ok\n",
" print(f\" {desc:16s} -> {'์ฐจ๋จ' if not ok else 'ํต๊ณผ(๋ฌธ์ )'}: {wf.log.rows[-1]['detail']}\")\n",
"print(f\"์ฐํยท์๋ฐ ์๋ ์ฐจ๋จ {blocked}/{n_v} ({blocked / n_v:.0%})\")\n",
"\n",
"# 4) ๊ฐ์ฌ ๋ก๊ทธ ๋ฌด๊ฒฐ์ฑ๊ณผ ๋ก๊น
์ฑ๊ณต๋ฅ \n",
"n_log = len(wf.log.rows)\n",
"print(f\"\\n๊ฐ์ฌ ๋ก๊ทธ {n_log}๊ฑด / ์์ฒญ {wf.n_try}๊ฑด -> ๋ก๊น
์ฑ๊ณต๋ฅ {n_log / wf.n_try:.0%}, ํด์ ์ฒด์ธ ๊ฒ์ฆ: {'์ ์' if wf.log.verify() else '์๋ณ์กฐ'}\")\n",
"wf.log.rows[5][\"detail\"] = \"๊ธฐ๋ก ์์ ์์ \"\n",
"print(f\"5๋ฒ์งธ ๊ธฐ๋ก ์์ ์์ ํ ๊ฒ์ฆ: {'์ ์' if wf.log.verify() else '์๋ณ์กฐ ํ์ง'}\")\n",
"print(\"โป ๊ธ์ ์ค๋จยท์ฌ๊ฐ๋ ์งํ๊ด ๊ฒฐ์ ์ฌํญ์ด๋ฉฐ, ์์คํ
์ ๊ฒฝ๋ณด์ ๊ทผ๊ฑฐ๋ง ์ ๊ณตํ๋ค. ์ค์ ํ์ ์ ๊ณต์ ๊ธฐ์ค๊ณผ ์๋ฌดยทํ๊ฒฝ ๋ด๋น ํ์ธ์ ๋ฐ๋ฅธ๋ค.\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
07 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฒฝ๋ณด ์ฌ๊ฑด 10๊ฑด์ด ๊ฒํ ํ์ ๋ฑ๋ก๋จ(์๋ ๊ธ์ ์ค๋จ 0๊ฑด)\n",
"๊ฒฐ์ฌ ๊ฒฐ๊ณผ: {'๊ธ์์ค๋จ': 9, '๊ธ์์ ์ง': 1} / ๊ฒฝ๋ณด๋ถํฐ ๊ฒฐ์ ๊น์ง ํ๊ท 69๋ถ, ์ต๋ 82๋ถ\n",
"\n",
"[๊ฒํ ํ ์์ 5๊ฑด]\n",
" case source type t hours priority basis\n",
"AL-02 W03 purification_unit 243 23 19 ์๋ฅ์ผ์ 0.00mg/L, ๋์ฅ๊ท 9\n",
"AL-01 W02 water_truck 147 21 16 pH 8.87, ํ๋ 1.21NTU\n",
"AL-03 W04 purification_unit 288 16 16 pH 8.72, ํ๋ 1.12NTU\n",
"AL-09 W08 river_intake 69 22 15 ์ค๊ธ์ 15.0ug/L\n",
"AL-07 W07 river_intake 115 29 15 ํ๋ 3.11NTU, ์๋ฅ์ผ์ 0.08mg/L, ๋์ฅ๊ท 39\n",
"\n",
"[๊ถํ ์ฐํยท์ ์ฐจ ์๋ฐ ์๋]\n",
" ์ธก์ ๋ณ์ด ์ง์ ๊ธ์ ์ค๋จ -> ์ฐจ๋จ: ๊ถํ ์์(์ธก์ ๋ณ ์ญํ ์ approve_stop ๊ถํ ์์)\n",
" ์์คํ
์๋ ํ์ -> ์ฐจ๋จ: ๊ถํ ์์(์์คํ
์ญํ ์ approve_stop ๊ถํ ์์)\n",
" ์ฑ์ยท๊ฒํ ์์ด ์งํ๊ด ์น์ธ -> ์ฐจ๋จ: ์์ ์๋ฐ(ํ์ฌ ์ฑ์๋๊ธฐ)\n",
" ๋ฏธ๋ฑ๋ก ์ฌ์ฉ์ ๊ฒํ -> ์ฐจ๋จ: ๊ถํ ์์(๋ฏธ๋ฑ๋ก ์ญํ ์ review ๊ถํ ์์)\n",
" ๊ทผ๊ฑฐ ์๋ ๊ฒํ ์๊ฒฌ -> ์ฐจ๋จ: ์ฌ์ ยท๊ทผ๊ฑฐ ์
๋ ฅ ํ์\n",
" ์ฌ์ ์๋ ๋ฐ๋ ค -> ์ฐจ๋จ: ์ฌ์ ยท๊ทผ๊ฑฐ ์
๋ ฅ ํ์\n",
"์ฐํยท์๋ฐ ์๋ ์ฐจ๋จ 6/6 (100%)\n",
"\n",
"๊ฐ์ฌ ๋ก๊ทธ 49๊ฑด / ์์ฒญ 49๊ฑด -> ๋ก๊น
์ฑ๊ณต๋ฅ 100%, ํด์ ์ฒด์ธ ๊ฒ์ฆ: ์ ์\n",
"5๋ฒ์งธ ๊ธฐ๋ก ์์ ์์ ํ ๊ฒ์ฆ: ์๋ณ์กฐ ํ์ง\n",
"โป ๊ธ์ ์ค๋จยท์ฌ๊ฐ๋ ์งํ๊ด ๊ฒฐ์ ์ฌํญ์ด๋ฉฐ, ์์คํ
์ ๊ฒฝ๋ณด์ ๊ทผ๊ฑฐ๋ง ์ ๊ณตํ๋ค. ์ค์ ํ์ ์ ๊ณต์ ๊ธฐ์ค๊ณผ ์๋ฌดยทํ๊ฒฝ ๋ด๋น ํ์ธ์ ๋ฐ๋ฅธ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
08. ์ค์ผ๋ฌผ์ง ๋๋ ์ ์
๊ฐ์ ์๋๋ฆฌ์ค ๋ถํ ํ
์คํธ ๋ฐ ๊ฒฝ๋ณด ๋ฐ์ ์๊ฐ ๊ฒ์ฆ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ค์ผ๋ฌผ์ง ๋๋ ์ ์
๊ฐ์ ์๋๋ฆฌ์ค ๋ถํ ํ
์คํธ๋ฅผ ์ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ค์ผ๋ฌผ์ง์ด ๋๋ ์ ์
๋๋ ๊ฐ์ ์๋๋ฆฌ์ค์์ ํ์ดํ๋ผ์ธ ๋ถํ ์ํ์ ํ๊ณ ๊ฒฝ๋ณด ๋ฐ์ ์๊ฐ๊ณผ Fail-safe ๋์์ ๊ฒ์ฆํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ธ์์์ 8๊ณณ์์ 48๊ณณ์ผ๋ก ๋๋ ค ํธ๋ํฝ์ 500% ํค์ฐ๊ณ 30๊ณณ์ ์ค์ผ์ด ๋์์ ์ ์
๋๋ ์ํฉ์์ ์์งยท์ ์ ยทํ์ ์ฃผ๊ธฐ์ ์ฒ๋ฆฌ ์๊ฐ, ์์คํ
์ค๋จ, ๋ฐ์ดํฐ ์ ์ค, ๋ฉ๋ชจ๋ฆฌ ์ฆ๊ฐ, ๊ฒฝ๋ณด ํ์ง์ ๋ฐ์ ์๊ฐ์ ํ์์ ๋น๊ต ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `tests/wel03_stress_test.py ๋ฐ ์คํธ๋ ์ค ํ
์คํธ ๋ถ์ ๋ณด๊ณ ์` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ทน๋จ ์ํฉ์์์ ์์ ์คํจ(Fail-safe) ๋ฐ ๊ธฐ๋ฅ ์ถ์ ์๋ ์ ์ ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๋์ ํธ๋ํฝ 500% ์ฆ๊ฐ ์ ์์คํ
๋ค์ด 0๊ฑด, ๋ฐ์ดํฐ ์ ์ค๋ฅ 0%\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ๋๋ ์๋ฎฌ๋ ์ด์
๋ถํ ์ฃผ์
ํ
์คํธ ํต๊ณผ ๋ฐ ๋ณ๋ชฉ ๊ตฌ๊ฐ ํ๋กํ์ผ๋ง ์๋ฃ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ๋ถํ ํ
์คํธ ๋๊ตฌ๋ฅผ ํตํ ์ฒ๋ฆฌ๋(Throughput) ๋ฐ ๋ฉ๋ชจ๋ฆฌ ๋์ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"๊ฐ์ ๊ธ์์์ 8๊ณณ์์ 48๊ณณ(ํธ๋ํฝ 500% ์ฆ๊ฐ)์ผ๋ก ๋๋ฆฌ๊ณ 30๊ณณ์ ์ค์ผ๋ฌผ์ง์ด ๋์์ ์ ์
๋๋ ์๋๋ฆฌ์ค๋ก ์์งยทํ์ ํ์ดํ๋ผ์ธ ๋ถํ ์ํ์ ์ํํด, ์์คํ
์ค๋จยท๋ฐ์ดํฐ ์ ์คยท๋ฉ๋ชจ๋ฆฌ ์ฆ๊ฐยท๊ฒฝ๋ณด ๋ฐ์ ์๊ฐ๊ณผ ๊ณผ๋ถํ ์ ๊ธฐ๋ฅ ์ถ์(Fail-safe) ๋์์ ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ผ์ ๊ธ์์ ๊ฐ์ ์์ง ์ผ์ ๋ฐ์ดํฐ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์์นยท๊ฒ์ฌ ๊ฒฐ๊ณผ ์์)\n",
"# ๊ฐ์ ๊ธ์์ 8๊ณณ x 14์ผ x 1์๊ฐ ๊ฐ๊ฒฉ. ์ค์ ์์ง(true_*)ยท์ผ์ ์ธก์ ๊ฐยท์ค์ผ ์ฌ๊ฑดยท์ผ์ ๊ณ ์ฅ ์ ๋ต์ ํจ๊ป ๋ง๋ ๋ค.\n",
"# ๊ธฐ์ค์น๋ ๊ต์ก์ฉ ๊ฐ์ ๊ฐ์ด๋ฉฐ, ์ค์ ์์ฉ ํ์ ์ ๊ณต์ธ ๊ฒ์ฌ ๊ฒฐ๊ณผ์ ์๋ฌด(์๋ฐฉ์ํ) ๋ด๋น ๊ธฐ์ค์ ๋ฐ๋ฅธ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"PARAMS = [\"ph\", \"turbidity\", \"chlorine\", \"metal\", \"ecoli\"]\n",
"SOURCES = [(\"W01\", \"water_truck\", 1.0, 2.0), (\"W02\", \"water_truck\", 3.5, 1.2), (\"W03\", \"purification_unit\", 2.2, 4.1),\n",
" (\"W04\", \"purification_unit\", 5.0, 3.3), (\"W05\", \"well\", 0.8, 5.6), (\"W06\", \"well\", 4.4, 6.0),\n",
" (\"W07\", \"river_intake\", 6.3, 1.8), (\"W08\", \"river_intake\", 6.9, 4.9)]\n",
"BASE = {\"water_truck\": (7.2, 0.25, 0.75, 1.5), \"purification_unit\": (7.0, 0.20, 0.85, 1.2),\n",
" \"well\": (6.8, 0.45, 0.50, 3.0), \"river_intake\": (7.3, 0.55, 0.60, 2.0)} # pH, ํ๋(NTU), ์๋ฅ์ผ์(mg/L), ์ค๊ธ์(ug/L)\n",
"LIMITS = {\"ph\": (5.8, 8.5), \"turbidity\": (1.0, 2.0), \"chlorine\": (0.2, 0.1), \"metal\": (5.0, 10.0), \"ecoli\": (1, 1)} # (์ฃผ์, ์ํ) ๊ฐ์ ๊ธฐ์ค\n",
"EVENT_TYPES = { # ์ค์ผ ์ฌ๊ฑด: ๋ณ์๋ณ ์ต๋ ๋ณํ๋, ์ต๋์น ๋๋ฌ ์๊ฐ(h)\n",
" \"sewage\": ({\"turbidity\": 2.8, \"chlorine\": -0.5, \"ecoli\": 30, \"ph\": -0.3}, 4),\n",
" \"metal\": ({\"metal\": 22.0, \"ph\": -0.5}, 4),\n",
" \"disinfect_fail\": ({\"chlorine\": -0.8, \"ecoli\": 6}, 6),\n",
" \"chemical\": ({\"ph\": 1.7, \"turbidity\": 1.0}, 3),\n",
" \"creeping_metal\": ({\"metal\": 13.0}, 18),\n",
"}\n",
"FAULT_TYPES = [\"stuck\", \"spike\", \"drift\", \"dropout\"]\n",
"NOISE = {\"ph\": 0.04, \"turbidity\": 0.06, \"chlorine\": 0.03, \"metal\": 0.35}\n",
"DIGITS = {\"ph\": 2, \"turbidity\": 2, \"chlorine\": 2, \"metal\": 1}\n",
"\n",
"\n",
"def make_dataset(days=14, seed=7, n_events=6, n_faults=10, events=None, faults=None, sources=None):\n",
" \"\"\"events: [(๊ธ์์ ๋ฒํธ, ์ฌ๊ฑด ์ ํ, ์์ ์๊ฐ h, ์ง์ h, ๊ฐ๋)], faults: [(๊ธ์์ ๋ฒํธ, ๊ณ ์ฅ ์ ํ, ํญ๋ชฉ, ์์ h, ์ง์ h)]\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" sources = sources or SOURCES\n",
" T = days * 24\n",
" tt = np.arange(T)\n",
" hour = tt % 24\n",
" night = (hour < 6) | (hour >= 20)\n",
" rain = np.zeros(T)\n",
" for _ in range(max(1, days // 4)):\n",
" s0, d = int(rng.integers(0, T - 14)), int(rng.integers(6, 14))\n",
" rain[s0:s0 + d] = np.maximum(rain[s0:s0 + d], rng.uniform(0.6, 1.0))\n",
" if events is None:\n",
" events = [(int(rng.integers(len(sources))), str(rng.choice(list(EVENT_TYPES))), int(rng.integers(min(24, T // 4), max(T // 4 + 1, T - 48))),\n",
" int(rng.integers(10, 30)), 1.0) for _ in range(n_events)]\n",
" if faults is None:\n",
" faults = [(int(rng.integers(len(sources))), str(rng.choice(FAULT_TYPES)), str(rng.choice([\"ph\", \"turbidity\", \"chlorine\", \"metal\"])),\n",
" int(rng.integers(12, max(13, T - 30))), int(rng.integers(6, 24))) for _ in range(n_faults)]\n",
" out = []\n",
" for si, (sid, stype, x, y) in enumerate(sources):\n",
" ph0, tb0, cl0, mt0 = BASE[stype]\n",
" tr = {\"ph\": ph0 + 0.08 * np.sin(2 * np.pi * hour / 24) + rng.normal(0, 0.03, T),\n",
" \"turbidity\": tb0 * (1 + 0.1 * rng.normal(0, 1, T)),\n",
" \"chlorine\": (cl0 * np.exp(-0.022 * hour) if stype == \"water_truck\" else cl0 + rng.normal(0, 0.03, T)),\n",
" \"metal\": mt0 + rng.normal(0, 0.2, T), \"ecoli\": np.zeros(T)}\n",
" if stype in (\"river_intake\", \"well\"):\n",
" tr[\"turbidity\"] = tr[\"turbidity\"] + rain * (0.9 if stype == \"river_intake\" else 0.45)\n",
" f_all, etype = np.zeros(T), np.array([\"none\"] * T, dtype=object)\n",
" for (s_i, et, onset, dur, scale) in events:\n",
" if s_i != si:\n",
" continue\n",
" delta, ramp = EVENT_TYPES[et]\n",
" f = np.clip((tt - onset + 1) / ramp, 0, 1) * ((tt >= onset) & (tt < onset + dur + ramp))\n",
" for k, v in delta.items():\n",
" tr[k] = tr[k] + v * f * scale\n",
" etype[f > 0] = et\n",
" f_all = np.maximum(f_all, f)\n",
" tr[\"chlorine\"] = np.clip(tr[\"chlorine\"], 0.02, None)\n",
" tr[\"turbidity\"] = np.clip(tr[\"turbidity\"], 0.05, None)\n",
" tr[\"metal\"] = np.clip(tr[\"metal\"], 0.1, None)\n",
" grade = np.where(f_all >= 0.2, 2, 0)\n",
" mild = (tr[\"turbidity\"] > LIMITS[\"turbidity\"][0]) | (tr[\"chlorine\"] < LIMITS[\"chlorine\"][0]) | (tr[\"metal\"] > LIMITS[\"metal\"][0])\n",
" grade = np.where((grade == 0) & mild, 1, grade)\n",
" ms = {}\n",
" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n",
" sd = NOISE[p] * np.where(night, 1.5, 1.0) * (tr[p] if p == \"turbidity\" else 1)\n",
" ms[p] = np.round(np.clip(tr[p] + rng.normal(0, 1, T) * sd, 0, None), DIGITS[p])\n",
" ms[\"ecoli\"] = (rng.poisson(tr[\"ecoli\"]) + (rng.random(T) < 0.004)).astype(float)\n",
" fault, fparam = np.array([\"none\"] * T, dtype=object), np.array([\"-\"] * T, dtype=object)\n",
" for (s_i, ft, p, s0, d) in faults:\n",
" if s_i != si:\n",
" continue\n",
" seg = slice(s0, min(T, s0 + d))\n",
" if ft == \"stuck\":\n",
" ms[p][seg] = ms[p][s0]\n",
" elif ft == \"spike\":\n",
" seg = slice(s0, s0 + 1)\n",
" ms[p][s0] = {\"ph\": 11.8, \"turbidity\": 38.0, \"chlorine\": 0.0, \"metal\": 75.0}[p]\n",
" elif ft == \"drift\":\n",
" ramp = np.linspace(0, 1, seg.stop - s0)\n",
" ms[p][seg] = np.clip(ms[p][seg] + ramp * {\"ph\": -1.6, \"turbidity\": 2.6, \"chlorine\": -0.75, \"metal\": 12.0}[p], 0, None).round(DIGITS[p])\n",
" elif ft == \"dropout\":\n",
" for q in PARAMS:\n",
" ms[q][seg] = np.nan\n",
" p = \"all\"\n",
" fault[seg], fparam[seg] = ft, p\n",
" df = pd.DataFrame({\"source_id\": sid, \"source_type\": stype, \"x_km\": x, \"y_km\": y, \"t\": tt,\n",
" \"cond\": np.where(rain > 0, \"storm\", np.where(night, \"night\", \"day\")),\n",
" **{p: ms[p] for p in PARAMS}, **{f\"true_{p}\": tr[p] for p in PARAMS},\n",
" \"event\": etype, \"fault\": fault, \"fault_param\": fparam, \"grade\": grade})\n",
" out.append(df)\n",
" return pd.concat(out, ignore_index=True)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌ ๊ณตํต ํจ์: ๋ฌผ๋ฆฌ ๋ฒ์ ์ ๊ฒ -> ๊ณ ์ ๊ฐ(stuck) ํ์ -> ๋จ๋ฐ ์คํ์ดํฌ ์ ๊ฑฐ -> ๊ฒฐ์ธก ๋ณด์ ----\n",
"PHYS = {\"ph\": (0, 14), \"turbidity\": (0, 1000), \"chlorine\": (0, 10), \"metal\": (0, 1000), \"ecoli\": (0, 100000)}\n",
"SPIKE_K = {\"day\": 6.0, \"night\": 7.0, \"storm\": 9.0} # ์ํฉ๋ณ(์ฃผ๊ฐ/์ผ๊ฐ/์
์ฒํ) ์คํ์ดํฌ ํ์ ๋ฐฐ์\n",
"MIN_JUMP = {\"ph\": 0.8, \"turbidity\": 1.5, \"chlorine\": 0.3, \"metal\": 6.0}\n",
"STUCK_RUN, SHORT_GAP = 6, 3\n",
"\n",
"\n",
"def clean(df, spike_k=None, min_jump=None):\n",
" \"\"\"๊ธ์์๋ณ ์๊ณ์ด์ ์ ์ ํ๊ณ ํญ๋ชฉ๋ณ ์ฒ๋ฆฌ ๋ด์ญ(<ํญ๋ชฉ>_flag: range/stuck/spike/interp/long_gap)์ ๋จ๊ธด๋ค.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 8] ์ค์ผ๋ฌผ์ง ๋๋ ์ ์
๊ฐ์ ์๋๋ฆฌ์ค ๋ถํ ํ
์คํธ ๋ฐ ๊ฒฝ๋ณด ๋ฐ์ ์๊ฐ ๊ฒ์ฆ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"GRADE_NAME = {0: \"์์ \", 1: \"์ฃผ์\", 2: \"์ํ\"}\n",
"\n",
"\n",
"def rule_grade(df):\n",
" \"\"\"๊ธฐ์ค์น 1์ฐจ ํํฐ: ์ํ(์์ฉ ๋ถ์ ํฉ ์์ฌ) = ํ ํญ๋ชฉ์ด๋ผ๋ ์ํ ๊ธฐ์ค ์ด๊ณผ, ์ฃผ์ = ์ฃผ์ ๊ธฐ์ค ์ด๊ณผ.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 8] ์ค์ผ๋ฌผ์ง ๋๋ ์ ์
๊ฐ์ ์๋๋ฆฌ์ค ๋ถํ ํ
์คํธ ๋ฐ ๊ฒฝ๋ณด ๋ฐ์ ์๊ฐ ๊ฒ์ฆ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def reasons(row):\n",
" r = []\n",
" if not LIMITS[\"ph\"][0] <= row[\"ph\"] <= LIMITS[\"ph\"][1]:\n",
" r.append(f\"pH {row['ph']:.2f}\")\n",
" if row[\"turbidity\"] > LIMITS[\"turbidity\"][0]:\n",
" r.append(f\"ํ๋ {row['turbidity']:.2f}NTU\")\n",
" if row[\"chlorine\"] < LIMITS[\"chlorine\"][0]:\n",
" r.append(f\"์๋ฅ์ผ์ {row['chlorine']:.2f}mg/L\")\n",
" if row[\"metal\"] > LIMITS[\"metal\"][0]:\n",
" r.append(f\"์ค๊ธ์ {row['metal']:.1f}ug/L\")\n",
" if row[\"ecoli\"] >= 1:\n",
" r.append(f\"๋์ฅ๊ท {row['ecoli']:.0f}\")\n",
" return \", \".join(r) or \"-\"\n",
"\n",
"\n",
"FEATS = [f\"{p}{s}\" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"] for s in [\"\", \"_d1\", \"_d3\", \"_dev\", \"_sd6\"]] + \\\n",
" [\"ecoli\", \"ecoli_3h\", \"storm\", \"night\", \"n_flag\", \"truck\", \"purif\", \"well\", \"river\"]\n",
"\n",
"\n",
"def add_features(c):\n",
" \"\"\"์๊ฐ ์์๋ฅผ ์งํค๋(๊ณผ๊ฑฐ ๊ฐ๋ง ์ฐ๋) ๊ธ์์๋ณ ํน์ง: ๋ณํ๋, 24์๊ฐ ๊ธฐ์ค์ ๋๋น ํธ์ฐจ, 6์๊ฐ ๋ณ๋์ฑ, ๋์ฅ๊ท ๋์ ๋ฑ.\"\"\"\n",
" c = c.sort_values([\"source_id\", \"t\"]).reset_index(drop=True)\n",
" g = c.groupby(\"source_id\")\n",
" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n",
" c[f\"{p}_d1\"] = g[p].diff().fillna(0)\n",
" c[f\"{p}_d3\"] = g[p].diff(3).fillna(0)\n",
" base = g[p].transform(lambda s: s.shift(1).rolling(24, min_periods=4).median())\n",
" c[f\"{p}_dev\"] = (c[p] - base).fillna(0)\n",
" c[f\"{p}_sd6\"] = g[p].transform(lambda s: s.rolling(6, min_periods=2).std()).fillna(0)\n",
" c[\"ecoli_3h\"] = g[\"ecoli\"].transform(lambda s: s.rolling(3, min_periods=1).sum())\n",
" c[\"storm\"], c[\"night\"] = (c[\"cond\"] == \"storm\").astype(int), (c[\"cond\"] == \"night\").astype(int)\n",
" flags = [f\"{p}_flag\" for p in PARAMS if f\"{p}_flag\" in c]\n",
" c[\"n_flag\"] = sum(c[f].isin([\"stuck\", \"spike\", \"long_gap\", \"range\"]).astype(int) for f in flags) if flags else 0\n",
" for k, name in [(\"truck\", \"water_truck\"), (\"purif\", \"purification_unit\"), (\"well\", \"well\"), (\"river\", \"river_intake\")]:\n",
" c[k] = (c[\"source_type\"] == name).astype(int)\n",
" return c\n",
"\n",
"BUDGET_MS = 300 # 1์๊ฐ ์ฃผ๊ธฐ ์ฒ๋ฆฌ ์์ฐ(๋ชจ์): ์ง์ ์ฃผ๊ธฐ๊ฐ ์์ฐ์ ๋์ผ๋ฉด ๊ธฐ๋ฅ ์ถ์ ๋ชจ๋๋ก ์ ํ\n",
"FULL_EVERY = 12 # ๊ธฐ๋ฅ ์ถ์ ์ค์๋ 12์๊ฐ๋ง๋ค ์ ์ฒด ์ ์ (์ง์ฐ ์ฒ๋ฆฌ)๋ฅผ ์ํ\n",
"\n",
"\n",
"def make_sources(n, seed=0):\n",
" rng = np.random.default_rng(seed)\n",
" types = [s[1] for s in SOURCES]\n",
" return [(f\"W{i + 1:02d}\", types[i % len(types)], float(round(rng.uniform(0, 30), 1)), float(round(rng.uniform(0, 30), 1))) for i in range(n)]\n",
"\n",
"\n",
"def lite_check(batch, last):\n",
" \"\"\"๊ธฐ๋ฅ ์ถ์ ๋ชจ๋: ๋ ์ฝ๋ ๋จ์ ๋ฒ์ ์ ๊ฒ -> ์ง์ ์ ์๊ฐ ์ ์ง -> ๊ธฐ์ค์น ํ์ (์ ์ ยทํน์ง ๊ณ์ฐ ์๋ต).\"\"\"\n",
" b = batch.copy()\n",
" for p in PARAMS:\n",
" v = pd.to_numeric(b[p], errors=\"coerce\")\n",
" lo, hi = PHYS[p]\n",
" v = v.where(v.between(lo, hi))\n",
" b[p] = v.fillna(b[\"source_id\"].map(last.get(p, {}))).fillna(0 if p == \"ecoli\" else np.nan)\n",
" last.setdefault(p, {}).update(b.dropna(subset=[p]).set_index(\"source_id\")[p].to_dict())\n",
" b = b.dropna(subset=[\"ph\", \"turbidity\", \"chlorine\", \"metal\"])\n",
" b[\"g\"] = rule_grade(b)\n",
" return b\n",
"\n",
"\n",
"def run(n_src, days, events, seed, mode_auto=True, hours=None):\n",
" src = make_sources(n_src)\n",
" data = make_dataset(days=days, seed=seed, events=events, faults=[], sources=src)\n",
" rng = np.random.default_rng(seed)\n",
" bad_idx = rng.choice(len(data), int(len(data) * 0.01), replace=False) # 1% ์์ ๋ ์ฝ๋ ์ฃผ์
(๋ฌธ์ยทNone)\n",
" data[PARAMS] = data[PARAMS].astype(object)\n",
" data.loc[bad_idx[: len(bad_idx) // 2], \"turbidity\"] = \"ERR\"\n",
" data.loc[bad_idx[len(bad_idx) // 2:], \"ph\"] = None\n",
" stored, quarantine, alerts, ticks, down, last = 0, 0, {}, [], 0, {}\n",
" buf, mode, switched = [], \"full\", None\n",
" mem = []\n",
" for h in range(hours or days * 24):\n",
" t0 = time.perf_counter()\n",
" try:\n",
" batch = data[data[\"t\"] == h]\n",
" if mode_auto and mode == \"full\" and ticks and ticks[-1] > BUDGET_MS:\n",
" mode, switched = \"lite\", h # ํ ๋ฒ ์ ํํ๋ฉด ๋ถํ ํด์ ํ์ธ ์ ๊น์ง ์ ์ง\n",
" ok_mask = batch[[\"ph\", \"turbidity\", \"chlorine\", \"metal\"]].apply(pd.to_numeric, errors=\"coerce\").notna().all(axis=1)\n",
" quarantine += int((~ok_mask).sum())\n",
" if mode == \"full\":\n",
" buf.append(batch[ok_mask].astype({p: float for p in PARAMS}))\n",
" win = pd.concat(buf[-24:])\n",
" c = clean(win)\n",
" res = c[c[\"t\"] == h].assign(g=lambda d: rule_grade(d))\n",
" else:\n",
" res = lite_check(batch, last)\n",
" buf.append(batch[ok_mask].astype({p: float for p in PARAMS}))\n",
" if h % FULL_EVERY == FULL_EVERY - 1: # ์ง์ฐ ์ ์ฒด ์ ์ (๊ธฐ๋ก ๋ณด์ ์ฉ)\n",
" clean(pd.concat(buf[-FULL_EVERY:]))\n",
" buf = buf[-24:]\n",
" stored += len(batch)\n",
" for sid in res.loc[res[\"g\"] == 2, \"source_id\"]:\n",
" alerts.setdefault(sid, []).append(h)\n",
" except Exception:\n",
" down += 1\n",
" ticks.append((time.perf_counter() - t0) * 1000)\n",
" mem.append(sum(b.memory_usage(deep=True).sum() for b in buf) / 1e6) # ๋ณด๊ด ๋ฒํผ ๋ฉ๋ชจ๋ฆฌ(MB)\n",
" last = {p: last.get(p, {}) for p in PARAMS}\n",
" react = []\n",
" for e in events:\n",
" hs = [h for h in alerts.get(src[e[0]][0], []) if h >= e[2]]\n",
" if hs:\n",
" react.append(hs[0] - e[2])\n",
" return {\"src\": n_src, \"recs\": len(data), \"stored\": stored, \"quarantine\": quarantine, \"down\": down, \"mode\": mode if switched is None else f\"lite({switched}h~)\",\n",
" \"tick_avg\": np.mean(ticks), \"tick_max\": np.max(ticks), \"mem_first\": np.mean(mem[24:48]) if len(mem) >= 48 else mem[-1], \"mem_last\": np.mean(mem[-24:]),\n",
" \"react\": react, \"detected\": len(react), \"n_events\": len(events), \"ticks\": ticks}\n",
"\n",
"\n",
"days = 2\n",
"ev8 = [(i, et, 20 + i, 30, 1.0) for i, et in zip(range(5), [\"sewage\", \"metal\", \"chemical\", \"disinfect_fail\", \"sewage\"])]\n",
"ev48 = [(i, [\"sewage\", \"metal\", \"chemical\", \"disinfect_fail\"][i % 4], 20 + i % 6, 30, 1.0) for i in range(0, 48, 48 // 30 + 0)][:30]\n",
"t_all = time.perf_counter()\n",
"R = {\"ํ์(8๊ณณ)\": run(8, days, ev8, 1), \"๋ถํ 500%(48๊ณณ, 30๊ณณ ๋์ ์ค์ผ)\": run(48, days, ev48, 2)}\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ถํ ์ํ {days}์ผ ๋ชจ์, ์ ์ฒด ์คํ {time.perf_counter() - t_all:.1f}์ด\")\n",
"rows = []\n",
"for k, r in R.items():\n",
" loss = 1 - r[\"stored\"] / r[\"recs\"]\n",
" rows.append({\"๊ตฌ๋ถ\": k, \"๋ ์ฝ๋\": r[\"recs\"], \"๊ฒฉ๋ฆฌ(์์)\": r[\"quarantine\"], \"์ ์ค๋ฅ \": f\"{loss:.1%}\", \"์ค๋จ\": r[\"down\"],\n",
" \"๋ชจ๋\": r[\"mode\"], \"์ฃผ๊ธฐ ํ๊ท ms\": round(r[\"tick_avg\"], 1), \"์ฃผ๊ธฐ ์ต๋ms\": round(r[\"tick_max\"], 1),\n",
" \"๋ฉ๋ชจ๋ฆฌMB(์ด๊ธฐ->๋ง๊ธฐ)\": f\"{r['mem_first']:.1f}->{r['mem_last']:.1f}\",\n",
" \"๊ฒฝ๋ณด ํ์ง\": f\"{r['detected']}/{r['n_events']}\", \"๋ฐ์ ํ๊ท h\": round(float(np.mean(r[\"react\"])), 1)})\n",
"print(pd.DataFrame(rows).set_index(\"๊ตฌ๋ถ\").T.to_string())\n",
"S = R[\"๋ถํ 500%(48๊ณณ, 30๊ณณ ๋์ ์ค์ผ)\"]\n",
"print(f\"\\n๋ถํ 500%: ์์คํ
์ค๋จ {S['down']}๊ฑด(๋ชฉํ 0๊ฑด: {'๋ฌ์ฑ' if S['down'] == 0 else '๋ฏธ๋ฌ'}), \"\n",
" f\"๋ฐ์ดํฐ ์ ์ค๋ฅ {1 - S['stored'] / S['recs']:.1%}(๋ชฉํ 0%: {'๋ฌ์ฑ' if S['stored'] == S['recs'] else '๋ฏธ๋ฌ'})\")\n",
"print(f\"๋ฉ๋ชจ๋ฆฌ ๋ง๊ธฐ/์ด๊ธฐ ๋น์จ {S['mem_last'] / max(S['mem_first'], 1e-6):.2f} (๋ฒํผ 24์๊ฐ ๊ณ ์ , 1.2 ์ดํ๋ฉด ๋์ ์์), \"\n",
" f\"๊ฒฝ๋ณด ๋ฐ์ ์ต๋ {max(S['react'])}์๊ฐ\")\n",
"\n",
"# ๊ธฐ๋ฅ ์ถ์๋ฅผ ๋๊ณ ์ ์ฒด ์ ์ ๋ฅผ ๊ฐํํ๋ฉด ์ฃผ๊ธฐ ์๊ฐ์ด ์ผ๋ง๋ ๋์ด๋๋์ง (6์๊ฐ๋ง ์ธก์ )\n",
"F = run(48, 1, [], 3, mode_auto=False, hours=6)\n",
"print(f\"๊ธฐ๋ฅ ์ถ์ ๋ฏธ์ ์ฉ(์ ์ฒด ์ ์ ๊ฐํ) ์ฃผ๊ธฐ ํ๊ท {np.mean(F['ticks']):.0f}ms vs ๊ธฐ๋ฅ ์ถ์ {S['tick_avg']:.0f}ms \"\n",
" f\"(์์ฐ {BUDGET_MS}ms) -> ๋ณ๋ชฉ: ๊ธ์์๋ณ ์ ์ ๋ฐ๋ณต(clean ํจ์)\")\n",
"print(\"Fail-safe ์์น: ์์ ๋ ์ฝ๋๋ ๋ฒ๋ฆฌ์ง ์๊ณ ๊ฒฉ๋ฆฌ ๋ณด๊ด, ํ์ ์ ์ง์ ์ ์๊ฐ์ผ๋ก ์ด์ด๊ฐ๋ฉฐ, ๊ณผ๋ถํ ์ ์ ์ ยทํน์ง ๊ณ์ฐ์ ๋ฏธ๋ฃจ๊ณ ๊ฒฝ๋ณด ํ์ ์ ์ ์ง\")\n",
"\n",
"plt.figure(figsize=(9, 3.5))\n",
"plt.plot(R[\"ํ์(8๊ณณ)\"][\"ticks\"], label=\"normal (8 sources)\")\n",
"plt.plot(S[\"ticks\"], label=\"500% load (48 sources, degraded mode)\")\n",
"plt.plot(F[\"ticks\"], label=\"500% load, full mode forced (6 h)\")\n",
"plt.axhline(BUDGET_MS, color=\"red\", ls=\"--\", lw=1, label=\"budget\")\n",
"plt.xlabel(\"simulated hour\"); plt.ylabel(\"processing ms per tick\"); plt.legend(); plt.title(\"Stress test tick time (synthetic)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
08 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ถํ ์ํ 2์ผ ๋ชจ์, ์ ์ฒด ์คํ 11.9์ด\n",
"๊ตฌ๋ถ ํ์(8๊ณณ) ๋ถํ 500%(48๊ณณ, 30๊ณณ ๋์ ์ค์ผ)\n",
"๋ ์ฝ๋ 384 2304\n",
"๊ฒฉ๋ฆฌ(์์) 3 23\n",
"์ ์ค๋ฅ 0.0% 0.0%\n",
"์ค๋จ 0 0\n",
"๋ชจ๋ full lite(1h~)\n",
"์ฃผ๊ธฐ ํ๊ท ms 127.1 87.7\n",
"์ฃผ๊ธฐ ์ต๋ms 183.6 742.7\n",
"๋ฉ๋ชจ๋ฆฌMB(์ด๊ธฐ->๋ง๊ธฐ) 0.1->0.1 0.6->0.6\n",
"๊ฒฝ๋ณด ํ์ง 5/5 30/30\n",
"๋ฐ์ ํ๊ท h 0.8 0.9\n",
"\n",
"๋ถํ 500%: ์์คํ
์ค๋จ 0๊ฑด(๋ชฉํ 0๊ฑด: ๋ฌ์ฑ), ๋ฐ์ดํฐ ์ ์ค๋ฅ 0.0%(๋ชฉํ 0%: ๋ฌ์ฑ)\n",
"๋ฉ๋ชจ๋ฆฌ ๋ง๊ธฐ/์ด๊ธฐ ๋น์จ 1.00 (๋ฒํผ 24์๊ฐ ๊ณ ์ , 1.2 ์ดํ๋ฉด ๋์ ์์), ๊ฒฝ๋ณด ๋ฐ์ ์ต๋ 2์๊ฐ\n",
"๊ธฐ๋ฅ ์ถ์ ๋ฏธ์ ์ฉ(์ ์ฒด ์ ์ ๊ฐํ) ์ฃผ๊ธฐ ํ๊ท 808ms vs ๊ธฐ๋ฅ ์ถ์ 88ms (์์ฐ 300ms) -> ๋ณ๋ชฉ: ๊ธ์์๋ณ ์ ์ ๋ฐ๋ณต(clean ํจ์)\n",
"Fail-safe ์์น: ์์ ๋ ์ฝ๋๋ ๋ฒ๋ฆฌ์ง ์๊ณ ๊ฒฉ๋ฆฌ ๋ณด๊ด, ํ์ ์ ์ง์ ์ ์๊ฐ์ผ๋ก ์ด์ด๊ฐ๋ฉฐ, ๊ณผ๋ถํ ์ ์ ์ ยทํน์ง ๊ณ์ฐ์ ๋ฏธ๋ฃจ๊ณ ๊ฒฝ๋ณด ํ์ ์ ์ ์ง\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
09. AAR ๊ธฐ๋ฐ ์์ง ์ด์ ์คํ ์ฌ๋ก ๋ถ์ ๋ฐ ์์ง ์ํ ํ๋จ ์๊ณ์น ์ฌ์กฐ์ \n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"AAR ๊ธฐ๋ฐ ์์ง ์ด์ ์คํ ์ฌ๋ก ๋ถ์ ๋ฐ ํ๋จ ์๊ณ์น๋ฅผ ์ฌ์กฐ์ ํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **AAR๋ก ์์ง ์ด์ ์คํ ์ฌ๋ก๋ฅผ ๋ถ์ํ๊ณ ํ๋จ ์๊ณ์น๋ฅผ ์ฌ์กฐ์ ํด ํจ๊ณผ๋ฅผ ๋ณ๋ ๊ฒ์ฆ ์ธํธ๋ก ํ์ธํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **ํ๋์ฉ ๊ฐ์ ์๋ฃ์์ Baseline ์คํ์ ์ค๊ฒฝ๋ณด(์ผ์ ๋๋ฆฌํํธ, ๋จ๋ฐ ๋์ฅ๊ท ), ๊ฒฝ๊ณ ํผ๋, ๋์นจ(ํํ, ์ค๊ธ์ ์๋ง ์ฆ๊ฐ) ๋ฑ ์์ธ๋ณ๋ก ๋ถ๋ฅ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `docs/wel03_aar_report.md ๋ฐ configs/wel03_tuned_params.yaml` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ค๋ฅ ์์ธ ๊ท๋ช
์ ๊ฐ๊ด์ฑ ๋ฐ ๊ฐ์ ์กฐ์น์ ๋
ผ๋ฆฌ์ ํ๋น์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: AAR ํผ๋๋ฐฑ ์ ์ฉ ํ ์ข
ํฉ ์ ํ๋ 10%p ์ด์ ํฅ์, ์ฌ๋ฐ ์ค๋ฅ 0๊ฑด\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: AAR ๋ณด๊ณ ์ ์์ฑ ๋ฐ ํ๋ผ๋ฏธํฐ ํ๋ ์ ํ ํ๊ท ํ
์คํธ ํต๊ณผ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ํ๊ท ํ
์คํธ(Regression Test) ์ค์ํธ ์ ์ฒด ์๋ ์คํ ๋ฐ ๋น๊ต\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"AAR(์ฌํ ๊ฒํ )๋ก ๊ธฐ์ค์น Baseline์ ์์ง ์ด์ ์คํ ์ฌ๋ก๋ฅผ ์์ธ๋ณ๋ก ๋ถ๋ฅํ๊ณ , ๋จ๋ฐ ๋์ฅ๊ท ๊ฒ์ถยท์๋งํ ์ค๊ธ์ ์ฆ๊ฐ์ ๋ํ ํ๋จ ์๊ณ์น๋ฅผ ํ๋ ์ธํธ์์ ์ฌ์กฐ์ ํ ๋ค ๊ฒ์ฆ ์ธํธยท๋ฒค์น๋งํฌ ํ๊ท ํ
์คํธ๋ก ์ ํ๋ฅผ ๋น๊ตํ๊ณ ์กฐ์ ํ๋ผ๋ฏธํฐ๋ฅผ YAML๋ก ์ถ๋ ฅํ๋ค.\"\"\"\n",
"import itertools\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ผ์ ๊ธ์์ ๊ฐ์ ์์ง ์ผ์ ๋ฐ์ดํฐ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์์นยท๊ฒ์ฌ ๊ฒฐ๊ณผ ์์)\n",
"# ๊ฐ์ ๊ธ์์ 8๊ณณ x 14์ผ x 1์๊ฐ ๊ฐ๊ฒฉ. ์ค์ ์์ง(true_*)ยท์ผ์ ์ธก์ ๊ฐยท์ค์ผ ์ฌ๊ฑดยท์ผ์ ๊ณ ์ฅ ์ ๋ต์ ํจ๊ป ๋ง๋ ๋ค.\n",
"# ๊ธฐ์ค์น๋ ๊ต์ก์ฉ ๊ฐ์ ๊ฐ์ด๋ฉฐ, ์ค์ ์์ฉ ํ์ ์ ๊ณต์ธ ๊ฒ์ฌ ๊ฒฐ๊ณผ์ ์๋ฌด(์๋ฐฉ์ํ) ๋ด๋น ๊ธฐ์ค์ ๋ฐ๋ฅธ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"PARAMS = [\"ph\", \"turbidity\", \"chlorine\", \"metal\", \"ecoli\"]\n",
"SOURCES = [(\"W01\", \"water_truck\", 1.0, 2.0), (\"W02\", \"water_truck\", 3.5, 1.2), (\"W03\", \"purification_unit\", 2.2, 4.1),\n",
" (\"W04\", \"purification_unit\", 5.0, 3.3), (\"W05\", \"well\", 0.8, 5.6), (\"W06\", \"well\", 4.4, 6.0),\n",
" (\"W07\", \"river_intake\", 6.3, 1.8), (\"W08\", \"river_intake\", 6.9, 4.9)]\n",
"BASE = {\"water_truck\": (7.2, 0.25, 0.75, 1.5), \"purification_unit\": (7.0, 0.20, 0.85, 1.2),\n",
" \"well\": (6.8, 0.45, 0.50, 3.0), \"river_intake\": (7.3, 0.55, 0.60, 2.0)} # pH, ํ๋(NTU), ์๋ฅ์ผ์(mg/L), ์ค๊ธ์(ug/L)\n",
"LIMITS = {\"ph\": (5.8, 8.5), \"turbidity\": (1.0, 2.0), \"chlorine\": (0.2, 0.1), \"metal\": (5.0, 10.0), \"ecoli\": (1, 1)} # (์ฃผ์, ์ํ) ๊ฐ์ ๊ธฐ์ค\n",
"EVENT_TYPES = { # ์ค์ผ ์ฌ๊ฑด: ๋ณ์๋ณ ์ต๋ ๋ณํ๋, ์ต๋์น ๋๋ฌ ์๊ฐ(h)\n",
" \"sewage\": ({\"turbidity\": 2.8, \"chlorine\": -0.5, \"ecoli\": 30, \"ph\": -0.3}, 4),\n",
" \"metal\": ({\"metal\": 22.0, \"ph\": -0.5}, 4),\n",
" \"disinfect_fail\": ({\"chlorine\": -0.8, \"ecoli\": 6}, 6),\n",
" \"chemical\": ({\"ph\": 1.7, \"turbidity\": 1.0}, 3),\n",
" \"creeping_metal\": ({\"metal\": 13.0}, 18),\n",
"}\n",
"FAULT_TYPES = [\"stuck\", \"spike\", \"drift\", \"dropout\"]\n",
"NOISE = {\"ph\": 0.04, \"turbidity\": 0.06, \"chlorine\": 0.03, \"metal\": 0.35}\n",
"DIGITS = {\"ph\": 2, \"turbidity\": 2, \"chlorine\": 2, \"metal\": 1}\n",
"\n",
"\n",
"def make_dataset(days=14, seed=7, n_events=6, n_faults=10, events=None, faults=None, sources=None):\n",
" \"\"\"events: [(๊ธ์์ ๋ฒํธ, ์ฌ๊ฑด ์ ํ, ์์ ์๊ฐ h, ์ง์ h, ๊ฐ๋)], faults: [(๊ธ์์ ๋ฒํธ, ๊ณ ์ฅ ์ ํ, ํญ๋ชฉ, ์์ h, ์ง์ h)]\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" sources = sources or SOURCES\n",
" T = days * 24\n",
" tt = np.arange(T)\n",
" hour = tt % 24\n",
" night = (hour < 6) | (hour >= 20)\n",
" rain = np.zeros(T)\n",
" for _ in range(max(1, days // 4)):\n",
" s0, d = int(rng.integers(0, T - 14)), int(rng.integers(6, 14))\n",
" rain[s0:s0 + d] = np.maximum(rain[s0:s0 + d], rng.uniform(0.6, 1.0))\n",
" if events is None:\n",
" events = [(int(rng.integers(len(sources))), str(rng.choice(list(EVENT_TYPES))), int(rng.integers(min(24, T // 4), max(T // 4 + 1, T - 48))),\n",
" int(rng.integers(10, 30)), 1.0) for _ in range(n_events)]\n",
" if faults is None:\n",
" faults = [(int(rng.integers(len(sources))), str(rng.choice(FAULT_TYPES)), str(rng.choice([\"ph\", \"turbidity\", \"chlorine\", \"metal\"])),\n",
" int(rng.integers(12, max(13, T - 30))), int(rng.integers(6, 24))) for _ in range(n_faults)]\n",
" out = []\n",
" for si, (sid, stype, x, y) in enumerate(sources):\n",
" ph0, tb0, cl0, mt0 = BASE[stype]\n",
" tr = {\"ph\": ph0 + 0.08 * np.sin(2 * np.pi * hour / 24) + rng.normal(0, 0.03, T),\n",
" \"turbidity\": tb0 * (1 + 0.1 * rng.normal(0, 1, T)),\n",
" \"chlorine\": (cl0 * np.exp(-0.022 * hour) if stype == \"water_truck\" else cl0 + rng.normal(0, 0.03, T)),\n",
" \"metal\": mt0 + rng.normal(0, 0.2, T), \"ecoli\": np.zeros(T)}\n",
" if stype in (\"river_intake\", \"well\"):\n",
" tr[\"turbidity\"] = tr[\"turbidity\"] + rain * (0.9 if stype == \"river_intake\" else 0.45)\n",
" f_all, etype = np.zeros(T), np.array([\"none\"] * T, dtype=object)\n",
" for (s_i, et, onset, dur, scale) in events:\n",
" if s_i != si:\n",
" continue\n",
" delta, ramp = EVENT_TYPES[et]\n",
" f = np.clip((tt - onset + 1) / ramp, 0, 1) * ((tt >= onset) & (tt < onset + dur + ramp))\n",
" for k, v in delta.items():\n",
" tr[k] = tr[k] + v * f * scale\n",
" etype[f > 0] = et\n",
" f_all = np.maximum(f_all, f)\n",
" tr[\"chlorine\"] = np.clip(tr[\"chlorine\"], 0.02, None)\n",
" tr[\"turbidity\"] = np.clip(tr[\"turbidity\"], 0.05, None)\n",
" tr[\"metal\"] = np.clip(tr[\"metal\"], 0.1, None)\n",
" grade = np.where(f_all >= 0.2, 2, 0)\n",
" mild = (tr[\"turbidity\"] > LIMITS[\"turbidity\"][0]) | (tr[\"chlorine\"] < LIMITS[\"chlorine\"][0]) | (tr[\"metal\"] > LIMITS[\"metal\"][0])\n",
" grade = np.where((grade == 0) & mild, 1, grade)\n",
" ms = {}\n",
" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n",
" sd = NOISE[p] * np.where(night, 1.5, 1.0) * (tr[p] if p == \"turbidity\" else 1)\n",
" ms[p] = np.round(np.clip(tr[p] + rng.normal(0, 1, T) * sd, 0, None), DIGITS[p])\n",
" ms[\"ecoli\"] = (rng.poisson(tr[\"ecoli\"]) + (rng.random(T) < 0.004)).astype(float)\n",
" fault, fparam = np.array([\"none\"] * T, dtype=object), np.array([\"-\"] * T, dtype=object)\n",
" for (s_i, ft, p, s0, d) in faults:\n",
" if s_i != si:\n",
" continue\n",
" seg = slice(s0, min(T, s0 + d))\n",
" if ft == \"stuck\":\n",
" ms[p][seg] = ms[p][s0]\n",
" elif ft == \"spike\":\n",
" seg = slice(s0, s0 + 1)\n",
" ms[p][s0] = {\"ph\": 11.8, \"turbidity\": 38.0, \"chlorine\": 0.0, \"metal\": 75.0}[p]\n",
" elif ft == \"drift\":\n",
" ramp = np.linspace(0, 1, seg.stop - s0)\n",
" ms[p][seg] = np.clip(ms[p][seg] + ramp * {\"ph\": -1.6, \"turbidity\": 2.6, \"chlorine\": -0.75, \"metal\": 12.0}[p], 0, None).round(DIGITS[p])\n",
" elif ft == \"dropout\":\n",
" for q in PARAMS:\n",
" ms[q][seg] = np.nan\n",
" p = \"all\"\n",
" fault[seg], fparam[seg] = ft, p\n",
" df = pd.DataFrame({\"source_id\": sid, \"source_type\": stype, \"x_km\": x, \"y_km\": y, \"t\": tt,\n",
" \"cond\": np.where(rain > 0, \"storm\", np.where(night, \"night\", \"day\")),\n",
" **{p: ms[p] for p in PARAMS}, **{f\"true_{p}\": tr[p] for p in PARAMS},\n",
" \"event\": etype, \"fault\": fault, \"fault_param\": fparam, \"grade\": grade})\n",
" out.append(df)\n",
" return pd.concat(out, ignore_index=True)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌ ๊ณตํต ํจ์: ๋ฌผ๋ฆฌ ๋ฒ์ ์ ๊ฒ -> ๊ณ ์ ๊ฐ(stuck) ํ์ -> ๋จ๋ฐ ์คํ์ดํฌ ์ ๊ฑฐ -> ๊ฒฐ์ธก ๋ณด์ ----\n",
"PHYS = {\"ph\": (0, 14), \"turbidity\": (0, 1000), \"chlorine\": (0, 10), \"metal\": (0, 1000), \"ecoli\": (0, 100000)}\n",
"SPIKE_K = {\"day\": 6.0, \"night\": 7.0, \"storm\": 9.0} # ์ํฉ๋ณ(์ฃผ๊ฐ/์ผ๊ฐ/์
์ฒํ) ์คํ์ดํฌ ํ์ ๋ฐฐ์\n",
"MIN_JUMP = {\"ph\": 0.8, \"turbidity\": 1.5, \"chlorine\": 0.3, \"metal\": 6.0}\n",
"STUCK_RUN, SHORT_GAP = 6, 3\n",
"\n",
"\n",
"def clean(df, spike_k=None, min_jump=None):\n",
" \"\"\"๊ธ์์๋ณ ์๊ณ์ด์ ์ ์ ํ๊ณ ํญ๋ชฉ๋ณ ์ฒ๋ฆฌ ๋ด์ญ(<ํญ๋ชฉ>_flag: range/stuck/spike/interp/long_gap)์ ๋จ๊ธด๋ค.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 9] AAR ๊ธฐ๋ฐ ์์ง ์ด์ ์คํ ์ฌ๋ก ๋ถ์ ๋ฐ ์์ง ์ํ ํ๋จ ์๊ณ์น ์ฌ์กฐ์ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"GRADE_NAME = {0: \"์์ \", 1: \"์ฃผ์\", 2: \"์ํ\"}\n",
"\n",
"\n",
"def rule_grade(df):\n",
" \"\"\"๊ธฐ์ค์น 1์ฐจ ํํฐ: ์ํ(์์ฉ ๋ถ์ ํฉ ์์ฌ) = ํ ํญ๋ชฉ์ด๋ผ๋ ์ํ ๊ธฐ์ค ์ด๊ณผ, ์ฃผ์ = ์ฃผ์ ๊ธฐ์ค ์ด๊ณผ.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 9] AAR ๊ธฐ๋ฐ ์์ง ์ด์ ์คํ ์ฌ๋ก ๋ถ์ ๋ฐ ์์ง ์ํ ํ๋จ ์๊ณ์น ์ฌ์กฐ์ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def reasons(row):\n",
" r = []\n",
" if not LIMITS[\"ph\"][0] <= row[\"ph\"] <= LIMITS[\"ph\"][1]:\n",
" r.append(f\"pH {row['ph']:.2f}\")\n",
" if row[\"turbidity\"] > LIMITS[\"turbidity\"][0]:\n",
" r.append(f\"ํ๋ {row['turbidity']:.2f}NTU\")\n",
" if row[\"chlorine\"] < LIMITS[\"chlorine\"][0]:\n",
" r.append(f\"์๋ฅ์ผ์ {row['chlorine']:.2f}mg/L\")\n",
" if row[\"metal\"] > LIMITS[\"metal\"][0]:\n",
" r.append(f\"์ค๊ธ์ {row['metal']:.1f}ug/L\")\n",
" if row[\"ecoli\"] >= 1:\n",
" r.append(f\"๋์ฅ๊ท {row['ecoli']:.0f}\")\n",
" return \", \".join(r) or \"-\"\n",
"\n",
"\n",
"FEATS = [f\"{p}{s}\" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"] for s in [\"\", \"_d1\", \"_d3\", \"_dev\", \"_sd6\"]] + \\\n",
" [\"ecoli\", \"ecoli_3h\", \"storm\", \"night\", \"n_flag\", \"truck\", \"purif\", \"well\", \"river\"]\n",
"\n",
"\n",
"def add_features(c):\n",
" \"\"\"์๊ฐ ์์๋ฅผ ์งํค๋(๊ณผ๊ฑฐ ๊ฐ๋ง ์ฐ๋) ๊ธ์์๋ณ ํน์ง: ๋ณํ๋, 24์๊ฐ ๊ธฐ์ค์ ๋๋น ํธ์ฐจ, 6์๊ฐ ๋ณ๋์ฑ, ๋์ฅ๊ท ๋์ ๋ฑ.\"\"\"\n",
" c = c.sort_values([\"source_id\", \"t\"]).reset_index(drop=True)\n",
" g = c.groupby(\"source_id\")\n",
" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n",
" c[f\"{p}_d1\"] = g[p].diff().fillna(0)\n",
" c[f\"{p}_d3\"] = g[p].diff(3).fillna(0)\n",
" base = g[p].transform(lambda s: s.shift(1).rolling(24, min_periods=4).median())\n",
" c[f\"{p}_dev\"] = (c[p] - base).fillna(0)\n",
" c[f\"{p}_sd6\"] = g[p].transform(lambda s: s.rolling(6, min_periods=2).std()).fillna(0)\n",
" c[\"ecoli_3h\"] = g[\"ecoli\"].transform(lambda s: s.rolling(3, min_periods=1).sum())\n",
" c[\"storm\"], c[\"night\"] = (c[\"cond\"] == \"storm\").astype(int), (c[\"cond\"] == \"night\").astype(int)\n",
" flags = [f\"{p}_flag\" for p in PARAMS if f\"{p}_flag\" in c]\n",
" c[\"n_flag\"] = sum(c[f].isin([\"stuck\", \"spike\", \"long_gap\", \"range\"]).astype(int) for f in flags) if flags else 0\n",
" for k, name in [(\"truck\", \"water_truck\"), (\"purif\", \"purification_unit\"), (\"well\", \"well\"), (\"river\", \"river_intake\")]:\n",
" c[k] = (c[\"source_type\"] == name).astype(int)\n",
" return c\n",
"\n",
"BEFORE = {\"ecoli_3h_min\": 1, \"metal_dev\": 99.0, \"metal_min\": 99.0} # ํํ: ๋์ฅ๊ท 1ํ ๊ฒ์ถ ์ฆ์ ์ํ, ์ถ์ธ ๊ท์น ์์\n",
"\n",
"\n",
"def tuned_grade(c, P):\n",
" \"\"\"์ฌ์กฐ์ ๊ท์น: ๋์ฅ๊ท ์ 3์๊ฐ ๋์ ๊ธฐ์ค(๋๋ ํ๋ยท์ผ์ ์ด์ ๋๋ฐ ์ ์ฆ์), ์ค๊ธ์์ ๊ธฐ์ค์ ๋๋น ์์น ์ถ์ธ ๊ท์น ์ถ๊ฐ.\"\"\"\n",
" ph, tb, cl, mt, ec = (c[p].to_numpy(float) for p in PARAMS)\n",
" if P[\"ecoli_3h_min\"] <= 1: # ํํ ๊ท์น: ๊ทธ ์๊ฐ 1ํ ๊ฒ์ถ์ด๋ฉด ์ํ\n",
" eco = ec >= 1\n",
" else:\n",
" eco = (c[\"ecoli_3h\"].to_numpy() >= P[\"ecoli_3h_min\"]) | ((ec >= 1) & ((tb > LIMITS[\"turbidity\"][0]) | (cl < LIMITS[\"chlorine\"][0])))\n",
" trend = (c[\"metal_dev\"].to_numpy() > P[\"metal_dev\"]) & (mt > P[\"metal_min\"])\n",
" danger = (ph < LIMITS[\"ph\"][0]) | (ph > LIMITS[\"ph\"][1]) | (tb > LIMITS[\"turbidity\"][1]) | (cl < LIMITS[\"chlorine\"][1]) \\\n",
" | (mt > LIMITS[\"metal\"][1]) | eco | trend\n",
" caution = (tb > LIMITS[\"turbidity\"][0]) | (cl < LIMITS[\"chlorine\"][0]) | (mt > LIMITS[\"metal\"][0]) | (ec >= 1)\n",
" return np.where(danger, 2, np.where(caution, 1, 0))\n",
"\n",
"\n",
"def cause(r, pred):\n",
" if r[\"grade\"] == 2 and pred != 2:\n",
" return {\"creeping_metal\": \"๋์นจ-์ค๊ธ์ ์๋ง ์ฆ๊ฐ\", \"chemical\": \"๋์นจ-ํํ(์ด๊ธฐ pH ๋ณํ)\"}.get(r[\"event\"], f\"๋์นจ-{r['event']}\")\n",
" if r[\"grade\"] != 2 and pred == 2:\n",
" if r[\"fault\"] != \"none\":\n",
" return f\"์ค๊ฒฝ๋ณด-์ผ์ {r['fault']}\"\n",
" return \"์ค๊ฒฝ๋ณด-๋จ๋ฐ ๋์ฅ๊ท \" if r[\"ecoli\"] >= 1 else \"์ค๊ฒฝ๋ณด-๊ธฐํ\"\n",
" return \"์ฃผ์/์์ ๊ฒฝ๊ณ ํผ๋\"\n",
"\n",
"\n",
"D = {name: add_features(clean(make_dataset(seed=s, n_events=10, n_faults=14))) for name, s in\n",
" [(\"ํ๋(seed31)\", 31), (\"๊ฒ์ฆ(seed41)\", 41), (\"๋ฒค์น๋งํฌ(seed11)\", 11)]}\n",
"\n",
"# 1) AAR: ํ๋ ์ธํธ์ ์คํ ์์ธ ๋ถ๋ฅ\n",
"tune = D[\"ํ๋(seed31)\"]\n",
"p0 = tuned_grade(tune, BEFORE)\n",
"err = tune[p0 != tune[\"grade\"]]\n",
"causes = pd.Series([cause(r, p) for (_, r), p in zip(err.iterrows(), p0[p0 != tune[\"grade\"]])]).value_counts()\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] AAR ๋์ ์คํ {len(err)}๊ฑด(ํ๋ ์ธํธ {len(tune):,}๊ฑด) ์์ธ ๋ถ๋ฅ\")\n",
"print(causes.to_string())\n",
"\n",
"# 2) ์ฌ์กฐ์ : ๊ณ ์น ์ ์๋ ์์ธ(๋จ๋ฐ ๋์ฅ๊ท , ์ค๊ธ์ ์๋ง ์ฆ๊ฐ)์ ๋ํ ์๊ณ์น ๊ฒฉ์ ํ์\n",
"# AAR ๊ฒฐ์ : ๋์ฅ๊ท ๋จ๋ฐ 1ํ ๊ฒ์ถ์ '์ฃผ์ + ์ฌ์ฑ์'๋ก ๋ฎ์ถ๊ณ (3์๊ฐ ๋์ 2ํ ์ด์๋ถํฐ ์ํ), ๊ทธ ๊ธฐ์ค๊ฐ๋ง ํ์ํ๋ค\n",
"best = None\n",
"for e3, md, mm in itertools.product([2, 3], [1.0, 1.5, 2.0, 3.0], [4.0, 5.0, 6.0]):\n",
" P = {\"ecoli_3h_min\": e3, \"metal_dev\": md, \"metal_min\": mm}\n",
" acc = (tuned_grade(tune, P) == tune[\"grade\"]).mean()\n",
" if best is None or acc > best[0]:\n",
" best = (acc, P)\n",
"AFTER = best[1]\n",
"print(\"\\n์ฌ์กฐ์ ํ๋ผ๋ฏธํฐ:\", AFTER)\n",
"\n",
"# 3) ํ๊ท ํ
์คํธ: ์ธ ๋ฐ์ดํฐ์
์์ ์ ํ ๋น๊ต\n",
"rows = []\n",
"for name, c in D.items():\n",
" b, a = tuned_grade(c, BEFORE), tuned_grade(c, AFTER)\n",
" d = c[\"grade\"] == 2\n",
" rows.append({\"๋ฐ์ดํฐ\": name, \"์ ํ๋ ์ \": f\"{(b == c['grade']).mean():.1%}\", \"์ ํ๋ ํ\": f\"{(a == c['grade']).mean():.1%}\",\n",
" \"ํฅ์(%p)\": round(((a == c[\"grade\"]).mean() - (b == c[\"grade\"]).mean()) * 100, 1),\n",
" \"์คํ ์ โํ\": f\"{int((b != c['grade']).sum())}โ{int((a != c['grade']).sum())}\",\n",
" \"์๋ก ํ๋ฆผ\": int(((b == c[\"grade\"]) & (a != c[\"grade\"])).sum()),\n",
" \"์ํ ์ฌํ์จ ํ\": f\"{((a == 2) & d).sum() / d.sum():.1%}\"})\n",
"reg = pd.DataFrame(rows)\n",
"print(\"\\n[ํ๊ท ํ
์คํธ]\")\n",
"print(reg.to_string(index=False))\n",
"gain = reg.loc[1, \"ํฅ์(%p)\"]\n",
"print(f\"๊ฒ์ฆ ์ธํธ ์ข
ํฉ ์ ํ๋ ํฅ์ {gain}%p (๋ชฉํ 10%p ์ด์: {'๋ฌ์ฑ' if gain >= 10 else '๋ฏธ๋ฌ'} - Baseline ์ ํ๋๊ฐ ์ด๋ฏธ 97% ์์ค์ด๋ผ 10%p ์ฌ์ง๊ฐ ์์)\")\n",
"\n",
"# 4) ์ฌ๋ฐ ์ค๋ฅ: ๊ณ ์น ์์ธ ์ ํ์ด ๊ฒ์ฆ ์ธํธ์์ ๋ค์ ๋์ค๋๊ฐ\n",
"val = D[\"๊ฒ์ฆ(seed41)\"]\n",
"pa = tuned_grade(val, AFTER)\n",
"ev = val[pa != val[\"grade\"]]\n",
"vc = pd.Series([cause(r, p) for (_, r), p in zip(ev.iterrows(), pa[pa != val[\"grade\"]])]).value_counts()\n",
"fixed_types = [\"์ค๊ฒฝ๋ณด-๋จ๋ฐ ๋์ฅ๊ท \", \"๋์นจ-์ค๊ธ์ ์๋ง ์ฆ๊ฐ\"]\n",
"rec = {k: int(vc.get(k, 0)) for k in fixed_types}\n",
"print(f\"\\n์ฌ๋ฐ ์ค๋ฅ(๊ณ ์น ์ ํ์ ๊ฒ์ฆ ์ธํธ ์ฌ๋ฐ): {rec} -> ํฉ๊ณ {sum(rec.values())}๊ฑด(๋ชฉํ 0๊ฑด: {'๋ฌ์ฑ' if sum(rec.values()) == 0 else '๋ฏธ๋ฌ'})\")\n",
"print(\"๋จ์ ์์ธ ์์:\", vc.head(3).to_dict(), \"-> ์ผ์ ๊ณ ์ฅ(๋๋ฆฌํํธยทํต์ ๋จ์ ) ์ค๊ฒฝ๋ณด๋ ๊ต์ฐจ ์ผ์ยท๊ต์ ์ฃผ๊ธฐ์ ๋ฏธ์
5 ์ ๋ขฐ๋ ์ง์๋ก, ํํ ์ด๊ธฐ ๋ณํ๋ pH ๋ณํ์จ ๊ท์น ํ๋ณด๋ก ๋ค์ AAR์ ๋๊ธด๋ค\")\n",
"\n",
"print(\"\\n# configs/wel03_tuned_params.yaml\")\n",
"print(f\"ecoli:\\n danger_3h_count: {AFTER['ecoli_3h_min']} # ๋จ๋ฐ 1ํ๋ '์ฃผ์ + ์ฌ์ฑ์'\\nmetal_trend:\\n\"\n",
" f\" dev_from_24h_baseline_ugL: {AFTER['metal_dev']}\\n min_level_ugL: {AFTER['metal_min']}\\nreviewed_by: ์๋ฐฉ์ํ ๋ด๋น(๊ฐ์) # ์ค์ ๊ธฐ์ค์ ๊ณต์ ์์ง ๊ธฐ์ค ํ์ธ\")\n",
"\n",
"fig, ax = plt.subplots(figsize=(8, 3.5))\n",
"x = np.arange(len(reg))\n",
"ax.bar(x - 0.2, [(tuned_grade(c, BEFORE) != c[\"grade\"]).sum() for c in D.values()], 0.4, label=\"before\", color=\"tab:gray\")\n",
"ax.bar(x + 0.2, [(tuned_grade(c, AFTER) != c[\"grade\"]).sum() for c in D.values()], 0.4, label=\"after\", color=\"tab:blue\")\n",
"ax.set_xticks(x, [\"tune(31)\", \"valid(41)\", \"bench(11)\"]); ax.set_ylabel(\"misjudged hours\"); ax.legend()\n",
"ax.set_title(\"AAR retuning: misjudgments before/after (synthetic)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
09 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] AAR ๋์ ์คํ 110๊ฑด(ํ๋ ์ธํธ 2,688๊ฑด) ์์ธ ๋ถ๋ฅ\n",
"์ค๊ฒฝ๋ณด-์ผ์ drift 38\n",
"์ฃผ์/์์ ๊ฒฝ๊ณ ํผ๋ 28\n",
"๋์นจ-ํํ(์ด๊ธฐ pH ๋ณํ) 20\n",
"๋์นจ-์ค๊ธ์ ์๋ง ์ฆ๊ฐ 16\n",
"์ค๊ฒฝ๋ณด-๋จ๋ฐ ๋์ฅ๊ท 7\n",
"๋์นจ-metal 1\n",
"\n",
"์ฌ์กฐ์ ํ๋ผ๋ฏธํฐ: {'ecoli_3h_min': 2, 'metal_dev': 1.0, 'metal_min': 5.0}\n",
"\n",
"[ํ๊ท ํ
์คํธ]\n",
" ๋ฐ์ดํฐ ์ ํ๋ ์ ์ ํ๋ ํ ํฅ์(%p) ์คํ ์ โํ ์๋ก ํ๋ฆผ ์ํ ์ฌํ์จ ํ\n",
" ํ๋(seed31) 95.9% 96.2% 0.3 110โ102 8 91.8%\n",
" ๊ฒ์ฆ(seed41) 97.1% 97.2% 0.1 78โ75 6 92.2%\n",
"๋ฒค์น๋งํฌ(seed11) 97.9% 98.4% 0.5 56โ43 6 96.3%\n",
"๊ฒ์ฆ ์ธํธ ์ข
ํฉ ์ ํ๋ ํฅ์ 0.1%p (๋ชฉํ 10%p ์ด์: ๋ฏธ๋ฌ - Baseline ์ ํ๋๊ฐ ์ด๋ฏธ 97% ์์ค์ด๋ผ 10%p ์ฌ์ง๊ฐ ์์)\n",
"\n",
"์ฌ๋ฐ ์ค๋ฅ(๊ณ ์น ์ ํ์ ๊ฒ์ฆ ์ธํธ ์ฌ๋ฐ): {'์ค๊ฒฝ๋ณด-๋จ๋ฐ ๋์ฅ๊ท ': 0, '๋์นจ-์ค๊ธ์ ์๋ง ์ฆ๊ฐ': 1} -> ํฉ๊ณ 1๊ฑด(๋ชฉํ 0๊ฑด: ๋ฏธ๋ฌ)\n",
"๋จ์ ์์ธ ์์: {'์ฃผ์/์์ ๊ฒฝ๊ณ ํผ๋': 21, '์ค๊ฒฝ๋ณด-์ผ์ dropout': 18, '๋์นจ-ํํ(์ด๊ธฐ pH ๋ณํ)': 15} -> ์ผ์ ๊ณ ์ฅ(๋๋ฆฌํํธยทํต์ ๋จ์ ) ์ค๊ฒฝ๋ณด๋ ๊ต์ฐจ ์ผ์ยท๊ต์ ์ฃผ๊ธฐ์ ๋ฏธ์
5 ์ ๋ขฐ๋ ์ง์๋ก, ํํ ์ด๊ธฐ ๋ณํ๋ pH ๋ณํ์จ ๊ท์น ํ๋ณด๋ก ๋ค์ AAR์ ๋๊ธด๋ค\n",
"\n",
"# configs/wel03_tuned_params.yaml\n",
"ecoli:\n",
" danger_3h_count: 2 # ๋จ๋ฐ 1ํ๋ '์ฃผ์ + ์ฌ์ฑ์'\n",
"metal_trend:\n",
" dev_from_24h_baseline_ugL: 1.0\n",
" min_level_ugL: 5.0\n",
"reviewed_by: ์๋ฐฉ์ํ ๋ด๋น(๊ฐ์) # ์ค์ ๊ธฐ์ค์ ๊ณต์ ์์ง ๊ธฐ์ค ํ์ธ\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
10. ๊ฐ์ ์์ง๊ฒ์ฌ ๋ฐ์ดํฐ ์ํ ์ ํธ ๋์๋ณด๋ ์์คํ
ํตํฉ ํจํค์ง ์์ฑ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๊ฐ์ ์์ง๊ฒ์ฌ ๋ฐ์ดํฐ ์ํ ์ ํธ ๋์๋ณด๋ ์์คํ
ํตํฉ ํจํค์ง๋ฅผ ์์ฑํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๊ฐ์ ์์ง๊ฒ์ฌ ๋ฐ์ดํฐ ์ํ ์ ํธ ๋์๋ณด๋๋ฅผ ํ๋์ ํตํฉ ํจํค์ง๋ก ์์ฑํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์
๋ ฅ ๊ฒ์ฆ, ์ ์ฒ๋ฆฌ, ์ฌ์กฐ์ ๊ธฐ์ค์น ํ์ , ๋๋ค ํฌ๋ ์คํธ ์ํ ๋ถ๋ฅ, ์ ๋ขฐ๋ ๋ฑ๊ธ, ์ฌ๋ ๊ฒํ ํ ๋ฑ๋ก, ์ํฉํ ์์ฝ์ ํ ํ์ผ์ ํ์ดํ๋ผ์ธ์ผ๋ก ๋ฌถ๋๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/main_wel03.py, README.md, docs/wel03_final_report.pdf(md)` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ตฐ ์ค๋ฌด ์ฆ์ ๋์
๊ฐ๋ฅ์ฑ, ์ด์ฉ ์ ๋ขฐ๋ ๋ฐ ์ ๋ต์ ๊ธฐ๋ํจ๊ณผ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: End-to-End ํตํฉ ํ์ดํ๋ผ์ธ ์คํ ์ฑ๊ณต๋ฅ 100%, ํ
์คํธ ์ปค๋ฒ๋ฆฌ์ง 85% ์ด์\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์ํด๋ฆญ ์คํ ์คํฌ๋ฆฝํธ ๋์ ํ์ธ ๋ฐ ์ต์ข
๋ฐํ ์ฐ์ถ๋ฌผ ์์ฑ ์๋ฃ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ์ต์ข
๊ฒ์ฆ ์์ํ(ํ๊ฐ๊ด) ๋ชจ์ ์์ฐ ๋ฐ ์ ์ฒด ๊ธฐ๋ฅ ํตํฉ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์
๋ ฅ ๊ฒ์ฆ, ์ ์ฒ๋ฆฌ, ์ฌ์กฐ์ ๊ธฐ์ค์น ํ์ ๊ณผ ๋๋ค ํฌ๋ ์คํธ ์ํ ๋ถ๋ฅ, ์ ๋ขฐ๋ ๋ฑ๊ธ, ์ธ๊ฐ ๊ฒํ ํ ๋ฑ๋ก, ์ํฉํ ์์ฝ๊น์ง ๊ฐ์ ์์ง๊ฒ์ฌ ์ํ ์ ํธ ๋์๋ณด๋๋ฅผ ํ ํ์ผ๋ก ํตํฉ ์คํํ๊ณ , ์ฌ๋ฌ ์๋๋ฆฌ์ค End-to-End ์คํยท์์ฒด ์ ๊ฒ(ํจ์ ์ปค๋ฒ๋ฆฌ์ง)ยท์งํ๊ด ๋ธ๋ฆฌํ ์์ฝ์ ์ถ๋ ฅํ๋ค.\"\"\"\n",
"import sys\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.ensemble import RandomForestClassifier\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ผ์ ๊ธ์์ ๊ฐ์ ์์ง ์ผ์ ๋ฐ์ดํฐ ์์ฑ๊ธฐ (์ค์ ๋ถ๋ยท์์นยท๊ฒ์ฌ ๊ฒฐ๊ณผ ์์)\n",
"# ๊ฐ์ ๊ธ์์ 8๊ณณ x 14์ผ x 1์๊ฐ ๊ฐ๊ฒฉ. ์ค์ ์์ง(true_*)ยท์ผ์ ์ธก์ ๊ฐยท์ค์ผ ์ฌ๊ฑดยท์ผ์ ๊ณ ์ฅ ์ ๋ต์ ํจ๊ป ๋ง๋ ๋ค.\n",
"# ๊ธฐ์ค์น๋ ๊ต์ก์ฉ ๊ฐ์ ๊ฐ์ด๋ฉฐ, ์ค์ ์์ฉ ํ์ ์ ๊ณต์ธ ๊ฒ์ฌ ๊ฒฐ๊ณผ์ ์๋ฌด(์๋ฐฉ์ํ) ๋ด๋น ๊ธฐ์ค์ ๋ฐ๋ฅธ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"PARAMS = [\"ph\", \"turbidity\", \"chlorine\", \"metal\", \"ecoli\"]\n",
"SOURCES = [(\"W01\", \"water_truck\", 1.0, 2.0), (\"W02\", \"water_truck\", 3.5, 1.2), (\"W03\", \"purification_unit\", 2.2, 4.1),\n",
" (\"W04\", \"purification_unit\", 5.0, 3.3), (\"W05\", \"well\", 0.8, 5.6), (\"W06\", \"well\", 4.4, 6.0),\n",
" (\"W07\", \"river_intake\", 6.3, 1.8), (\"W08\", \"river_intake\", 6.9, 4.9)]\n",
"BASE = {\"water_truck\": (7.2, 0.25, 0.75, 1.5), \"purification_unit\": (7.0, 0.20, 0.85, 1.2),\n",
" \"well\": (6.8, 0.45, 0.50, 3.0), \"river_intake\": (7.3, 0.55, 0.60, 2.0)} # pH, ํ๋(NTU), ์๋ฅ์ผ์(mg/L), ์ค๊ธ์(ug/L)\n",
"LIMITS = {\"ph\": (5.8, 8.5), \"turbidity\": (1.0, 2.0), \"chlorine\": (0.2, 0.1), \"metal\": (5.0, 10.0), \"ecoli\": (1, 1)} # (์ฃผ์, ์ํ) ๊ฐ์ ๊ธฐ์ค\n",
"EVENT_TYPES = { # ์ค์ผ ์ฌ๊ฑด: ๋ณ์๋ณ ์ต๋ ๋ณํ๋, ์ต๋์น ๋๋ฌ ์๊ฐ(h)\n",
" \"sewage\": ({\"turbidity\": 2.8, \"chlorine\": -0.5, \"ecoli\": 30, \"ph\": -0.3}, 4),\n",
" \"metal\": ({\"metal\": 22.0, \"ph\": -0.5}, 4),\n",
" \"disinfect_fail\": ({\"chlorine\": -0.8, \"ecoli\": 6}, 6),\n",
" \"chemical\": ({\"ph\": 1.7, \"turbidity\": 1.0}, 3),\n",
" \"creeping_metal\": ({\"metal\": 13.0}, 18),\n",
"}\n",
"FAULT_TYPES = [\"stuck\", \"spike\", \"drift\", \"dropout\"]\n",
"NOISE = {\"ph\": 0.04, \"turbidity\": 0.06, \"chlorine\": 0.03, \"metal\": 0.35}\n",
"DIGITS = {\"ph\": 2, \"turbidity\": 2, \"chlorine\": 2, \"metal\": 1}\n",
"\n",
"\n",
"def make_dataset(days=14, seed=7, n_events=6, n_faults=10, events=None, faults=None, sources=None):\n",
" \"\"\"events: [(๊ธ์์ ๋ฒํธ, ์ฌ๊ฑด ์ ํ, ์์ ์๊ฐ h, ์ง์ h, ๊ฐ๋)], faults: [(๊ธ์์ ๋ฒํธ, ๊ณ ์ฅ ์ ํ, ํญ๋ชฉ, ์์ h, ์ง์ h)]\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" sources = sources or SOURCES\n",
" T = days * 24\n",
" tt = np.arange(T)\n",
" hour = tt % 24\n",
" night = (hour < 6) | (hour >= 20)\n",
" rain = np.zeros(T)\n",
" for _ in range(max(1, days // 4)):\n",
" s0, d = int(rng.integers(0, T - 14)), int(rng.integers(6, 14))\n",
" rain[s0:s0 + d] = np.maximum(rain[s0:s0 + d], rng.uniform(0.6, 1.0))\n",
" if events is None:\n",
" events = [(int(rng.integers(len(sources))), str(rng.choice(list(EVENT_TYPES))), int(rng.integers(min(24, T // 4), max(T // 4 + 1, T - 48))),\n",
" int(rng.integers(10, 30)), 1.0) for _ in range(n_events)]\n",
" if faults is None:\n",
" faults = [(int(rng.integers(len(sources))), str(rng.choice(FAULT_TYPES)), str(rng.choice([\"ph\", \"turbidity\", \"chlorine\", \"metal\"])),\n",
" int(rng.integers(12, max(13, T - 30))), int(rng.integers(6, 24))) for _ in range(n_faults)]\n",
" out = []\n",
" for si, (sid, stype, x, y) in enumerate(sources):\n",
" ph0, tb0, cl0, mt0 = BASE[stype]\n",
" tr = {\"ph\": ph0 + 0.08 * np.sin(2 * np.pi * hour / 24) + rng.normal(0, 0.03, T),\n",
" \"turbidity\": tb0 * (1 + 0.1 * rng.normal(0, 1, T)),\n",
" \"chlorine\": (cl0 * np.exp(-0.022 * hour) if stype == \"water_truck\" else cl0 + rng.normal(0, 0.03, T)),\n",
" \"metal\": mt0 + rng.normal(0, 0.2, T), \"ecoli\": np.zeros(T)}\n",
" if stype in (\"river_intake\", \"well\"):\n",
" tr[\"turbidity\"] = tr[\"turbidity\"] + rain * (0.9 if stype == \"river_intake\" else 0.45)\n",
" f_all, etype = np.zeros(T), np.array([\"none\"] * T, dtype=object)\n",
" for (s_i, et, onset, dur, scale) in events:\n",
" if s_i != si:\n",
" continue\n",
" delta, ramp = EVENT_TYPES[et]\n",
" f = np.clip((tt - onset + 1) / ramp, 0, 1) * ((tt >= onset) & (tt < onset + dur + ramp))\n",
" for k, v in delta.items():\n",
" tr[k] = tr[k] + v * f * scale\n",
" etype[f > 0] = et\n",
" f_all = np.maximum(f_all, f)\n",
" tr[\"chlorine\"] = np.clip(tr[\"chlorine\"], 0.02, None)\n",
" tr[\"turbidity\"] = np.clip(tr[\"turbidity\"], 0.05, None)\n",
" tr[\"metal\"] = np.clip(tr[\"metal\"], 0.1, None)\n",
" grade = np.where(f_all >= 0.2, 2, 0)\n",
" mild = (tr[\"turbidity\"] > LIMITS[\"turbidity\"][0]) | (tr[\"chlorine\"] < LIMITS[\"chlorine\"][0]) | (tr[\"metal\"] > LIMITS[\"metal\"][0])\n",
" grade = np.where((grade == 0) & mild, 1, grade)\n",
" ms = {}\n",
" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n",
" sd = NOISE[p] * np.where(night, 1.5, 1.0) * (tr[p] if p == \"turbidity\" else 1)\n",
" ms[p] = np.round(np.clip(tr[p] + rng.normal(0, 1, T) * sd, 0, None), DIGITS[p])\n",
" ms[\"ecoli\"] = (rng.poisson(tr[\"ecoli\"]) + (rng.random(T) < 0.004)).astype(float)\n",
" fault, fparam = np.array([\"none\"] * T, dtype=object), np.array([\"-\"] * T, dtype=object)\n",
" for (s_i, ft, p, s0, d) in faults:\n",
" if s_i != si:\n",
" continue\n",
" seg = slice(s0, min(T, s0 + d))\n",
" if ft == \"stuck\":\n",
" ms[p][seg] = ms[p][s0]\n",
" elif ft == \"spike\":\n",
" seg = slice(s0, s0 + 1)\n",
" ms[p][s0] = {\"ph\": 11.8, \"turbidity\": 38.0, \"chlorine\": 0.0, \"metal\": 75.0}[p]\n",
" elif ft == \"drift\":\n",
" ramp = np.linspace(0, 1, seg.stop - s0)\n",
" ms[p][seg] = np.clip(ms[p][seg] + ramp * {\"ph\": -1.6, \"turbidity\": 2.6, \"chlorine\": -0.75, \"metal\": 12.0}[p], 0, None).round(DIGITS[p])\n",
" elif ft == \"dropout\":\n",
" for q in PARAMS:\n",
" ms[q][seg] = np.nan\n",
" p = \"all\"\n",
" fault[seg], fparam[seg] = ft, p\n",
" df = pd.DataFrame({\"source_id\": sid, \"source_type\": stype, \"x_km\": x, \"y_km\": y, \"t\": tt,\n",
" \"cond\": np.where(rain > 0, \"storm\", np.where(night, \"night\", \"day\")),\n",
" **{p: ms[p] for p in PARAMS}, **{f\"true_{p}\": tr[p] for p in PARAMS},\n",
" \"event\": etype, \"fault\": fault, \"fault_param\": fparam, \"grade\": grade})\n",
" out.append(df)\n",
" return pd.concat(out, ignore_index=True)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์ ์ฒ๋ฆฌ ๊ณตํต ํจ์: ๋ฌผ๋ฆฌ ๋ฒ์ ์ ๊ฒ -> ๊ณ ์ ๊ฐ(stuck) ํ์ -> ๋จ๋ฐ ์คํ์ดํฌ ์ ๊ฑฐ -> ๊ฒฐ์ธก ๋ณด์ ----\n",
"PHYS = {\"ph\": (0, 14), \"turbidity\": (0, 1000), \"chlorine\": (0, 10), \"metal\": (0, 1000), \"ecoli\": (0, 100000)}\n",
"SPIKE_K = {\"day\": 6.0, \"night\": 7.0, \"storm\": 9.0} # ์ํฉ๋ณ(์ฃผ๊ฐ/์ผ๊ฐ/์
์ฒํ) ์คํ์ดํฌ ํ์ ๋ฐฐ์\n",
"MIN_JUMP = {\"ph\": 0.8, \"turbidity\": 1.5, \"chlorine\": 0.3, \"metal\": 6.0}\n",
"STUCK_RUN, SHORT_GAP = 6, 3\n",
"\n",
"\n",
"def clean(df, spike_k=None, min_jump=None):\n",
" \"\"\"๊ธ์์๋ณ ์๊ณ์ด์ ์ ์ ํ๊ณ ํญ๋ชฉ๋ณ ์ฒ๋ฆฌ ๋ด์ญ(<ํญ๋ชฉ>_flag: range/stuck/spike/interp/long_gap)์ ๋จ๊ธด๋ค.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 10] ๊ฐ์ ์์ง๊ฒ์ฌ ๋ฐ์ดํฐ ์ํ ์ ํธ ๋์๋ณด๋ ์์คํ
ํตํฉ ํจํค์ง ์์ฑ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"GRADE_NAME = {0: \"์์ \", 1: \"์ฃผ์\", 2: \"์ํ\"}\n",
"\n",
"\n",
"def rule_grade(df):\n",
" \"\"\"๊ธฐ์ค์น 1์ฐจ ํํฐ: ์ํ(์์ฉ ๋ถ์ ํฉ ์์ฌ) = ํ ํญ๋ชฉ์ด๋ผ๋ ์ํ ๊ธฐ์ค ์ด๊ณผ, ์ฃผ์ = ์ฃผ์ ๊ธฐ์ค ์ด๊ณผ.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 10] ๊ฐ์ ์์ง๊ฒ์ฌ ๋ฐ์ดํฐ ์ํ ์ ํธ ๋์๋ณด๋ ์์คํ
ํตํฉ ํจํค์ง ์์ฑ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def reasons(row):\n",
" r = []\n",
" if not LIMITS[\"ph\"][0] <= row[\"ph\"] <= LIMITS[\"ph\"][1]:\n",
" r.append(f\"pH {row['ph']:.2f}\")\n",
" if row[\"turbidity\"] > LIMITS[\"turbidity\"][0]:\n",
" r.append(f\"ํ๋ {row['turbidity']:.2f}NTU\")\n",
" if row[\"chlorine\"] < LIMITS[\"chlorine\"][0]:\n",
" r.append(f\"์๋ฅ์ผ์ {row['chlorine']:.2f}mg/L\")\n",
" if row[\"metal\"] > LIMITS[\"metal\"][0]:\n",
" r.append(f\"์ค๊ธ์ {row['metal']:.1f}ug/L\")\n",
" if row[\"ecoli\"] >= 1:\n",
" r.append(f\"๋์ฅ๊ท {row['ecoli']:.0f}\")\n",
" return \", \".join(r) or \"-\"\n",
"\n",
"\n",
"FEATS = [f\"{p}{s}\" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"] for s in [\"\", \"_d1\", \"_d3\", \"_dev\", \"_sd6\"]] + \\\n",
" [\"ecoli\", \"ecoli_3h\", \"storm\", \"night\", \"n_flag\", \"truck\", \"purif\", \"well\", \"river\"]\n",
"\n",
"\n",
"def add_features(c):\n",
" \"\"\"์๊ฐ ์์๋ฅผ ์งํค๋(๊ณผ๊ฑฐ ๊ฐ๋ง ์ฐ๋) ๊ธ์์๋ณ ํน์ง: ๋ณํ๋, 24์๊ฐ ๊ธฐ์ค์ ๋๋น ํธ์ฐจ, 6์๊ฐ ๋ณ๋์ฑ, ๋์ฅ๊ท ๋์ ๋ฑ.\"\"\"\n",
" c = c.sort_values([\"source_id\", \"t\"]).reset_index(drop=True)\n",
" g = c.groupby(\"source_id\")\n",
" for p in [\"ph\", \"turbidity\", \"chlorine\", \"metal\"]:\n",
" c[f\"{p}_d1\"] = g[p].diff().fillna(0)\n",
" c[f\"{p}_d3\"] = g[p].diff(3).fillna(0)\n",
" base = g[p].transform(lambda s: s.shift(1).rolling(24, min_periods=4).median())\n",
" c[f\"{p}_dev\"] = (c[p] - base).fillna(0)\n",
" c[f\"{p}_sd6\"] = g[p].transform(lambda s: s.rolling(6, min_periods=2).std()).fillna(0)\n",
" c[\"ecoli_3h\"] = g[\"ecoli\"].transform(lambda s: s.rolling(3, min_periods=1).sum())\n",
" c[\"storm\"], c[\"night\"] = (c[\"cond\"] == \"storm\").astype(int), (c[\"cond\"] == \"night\").astype(int)\n",
" flags = [f\"{p}_flag\" for p in PARAMS if f\"{p}_flag\" in c]\n",
" c[\"n_flag\"] = sum(c[f].isin([\"stuck\", \"spike\", \"long_gap\", \"range\"]).astype(int) for f in flags) if flags else 0\n",
" for k, name in [(\"truck\", \"water_truck\"), (\"purif\", \"purification_unit\"), (\"well\", \"well\"), (\"river\", \"river_intake\")]:\n",
" c[k] = (c[\"source_type\"] == name).astype(int)\n",
" return c\n",
"\n",
"CONFIG = {\"ecoli_3h_min\": 2, \"metal_dev\": 1.0, \"metal_min\": 5.0, # ๋ฏธ์
9 ์ฌ์กฐ์ ๊ฐ\n",
" \"conf_high\": 60, \"scale\": {\"ph\": 0.35, \"turbidity\": 0.6, \"chlorine\": 0.2, \"metal\": 3.0}}\n",
"README = {\"1. ๋ชฉ์ \": \"๊ฐ์ ์์ง ์ผ์ ์๋ฃ๋ก ๊ธ์์ ์ํ ์ ํธ๋ฅผ ์กฐ๊ธฐ์ ์๋ฆฌ๋ ํ๋จ ๋ณด์กฐ ๋๊ตฌ\",\n",
" \"2. ์คํ ๋ฐฉ๋ฒ\": \"python main_wel03.py (์ํด๋ฆญ ์คํ, ์ธ๋ถ ํ์ผยท๋คํธ์ํฌ ๋ถํ์)\",\n",
" \"3. ์
๋ ฅ ํ์\": \"๊ธ์์ IDยท์ ํยท์๊ฐยทpHยทํ๋ยท์๋ฅ์ผ์ยท์ค๊ธ์ยท๋์ฅ๊ท (๋ฏธ์
1 ์คํค๋ง)\",\n",
" \"4. ํ์ ๊ท์น\": \"๊ฐ์ ๊ธฐ์ค์น + ์ฌ์กฐ์ ๊ท์น + ๋ชจ๋ธ, ์ ๋ขฐ๋ High๋ง ๊ธ์ ์ค๋จ ๊ฒํ ์์ \",\n",
" \"5. ์ธ๊ฐ ๊ฒํ \": \"๊ธ์ ์ค๋จยท์ฌ๊ฐ๋ ์๋ฐฉ์ํ ๋ด๋น ๊ฒํ ํ ์งํ๊ด ์น์ธ, ์๋ ํ์ ์์\",\n",
" \"6. ํ๊ณ์ ์ฃผ์\": \"๊ต์ก์ฉ ๊ฐ์ ์๋ฃ, ์ค์ ํ์ ์ ๊ณต์ ์์ง ๊ธฐ์ค๊ณผ ์๋ฌดยทํ๊ฒฝ ๋ด๋น ํ์ธ\",\n",
" \"7. ์ฐ๋ ํ์ฅ\": \"์ธ์ ๋ถ๋ ๊ธ์ ์ ๋ณด ๊ตํ์ฉ ํ์ค ํ๋(JSON) API๋ฅผ ์ฐจ๊ธฐ ๊ณผ์ ๋ก ๋๋ค\"}\n",
"\n",
"\n",
"def validate(df):\n",
" ok = df[\"source_id\"].astype(str).str.match(r\"^W\\d{2}$\") & df[\"t\"].ge(0)\n",
" return df[ok].copy(), int((~ok).sum())\n",
"\n",
"\n",
"def tuned_grade(c):\n",
" ph, tb, cl, mt, ec = (c[p].to_numpy(float) for p in PARAMS)\n",
" eco = (c[\"ecoli_3h\"].to_numpy() >= CONFIG[\"ecoli_3h_min\"]) | ((ec >= 1) & ((tb > LIMITS[\"turbidity\"][0]) | (cl < LIMITS[\"chlorine\"][0])))\n",
" trend = (c[\"metal_dev\"].to_numpy() > CONFIG[\"metal_dev\"]) & (mt > CONFIG[\"metal_min\"])\n",
" danger = (ph < LIMITS[\"ph\"][0]) | (ph > LIMITS[\"ph\"][1]) | (tb > LIMITS[\"turbidity\"][1]) | (cl < LIMITS[\"chlorine\"][1]) \\\n",
" | (mt > LIMITS[\"metal\"][1]) | eco | trend\n",
" caution = (tb > LIMITS[\"turbidity\"][0]) | (cl < LIMITS[\"chlorine\"][0]) | (mt > LIMITS[\"metal\"][0]) | (ec >= 1)\n",
" return np.where(danger, 2, np.where(caution, 1, 0))\n",
"\n",
"\n",
"def confidence(c, alert):\n",
" S = CONFIG[\"scale\"]\n",
" n_dev = sum(((c[f\"{p}_dev\"].abs() > S[p]) | (c[f\"{p}_d3\"].abs() > S[p])).astype(int) for p in S) + (c[\"ecoli_3h\"] >= 2)\n",
" a = pd.Series(alert.astype(int), index=c.index)\n",
" run = a.groupby([c[\"source_id\"], (a != a.groupby(c[\"source_id\"]).shift()).cumsum()]).cumsum()\n",
" g = c.groupby(\"source_id\")\n",
" flagged = g[\"n_flag\"].transform(lambda s: s.rolling(6, min_periods=1).max()) > 0\n",
" # ๋๋ฆฌํํธ ์์ฌ: ํ ํญ๋ชฉ๋ง 6์๊ฐ ์ค 5์๊ฐ ์ด์ ๊ฐ์ ๋ฐฉํฅ์ผ๋ก ๊พธ์คํ ๋ณํจ(์ก์์ด ์ปค์ง๋ ์ผ๊ฐ์๋ ์ ์ง๋๋ ๊ธฐ์ค)\n",
" mono = pd.concat([g[f\"{p}_d1\"].transform(lambda s: np.sign(s).rolling(6, min_periods=3).sum()).abs() >= 4 for p in S], axis=1).any(axis=1)\n",
" drift = (n_dev <= 1) & mono & (c[\"ecoli_3h\"] < 2)\n",
" return 40 * np.minimum(n_dev, 3) / 3 + 25 * np.minimum(run, 3) / 3 + 20 * (~flagged) + 15 * (~drift)\n",
"\n",
"\n",
"def train_model():\n",
" tr = pd.concat([add_features(clean(make_dataset(seed=s, n_events=14, n_faults=14))) for s in (7, 8)], ignore_index=True)\n",
" return RandomForestClassifier(n_estimators=150, max_depth=10, class_weight=\"balanced_subsample\", random_state=0, n_jobs=1).fit(tr[FEATS], tr[\"grade\"])\n",
"\n",
"\n",
"def run_pipeline(raw, model):\n",
" data, n_bad = validate(raw)\n",
" c = add_features(clean(data))\n",
" c[\"rule\"] = tuned_grade(c)\n",
" c[\"ml\"] = model.predict(c[FEATS])\n",
" c[\"final\"] = np.maximum(c[\"rule\"], c[\"ml\"]) # ๋ ์ค ํ๋๋ผ๋ ์ํ์ด๋ฉด ์ํ(๋์นจ ๋ฐฉ์ง)\n",
" alert = (c[\"final\"] == 2).to_numpy()\n",
" c[\"conf\"] = np.where(alert, confidence(c, alert), np.nan)\n",
" queue = c[alert & (c[\"conf\"] >= CONFIG[\"conf_high\"])].groupby(\"source_id\").agg(first=(\"t\", \"min\"), hours=(\"t\", \"size\"))\n",
" queue[\"status\"] = \"PENDING(์งํ๊ด ๊ฒฐ์ ๋๊ธฐ)\"\n",
" return c, queue, n_bad\n",
"\n",
"\n",
"def self_tests(model):\n",
" res = {}\n",
" clean_df = make_dataset(days=2, seed=50, events=[], faults=[])\n",
" c, q, _ = run_pipeline(clean_df, model)\n",
" res[\"์ค์ผ ์๋ ์๋ฃ์์ ๊ธ์ ์ค๋จ ์์ 0๊ฑด\"] = len(q) == 0\n",
" ev = make_dataset(days=2, seed=50, events=[(0, \"sewage\", 20, 12, 1.0)], faults=[])\n",
" c, q, _ = run_pipeline(ev, model)\n",
" res[\"ํ์ ์ ์
๊ธ์์(W01) ์์ \"] = \"W01\" in q.index\n",
" res[\"์์ ๊ฑด์ ๋ชจ๋ PENDING(์๋ ํ์ ์์)\"] = q[\"status\"].str.startswith(\"PENDING\").all()\n",
" bad = ev.copy(); bad.loc[3, \"source_id\"] = \"??\"\n",
" res[\"์์ ๋ ์ฝ๋ ๊ฒฉ๋ฆฌ\"] = run_pipeline(bad, model)[2] == 1\n",
" dr = make_dataset(days=2, seed=50, events=[], faults=[(1, \"drift\", \"turbidity\", 10, 18)])\n",
" c, q, _ = run_pipeline(dr, model)\n",
" n_alert = int((c.loc[c[\"source_id\"] == \"W02\", \"final\"] == 2).sum()) # ๋๋ฆฌํํธ ๊ตฌ๊ฐ ์ํ ํ์ ์๊ฐ ์ค High ์์ ์๊ฐ์ด ์ ๋ฐ ๋ฏธ๋ง์ด๋ฉด ์ต์ ์๋\n",
" res[\"์ผ์ ๋๋ฆฌํํธ ์์ ์๊ฐ ์ ๋ฐ ๋ฏธ๋ง์ผ๋ก ์ต์ \"] = (\"W02\" not in q.index) or q.loc[\"W02\", \"hours\"] < 0.5 * n_alert\n",
" res[\"๊ธฐ์ค์น ํํฐยทํ์ ๊ทผ๊ฑฐ ๋ฌธ๊ตฌ ์์ฑ\"] = len(reasons(c[c[\"source_id\"] == \"W02\"].iloc[40])) > 0 and rule_grade(c).shape[0] == len(c)\n",
" res[\"๊ฒฐ์ธก ๋ณด์ ํ ๋จ์ ๊ฒฐ์ธก 0\"] = int(c[PARAMS].isna().sum().sum()) == 0\n",
" res[\"์ด์ ๋ฌธ์ 7๊ฐ ์ฅ ์์ฑ\"] = all(len(v) > 10 for v in README.values())\n",
" return res\n",
"\n",
"\n",
"# ---- ์์ฒด ์ ๊ฒ(ํจ์ ํธ์ถ ์ถ์ ์ผ๋ก ์ปค๋ฒ๋ฆฌ์ง ์ธก์ ) ----\n",
"t_all = time.perf_counter()\n",
"called = set()\n",
"own = {n for n, f in list(globals().items()) if callable(f) and getattr(f, \"__module__\", \"\") == \"__main__\" and hasattr(f, \"__code__\")}\n",
"sys.setprofile(lambda fr, ev, arg: called.add(fr.f_code.co_name) if ev == \"call\" else None)\n",
"model = train_model()\n",
"tests = self_tests(model)\n",
"e2e = []\n",
"for s in (11, 21, 31, 41, 51):\n",
" try:\n",
" c, q, nb = run_pipeline(make_dataset(seed=s, n_events=10, n_faults=14), model)\n",
" e2e.append((s, True, (c[\"final\"] == c[\"grade\"]).mean(), len(q)))\n",
" except Exception as e:\n",
" e2e.append((s, False, np.nan, 0))\n",
"sys.setprofile(None)\n",
"cov = len(own & called) / len(own)\n",
"print(f\"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ํตํฉ ์คํ {time.perf_counter() - t_all:.1f}์ด (๋ชจ๋ธ ํ์ต ํฌํจ)\")\n",
"print(\"[End-to-End ์๋๋ฆฌ์ค 5์ข
]\")\n",
"for s, ok, acc, nq in e2e:\n",
" print(f\" seed {s}: {'์ฑ๊ณต' if ok else '์คํจ'}, ์ต์ข
ํ์ ์ ํ๋ {acc:.1%}, ๊ธ์ ์ค๋จ ๊ฒํ ์์ {nq}๊ณณ\")\n",
"rate = np.mean([x[1] for x in e2e])\n",
"print(f\"E2E ์ฑ๊ณต๋ฅ {rate:.0%} (๋ชฉํ 100%: {'๋ฌ์ฑ' if rate == 1 else '๋ฏธ๋ฌ'})\")\n",
"print(\"[์์ฒด ์ ๊ฒ]\")\n",
"for k, v in tests.items():\n",
" print(f\" [{'PASS' if v else 'FAIL'}] {k}\")\n",
"print(f\"์์ฒด ์ ๊ฒ {sum(tests.values())}/{len(tests)} ํต๊ณผ, ํจ์ ์ปค๋ฒ๋ฆฌ์ง {cov:.0%}({len(own & called)}/{len(own)}, ๋ชฉํ 85% ์ด์: {'๋ฌ์ฑ' if cov >= 0.85 else '๋ฏธ๋ฌ'})\")\n",
"print(f\"์ด์ ๋ฌธ์(README) {len(README)}๊ฐ ์ฅ: \" + \", \".join(README))\n",
"\n",
"# ---- ๋ธ๋ฆฌํ ์์ฝ(๋ฒค์น๋งํฌ seed 11, ์ด์ฉ ์์ ๊ธฐ์ค) ----\n",
"c, q, _ = run_pipeline(make_dataset(seed=11, n_events=10, n_faults=14), model)\n",
"d = c[\"grade\"] == 2\n",
"print(\"\\n[์งํ๊ด ๋ธ๋ฆฌํ ์์ฝ]\")\n",
"print(f\"1. ๊ธ์์ 8๊ณณ 14์ผ ๋ชจ์: ์ํ ์ฌํ์จ {((c['final'] == 2) & d).sum() / d.sum():.1%}, ์ข
ํฉ ์ ํ๋ {(c['final'] == c['grade']).mean():.1%}\")\n",
"print(f\"2. ๊ธ์ ์ค๋จ ๊ฒํ ์์ {len(q)}๊ณณ({', '.join(q.index)}): ๋ชจ๋ ์๋ฐฉ์ํ ๋ด๋น ๊ฒํ ยท์งํ๊ด ๊ฒฐ์ ๋๊ธฐ\")\n",
"print(\"3. ์ผ์ ์ด์(๋๋ฆฌํํธยท์คํ์ดํฌยท๋จ์ )์ ์ฌ์ธก์ ยท๊ต์ ์์ฒญ์ผ๋ก ๋ถ๋ฆฌํด ๋ถํ์ํ ๋จ์๋ฅผ ์ค์ธ๋ค\")\n",
"print(\"4. ์ค์ ์์ฉ ํ์ ์ ๊ณต์ ์์ง ๊ธฐ์ค๊ณผ ์๋ฌด(์๋ฐฉ์ํ)ยทํ๊ฒฝ ๋ด๋น ํ์ธ์ ๋ฐ๋ฅธ๋ค(๋ณธ ๋๊ตฌ๋ ํ๋จ ๋ณด์กฐ)\")\n",
"\n",
"last = c[c[\"t\"] == c[\"t\"].max()].set_index(\"source_id\")\n",
"fig, ax = plt.subplots(1, 2, figsize=(11, 4))\n",
"ax[0].bar(last.index, last[\"final\"], color=[{0: \"tab:green\", 1: \"gold\", 2: \"red\"}[g] for g in last[\"final\"]])\n",
"ax[0].set_yticks([0, 1, 2], [\"safe\", \"caution\", \"danger\"]); ax[0].set_title(\"Final status by source (last hour)\")\n",
"ax[1].bar([str(x[0]) for x in e2e], [x[2] for x in e2e], color=\"tab:blue\"); ax[1].set_ylim(0.9, 1.0)\n",
"ax[1].set_title(\"E2E accuracy by scenario seed\"); ax[1].set_xlabel(\"seed\")\n",
"plt.suptitle(\"Integrated water risk dashboard package (synthetic)\"); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
10 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ต์ก์ฉ๊ฐ์์๋ฃ] ํตํฉ ์คํ 6.6์ด (๋ชจ๋ธ ํ์ต ํฌํจ)\n",
"[End-to-End ์๋๋ฆฌ์ค 5์ข
]\n",
" seed 11: ์ฑ๊ณต, ์ต์ข
ํ์ ์ ํ๋ 98.6%, ๊ธ์ ์ค๋จ ๊ฒํ ์์ 6๊ณณ\n",
" seed 21: ์ฑ๊ณต, ์ต์ข
ํ์ ์ ํ๋ 99.1%, ๊ธ์ ์ค๋จ ๊ฒํ ์์ 6๊ณณ\n",
" seed 31: ์ฑ๊ณต, ์ต์ข
ํ์ ์ ํ๋ 97.0%, ๊ธ์ ์ค๋จ ๊ฒํ ์์ 6๊ณณ\n",
" seed 41: ์ฑ๊ณต, ์ต์ข
ํ์ ์ ํ๋ 97.7%, ๊ธ์ ์ค๋จ ๊ฒํ ์์ 7๊ณณ\n",
" seed 51: ์ฑ๊ณต, ์ต์ข
ํ์ ์ ํ๋ 98.0%, ๊ธ์ ์ค๋จ ๊ฒํ ์์ 7๊ณณ\n",
"E2E ์ฑ๊ณต๋ฅ 100% (๋ชฉํ 100%: ๋ฌ์ฑ)\n",
"[์์ฒด ์ ๊ฒ]\n",
" [PASS] ์ค์ผ ์๋ ์๋ฃ์์ ๊ธ์ ์ค๋จ ์์ 0๊ฑด\n",
" [PASS] ํ์ ์ ์
๊ธ์์(W01) ์์ \n",
" [PASS] ์์ ๊ฑด์ ๋ชจ๋ PENDING(์๋ ํ์ ์์)\n",
" [PASS] ์์ ๋ ์ฝ๋ ๊ฒฉ๋ฆฌ\n",
" [PASS] ์ผ์ ๋๋ฆฌํํธ ์์ ์๊ฐ ์ ๋ฐ ๋ฏธ๋ง์ผ๋ก ์ต์ \n",
" [PASS] ๊ธฐ์ค์น ํํฐยทํ์ ๊ทผ๊ฑฐ ๋ฌธ๊ตฌ ์์ฑ\n",
" [PASS] ๊ฒฐ์ธก ๋ณด์ ํ ๋จ์ ๊ฒฐ์ธก 0\n",
" [PASS] ์ด์ ๋ฌธ์ 7๊ฐ ์ฅ ์์ฑ\n",
"์์ฒด ์ ๊ฒ 8/8 ํต๊ณผ, ํจ์ ์ปค๋ฒ๋ฆฌ์ง 100%(11/11, ๋ชฉํ 85% ์ด์: ๋ฌ์ฑ)\n",
"์ด์ ๋ฌธ์(README) 7๊ฐ ์ฅ: 1. ๋ชฉ์ , 2. ์คํ ๋ฐฉ๋ฒ, 3. ์
๋ ฅ ํ์, 4. ํ์ ๊ท์น, 5. ์ธ๊ฐ ๊ฒํ , 6. ํ๊ณ์ ์ฃผ์, 7. ์ฐ๋ ํ์ฅ\n",
"\n",
"[์งํ๊ด ๋ธ๋ฆฌํ ์์ฝ]\n",
"1. ๊ธ์์ 8๊ณณ 14์ผ ๋ชจ์: ์ํ ์ฌํ์จ 98.4%, ์ข
ํฉ ์ ํ๋ 98.6%\n",
"2. ๊ธ์ ์ค๋จ ๊ฒํ ์์ 6๊ณณ(W02, W03, W04, W05, W07, W08): ๋ชจ๋ ์๋ฐฉ์ํ ๋ด๋น ๊ฒํ ยท์งํ๊ด ๊ฒฐ์ ๋๊ธฐ\n",
"3. ์ผ์ ์ด์(๋๋ฆฌํํธยท์คํ์ดํฌยท๋จ์ )์ ์ฌ์ธก์ ยท๊ต์ ์์ฒญ์ผ๋ก ๋ถ๋ฆฌํด ๋ถํ์ํ ๋จ์๋ฅผ ์ค์ธ๋ค\n",
"4. ์ค์ ์์ฉ ํ์ ์ ๊ณต์ ์์ง ๊ธฐ์ค๊ณผ ์๋ฌด(์๋ฐฉ์ํ)ยทํ๊ฒฝ ๋ด๋น ํ์ธ์ ๋ฐ๋ฅธ๋ค(๋ณธ ๋๊ตฌ๋ ํ๋จ ๋ณด์กฐ)\n",
"```\n",
" \n"
]
}
],
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}