{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# ๐ฏ [WEL01] ๋๋ฏธ ์์ฒด์ ํธ ๊ธฐ๋ฐ ์ดยทํ๋ญ ์คํธ๋ ์ค ๊ฒฝ๋ณด ํ๋กํ ํ์
\n",
"### Google Colab ์ค์ต ํ๊ฒฝ (์ฅ๋ณ ๋ณต์งยท์ง์ / Level 2)\n",
"\n",
"[](https://colab.research.google.com/github/edunlab/milicontent2/blob/main/uploads/WEL01/WEL01_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",
"๋ฌ์์-์ฐํฌ๋ผ์ด๋ ์ ์์์๋ ํนํ๊ณผ ํญ์ผ ์์์ ์ฅ์๊ฐ ๊ธฐ๋ํ๋ ์ ํฌ์์ ์ดยทํ๋ญ ์์์ด ์์ ์ง์๋ ฅ์ ์ข์ฐํ๋ ๋ฌธ์ ๋ก ๋๋ฌ๋ฌ๊ณ , ์จ์ด๋ฌ๋ธ ์ผ์ ์ ํธ๋ก ์ํ ์งํ๋ฅผ ์ผ์ฐ ์์์ฐจ๋ฆฌ๋ ์ค๊ฒฝ๋ณด์ ์ผ์ ์ค๋ฅ๋ฅผ ๊ฐ๋ ค๋ด๋ ์ผ์ด ์ค์ํด์ก๋ค. ๋ณธ ํ๋ก์ ํธ์์๋ ๊ต์ก์ฉ ๊ฐ์ ๋ณ์ฌ์ ๋๋ฏธ ์์ฒด์ ํธ(์ฌ๋ฐ์, ์ฒด์จ, ํผ๋ถ์ ๋๋)์ ๊ธฐ์์ ์ฝ๋๋ก ์ง์ ๋ง๋ค์ด, ๋ฐ์ดํฐ ์คํค๋ง ์ ์, ๊ฒฐ์ธกยท์ผ์ ํ ์ ์ฒ๋ฆฌ, ์ฒด์จ-์ฌ๋ฐ ์๊ด ๊ธฐ๋ฐ 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. ์ ํฌ์ ์ฐฉ์ฉ ์จ์ด๋ฌ๋ธ ๋๋ฏธ ์์ฒด์ ํธ(์ฌ๋ฐ์, ์ฒด์จ, ํผ๋ถ์ ๋๋) ๋ฐ์ดํฐ ๊ท๊ฒฉ ์ ์\n",
"**์์์๊ฐ**: ์ฝ 1.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ ํฌ์ ์จ์ด๋ฌ๋ธ ๋๋ฏธ ์์ฒด์ ํธ(์ฌ๋ฐ์, ์ฒด์จ, ํผ๋ถ์ ๋๋) ๋ฐ์ดํฐ์
์ ์ ์ํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์จ์ด๋ฌ๋ธ ๋๋ฏธ ์์ฒด์ ํธ(์ฌ๋ฐ์, ์ฒด์จ, ํผ๋ถ์ ๋๋)์ ๊ฐ์ ๋ณ์ฌ ํน์ฑยท๊ธฐ์ยท๊ธฐ๊ธฐ ์ํ์ ๋ฐ์ดํฐ ๊ท๊ฒฉ์ ์ ์ํ๊ณ , ์๋ชป๋ ๋ฐ์ดํฐ๋ฅผ ๊ฑธ๋ฌ๋ด๋ ๊ฒ์ฆ๊ธฐ๋ฅผ ๋ง๋ ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ ํธ 3์ข
์ ํ์ฉ ๋ฒ์, ์ํ๋ง ๊ฐ๊ฒฉ, ๊ธฐ๊ธฐ ์ข
๋ฅ์ ์ ์ด ํ์ง, ๋ณ์ฌ ํน์ฑ(์์ ์ ์ฌ๋ฐ, ์ฐ๋ น, ์ฒด๊ฒฉ, ๋์ ์์ ์ฌ๋ถ), ๊ธฐ์(๊ธฐ์จ, ์ต๋, ํ์) ํ๋๋ฅผ ๋ด์ ์คํค๋ง ์ค๊ณํ๊ณ ์์ฑํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `data/schema/wel01_input_schema.json ๋ฐ docs/wel01_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": [
"\"\"\"์ ํฌ์ ์จ์ด๋ฌ๋ธ ๋๋ฏธ ์์ฒด์ ํธ(์ฌ๋ฐ์, ์ฒด์จ, ํผ๋ถ์ ๋๋)์ ๊ฐ์ ๋ณ์ฌ ํน์ฑยท๊ธฐ์ยท๊ธฐ๊ธฐ ์ํ์ ๋ฐ์ดํฐ ์คํค๋ง๋ฅผ ์ ์ํ๊ณ , ๊ฐ์ ์ํ 20๊ฑด์ ๊ฒ์ฆํ๋ค. ์ง๋จ ๋ชฉ์ ์ด ์๋ ๊ต์ก์ฉ ๊ฐ์ ์๋ฃ์ด๋ค.\"\"\"\n",
"import json\n",
"import re\n",
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"# ---------------- [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋๋ฏธ ์์ฒด์ ํธ ์์ฑ๊ธฐ (์ค์ ์ฅ๋ณยท๋ถ๋ยท์๋ฌด ๊ธฐ๋ก ์์) ----------------\n",
"# ๊ฐ์ ๋ณ์ฌ(๊ฐ๋ช
ID)๊ฐ 60๋ถ๊ฐ ๊ฑท๊ธฐยทํด์ยท๊ณ ๊ฐ๋ ํ๋์ ํ๋ ๋์์ ์ฌ๋ฐ์ยท์ฒด์จ(์ถ์ ์ฌ๋ถ)ยทํผ๋ถ์ ๋๋๋ฅผ 1๋ถ ๊ฐ๊ฒฉ์ผ๋ก ๋ง๋ ๋ค.\n",
"# ์ง๋จ ๋๊ตฌ๊ฐ ์๋๋ฉฐ, ์ํ ๋ผ๋ฒจ์ ๊ฐ์ ์๋ฆฌ ๋ชจ๋ธ์ '๊ต์ก์ฉ ๋จ๊ณ'์ผ ๋ฟ์ด๋ค.\n",
"N_MIN = 60\n",
"STAGE = {0: \"์ ์\", 1: \"์ฃผ์\", 2: \"๊ฒฝ๋ณด\"}\n",
"TYPES = [\"normal\", \"heat\", \"cold\"]\n",
"\n",
"\n",
"def make_person(i, rng):\n",
" return {\"id\": f\"S{i:03d}\", \"rest_hr\": float(rng.normal(64, 7)), \"age\": int(rng.integers(19, 27)), \"bmi\": float(rng.normal(23.5, 2.5)),\n",
" \"acclim\": int(rng.random() < 0.4), \"fit\": float(np.clip(rng.normal(0.5, 0.2), 0.05, 0.95))}\n",
"\n",
"\n",
"def make_env(kind, rng):\n",
" if kind == \"heat\":\n",
" return {\"temp_c\": float(rng.uniform(30, 40)), \"rh\": float(rng.uniform(45, 85)), \"wind\": float(rng.uniform(0.3, 3))}\n",
" if kind == \"cold\":\n",
" return {\"temp_c\": float(rng.uniform(-22, 2)), \"rh\": float(rng.uniform(40, 80)), \"wind\": float(rng.uniform(2, 11))}\n",
" return {\"temp_c\": float(rng.uniform(8, 24)), \"rh\": float(rng.uniform(35, 70)), \"wind\": float(rng.uniform(0.3, 5))}\n",
"\n",
"\n",
"def make_session(sid, kind, rng, p=None, glitch=True):\n",
" \"\"\"๋ฐํ: dict(person, env, kind, df[hr,temp,eda,act + ์ ๋ต coreยทstageยทrtype]). ์ผ์ ๊ฒฐ์ธกยทํยท๊ณ ์ฐฉ ํฌํจ.\"\"\"\n",
" p = p or make_person(sid, rng)\n",
" env = make_env(kind, rng)\n",
" act = np.zeros(N_MIN); t = 0\n",
" while t < N_MIN: # 10~20๋ถ ๋จ์ ํ๋ ๋ธ๋ก: ํด์ 1.5 / ํ๊ตฐ 4.5 / ๊ณ ๊ฐ๋ 7 MET\n",
" d = int(rng.integers(10, 21)); act[t:t + d] = rng.choice([1.5, 4.5, 7.0], p=[0.25, 0.5, 0.25]); t += d\n",
" hi = env[\"temp_c\"] + (0.12 * (env[\"rh\"] - 40) if env[\"temp_c\"] > 25 else 0)\n",
" wc = env[\"temp_c\"] - 1.6 * env[\"wind\"]\n",
" core, cores = 36.9, []\n",
" for m in range(N_MIN):\n",
" heat = 0.085 * max(0, hi - 24) * (1 - 0.25 * p[\"acclim\"]) * (1 + 0.5 * m / N_MIN) * (1 + 0.012 * (p[\"bmi\"] - 23.5))\n",
" cold = -0.075 * max(0, 8 - wc) * (1 - 0.09 * act[m])\n",
" ss = 36.9 + 0.09 * act[m] + heat + cold\n",
" core += (ss - core) / 18 + rng.normal(0, 0.03)\n",
" cores.append(core)\n",
" core = np.array(cores)\n",
" hr = p[\"rest_hr\"] + 9.5 * (act - 1.5) * (1.15 - 0.3 * p[\"fit\"]) + 11 * (core - 36.9) + rng.normal(0, 3.2, N_MIN)\n",
" eda = 2.0 + 0.25 * act + 3.2 * np.maximum(core - 37.2, 0) - 0.04 * np.maximum(8 - wc, 0) + rng.normal(0, 0.25, N_MIN)\n",
" bias = rng.normal(0, 0.1)\n",
" temp = core + bias + rng.normal(0, 0.1, N_MIN) - np.where((kind == \"cold\") & (env[\"wind\"] > 7), 0.25, 0.0) # ์ผ์ ์ ์ด ๋ถ์ ๋ฐ๋ ๋๊ฐ ์ค์ฐจ\n",
" df = pd.DataFrame({\"min\": np.arange(N_MIN), \"hr\": hr, \"temp\": temp, \"eda\": np.maximum(eda, 0.2), \"act\": act})\n",
" if glitch:\n",
" for col in (\"hr\", \"temp\", \"eda\"):\n",
" df.loc[rng.random(N_MIN) < 0.03, col] = np.nan # ๊ฒฐ์ธก\n",
" s = int(rng.integers(5, N_MIN))\n",
" if rng.random() < 0.12:\n",
" df.loc[s, \"hr\"] = 230.0 # ์ผ์ ํ\n",
" if rng.random() < 0.12:\n",
" df.loc[s, \"temp\"] = 45.0\n",
" ht = np.where(core >= 39.0, 2, np.where(core >= 38.3, 1, 0)); cd = np.where(core <= 35.4, 2, np.where(core <= 36.2, 1, 0))\n",
" df[\"core\"] = core\n",
" df[\"stage\"] = np.maximum(ht, cd); df[\"rtype\"] = np.where(ht > 0, \"heat\", np.where(cd > 0, \"cold\", \"normal\"))\n",
" return {\"id\": p[\"id\"], \"person\": p, \"env\": env, \"kind\": kind, \"df\": df}\n",
"\n",
"\n",
"def make_dataset(n, seed):\n",
" rng = np.random.default_rng(seed)\n",
" kinds = [\"normal\", \"heat\", \"cold\"]\n",
" return [make_session(i, kinds[i % 3], rng) for i in range(n)]\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# 1) ์คํค๋ง ์ ์: ์ ํธ ๊ท๊ฒฉ, ๊ฐ์ ๋ณ์ฌ ํน์ฑ, ๊ธฐ์(์จยท์ต๋ยทํ์), ๊ธฐ๊ธฐ ์ํ\n",
"SCHEMA = {\n",
" \"required\": [\"record_id\", \"soldier_id\", \"n_samples\", \"sampling_s\", \"device\", \"person\", \"weather\", \"samples\"],\n",
" \"fields\": {\n",
" \"record_id\": {\"type\": str, \"pattern\": r\"^REC-\\d{4}$\"},\n",
" \"soldier_id\": {\"type\": str, \"pattern\": r\"^S\\d{3}$\"},\n",
" \"n_samples\": {\"type\": int, \"min\": 1, \"max\": 100000},\n",
" \"sampling_s\": {\"type\": (int, float), \"min\": 1, \"max\": 600},\n",
" },\n",
" \"device\": {\n",
" \"type\": {\"type\": str, \"enum\": [\"wrist\", \"chest\"]},\n",
" \"battery_pct\": {\"type\": (int, float), \"min\": 0, \"max\": 100},\n",
" \"contact_quality\": {\"type\": (int, float), \"min\": 0, \"max\": 1},\n",
" },\n",
" \"person\": {\n",
" \"rest_hr\": {\"type\": (int, float), \"min\": 40, \"max\": 100},\n",
" \"age\": {\"type\": int, \"min\": 18, \"max\": 45},\n",
" \"bmi\": {\"type\": (int, float), \"min\": 15, \"max\": 40},\n",
" \"acclim\": {\"type\": int, \"min\": 0, \"max\": 1},\n",
" },\n",
" \"weather\": {\n",
" \"temp_c\": {\"type\": (int, float), \"min\": -40, \"max\": 55},\n",
" \"rh\": {\"type\": (int, float), \"min\": 0, \"max\": 100},\n",
" \"wind\": {\"type\": (int, float), \"min\": 0, \"max\": 30},\n",
" },\n",
" \"sample\": {\n",
" \"hr\": {\"type\": (int, float), \"min\": 30, \"max\": 220},\n",
" \"temp\": {\"type\": (int, float), \"min\": 30, \"max\": 43},\n",
" \"eda\": {\"type\": (int, float), \"min\": 0, \"max\": 40},\n",
" },\n",
"}\n",
"\n",
"\n",
"def check(name, value, rule):\n",
" \"\"\"๊ฐ ํ๋๋ฅผ ๊ท์น(type, enum, min, max, pattern)์ผ๋ก ๊ฒ์ฌํด ์ค๋ฅ ๋ชฉ๋ก์ ๋๋ ค์ค๋ค.\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 1] ์ ํฌ์ ์ฐฉ์ฉ ์จ์ด๋ฌ๋ธ ๋๋ฏธ ์์ฒด์ ํธ(์ฌ๋ฐ์, ์ฒด์จ, ํผ๋ถ์ ๋๋) ๋ฐ์ดํฐ ๊ท๊ฒฉ ์ ์ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def validate(rec):\n",
" # [TODO : ํ์ ์ค์ต 1] ์ ํฌ์ ์ฐฉ์ฉ ์จ์ด๋ฌ๋ธ ๋๋ฏธ ์์ฒด์ ํธ(์ฌ๋ฐ์, ์ฒด์จ, ํผ๋ถ์ ๋๋) ๋ฐ์ดํฐ ๊ท๊ฒฉ ์ ์ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"rng = np.random.default_rng(1)\n",
"records = []\n",
"for i in range(20):\n",
" s = make_session(i, TYPES[i % 3], rng, glitch=False)\n",
" smp = [{\"hr\": round(float(r.hr), 1), \"temp\": round(float(r.temp), 2), \"eda\": round(float(r.eda), 2)} for r in s[\"df\"].itertuples()]\n",
" p, e = s[\"person\"], s[\"env\"]\n",
" records.append({\"record_id\": f\"REC-{i:04d}\", \"soldier_id\": s[\"id\"], \"n_samples\": len(smp), \"sampling_s\": 60,\n",
" \"device\": {\"type\": str(rng.choice([\"wrist\", \"chest\"])), \"battery_pct\": int(rng.integers(30, 100)), \"contact_quality\": round(float(rng.uniform(0.7, 1)), 2)},\n",
" \"person\": {\"rest_hr\": round(p[\"rest_hr\"], 1), \"age\": p[\"age\"], \"bmi\": round(p[\"bmi\"], 1), \"acclim\": p[\"acclim\"]},\n",
" \"weather\": {k: round(v, 1) for k, v in e.items()}, \"samples\": smp})\n",
"\n",
"# 3) ์ ์ ๋ฐ์ดํฐ ๊ฒ์ฆ\n",
"ok = sum(1 for r in records if not validate(r))\n",
"print(f\"์ ์ ์ํ {len(records)}๊ฑด ์ค ๊ฒ์ฆ ํต๊ณผ {ok}๊ฑด, ์ค๋ฅ {len(records) - ok}๊ฑด -> ๋ชฉํ(๊ฒ์ฆ ์ค๋ฅ 0๊ฑด): {'๋ฌ์ฑ' if ok == len(records) else '๋ฏธ๋ฌ'}\")\n",
"\n",
"# 4) ์ค๋ฅ ์ฃผ์
์ํ\n",
"bad = json.loads(json.dumps(records[0]))\n",
"bad[\"soldier_id\"] = \"SOLDIER-1\" # ํ์\n",
"bad[\"samples\"][0][\"temp\"] = 45.0 # ๋ฒ์(์ผ์ ํ)\n",
"del bad[\"weather\"][\"wind\"] # ๋๋ฝ(ํ์)\n",
"bad[\"device\"][\"type\"] = \"ankle\" # ํ์ฉ๊ฐ\n",
"bad[\"n_samples\"] = 99 # ๋
ผ๋ฆฌ(๊ฐ์ ๋ถ์ผ์น)\n",
"errs = validate(bad)\n",
"print(f\"\\n[์ค๋ฅ ์ฃผ์
๊ฒฐ๊ณผ] ์ฃผ์
5์ข
-> ๊ฒ์ถ {len(errs)}๊ฑด\")\n",
"for e in errs:\n",
" print(\" -\", e)\n",
"print(f\"์ฃผ์
์ค๋ฅ ์ ๋ถ ๊ฒ์ถ: {'์' if len(errs) >= 5 else '์๋์ค'}\")\n",
"\n",
"# 5) ์คํค๋ง ํ๋ ์ ์์(๋ช
์ธ์ ์ด์)\n",
"rows = [(\"๊ธฐ๋ณธ\", k, r[\"type\"].__name__ if isinstance(r[\"type\"], type) else \"number\", str({x: y for x, y in r.items() if x != \"type\"})) for k, r in SCHEMA[\"fields\"].items()]\n",
"for g in (\"device\", \"person\", \"weather\", \"sample\"):\n",
" for k, r in SCHEMA[g].items():\n",
" t = r[\"type\"].__name__ if isinstance(r[\"type\"], type) else \"number\"\n",
" rows.append((g, k, t, str({x: y for x, y in r.items() if x != \"type\"})))\n",
"print(\"\\n[๋ฐ์ดํฐ ๋ช
์ธ ์์ฝ] ํ๋ ์\", len(rows))\n",
"print(pd.DataFrame(rows, columns=[\"๊ทธ๋ฃน\", \"ํ๋\", \"์๋ฃํ\", \"๊ท์น\"]).to_string(index=False))\n",
"print(\"\\n์ํ ๋จ๊ณ(๊ต์ก์ฉ ๊ฐ์ ๋ผ๋ฒจ):\", STAGE, \"/ ์ํ ์ ํ:\", TYPES)\n",
"print(\"๋ณธ ๋ฐ์ดํฐ๋ ์ฝ๋๋ก ๋ง๋ ๋๋ฏธ ์์ฒด์ ํธ์ด๋ฉฐ ์ง๋จยท์ฒ๋ฐฉ ์๋ฃ๊ฐ ์๋๋ค. ์ค์ ๊ฑด๊ฐ ์ด์์ ์๋ฌด๋ยท๊ตฐ์๊ด์๊ฒ ์ฐ๊ณํ๋ค.\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
01 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"์ ์ ์ํ 20๊ฑด ์ค ๊ฒ์ฆ ํต๊ณผ 20๊ฑด, ์ค๋ฅ 0๊ฑด -> ๋ชฉํ(๊ฒ์ฆ ์ค๋ฅ 0๊ฑด): ๋ฌ์ฑ\n",
"\n",
"[์ค๋ฅ ์ฃผ์
๊ฒฐ๊ณผ] ์ฃผ์
5์ข
-> ๊ฒ์ถ 5๊ฑด\n",
" - soldier_id: ํ์ ๋ถ์ผ์น 'SOLDIER-1'\n",
" - device.type: ํ์ฉ๋์ง ์์ ๊ฐ 'ankle'\n",
" - weather.wind: ํ์ ํ๋ ๋๋ฝ\n",
" - samples[0].temp: ์ต๋๊ฐ 43 ์ด๊ณผ\n",
" - n_samples: ์ํ ๊ฐ์์ ๋ถ์ผ์น\n",
"์ฃผ์
์ค๋ฅ ์ ๋ถ ๊ฒ์ถ: ์\n",
"\n",
"[๋ฐ์ดํฐ ๋ช
์ธ ์์ฝ] ํ๋ ์ 17\n",
" ๊ทธ๋ฃน ํ๋ ์๋ฃํ ๊ท์น\n",
" ๊ธฐ๋ณธ record_id str {'pattern': '^REC-\\\\d{4}$'}\n",
" ๊ธฐ๋ณธ soldier_id str {'pattern': '^S\\\\d{3}$'}\n",
" ๊ธฐ๋ณธ n_samples int {'min': 1, 'max': 100000}\n",
" ๊ธฐ๋ณธ sampling_s number {'min': 1, 'max': 600}\n",
" device type str {'enum': ['wrist', 'chest']}\n",
" device battery_pct number {'min': 0, 'max': 100}\n",
" device contact_quality number {'min': 0, 'max': 1}\n",
" person rest_hr number {'min': 40, 'max': 100}\n",
" person age int {'min': 18, 'max': 45}\n",
" person bmi number {'min': 15, 'max': 40}\n",
" person acclim int {'min': 0, 'max': 1}\n",
"weather temp_c number {'min': -40, 'max': 55}\n",
"weather rh number {'min': 0, 'max': 100}\n",
"weather wind number {'min': 0, 'max': 30}\n",
" sample hr number {'min': 30, 'max': 220}\n",
" sample temp number {'min': 30, 'max': 43}\n",
" sample eda number {'min': 0, 'max': 40}\n",
"\n",
"์ํ ๋จ๊ณ(๊ต์ก์ฉ ๊ฐ์ ๋ผ๋ฒจ): {0: '์ ์', 1: '์ฃผ์', 2: '๊ฒฝ๋ณด'} / ์ํ ์ ํ: ['normal', 'heat', 'cold']\n",
"๋ณธ ๋ฐ์ดํฐ๋ ์ฝ๋๋ก ๋ง๋ ๋๋ฏธ ์์ฒด์ ํธ์ด๋ฉฐ ์ง๋จยท์ฒ๋ฐฉ ์๋ฃ๊ฐ ์๋๋ค. ์ค์ ๊ฑด๊ฐ ์ด์์ ์๋ฌด๋ยท๊ตฐ์๊ด์๊ฒ ์ฐ๊ณํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
02. ์ผ์ ํ๋ ์ค ๋ฐ์ํ๋ ์์ฒด์ ํธ ๊ฒฐ์ธก์น ๋ณด์ ๋ฐ ์ผ์ ๋
ธ์ด์ฆ ํํฐ๋ง ์ ์ฒ๋ฆฌ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ผ์ ํ๋ ์ค ์์ฒด์ ํธ ๊ฒฐ์ธก์น ๋ณด์ ๋ฐ ์ผ์ ๋
ธ์ด์ฆ ํํฐ๋ง ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ผ์ ํ๋ ์ค ๋ฐ์ํ๋ ๊ฒฐ์ธก, ์ผ์ ํ, ์ผ์ ๊ณ ์ฐฉ(๋จ์ )์ ํ์งยท๋ณด์ ํ๋ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์๋ฆฌ์ ์ผ๋ก ๋ถ๊ฐ๋ฅํ ๋ฒ์ ๋ฐ ๊ฐ์ ๊ฒฐ์ธก์ผ๋ก ๋ฐ๊พธ๊ณ ์ ํ ๋ณด๊ฐ๊ณผ 3๋ถ ์ด๋ ์ค์๊ฐ์ผ๋ก ์ ๋ฆฌํ๋ฉฐ, ๊ฐ์ ๊ฐ์ด 8๋ถ ์ด์ ์ด์ด์ง๋ ์ผ์ ๊ณ ์ฐฉ์ ์ฐพ์ ๊ฒฐ์ธก ์ฒ๋ฆฌ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/preprocess/wel01_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": [
"\"\"\"์ผ์ ํ๋ ์ค ์์ฒด์ ํธ์ ๊ฒฐ์ธกยท์ผ์ ํยท๊ณ ์ฐฉ(์ผ์ ๋จ์ )์ ํ์งยท๋ณด์ ํ๋ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๊ณ , ์ฒ๋ฆฌ ์๋ยท๋จ์ ํ
์คํธ 5์ข
ยท๋ณด์ ์ ํ ์ฒด์จ ์ค์ฐจ์ ๊ฒฝ๋ณด ๋ถ๋ฅ ์ฑ๋ฅ์ ์ธก์ ํ๋ค. ์ง๋จ ๋ชฉ์ ์ด ์๋ ๊ต์ก์ฉ ๊ฐ์ ์๋ฃ์ด๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------- [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋๋ฏธ ์์ฒด์ ํธ ์์ฑ๊ธฐ (์ค์ ์ฅ๋ณยท๋ถ๋ยท์๋ฌด ๊ธฐ๋ก ์์) ----------------\n",
"# ๊ฐ์ ๋ณ์ฌ(๊ฐ๋ช
ID)๊ฐ 60๋ถ๊ฐ ๊ฑท๊ธฐยทํด์ยท๊ณ ๊ฐ๋ ํ๋์ ํ๋ ๋์์ ์ฌ๋ฐ์ยท์ฒด์จ(์ถ์ ์ฌ๋ถ)ยทํผ๋ถ์ ๋๋๋ฅผ 1๋ถ ๊ฐ๊ฒฉ์ผ๋ก ๋ง๋ ๋ค.\n",
"# ์ง๋จ ๋๊ตฌ๊ฐ ์๋๋ฉฐ, ์ํ ๋ผ๋ฒจ์ ๊ฐ์ ์๋ฆฌ ๋ชจ๋ธ์ '๊ต์ก์ฉ ๋จ๊ณ'์ผ ๋ฟ์ด๋ค.\n",
"N_MIN = 60\n",
"STAGE = {0: \"์ ์\", 1: \"์ฃผ์\", 2: \"๊ฒฝ๋ณด\"}\n",
"TYPES = [\"normal\", \"heat\", \"cold\"]\n",
"\n",
"\n",
"def make_person(i, rng):\n",
" return {\"id\": f\"S{i:03d}\", \"rest_hr\": float(rng.normal(64, 7)), \"age\": int(rng.integers(19, 27)), \"bmi\": float(rng.normal(23.5, 2.5)),\n",
" \"acclim\": int(rng.random() < 0.4), \"fit\": float(np.clip(rng.normal(0.5, 0.2), 0.05, 0.95))}\n",
"\n",
"\n",
"def make_env(kind, rng):\n",
" if kind == \"heat\":\n",
" return {\"temp_c\": float(rng.uniform(30, 40)), \"rh\": float(rng.uniform(45, 85)), \"wind\": float(rng.uniform(0.3, 3))}\n",
" if kind == \"cold\":\n",
" return {\"temp_c\": float(rng.uniform(-22, 2)), \"rh\": float(rng.uniform(40, 80)), \"wind\": float(rng.uniform(2, 11))}\n",
" return {\"temp_c\": float(rng.uniform(8, 24)), \"rh\": float(rng.uniform(35, 70)), \"wind\": float(rng.uniform(0.3, 5))}\n",
"\n",
"\n",
"def make_session(sid, kind, rng, p=None, glitch=True):\n",
" \"\"\"๋ฐํ: dict(person, env, kind, df[hr,temp,eda,act + ์ ๋ต coreยทstageยทrtype]). ์ผ์ ๊ฒฐ์ธกยทํยท๊ณ ์ฐฉ ํฌํจ.\"\"\"\n",
" p = p or make_person(sid, rng)\n",
" env = make_env(kind, rng)\n",
" act = np.zeros(N_MIN); t = 0\n",
" while t < N_MIN: # 10~20๋ถ ๋จ์ ํ๋ ๋ธ๋ก: ํด์ 1.5 / ํ๊ตฐ 4.5 / ๊ณ ๊ฐ๋ 7 MET\n",
" d = int(rng.integers(10, 21)); act[t:t + d] = rng.choice([1.5, 4.5, 7.0], p=[0.25, 0.5, 0.25]); t += d\n",
" hi = env[\"temp_c\"] + (0.12 * (env[\"rh\"] - 40) if env[\"temp_c\"] > 25 else 0)\n",
" wc = env[\"temp_c\"] - 1.6 * env[\"wind\"]\n",
" core, cores = 36.9, []\n",
" for m in range(N_MIN):\n",
" heat = 0.085 * max(0, hi - 24) * (1 - 0.25 * p[\"acclim\"]) * (1 + 0.5 * m / N_MIN) * (1 + 0.012 * (p[\"bmi\"] - 23.5))\n",
" cold = -0.075 * max(0, 8 - wc) * (1 - 0.09 * act[m])\n",
" ss = 36.9 + 0.09 * act[m] + heat + cold\n",
" core += (ss - core) / 18 + rng.normal(0, 0.03)\n",
" cores.append(core)\n",
" core = np.array(cores)\n",
" hr = p[\"rest_hr\"] + 9.5 * (act - 1.5) * (1.15 - 0.3 * p[\"fit\"]) + 11 * (core - 36.9) + rng.normal(0, 3.2, N_MIN)\n",
" eda = 2.0 + 0.25 * act + 3.2 * np.maximum(core - 37.2, 0) - 0.04 * np.maximum(8 - wc, 0) + rng.normal(0, 0.25, N_MIN)\n",
" bias = rng.normal(0, 0.1)\n",
" temp = core + bias + rng.normal(0, 0.1, N_MIN) - np.where((kind == \"cold\") & (env[\"wind\"] > 7), 0.25, 0.0) # ์ผ์ ์ ์ด ๋ถ์ ๋ฐ๋ ๋๊ฐ ์ค์ฐจ\n",
" df = pd.DataFrame({\"min\": np.arange(N_MIN), \"hr\": hr, \"temp\": temp, \"eda\": np.maximum(eda, 0.2), \"act\": act})\n",
" if glitch:\n",
" for col in (\"hr\", \"temp\", \"eda\"):\n",
" df.loc[rng.random(N_MIN) < 0.03, col] = np.nan # ๊ฒฐ์ธก\n",
" s = int(rng.integers(5, N_MIN))\n",
" if rng.random() < 0.12:\n",
" df.loc[s, \"hr\"] = 230.0 # ์ผ์ ํ\n",
" if rng.random() < 0.12:\n",
" df.loc[s, \"temp\"] = 45.0\n",
" ht = np.where(core >= 39.0, 2, np.where(core >= 38.3, 1, 0)); cd = np.where(core <= 35.4, 2, np.where(core <= 36.2, 1, 0))\n",
" df[\"core\"] = core\n",
" df[\"stage\"] = np.maximum(ht, cd); df[\"rtype\"] = np.where(ht > 0, \"heat\", np.where(cd > 0, \"cold\", \"normal\"))\n",
" return {\"id\": p[\"id\"], \"person\": p, \"env\": env, \"kind\": kind, \"df\": df}\n",
"\n",
"\n",
"def make_dataset(n, seed):\n",
" rng = np.random.default_rng(seed)\n",
" kinds = [\"normal\", \"heat\", \"cold\"]\n",
" return [make_session(i, kinds[i % 3], rng) for i in range(n)]\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---------------- ์ ์ฒ๋ฆฌยทํน์ง ์ถ์ถ ๊ณตํต ํจ์ ----------------\n",
"LIMITS = {\"hr\": (30, 220), \"temp\": (30.0, 43.0), \"eda\": (0.0, 40.0)} # ์๋ฆฌ์ ์ผ๋ก ๊ฐ๋ฅํ ๋ฒ์(๋ฒ์ ๋ฐ = ์ผ์ ์ค๋ฅ)\n",
"\n",
"\n",
"def clean(df):\n",
" \"\"\"๋ฒ์ ๋ฐ ๊ฐ์ ๊ฒฐ์ธก ์ฒ๋ฆฌ -> ์ ํ ๋ณด๊ฐ(์ ๋์ ๊ฐ๊น์ด ๊ฐ) -> 3๋ถ ์ด๋ ์ค์๊ฐ์ผ๋ก ํ ์ ๊ฑฐ. ๋ฐํ: (์ ์ df, ๋ณด์ ๊ฑด์)\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 2] ์ผ์ ํ๋ ์ค ๋ฐ์ํ๋ ์์ฒด์ ํธ ๊ฒฐ์ธก์น ๋ณด์ ๋ฐ ์ผ์ ๋
ธ์ด์ฆ ํํฐ๋ง ์ ์ฒ๋ฆฌ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def env_index(env):\n",
" # [TODO : ํ์ ์ค์ต 2] ์ผ์ ํ๋ ์ค ๋ฐ์ํ๋ ์์ฒด์ ํธ ๊ฒฐ์ธก์น ๋ณด์ ๋ฐ ์ผ์ ๋
ธ์ด์ฆ ํํฐ๋ง ์ ์ฒ๋ฆฌ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def features(s, use_env=True, personal=True):\n",
" \"\"\"์ธ์
1๊ฐ -> ๋ถ ๋จ์ ํน์งํ(์ต๊ทผ 5๋ถ ํ๊ท ยท๊ธฐ์ธ๊ธฐ ํฌํจ). ์ ๋ต stage/rtype ํฌํจ.\"\"\"\n",
" df, _ = clean(s[\"df\"]); p, e = s[\"person\"], s[\"env\"]\n",
" f = pd.DataFrame({\"sid\": s[\"id\"], \"min\": df[\"min\"]})\n",
" f[\"temp5\"] = df[\"temp\"].rolling(5, min_periods=1).mean()\n",
" f[\"hr5\"] = df[\"hr\"].rolling(5, min_periods=1).mean()\n",
" f[\"eda5\"] = df[\"eda\"].rolling(5, min_periods=1).mean()\n",
" f[\"dtemp\"] = (f[\"temp5\"] - f[\"temp5\"].shift(4)).fillna(0)\n",
" f[\"act\"] = df[\"act\"]\n",
" if personal:\n",
" f[\"hr_rel\"] = f[\"hr5\"] - p[\"rest_hr\"]\n",
" f[\"acclim\"], f[\"bmi\"], f[\"fit\"] = p[\"acclim\"], p[\"bmi\"], p[\"fit\"]\n",
" if use_env:\n",
" f[\"hi\"], f[\"wc\"] = env_index(e); f[\"wind\"] = e[\"wind\"]\n",
" f[\"stage\"], f[\"rtype\"], f[\"kind\"] = df[\"stage\"], df[\"rtype\"], s[\"kind\"]\n",
" return f[f[\"min\"] >= 5].reset_index(drop=True)\n",
"\n",
"\n",
"def table(sessions, **kw):\n",
" return pd.concat([features(s, **kw) for s in sessions], ignore_index=True)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"\n",
"def flatline(series, run=8):\n",
" \"\"\"๊ฐ์ ๊ฐ์ด run๋ถ ์ด์ ์ด์ด์ง๋ฉด ์ผ์ ๊ณ ์ฐฉ(๋จ์ )์ผ๋ก ๋ณด๊ณ ํด๋น ๊ตฌ๊ฐ์ True๋ก ํ์ํ๋ค.\"\"\"\n",
" same = series.diff().abs().fillna(1) < 1e-9\n",
" grp = (~same).cumsum()\n",
" return same.groupby(grp).transform(\"sum\").add(1).where(same | same.shift(-1, fill_value=False), 1) >= run\n",
"\n",
"\n",
"def preprocess(df):\n",
" \"\"\"๊ณ ์ฐฉ ๊ตฌ๊ฐ์ ๊ฒฐ์ธก์ผ๋ก ๋ฐ๊พผ ๋ค clean()์ผ๋ก ๋ฒ์ ๊ฒ์ฌยท๋ณด๊ฐยทํ ์ ๊ฑฐ. ๋ฐํ: (์ ์ df, ๋ณด์ ๊ฑด์, ๊ณ ์ฐฉ ๊ฑด์)\"\"\"\n",
" d = df.copy(); stuck = 0\n",
" for c in (\"hr\", \"temp\"):\n",
" fl = flatline(d[c].round(3))\n",
" stuck += int(fl.sum()); d.loc[fl, c] = np.nan\n",
" out, fixed = clean(d)\n",
" return out, fixed, stuck\n",
"\n",
"\n",
"def inject_faults(df, rng):\n",
" \"\"\"์ผ์ ๋จ์ ๋ชจ์: ์ฒด์จ์ด ๊ฐ์ฅ ํฌ๊ฒ ๋ณํ๋ ์์ ์ง์ ๋ถํฐ ์ผ์๊ฐ 12๋ถ๊ฐ ๊ฐ์ ๊ฐ์ผ๋ก ๊ณ ์ฐฉ๋๋ค(์ค์ ๋ณํ๋ฅผ ๋ฐ๋ผ๊ฐ์ง ๋ชปํจ).\"\"\"\n",
" d = df.copy(); s = int(np.clip(np.argmax(np.abs(d[\"core\"].values - 36.9)) - 12 + rng.integers(0, 4), 3, N_MIN - 13))\n",
" d.loc[s:s + 11, \"temp\"] = d.loc[s, \"temp\"]\n",
" return d\n",
"\n",
"\n",
"rng = np.random.default_rng(5)\n",
"data = make_dataset(120, seed=21)\n",
"\n",
"# 1) ์ฒ๋ฆฌ ์๋์ ์ค๋ฅ์จ\n",
"t0 = time.perf_counter(); n_rows = 0; errors = 0; fixed_all = 0\n",
"for s in data:\n",
" try:\n",
" out, fx, st = preprocess(s[\"df\"]); n_rows += len(out) * 3; fixed_all += fx\n",
" errors += int(out[[\"hr\", \"temp\", \"eda\"]].isna().any().any() or (out[\"temp\"] > 43).any())\n",
" except Exception:\n",
" errors += 1\n",
"sec = time.perf_counter() - t0\n",
"print(f\"์ ์ฒ๋ฆฌ {n_rows:,}๊ฑด(์ ํธ๊ฐ) / {sec:.2f}์ด = {n_rows / sec:,.0f}๊ฑด/์ด (๋ชฉํ 1,000 ์ด์: {'๋ฌ์ฑ' if n_rows / sec >= 1000 else '๋ฏธ๋ฌ'})\")\n",
"print(f\"์ ์ฒ๋ฆฌ ์ค๋ฅ {errors}๊ฑด -> ์ค๋ฅ์จ {errors / len(data):.0%} (๋ชฉํ 0%), ๋ฒ์ ๋ฐยท๊ฒฐ์ธก ๋ณด์ {fixed_all}๊ฑด\")\n",
"\n",
"\n",
"# 2) ๋จ์ ํ
์คํธ 5์ข
\n",
"def t1():\n",
" d = pd.DataFrame({\"min\": range(6), \"hr\": [80, np.nan, 82, 83, 84, 85.], \"temp\": 37.0, \"eda\": 2.0, \"act\": 1.5, \"stage\": 0, \"rtype\": \"normal\"})\n",
" out, fx = clean(d); assert out[\"hr\"].notna().all() and fx == 1\n",
"def t2():\n",
" d = pd.DataFrame({\"min\": range(6), \"hr\": [80, 81, 230, 82, 83, 84.], \"temp\": 37.0, \"eda\": 2.0, \"act\": 1.5, \"stage\": 0, \"rtype\": \"normal\"})\n",
" out, fx = clean(d); assert out[\"hr\"].max() < 100 and fx == 1\n",
"def t3():\n",
" s = pd.Series([36.9, 37.0, 37.0, 37.0, 37.0, 37.0, 37.0, 37.0, 37.0, 37.0, 37.1]); assert flatline(s).sum() >= 8\n",
"def t4():\n",
" s = pd.Series(np.linspace(36.5, 37.5, 20)); assert not flatline(s).any()\n",
"def t5():\n",
" out, fx, st = preprocess(inject_faults(make_session(1, \"normal\", np.random.default_rng(3))[\"df\"], np.random.default_rng(4))); assert st >= 8 and len(out) == N_MIN\n",
"tests = {\"๊ฒฐ์ธก ๋ณด๊ฐ\": t1, \"์ผ์ ํ ์ ๊ฑฐ\": t2, \"๊ณ ์ฐฉ ๊ตฌ๊ฐ ํ์ง\": t3, \"์ ์ ๋ณํ ์คํ ์์\": t4, \"๊ณ ์ฐฉ ์ฃผ์
ํ ๋ณด์ \": t5}\n",
"passed = 0\n",
"for name, fn in tests.items():\n",
" try:\n",
" fn(); passed += 1; print(f\"[PASS] {name}\")\n",
" except AssertionError:\n",
" print(f\"[FAIL] {name}\")\n",
"print(f\"๋จ์ ํ
์คํธ {passed}/5 ํต๊ณผ\")\n",
"\n",
"# 3) ๋ณด์ ์ ํ ์ฒด์จ ์ค์ฐจ(์ ๋ต core ๋๋น RMSE)์ ๊ฒฝ๋ณด ๋ถ๋ฅ ์ฑ๋ฅ\n",
"def rmse(a, b):\n",
" m = ~np.isnan(a)\n",
" return float(np.sqrt(np.mean((a[m] - b[m]) ** 2)))\n",
"\n",
"\n",
"def hot_cold_f1(df_use, stage_src):\n",
" \"\"\"์ฒด์จ 38.0 ์ด์ ๋๋ 36.3 ์ดํ๋ฅผ ์ํ์ผ๋ก ๋ณธ ๊ฒฝ๋ณด์ ์ ๋ต stage>=1์ F1.\"\"\"\n",
" pred = (df_use[\"temp\"] >= 38.0) | (df_use[\"temp\"] <= 36.3)\n",
" truth = stage_src[\"stage\"] >= 1\n",
" tp = (pred & truth).sum(); p = tp / max(pred.sum(), 1); r = tp / max(truth.sum(), 1)\n",
" return round(2 * p * r / max(p + r, 1e-9), 3), int((pred & ~truth).sum())\n",
"\n",
"\n",
"rows = []\n",
"for kind, label in ((\"normal\", \"ํ์\"), (\"heat\", \"ํญ์ผ\"), (\"cold\", \"ํนํ\")):\n",
" raw, cln, tr_ = [], [], []\n",
" for s in [x for x in data if x[\"kind\"] == kind]:\n",
" bad = inject_faults(s[\"df\"], rng)\n",
" out, _, _ = preprocess(bad)\n",
" raw.append(bad); cln.append(out); tr_.append(s[\"df\"])\n",
" raw, cln, tr_ = pd.concat(raw), pd.concat(cln), pd.concat(tr_)\n",
" f_raw, fp_raw = hot_cold_f1(raw, tr_)\n",
" f_cln, fp_cln = hot_cold_f1(cln, tr_)\n",
" rows.append({\"์ํฉ\": label, \"์ฒด์จ RMSE ์ \": round(rmse(raw[\"temp\"].where(raw[\"temp\"] <= 43).values, tr_[\"core\"].values), 3),\n",
" \"์ฒด์จ RMSE ํ\": round(rmse(cln[\"temp\"].values, tr_[\"core\"].values), 3), \"๊ฒฝ๋ณด F1 ์ \": f_raw, \"๊ฒฝ๋ณด F1 ํ\": f_cln, \"์ค๊ฒฝ๋ณด ์ \": fp_raw, \"์ค๊ฒฝ๋ณด ํ\": fp_cln})\n",
"print(\"\\n[๋ณด์ ์ ํ ๋น๊ต] (์ : ์ผ์ ํยท๊ฒฐ์ธกยท๊ณ ์ฐฉ ๊ทธ๋๋ก / ํ: ์ ์ฒ๋ฆฌ ์ ์ฉ)\")\n",
"print(pd.DataFrame(rows).to_string(index=False))\n",
"print(\"์ ์ ๋
ธ์ด์ฆ ๋์: ์ฒด์จ ์ผ์ ๊ณ ์ฐฉ(๋จ์ )์ 8๋ถ ์ด์ ๊ฐ์ ๊ฐ์ด๋ฉด ๊ฒฐ์ธก ์ฒ๋ฆฌ ํ ๋ณด๊ฐํ๊ณ , ๋ฒ์ ๋ฐ ๊ฐ์ ์ผ์ ์ค๋ฅ๋ก ๋ณธ๋ค. ์คํธํ ํ๋ณ์ ๋ณธ ๊ณผ์ ๋ฒ์ ๋ฐ์ด๋ค.\")\n",
"\n",
"s = data[1]; bad = inject_faults(s[\"df\"], rng); out, _, _ = preprocess(bad)\n",
"plt.figure(figsize=(9, 4))\n",
"plt.plot(bad[\"min\"], bad[\"temp\"], \".\", color=\"tab:red\", label=\"raw (faults)\"); plt.plot(out[\"min\"], out[\"temp\"], color=\"tab:blue\", label=\"cleaned\")\n",
"plt.plot(s[\"df\"][\"min\"], s[\"df\"][\"core\"], \"k:\", label=\"true (simulated)\"); plt.ylim(34, 41); plt.xlabel(\"minute\"); plt.ylabel(\"temp (C)\")\n",
"plt.title(f\"{s['id']} temperature before/after preprocessing (synthetic)\"); plt.legend(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
02 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"์ ์ฒ๋ฆฌ 21,600๊ฑด(์ ํธ๊ฐ) / 1.20์ด = 18,030๊ฑด/์ด (๋ชฉํ 1,000 ์ด์: ๋ฌ์ฑ)\n",
"์ ์ฒ๋ฆฌ ์ค๋ฅ 0๊ฑด -> ์ค๋ฅ์จ 0% (๋ชฉํ 0%), ๋ฒ์ ๋ฐยท๊ฒฐ์ธก ๋ณด์ 663๊ฑด\n",
"[PASS] ๊ฒฐ์ธก ๋ณด๊ฐ\n",
"[PASS] ์ผ์ ํ ์ ๊ฑฐ\n",
"[PASS] ๊ณ ์ฐฉ ๊ตฌ๊ฐ ํ์ง\n",
"[PASS] ์ ์ ๋ณํ ์คํ ์์\n",
"[PASS] ๊ณ ์ฐฉ ์ฃผ์
ํ ๋ณด์ \n",
"๋จ์ ํ
์คํธ 5/5 ํต๊ณผ\n",
"\n",
"[๋ณด์ ์ ํ ๋น๊ต] (์ : ์ผ์ ํยท๊ฒฐ์ธกยท๊ณ ์ฐฉ ๊ทธ๋๋ก / ํ: ์ ์ฒ๋ฆฌ ์ ์ฉ)\n",
"์ํฉ ์ฒด์จ RMSE ์ ์ฒด์จ RMSE ํ ๊ฒฝ๋ณด F1 ์ ๊ฒฝ๋ณด F1 ํ ์ค๊ฒฝ๋ณด ์ ์ค๊ฒฝ๋ณด ํ\n",
"ํ์ 0.141 0.121 0.000 0.000 4 0\n",
"ํญ์ผ 0.146 0.104 0.818 0.820 378 417\n",
"ํนํ 0.219 0.203 0.814 0.828 397 419\n",
"์ ์ ๋
ธ์ด์ฆ ๋์: ์ฒด์จ ์ผ์ ๊ณ ์ฐฉ(๋จ์ )์ 8๋ถ ์ด์ ๊ฐ์ ๊ฐ์ด๋ฉด ๊ฒฐ์ธก ์ฒ๋ฆฌ ํ ๋ณด๊ฐํ๊ณ , ๋ฒ์ ๋ฐ ๊ฐ์ ์ผ์ ์ค๋ฅ๋ก ๋ณธ๋ค. ์คํธํ ํ๋ณ์ ๋ณธ ๊ณผ์ ๋ฒ์ ๋ฐ์ด๋ค.\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",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ต๊ทผ 5๋ถ ํ๊ท ์ฒด์จ์ด 38.0๋ ์ด์์ด๋ฉด์ ์ฌ๋ฐ์๊ฐ 95 ์ด์์ด๋ฉด ์จ์ด ์คํธ๋ ์ค ์์ฌ, 36.3๋ ์ดํ์ด๋ฉด ํ๋ญ ์คํธ๋ ์ค ์์ฌ์ผ๋ก ๋ถ๋ฅ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/wel01_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 numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------- [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋๋ฏธ ์์ฒด์ ํธ ์์ฑ๊ธฐ (์ค์ ์ฅ๋ณยท๋ถ๋ยท์๋ฌด ๊ธฐ๋ก ์์) ----------------\n",
"# ๊ฐ์ ๋ณ์ฌ(๊ฐ๋ช
ID)๊ฐ 60๋ถ๊ฐ ๊ฑท๊ธฐยทํด์ยท๊ณ ๊ฐ๋ ํ๋์ ํ๋ ๋์์ ์ฌ๋ฐ์ยท์ฒด์จ(์ถ์ ์ฌ๋ถ)ยทํผ๋ถ์ ๋๋๋ฅผ 1๋ถ ๊ฐ๊ฒฉ์ผ๋ก ๋ง๋ ๋ค.\n",
"# ์ง๋จ ๋๊ตฌ๊ฐ ์๋๋ฉฐ, ์ํ ๋ผ๋ฒจ์ ๊ฐ์ ์๋ฆฌ ๋ชจ๋ธ์ '๊ต์ก์ฉ ๋จ๊ณ'์ผ ๋ฟ์ด๋ค.\n",
"N_MIN = 60\n",
"STAGE = {0: \"์ ์\", 1: \"์ฃผ์\", 2: \"๊ฒฝ๋ณด\"}\n",
"TYPES = [\"normal\", \"heat\", \"cold\"]\n",
"\n",
"\n",
"def make_person(i, rng):\n",
" return {\"id\": f\"S{i:03d}\", \"rest_hr\": float(rng.normal(64, 7)), \"age\": int(rng.integers(19, 27)), \"bmi\": float(rng.normal(23.5, 2.5)),\n",
" \"acclim\": int(rng.random() < 0.4), \"fit\": float(np.clip(rng.normal(0.5, 0.2), 0.05, 0.95))}\n",
"\n",
"\n",
"def make_env(kind, rng):\n",
" if kind == \"heat\":\n",
" return {\"temp_c\": float(rng.uniform(30, 40)), \"rh\": float(rng.uniform(45, 85)), \"wind\": float(rng.uniform(0.3, 3))}\n",
" if kind == \"cold\":\n",
" return {\"temp_c\": float(rng.uniform(-22, 2)), \"rh\": float(rng.uniform(40, 80)), \"wind\": float(rng.uniform(2, 11))}\n",
" return {\"temp_c\": float(rng.uniform(8, 24)), \"rh\": float(rng.uniform(35, 70)), \"wind\": float(rng.uniform(0.3, 5))}\n",
"\n",
"\n",
"def make_session(sid, kind, rng, p=None, glitch=True):\n",
" \"\"\"๋ฐํ: dict(person, env, kind, df[hr,temp,eda,act + ์ ๋ต coreยทstageยทrtype]). ์ผ์ ๊ฒฐ์ธกยทํยท๊ณ ์ฐฉ ํฌํจ.\"\"\"\n",
" p = p or make_person(sid, rng)\n",
" env = make_env(kind, rng)\n",
" act = np.zeros(N_MIN); t = 0\n",
" while t < N_MIN: # 10~20๋ถ ๋จ์ ํ๋ ๋ธ๋ก: ํด์ 1.5 / ํ๊ตฐ 4.5 / ๊ณ ๊ฐ๋ 7 MET\n",
" d = int(rng.integers(10, 21)); act[t:t + d] = rng.choice([1.5, 4.5, 7.0], p=[0.25, 0.5, 0.25]); t += d\n",
" hi = env[\"temp_c\"] + (0.12 * (env[\"rh\"] - 40) if env[\"temp_c\"] > 25 else 0)\n",
" wc = env[\"temp_c\"] - 1.6 * env[\"wind\"]\n",
" core, cores = 36.9, []\n",
" for m in range(N_MIN):\n",
" heat = 0.085 * max(0, hi - 24) * (1 - 0.25 * p[\"acclim\"]) * (1 + 0.5 * m / N_MIN) * (1 + 0.012 * (p[\"bmi\"] - 23.5))\n",
" cold = -0.075 * max(0, 8 - wc) * (1 - 0.09 * act[m])\n",
" ss = 36.9 + 0.09 * act[m] + heat + cold\n",
" core += (ss - core) / 18 + rng.normal(0, 0.03)\n",
" cores.append(core)\n",
" core = np.array(cores)\n",
" hr = p[\"rest_hr\"] + 9.5 * (act - 1.5) * (1.15 - 0.3 * p[\"fit\"]) + 11 * (core - 36.9) + rng.normal(0, 3.2, N_MIN)\n",
" eda = 2.0 + 0.25 * act + 3.2 * np.maximum(core - 37.2, 0) - 0.04 * np.maximum(8 - wc, 0) + rng.normal(0, 0.25, N_MIN)\n",
" bias = rng.normal(0, 0.1)\n",
" temp = core + bias + rng.normal(0, 0.1, N_MIN) - np.where((kind == \"cold\") & (env[\"wind\"] > 7), 0.25, 0.0) # ์ผ์ ์ ์ด ๋ถ์ ๋ฐ๋ ๋๊ฐ ์ค์ฐจ\n",
" df = pd.DataFrame({\"min\": np.arange(N_MIN), \"hr\": hr, \"temp\": temp, \"eda\": np.maximum(eda, 0.2), \"act\": act})\n",
" if glitch:\n",
" for col in (\"hr\", \"temp\", \"eda\"):\n",
" df.loc[rng.random(N_MIN) < 0.03, col] = np.nan # ๊ฒฐ์ธก\n",
" s = int(rng.integers(5, N_MIN))\n",
" if rng.random() < 0.12:\n",
" df.loc[s, \"hr\"] = 230.0 # ์ผ์ ํ\n",
" if rng.random() < 0.12:\n",
" df.loc[s, \"temp\"] = 45.0\n",
" ht = np.where(core >= 39.0, 2, np.where(core >= 38.3, 1, 0)); cd = np.where(core <= 35.4, 2, np.where(core <= 36.2, 1, 0))\n",
" df[\"core\"] = core\n",
" df[\"stage\"] = np.maximum(ht, cd); df[\"rtype\"] = np.where(ht > 0, \"heat\", np.where(cd > 0, \"cold\", \"normal\"))\n",
" return {\"id\": p[\"id\"], \"person\": p, \"env\": env, \"kind\": kind, \"df\": df}\n",
"\n",
"\n",
"def make_dataset(n, seed):\n",
" rng = np.random.default_rng(seed)\n",
" kinds = [\"normal\", \"heat\", \"cold\"]\n",
" return [make_session(i, kinds[i % 3], rng) for i in range(n)]\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---------------- ์ ์ฒ๋ฆฌยทํน์ง ์ถ์ถ ๊ณตํต ํจ์ ----------------\n",
"LIMITS = {\"hr\": (30, 220), \"temp\": (30.0, 43.0), \"eda\": (0.0, 40.0)} # ์๋ฆฌ์ ์ผ๋ก ๊ฐ๋ฅํ ๋ฒ์(๋ฒ์ ๋ฐ = ์ผ์ ์ค๋ฅ)\n",
"\n",
"\n",
"def clean(df):\n",
" \"\"\"๋ฒ์ ๋ฐ ๊ฐ์ ๊ฒฐ์ธก ์ฒ๋ฆฌ -> ์ ํ ๋ณด๊ฐ(์ ๋์ ๊ฐ๊น์ด ๊ฐ) -> 3๋ถ ์ด๋ ์ค์๊ฐ์ผ๋ก ํ ์ ๊ฑฐ. ๋ฐํ: (์ ์ df, ๋ณด์ ๊ฑด์)\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 3] ์ฒด์จ-์ฌ๋ฐ์ ์๊ด๊ด๊ณ ๊ธฐ๋ฐ 1์ฐจ ์ด์ฌ๋ณ/์ ์ฒด์จ์ฆ ๊ฒฝ๋ณด Baseline ๋ชจ๋ธ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def env_index(env):\n",
" # [TODO : ํ์ ์ค์ต 3] ์ฒด์จ-์ฌ๋ฐ์ ์๊ด๊ด๊ณ ๊ธฐ๋ฐ 1์ฐจ ์ด์ฌ๋ณ/์ ์ฒด์จ์ฆ ๊ฒฝ๋ณด Baseline ๋ชจ๋ธ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def features(s, use_env=True, personal=True):\n",
" \"\"\"์ธ์
1๊ฐ -> ๋ถ ๋จ์ ํน์งํ(์ต๊ทผ 5๋ถ ํ๊ท ยท๊ธฐ์ธ๊ธฐ ํฌํจ). ์ ๋ต stage/rtype ํฌํจ.\"\"\"\n",
" df, _ = clean(s[\"df\"]); p, e = s[\"person\"], s[\"env\"]\n",
" f = pd.DataFrame({\"sid\": s[\"id\"], \"min\": df[\"min\"]})\n",
" f[\"temp5\"] = df[\"temp\"].rolling(5, min_periods=1).mean()\n",
" f[\"hr5\"] = df[\"hr\"].rolling(5, min_periods=1).mean()\n",
" f[\"eda5\"] = df[\"eda\"].rolling(5, min_periods=1).mean()\n",
" f[\"dtemp\"] = (f[\"temp5\"] - f[\"temp5\"].shift(4)).fillna(0)\n",
" f[\"act\"] = df[\"act\"]\n",
" if personal:\n",
" f[\"hr_rel\"] = f[\"hr5\"] - p[\"rest_hr\"]\n",
" f[\"acclim\"], f[\"bmi\"], f[\"fit\"] = p[\"acclim\"], p[\"bmi\"], p[\"fit\"]\n",
" if use_env:\n",
" f[\"hi\"], f[\"wc\"] = env_index(e); f[\"wind\"] = e[\"wind\"]\n",
" f[\"stage\"], f[\"rtype\"], f[\"kind\"] = df[\"stage\"], df[\"rtype\"], s[\"kind\"]\n",
" return f[f[\"min\"] >= 5].reset_index(drop=True)\n",
"\n",
"\n",
"def table(sessions, **kw):\n",
" return pd.concat([features(s, **kw) for s in sessions], ignore_index=True)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ํ๋จ ๊ธฐ์ค(ํฌ๋ช
์ฑ): โ ์ต๊ทผ 5๋ถ ํ๊ท ์ฒด์จ์ด 38.0๋ ์ด์์ด๊ณ ์ฌ๋ฐ์๊ฐ 95 ์ด์์ด๋ฉด '์จ์ด ์คํธ๋ ์ค ์์ฌ'\n",
"# โก ์ต๊ทผ 5๋ถ ํ๊ท ์ฒด์จ์ด 36.3๋ ์ดํ์ด๋ฉด 'ํ๋ญ ์คํธ๋ ์ค ์์ฌ' โข ๊ทธ ์ธ ์ ์\n",
"TH_HEAT, TH_COLD, HR_GATE = 38.0, 36.3, 95\n",
"\n",
"\n",
"def rule(r, th=TH_HEAT, tc=TH_COLD, hg=HR_GATE):\n",
" if r[\"temp5\"] >= th and r[\"hr5\"] >= hg:\n",
" return \"heat\"\n",
" if r[\"temp5\"] <= tc:\n",
" return \"cold\"\n",
" return \"normal\"\n",
"\n",
"\n",
"data = make_dataset(90, seed=7)\n",
"tab = table(data, use_env=False, personal=False)\n",
"tab[\"pred\"] = tab.apply(rule, axis=1)\n",
"acc = (tab[\"pred\"] == tab[\"rtype\"]).mean()\n",
"print(f\"[Baseline] ๊ฐ์ ๋ณ์ฌ 90๋ช
x 55๋ถ = {len(tab):,}๊ฐ ๊ตฌ๊ฐ, ์ ๋ต ๋ถํฌ {tab['rtype'].value_counts().to_dict()}\")\n",
"print(f\"๊ฒฝ๋ณด ์ ํ ๋ถ๋ฅ ์ ํ๋ {acc:.1%} (๋ชฉํ 75% ์ด์) -> {'๋ฌ์ฑ' if acc >= 0.75 else '๋ฏธ๋ฌ'}\")\n",
"\n",
"print(\"\\n[์ ํ๋ณ ํ์ ] (ํ: ์ ๋ต, ์ด: ํ์ )\")\n",
"cm = pd.crosstab(tab[\"rtype\"], tab[\"pred\"]).reindex(index=TYPES, columns=TYPES, fill_value=0)\n",
"print(cm.to_string())\n",
"for c in (\"heat\", \"cold\"):\n",
" rec = cm.loc[c, c] / cm.loc[c].sum(); prec = cm.loc[c, c] / max(cm[c].sum(), 1)\n",
" print(f\"{c}: ์ฌํ์จ {rec:.1%}, ์ ๋ฐ๋ {prec:.1%}\")\n",
"\n",
"print(\"\\n[๋จ๊ณ๋ณ ์ํ ๊ตฌ๊ฐ ์ฌํ์จ] (์ํ ์ ํ์ ์ํ์ผ๋ก ๋ง์ถ ๋น์จ)\")\n",
"risk = tab[tab[\"stage\"] >= 1]\n",
"print(risk.groupby(\"stage\").apply(lambda g: f\"{(g['pred'] == g['rtype']).mean():.1%} ({len(g)}๊ตฌ๊ฐ)\").to_string())\n",
"\n",
"print(\"\\n[์ฒด์จ-์ฌ๋ฐ์ ์๊ด] ์ ์ฒด ์๊ด๊ณ์ %.2f / ํญ์ผ ์ธ์
%.2f / ํนํ ์ธ์
%.2f\" % (\n",
" tab[[\"temp5\", \"hr5\"]].corr().iloc[0, 1], tab[tab[\"kind\"] == \"heat\"][[\"temp5\", \"hr5\"]].corr().iloc[0, 1], tab[tab[\"kind\"] == \"cold\"][[\"temp5\", \"hr5\"]].corr().iloc[0, 1]))\n",
"\n",
"print(\"\\n[์๊ณ๊ฐ ๋ฏผ๊ฐ๋] ์จ์ด ์ฒด์จ ๊ธฐ์ค ๋ณํ\")\n",
"for th in (37.7, 38.0, 38.3, 38.6):\n",
" p = tab.apply(lambda r: rule(r, th=th), axis=1)\n",
" r = ((p == \"heat\") & (tab[\"rtype\"] == \"heat\")).sum() / (tab[\"rtype\"] == \"heat\").sum(); f = ((p == \"heat\") & (tab[\"rtype\"] != \"heat\")).sum()\n",
" print(f\"๊ธฐ์ค {th}: ์จ์ด ์ฌํ์จ {r:.1%}, ์ค๊ฒฝ๋ณด {f}๊ตฌ๊ฐ\")\n",
"print(\"ํ๊ณ: ๊ฐ์ธ๋ณ ์์ ์ ์ฌ๋ฐยท๊ธฐ์ยทํ๋ ๊ฐ๋๋ฅผ ๋ฐ์ํ์ง ์์ ์ด๋ ์งํ ์ฌ๋ฐ ์์น๊ณผ ๋ฐ๋์ ์ํ ์ผ์ ๋๊ฐ์ ๊ตฌ๋ถํ์ง ๋ชปํ๋ค -> ๋ฏธ์
4ยท5์์ ๋ณด์ํ๋ค.\")\n",
"print(\"๋ณธ ๊ฒฐ๊ณผ๋ ๊ต์ก์ฉ ๊ฐ์ ์๋ฃ์ ๊ฒฝ๋ณด ๋จ๊ณ์ด๋ฉฐ ์ง๋จ์ด ์๋๋ค. ์ค์ ์ด์ ์งํ๋ ์๋ฌด๋ยท๊ตฐ์๊ด์๊ฒ ์ฐ๊ณํ๋ค.\")\n",
"\n",
"colr = {\"normal\": \"tab:gray\", \"heat\": \"tab:red\", \"cold\": \"tab:blue\"}\n",
"plt.figure(figsize=(7, 5))\n",
"for k, g in tab.groupby(\"rtype\"):\n",
" plt.scatter(g[\"temp5\"], g[\"hr5\"], s=6, alpha=0.4, color=colr[k], label=k)\n",
"plt.axvline(TH_HEAT, ls=\"--\", color=\"k\"); plt.axvline(TH_COLD, ls=\"--\", color=\"k\"); plt.axhline(HR_GATE, ls=\":\", color=\"k\")\n",
"plt.xlabel(\"temperature (5-min mean, C)\"); plt.ylabel(\"heart rate (5-min mean, bpm)\"); plt.legend(); plt.title(\"Baseline rule on dummy signals\")\n",
"plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
03 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[Baseline] ๊ฐ์ ๋ณ์ฌ 90๋ช
x 55๋ถ = 4,950๊ฐ ๊ตฌ๊ฐ, ์ ๋ต ๋ถํฌ {'normal': 3536, 'cold': 761, 'heat': 653}\n",
"๊ฒฝ๋ณด ์ ํ ๋ถ๋ฅ ์ ํ๋ 86.9% (๋ชฉํ 75% ์ด์) -> ๋ฌ์ฑ\n",
"\n",
"[์ ํ๋ณ ํ์ ] (ํ: ์ ๋ต, ์ด: ํ์ )\n",
"pred normal heat cold\n",
"rtype \n",
"normal 3056 246 234\n",
"heat 160 493 0\n",
"cold 7 0 754\n",
"heat: ์ฌํ์จ 75.5%, ์ ๋ฐ๋ 66.7%\n",
"cold: ์ฌํ์จ 99.1%, ์ ๋ฐ๋ 76.3%\n",
"\n",
"[๋จ๊ณ๋ณ ์ํ ๊ตฌ๊ฐ ์ฌํ์จ] (์ํ ์ ํ์ ์ํ์ผ๋ก ๋ง์ถ ๋น์จ)\n",
"stage\n",
"1 86.8% (1221๊ตฌ๊ฐ)\n",
"2 96.9% (193๊ตฌ๊ฐ)\n",
"\n",
"[์ฒด์จ-์ฌ๋ฐ์ ์๊ด] ์ ์ฒด ์๊ด๊ณ์ 0.48 / ํญ์ผ ์ธ์
0.32 / ํนํ ์ธ์
0.41\n",
"\n",
"[์๊ณ๊ฐ ๋ฏผ๊ฐ๋] ์จ์ด ์ฒด์จ ๊ธฐ์ค ๋ณํ\n",
"๊ธฐ์ค 37.7: ์จ์ด ์ฌํ์จ 75.7%, ์ค๊ฒฝ๋ณด 462๊ตฌ๊ฐ\n",
"๊ธฐ์ค 38.0: ์จ์ด ์ฌํ์จ 75.5%, ์ค๊ฒฝ๋ณด 246๊ตฌ๊ฐ\n",
"๊ธฐ์ค 38.3: ์จ์ด ์ฌํ์จ 67.7%, ์ค๊ฒฝ๋ณด 38๊ตฌ๊ฐ\n",
"๊ธฐ์ค 38.6: ์จ์ด ์ฌํ์จ 31.1%, ์ค๊ฒฝ๋ณด 0๊ตฌ๊ฐ\n",
"ํ๊ณ: ๊ฐ์ธ๋ณ ์์ ์ ์ฌ๋ฐยท๊ธฐ์ยทํ๋ ๊ฐ๋๋ฅผ ๋ฐ์ํ์ง ์์ ์ด๋ ์งํ ์ฌ๋ฐ ์์น๊ณผ ๋ฐ๋์ ์ํ ์ผ์ ๋๊ฐ์ ๊ตฌ๋ถํ์ง ๋ชปํ๋ค -> ๋ฏธ์
4ยท5์์ ๋ณด์ํ๋ค.\n",
"๋ณธ ๊ฒฐ๊ณผ๋ ๊ต์ก์ฉ ๊ฐ์ ์๋ฃ์ ๊ฒฝ๋ณด ๋จ๊ณ์ด๋ฉฐ ์ง๋จ์ด ์๋๋ค. ์ค์ ์ด์ ์งํ๋ ์๋ฌด๋ยท๊ตฐ์๊ด์๊ฒ ์ฐ๊ณํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
04. ๊ธฐ์ ๋ฐ์ดํฐ(์จยท์ต๋, ํ์) ๊ฒฐํฉ ์จ์ด/ํ๋ญ ์คํธ๋ ์ค ๋ณตํฉ ์ํ๋ ์์ธก ๋ชจ๋ธ ๊ณ ๋ํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๊ธฐ์ ๋ฐ์ดํฐ์ ๊ฒฐํฉํ ๋ณตํฉ ํ๊ฒฝ ์ํ๋ ์์ธก ๋ชจ๋ธ์ ๊ณ ๋ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๊ธฐ์(์จ๋ยท์ต๋ยทํ์)๊ณผ ๊ฐ์ธ ํน์ฑ์ ๊ฒฐํฉํ ๋ณตํฉ ํ๊ฒฝ ์ํ๋ ๋ถ๋ฅ ๋ชจ๋ธ๋ก ๊ณ ๋ํํ๊ณ , ์ผ์ ์ด์ยท๊ต๋ ์ ํธ๋ฅผ ์ ๊ฒํ๋ ์์ธ ํ์ง๋ฅผ ์ถ๊ฐํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ฒด๊ฐ ๋์ยท์ถ์ ์ง์, ์์ ์ ์ฌ๋ฐ ๋๋น ์ฌ๋ฐ ์์น, ์ฒด์จ ๋ณํ์จ, ๋์ ์์ ์ฌ๋ถ ๋ฑ์ ํน์ง์ผ๋ก ๊ทธ๋๋์ธํธ ๋ถ์คํ
๋ชจ๋ธ์ ํ์ต ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/wel01_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": [
"\"\"\"์ฒด์จยท์ฌ๋ฐ์ยทํผ๋ถ์ ๋๋์ ๊ธฐ์(์จ๋ยท์ต๋ยทํ์)๊ณผ ๊ฐ์ธ ํน์ฑ(์์ ์ ์ฌ๋ฐ, ์์ ์ฌ๋ถ, ์ฒด๊ฒฉ)์ ๊ฒฐํฉํ ๋ณตํฉ ํ๊ฒฝ ์ํ๋ ๋ถ๋ฅ ๋ชจ๋ธ(๊ทธ๋๋์ธํธ ๋ถ์คํ
)๋ก ๊ณ ๋ํํ๊ณ , ์ํ ๊ตฌ๊ฐ ์ฌํ์จยท์ฒ๋ฆฌ ์ง์ฐ์ Baseline๊ณผ ๋น๊ตํ๋ค. ์ผ์ ์ด์ ์์ธ ํ์ง 10์ข
๋ ์ํํ๋ค. ์ง๋จ ์๋ยท์๋ฌด๋ ์ฐ๊ณ.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"from sklearn.ensemble import GradientBoostingClassifier\n",
"\n",
"# ---------------- [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋๋ฏธ ์์ฒด์ ํธ ์์ฑ๊ธฐ (์ค์ ์ฅ๋ณยท๋ถ๋ยท์๋ฌด ๊ธฐ๋ก ์์) ----------------\n",
"# ๊ฐ์ ๋ณ์ฌ(๊ฐ๋ช
ID)๊ฐ 60๋ถ๊ฐ ๊ฑท๊ธฐยทํด์ยท๊ณ ๊ฐ๋ ํ๋์ ํ๋ ๋์์ ์ฌ๋ฐ์ยท์ฒด์จ(์ถ์ ์ฌ๋ถ)ยทํผ๋ถ์ ๋๋๋ฅผ 1๋ถ ๊ฐ๊ฒฉ์ผ๋ก ๋ง๋ ๋ค.\n",
"# ์ง๋จ ๋๊ตฌ๊ฐ ์๋๋ฉฐ, ์ํ ๋ผ๋ฒจ์ ๊ฐ์ ์๋ฆฌ ๋ชจ๋ธ์ '๊ต์ก์ฉ ๋จ๊ณ'์ผ ๋ฟ์ด๋ค.\n",
"N_MIN = 60\n",
"STAGE = {0: \"์ ์\", 1: \"์ฃผ์\", 2: \"๊ฒฝ๋ณด\"}\n",
"TYPES = [\"normal\", \"heat\", \"cold\"]\n",
"\n",
"\n",
"def make_person(i, rng):\n",
" return {\"id\": f\"S{i:03d}\", \"rest_hr\": float(rng.normal(64, 7)), \"age\": int(rng.integers(19, 27)), \"bmi\": float(rng.normal(23.5, 2.5)),\n",
" \"acclim\": int(rng.random() < 0.4), \"fit\": float(np.clip(rng.normal(0.5, 0.2), 0.05, 0.95))}\n",
"\n",
"\n",
"def make_env(kind, rng):\n",
" if kind == \"heat\":\n",
" return {\"temp_c\": float(rng.uniform(30, 40)), \"rh\": float(rng.uniform(45, 85)), \"wind\": float(rng.uniform(0.3, 3))}\n",
" if kind == \"cold\":\n",
" return {\"temp_c\": float(rng.uniform(-22, 2)), \"rh\": float(rng.uniform(40, 80)), \"wind\": float(rng.uniform(2, 11))}\n",
" return {\"temp_c\": float(rng.uniform(8, 24)), \"rh\": float(rng.uniform(35, 70)), \"wind\": float(rng.uniform(0.3, 5))}\n",
"\n",
"\n",
"def make_session(sid, kind, rng, p=None, glitch=True):\n",
" \"\"\"๋ฐํ: dict(person, env, kind, df[hr,temp,eda,act + ์ ๋ต coreยทstageยทrtype]). ์ผ์ ๊ฒฐ์ธกยทํยท๊ณ ์ฐฉ ํฌํจ.\"\"\"\n",
" p = p or make_person(sid, rng)\n",
" env = make_env(kind, rng)\n",
" act = np.zeros(N_MIN); t = 0\n",
" while t < N_MIN: # 10~20๋ถ ๋จ์ ํ๋ ๋ธ๋ก: ํด์ 1.5 / ํ๊ตฐ 4.5 / ๊ณ ๊ฐ๋ 7 MET\n",
" d = int(rng.integers(10, 21)); act[t:t + d] = rng.choice([1.5, 4.5, 7.0], p=[0.25, 0.5, 0.25]); t += d\n",
" hi = env[\"temp_c\"] + (0.12 * (env[\"rh\"] - 40) if env[\"temp_c\"] > 25 else 0)\n",
" wc = env[\"temp_c\"] - 1.6 * env[\"wind\"]\n",
" core, cores = 36.9, []\n",
" for m in range(N_MIN):\n",
" heat = 0.085 * max(0, hi - 24) * (1 - 0.25 * p[\"acclim\"]) * (1 + 0.5 * m / N_MIN) * (1 + 0.012 * (p[\"bmi\"] - 23.5))\n",
" cold = -0.075 * max(0, 8 - wc) * (1 - 0.09 * act[m])\n",
" ss = 36.9 + 0.09 * act[m] + heat + cold\n",
" core += (ss - core) / 18 + rng.normal(0, 0.03)\n",
" cores.append(core)\n",
" core = np.array(cores)\n",
" hr = p[\"rest_hr\"] + 9.5 * (act - 1.5) * (1.15 - 0.3 * p[\"fit\"]) + 11 * (core - 36.9) + rng.normal(0, 3.2, N_MIN)\n",
" eda = 2.0 + 0.25 * act + 3.2 * np.maximum(core - 37.2, 0) - 0.04 * np.maximum(8 - wc, 0) + rng.normal(0, 0.25, N_MIN)\n",
" bias = rng.normal(0, 0.1)\n",
" temp = core + bias + rng.normal(0, 0.1, N_MIN) - np.where((kind == \"cold\") & (env[\"wind\"] > 7), 0.25, 0.0) # ์ผ์ ์ ์ด ๋ถ์ ๋ฐ๋ ๋๊ฐ ์ค์ฐจ\n",
" df = pd.DataFrame({\"min\": np.arange(N_MIN), \"hr\": hr, \"temp\": temp, \"eda\": np.maximum(eda, 0.2), \"act\": act})\n",
" if glitch:\n",
" for col in (\"hr\", \"temp\", \"eda\"):\n",
" df.loc[rng.random(N_MIN) < 0.03, col] = np.nan # ๊ฒฐ์ธก\n",
" s = int(rng.integers(5, N_MIN))\n",
" if rng.random() < 0.12:\n",
" df.loc[s, \"hr\"] = 230.0 # ์ผ์ ํ\n",
" if rng.random() < 0.12:\n",
" df.loc[s, \"temp\"] = 45.0\n",
" ht = np.where(core >= 39.0, 2, np.where(core >= 38.3, 1, 0)); cd = np.where(core <= 35.4, 2, np.where(core <= 36.2, 1, 0))\n",
" df[\"core\"] = core\n",
" df[\"stage\"] = np.maximum(ht, cd); df[\"rtype\"] = np.where(ht > 0, \"heat\", np.where(cd > 0, \"cold\", \"normal\"))\n",
" return {\"id\": p[\"id\"], \"person\": p, \"env\": env, \"kind\": kind, \"df\": df}\n",
"\n",
"\n",
"def make_dataset(n, seed):\n",
" rng = np.random.default_rng(seed)\n",
" kinds = [\"normal\", \"heat\", \"cold\"]\n",
" return [make_session(i, kinds[i % 3], rng) for i in range(n)]\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---------------- ์ ์ฒ๋ฆฌยทํน์ง ์ถ์ถ ๊ณตํต ํจ์ ----------------\n",
"LIMITS = {\"hr\": (30, 220), \"temp\": (30.0, 43.0), \"eda\": (0.0, 40.0)} # ์๋ฆฌ์ ์ผ๋ก ๊ฐ๋ฅํ ๋ฒ์(๋ฒ์ ๋ฐ = ์ผ์ ์ค๋ฅ)\n",
"\n",
"\n",
"def clean(df):\n",
" \"\"\"๋ฒ์ ๋ฐ ๊ฐ์ ๊ฒฐ์ธก ์ฒ๋ฆฌ -> ์ ํ ๋ณด๊ฐ(์ ๋์ ๊ฐ๊น์ด ๊ฐ) -> 3๋ถ ์ด๋ ์ค์๊ฐ์ผ๋ก ํ ์ ๊ฑฐ. ๋ฐํ: (์ ์ df, ๋ณด์ ๊ฑด์)\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 4] ๊ธฐ์ ๋ฐ์ดํฐ(์จยท์ต๋, ํ์) ๊ฒฐํฉ ์จ์ด/ํ๋ญ ์คํธ๋ ์ค ๋ณตํฉ ์ํ๋ ์์ธก ๋ชจ๋ธ ๊ณ ๋ํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def env_index(env):\n",
" # [TODO : ํ์ ์ค์ต 4] ๊ธฐ์ ๋ฐ์ดํฐ(์จยท์ต๋, ํ์) ๊ฒฐํฉ ์จ์ด/ํ๋ญ ์คํธ๋ ์ค ๋ณตํฉ ์ํ๋ ์์ธก ๋ชจ๋ธ ๊ณ ๋ํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def features(s, use_env=True, personal=True):\n",
" \"\"\"์ธ์
1๊ฐ -> ๋ถ ๋จ์ ํน์งํ(์ต๊ทผ 5๋ถ ํ๊ท ยท๊ธฐ์ธ๊ธฐ ํฌํจ). ์ ๋ต stage/rtype ํฌํจ.\"\"\"\n",
" df, _ = clean(s[\"df\"]); p, e = s[\"person\"], s[\"env\"]\n",
" f = pd.DataFrame({\"sid\": s[\"id\"], \"min\": df[\"min\"]})\n",
" f[\"temp5\"] = df[\"temp\"].rolling(5, min_periods=1).mean()\n",
" f[\"hr5\"] = df[\"hr\"].rolling(5, min_periods=1).mean()\n",
" f[\"eda5\"] = df[\"eda\"].rolling(5, min_periods=1).mean()\n",
" f[\"dtemp\"] = (f[\"temp5\"] - f[\"temp5\"].shift(4)).fillna(0)\n",
" f[\"act\"] = df[\"act\"]\n",
" if personal:\n",
" f[\"hr_rel\"] = f[\"hr5\"] - p[\"rest_hr\"]\n",
" f[\"acclim\"], f[\"bmi\"], f[\"fit\"] = p[\"acclim\"], p[\"bmi\"], p[\"fit\"]\n",
" if use_env:\n",
" f[\"hi\"], f[\"wc\"] = env_index(e); f[\"wind\"] = e[\"wind\"]\n",
" f[\"stage\"], f[\"rtype\"], f[\"kind\"] = df[\"stage\"], df[\"rtype\"], s[\"kind\"]\n",
" return f[f[\"min\"] >= 5].reset_index(drop=True)\n",
"\n",
"\n",
"def table(sessions, **kw):\n",
" return pd.concat([features(s, **kw) for s in sessions], ignore_index=True)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"ADV_FEATS = [\"temp5\", \"hr5\", \"eda5\", \"dtemp\", \"act\", \"hr_rel\", \"acclim\", \"bmi\", \"fit\", \"hi\", \"wc\", \"wind\"]\n",
"LABEL = {\"normal\": 0, \"heat\": 1, \"cold\": 2}\n",
"\n",
"\n",
"def baseline_pred(t):\n",
" return np.where((t[\"temp5\"] >= 38.0) & (t[\"hr5\"] >= 95), \"heat\", np.where(t[\"temp5\"] <= 36.3, \"cold\", \"normal\"))\n",
"\n",
"\n",
"train = table(make_dataset(150, seed=31))\n",
"clf = GradientBoostingClassifier(n_estimators=80, max_depth=3, learning_rate=0.1, random_state=0).fit(train[ADV_FEATS], train[\"rtype\"].map(LABEL))\n",
"inv = {v: k for k, v in LABEL.items()}\n",
"\n",
"# ์ํ์
2์ข
: ์ผ๋ฐ / ๋ณตํฉ ๊ต๋(๊ฐํ ํนํ + ๊ณ ๊ฐ๋ ํ๋ + ์ผ์ ํ ๋ง์)\n",
"std = make_dataset(90, seed=77)\n",
"rng = np.random.default_rng(9)\n",
"hard = []\n",
"for i in range(60):\n",
" s = make_session(i, [\"heat\", \"cold\"][i % 2], rng)\n",
" if s[\"kind\"] == \"cold\":\n",
" s[\"env\"][\"wind\"] = float(rng.uniform(7, 11))\n",
" for col, val in ((\"hr\", 230.0), (\"temp\", 45.0)):\n",
" idx = rng.choice(np.arange(5, N_MIN), 3, replace=False); s[\"df\"].loc[idx, col] = val\n",
" hard.append(s)\n",
"\n",
"rows = []\n",
"for name, sess in ((\"์ผ๋ฐ\", std), (\"๋ณตํฉ ๊ต๋\", hard)):\n",
" t = table(sess)\n",
" pb = baseline_pred(t)\n",
" t0 = time.perf_counter(); pa = np.array([inv[v] for v in clf.predict(t[ADV_FEATS])]); ms = (time.perf_counter() - t0) * 1000 / len(t)\n",
" risk = t[\"stage\"] >= 1; danger = t[\"stage\"] == 2\n",
" rows.append({\"์ํ์
\": name, \"๊ตฌ๊ฐ ์\": len(t), \"Baseline ์ํ ์ฌํ์จ\": round(float((pb[risk] == t[\"rtype\"][risk]).mean()), 3),\n",
" \"๊ณ ๋ํ ์ํ ์ฌํ์จ\": round(float((pa[risk] == t[\"rtype\"][risk]).mean()), 3),\n",
" \"๊ณ ๋ํ ๊ฒฝ๋ณด(2๋จ๊ณ) ์ฌํ์จ\": round(float((pa[danger] == t[\"rtype\"][danger]).mean()), 3),\n",
" \"Baseline ์ค๊ฒฝ๋ณด\": int(((pb != \"normal\") & ~risk).sum()), \"๊ณ ๋ํ ์ค๊ฒฝ๋ณด\": int(((pa != \"normal\") & ~risk).sum()), \"์ง์ฐ(ms/๊ตฌ๊ฐ)\": round(ms, 3)})\n",
"out = pd.DataFrame(rows)\n",
"print(out.T.to_string(header=False))\n",
"rec = out[\"๊ณ ๋ํ ์ํ ์ฌํ์จ\"].min(); lat = out[\"์ง์ฐ(ms/๊ตฌ๊ฐ)\"].max()\n",
"imp = pd.Series(clf.feature_importances_, index=ADV_FEATS).sort_values(ascending=False)\n",
"print(\"\\nํน์ง ์ค์๋ ์์:\", \", \".join(f\"{k} {v:.2f}\" for k, v in imp.head(5).items()))\n",
"print(f\"\\n๋ณตํฉ ์ํ ํ์ง ์ฌํ์จ(์ต์ ์ํ์
) {rec:.1%} (๋ชฉํ 85% ์ด์) -> {'๋ฌ์ฑ' if rec >= 0.85 else '๋ฏธ๋ฌ'}\")\n",
"print(f\"์ฒ๋ฆฌ ์ง์ฐ ์ต๋ {lat:.3f} ms/๊ตฌ๊ฐ (๋ชฉํ 200 ms ์ดํ) -> {'๋ฌ์ฑ' if lat <= 200 else '๋ฏธ๋ฌ'}\")\n",
"\n",
"\n",
"# ---- ์์ธ(์ผ์ ์ด์ยท๊ธฐ๋ง) ํ์ง 10์ข
: ๋ชจ๋ธ์ ๋ฃ๊ธฐ ์ ์ ํธ ์์ฒด์ ์ ๋ขฐ์ฑ์ ์ ๊ฒํ๋ค ----\n",
"def anomaly(w):\n",
" \"\"\"์ต๊ทผ 10๋ถ ์์ ์ ํธ ์ฐฝ(hr, temp, eda ๋ฐฐ์ด dict)์ ์ด์ ์ ํ ๋ชฉ๋ก\"\"\"\n",
" flags = []\n",
" for c in (\"hr\", \"temp\", \"eda\"):\n",
" v = w[c]\n",
" if np.isnan(v).all():\n",
" flags.append(f\"{c}_missing_all\")\n",
" else:\n",
" lo, hi = LIMITS[c]\n",
" if ((v < lo) | (v > hi)).any():\n",
" flags.append(f\"{c}_out_of_range\")\n",
" if np.isnan(v).mean() > 0.3:\n",
" flags.append(f\"{c}_many_missing\")\n",
" ok = v[~np.isnan(v)]\n",
" if len(ok) >= 8 and np.ptp(ok) < 1e-6:\n",
" flags.append(f\"{c}_flatline\")\n",
" if len(ok) >= 3 and np.abs(np.diff(ok)).max() > {\"hr\": 60, \"temp\": 1.5, \"eda\": 10}[c]:\n",
" flags.append(f\"{c}_jump\")\n",
" if \"temp_out_of_range\" not in flags and \"hr_out_of_range\" not in flags and not np.isnan(w[\"temp\"]).all() and not np.isnan(w[\"hr\"]).all():\n",
" if np.nanmean(w[\"temp\"]) >= 38.5 and np.nanmean(w[\"hr\"]) < 60:\n",
" flags.append(\"temp_hr_inconsistent\")\n",
" return flags\n",
"\n",
"\n",
"def win(hr=80, temp=37.0, eda=2.0, n=10):\n",
" return {\"hr\": np.full(n, float(hr)) + np.arange(n) % 3, \"temp\": np.full(n, float(temp)) + np.arange(n) * 0.01, \"eda\": np.full(n, float(eda)) + np.arange(n) % 2 * 0.1}\n",
"\n",
"\n",
"cases = []\n",
"w = win(); cases.append((\"1 ์ ์\", w, []))\n",
"w = win(); w[\"hr\"][4] = 230; cases.append((\"2 ์ฌ๋ฐ ํ\", w, [\"hr_out_of_range\", \"hr_jump\"]))\n",
"w = win(); w[\"temp\"][:] = 37.0; cases.append((\"3 ์ฒด์จ ๊ณ ์ฐฉ\", w, [\"temp_flatline\"]))\n",
"w = win(); w[\"temp\"][:6] = np.nan; cases.append((\"4 ์ฒด์จ ๊ฒฐ์ธก ๋ค์\", w, [\"temp_many_missing\"]))\n",
"w = win(); w[\"hr\"][:] = np.nan; cases.append((\"5 ์ฌ๋ฐ ์ ์ฒด ๋์ \", w, [\"hr_missing_all\"]))\n",
"w = win(); w[\"temp\"][3] = 45.0; cases.append((\"6 ์ฒด์จ ๋ฒ์ ์ดํ\", w, [\"temp_out_of_range\", \"temp_jump\"]))\n",
"w = win(temp=39.0, hr=55); cases.append((\"7 ์ฒด์จ-์ฌ๋ฐ ๋ถ์ผ์น\", w, [\"temp_hr_inconsistent\"]))\n",
"w = win(); w[\"eda\"][:] = 2.0; cases.append((\"8 ํผ๋ถ์ ๋ ๊ณ ์ฐฉ\", w, [\"eda_flatline\"]))\n",
"w = win(); w[\"temp\"][5:] += 2.0; cases.append((\"9 ์ฒด์จ ๊ธ๋ณ\", w, [\"temp_jump\"]))\n",
"w = win(); w[\"hr\"][:] = 80.0; w[\"temp\"][:] = 37.0; cases.append((\"10 ์ฌ๋ฐยท์ฒด์จ ๋์ ๊ณ ์ฐฉ\", w, [\"hr_flatline\", \"temp_flatline\"]))\n",
"hit = 0\n",
"print(\"\\n[์ผ์ ์ด์ ํ์ง ์ํ 10์ข
]\")\n",
"for name, w, expect in cases:\n",
" got = anomaly(w); good = all(e in got for e in expect) and (bool(got) == bool(expect)); hit += good\n",
" print(f\"[{'PASS' if good else 'FAIL'}] {name}: {', '.join(got) or '์ด์ ์์'}\")\n",
"print(f\"ํต๊ณผ {hit}/10. ์ด์ ํ๋๊ทธ๊ฐ ์์ผ๋ฉด ๋ชจ๋ธ ๊ฒฝ๋ณด๋ณด๋ค ๋จผ์ '์ผ์ ์ ๊ฒ ํ์'๋ฅผ ํ์ํ๊ณ , ํ๋จ์ ์๋์ฅยท์๋ฌด๊ด ํ์ธ์ผ๋ก ๋๊ธด๋ค(๊ธฐ๋งยท๊ต๋ ์ ํธ๋ก ์ธํ ์คํ ๋ฐฉ์ง).\")\n",
"print(\"๋ณธ ๊ฒฐ๊ณผ๋ ๊ต์ก์ฉ ๊ฐ์ ์๋ฃ ๊ธฐ์ค์ด๋ฉฐ ์ง๋จ์ด ์๋๋ค. ์ค์ ์ด์ ์งํ๋ ์๋ฌด๋ยท๊ตฐ์๊ด์๊ฒ ์ฐ๊ณํ๋ค.\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
04 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"์ํ์
์ผ๋ฐ ๋ณตํฉ ๊ต๋\n",
"๊ตฌ๊ฐ ์ 4950 3300\n",
"Baseline ์ํ ์ฌํ์จ 0.879 0.916\n",
"๊ณ ๋ํ ์ํ ์ฌํ์จ 0.929 0.934\n",
"๊ณ ๋ํ ๊ฒฝ๋ณด(2๋จ๊ณ) ์ฌํ์จ 1.0 1.0\n",
"Baseline ์ค๊ฒฝ๋ณด 446 422\n",
"๊ณ ๋ํ ์ค๊ฒฝ๋ณด 108 98\n",
"์ง์ฐ(ms/๊ตฌ๊ฐ) 0.004 0.004\n",
"\n",
"ํน์ง ์ค์๋ ์์: temp5 0.48, eda5 0.42, hr_rel 0.05, hr5 0.01, fit 0.01\n",
"\n",
"๋ณตํฉ ์ํ ํ์ง ์ฌํ์จ(์ต์ ์ํ์
) 92.9% (๋ชฉํ 85% ์ด์) -> ๋ฌ์ฑ\n",
"์ฒ๋ฆฌ ์ง์ฐ ์ต๋ 0.004 ms/๊ตฌ๊ฐ (๋ชฉํ 200 ms ์ดํ) -> ๋ฌ์ฑ\n",
"\n",
"[์ผ์ ์ด์ ํ์ง ์ํ 10์ข
]\n",
"[PASS] 1 ์ ์: ์ด์ ์์\n",
"[PASS] 2 ์ฌ๋ฐ ํ: hr_out_of_range, hr_jump\n",
"[PASS] 3 ์ฒด์จ ๊ณ ์ฐฉ: temp_flatline\n",
"[PASS] 4 ์ฒด์จ ๊ฒฐ์ธก ๋ค์: temp_many_missing\n",
"[PASS] 5 ์ฌ๋ฐ ์ ์ฒด ๋์ : hr_missing_all\n",
"[PASS] 6 ์ฒด์จ ๋ฒ์ ์ดํ: temp_out_of_range, temp_jump\n",
"[PASS] 7 ์ฒด์จ-์ฌ๋ฐ ๋ถ์ผ์น: temp_hr_inconsistent\n",
"[PASS] 8 ํผ๋ถ์ ๋ ๊ณ ์ฐฉ: eda_flatline\n",
"[PASS] 9 ์ฒด์จ ๊ธ๋ณ: temp_jump\n",
"[PASS] 10 ์ฌ๋ฐยท์ฒด์จ ๋์ ๊ณ ์ฐฉ: hr_flatline, temp_flatline\n",
"ํต๊ณผ 10/10. ์ด์ ํ๋๊ทธ๊ฐ ์์ผ๋ฉด ๋ชจ๋ธ ๊ฒฝ๋ณด๋ณด๋ค ๋จผ์ '์ผ์ ์ ๊ฒ ํ์'๋ฅผ ํ์ํ๊ณ , ํ๋จ์ ์๋์ฅยท์๋ฌด๊ด ํ์ธ์ผ๋ก ๋๊ธด๋ค(๊ธฐ๋งยท๊ต๋ ์ ํธ๋ก ์ธํ ์คํ ๋ฐฉ์ง).\n",
"๋ณธ ๊ฒฐ๊ณผ๋ ๊ต์ก์ฉ ๊ฐ์ ์๋ฃ ๊ธฐ์ค์ด๋ฉฐ ์ง๋จ์ด ์๋๋ค. ์ค์ ์ด์ ์งํ๋ ์๋ฌด๋ยท๊ตฐ์๊ด์๊ฒ ์ฐ๊ณํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
05. ์ ํฌ์ ์ ์ฒด ์กฐ๊ฑด๋ณ ๊ฐ์ธํ ์ํ ์ง์ ์ฐ์ถ ๋ฐ ์ค๊ฒฝ๋ณด ๋ฐฉ์ง ์ ๋ขฐ๋ ์์ง ๊ฐ๋ฐ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๊ฐ์ธํ ์ํ ์ง์ ์ฐ์ถ ๋ฐ ์ค๊ฒฝ๋ณด ๋ฐฉ์ง ์ ๋ขฐ๋ ์์ง์ ๊ฐ๋ฐํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋ณ์ฌ ๊ฐ์ธ์ ์ฒด๋ ฅยท์์ ์ฌ๋ถ๋ฅผ ๋ฐ์ํ ๊ฐ์ธํ ์ํ ์ง์๋ฅผ ์ฐ์ถํ๊ณ ์ค๊ฒฝ๋ณด๋ฅผ ์ค์ด๋ ์ ๋ขฐ๋ ์์ง์ ๊ฐ๋ฐํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **ํ๋ ๊ฐ๋์ ๊ฐ์ธ ์ฒด๋ ฅ์ผ๋ก ์์๋๋ ์ฌ๋ฐ์ ๋นผ '์ค๋ช
๋์ง ์๋ ์ฌ๋ฐ ์์น'๊ณผ ์ฒด์จ ํธ์ฐจ๋ฅผ ๋ง๋ค๊ณ , ๋ก์ง์คํฑ ํ๊ท๋ก ์ํ ํ๋ฅ ์ ๊ณ์ฐํด 0.8 ์ด์ High, 0.5 ์ด์ Medium, ๊ทธ ๋ฏธ๋ง Low ๋ฑ๊ธ์ ๋ถ์ฌ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/verification/wel01_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": [
"\"\"\"๊ฐ์ธ๋ณ ์์ ์ ์ฌ๋ฐยท์ฒด๊ฒฉยท์์ ์ฌ๋ถ๋ฅผ ๋ฐ์ํด ํ๋ ๊ฐ๋๋ก ์ค๋ช
๋๋ ์ฌ๋ฐ ์์น์ ๋ณด์ ํ ๊ฐ์ธํ ์ํ ์ง์๋ฅผ ๊ณ์ฐํ๊ณ , ์ค๊ฒฝ๋ณด๋ฅผ ์ค์ด๋ ์ ๋ขฐ๋ ์ ์์ ๋ฑ๊ธ(High/Medium/Low)์ ์ฐ์ถํด ๊ณ ์ ์๊ณ๊ฐ ๋ฐฉ์ ๋๋น ์ค๊ฒฝ๋ณด์จ(FAR) ๊ฐ์ถ๊ณผ ์ ์ ์ผ๊ด์ฑ์ ๊ฒ์ฆํ๋ค. ์ง๋จ ์๋ยท์๋ฌด๋ ์ฐ๊ณ.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.linear_model import LogisticRegression\n",
"from sklearn.metrics import roc_auc_score, roc_curve\n",
"from sklearn.pipeline import make_pipeline\n",
"from sklearn.preprocessing import StandardScaler\n",
"\n",
"# ---------------- [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋๋ฏธ ์์ฒด์ ํธ ์์ฑ๊ธฐ (์ค์ ์ฅ๋ณยท๋ถ๋ยท์๋ฌด ๊ธฐ๋ก ์์) ----------------\n",
"# ๊ฐ์ ๋ณ์ฌ(๊ฐ๋ช
ID)๊ฐ 60๋ถ๊ฐ ๊ฑท๊ธฐยทํด์ยท๊ณ ๊ฐ๋ ํ๋์ ํ๋ ๋์์ ์ฌ๋ฐ์ยท์ฒด์จ(์ถ์ ์ฌ๋ถ)ยทํผ๋ถ์ ๋๋๋ฅผ 1๋ถ ๊ฐ๊ฒฉ์ผ๋ก ๋ง๋ ๋ค.\n",
"# ์ง๋จ ๋๊ตฌ๊ฐ ์๋๋ฉฐ, ์ํ ๋ผ๋ฒจ์ ๊ฐ์ ์๋ฆฌ ๋ชจ๋ธ์ '๊ต์ก์ฉ ๋จ๊ณ'์ผ ๋ฟ์ด๋ค.\n",
"N_MIN = 60\n",
"STAGE = {0: \"์ ์\", 1: \"์ฃผ์\", 2: \"๊ฒฝ๋ณด\"}\n",
"TYPES = [\"normal\", \"heat\", \"cold\"]\n",
"\n",
"\n",
"def make_person(i, rng):\n",
" return {\"id\": f\"S{i:03d}\", \"rest_hr\": float(rng.normal(64, 7)), \"age\": int(rng.integers(19, 27)), \"bmi\": float(rng.normal(23.5, 2.5)),\n",
" \"acclim\": int(rng.random() < 0.4), \"fit\": float(np.clip(rng.normal(0.5, 0.2), 0.05, 0.95))}\n",
"\n",
"\n",
"def make_env(kind, rng):\n",
" if kind == \"heat\":\n",
" return {\"temp_c\": float(rng.uniform(30, 40)), \"rh\": float(rng.uniform(45, 85)), \"wind\": float(rng.uniform(0.3, 3))}\n",
" if kind == \"cold\":\n",
" return {\"temp_c\": float(rng.uniform(-22, 2)), \"rh\": float(rng.uniform(40, 80)), \"wind\": float(rng.uniform(2, 11))}\n",
" return {\"temp_c\": float(rng.uniform(8, 24)), \"rh\": float(rng.uniform(35, 70)), \"wind\": float(rng.uniform(0.3, 5))}\n",
"\n",
"\n",
"def make_session(sid, kind, rng, p=None, glitch=True):\n",
" \"\"\"๋ฐํ: dict(person, env, kind, df[hr,temp,eda,act + ์ ๋ต coreยทstageยทrtype]). ์ผ์ ๊ฒฐ์ธกยทํยท๊ณ ์ฐฉ ํฌํจ.\"\"\"\n",
" p = p or make_person(sid, rng)\n",
" env = make_env(kind, rng)\n",
" act = np.zeros(N_MIN); t = 0\n",
" while t < N_MIN: # 10~20๋ถ ๋จ์ ํ๋ ๋ธ๋ก: ํด์ 1.5 / ํ๊ตฐ 4.5 / ๊ณ ๊ฐ๋ 7 MET\n",
" d = int(rng.integers(10, 21)); act[t:t + d] = rng.choice([1.5, 4.5, 7.0], p=[0.25, 0.5, 0.25]); t += d\n",
" hi = env[\"temp_c\"] + (0.12 * (env[\"rh\"] - 40) if env[\"temp_c\"] > 25 else 0)\n",
" wc = env[\"temp_c\"] - 1.6 * env[\"wind\"]\n",
" core, cores = 36.9, []\n",
" for m in range(N_MIN):\n",
" heat = 0.085 * max(0, hi - 24) * (1 - 0.25 * p[\"acclim\"]) * (1 + 0.5 * m / N_MIN) * (1 + 0.012 * (p[\"bmi\"] - 23.5))\n",
" cold = -0.075 * max(0, 8 - wc) * (1 - 0.09 * act[m])\n",
" ss = 36.9 + 0.09 * act[m] + heat + cold\n",
" core += (ss - core) / 18 + rng.normal(0, 0.03)\n",
" cores.append(core)\n",
" core = np.array(cores)\n",
" hr = p[\"rest_hr\"] + 9.5 * (act - 1.5) * (1.15 - 0.3 * p[\"fit\"]) + 11 * (core - 36.9) + rng.normal(0, 3.2, N_MIN)\n",
" eda = 2.0 + 0.25 * act + 3.2 * np.maximum(core - 37.2, 0) - 0.04 * np.maximum(8 - wc, 0) + rng.normal(0, 0.25, N_MIN)\n",
" bias = rng.normal(0, 0.1)\n",
" temp = core + bias + rng.normal(0, 0.1, N_MIN) - np.where((kind == \"cold\") & (env[\"wind\"] > 7), 0.25, 0.0) # ์ผ์ ์ ์ด ๋ถ์ ๋ฐ๋ ๋๊ฐ ์ค์ฐจ\n",
" df = pd.DataFrame({\"min\": np.arange(N_MIN), \"hr\": hr, \"temp\": temp, \"eda\": np.maximum(eda, 0.2), \"act\": act})\n",
" if glitch:\n",
" for col in (\"hr\", \"temp\", \"eda\"):\n",
" df.loc[rng.random(N_MIN) < 0.03, col] = np.nan # ๊ฒฐ์ธก\n",
" s = int(rng.integers(5, N_MIN))\n",
" if rng.random() < 0.12:\n",
" df.loc[s, \"hr\"] = 230.0 # ์ผ์ ํ\n",
" if rng.random() < 0.12:\n",
" df.loc[s, \"temp\"] = 45.0\n",
" ht = np.where(core >= 39.0, 2, np.where(core >= 38.3, 1, 0)); cd = np.where(core <= 35.4, 2, np.where(core <= 36.2, 1, 0))\n",
" df[\"core\"] = core\n",
" df[\"stage\"] = np.maximum(ht, cd); df[\"rtype\"] = np.where(ht > 0, \"heat\", np.where(cd > 0, \"cold\", \"normal\"))\n",
" return {\"id\": p[\"id\"], \"person\": p, \"env\": env, \"kind\": kind, \"df\": df}\n",
"\n",
"\n",
"def make_dataset(n, seed):\n",
" rng = np.random.default_rng(seed)\n",
" kinds = [\"normal\", \"heat\", \"cold\"]\n",
" return [make_session(i, kinds[i % 3], rng) for i in range(n)]\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---------------- ์ ์ฒ๋ฆฌยทํน์ง ์ถ์ถ ๊ณตํต ํจ์ ----------------\n",
"LIMITS = {\"hr\": (30, 220), \"temp\": (30.0, 43.0), \"eda\": (0.0, 40.0)} # ์๋ฆฌ์ ์ผ๋ก ๊ฐ๋ฅํ ๋ฒ์(๋ฒ์ ๋ฐ = ์ผ์ ์ค๋ฅ)\n",
"\n",
"\n",
"def clean(df):\n",
" \"\"\"๋ฒ์ ๋ฐ ๊ฐ์ ๊ฒฐ์ธก ์ฒ๋ฆฌ -> ์ ํ ๋ณด๊ฐ(์ ๋์ ๊ฐ๊น์ด ๊ฐ) -> 3๋ถ ์ด๋ ์ค์๊ฐ์ผ๋ก ํ ์ ๊ฑฐ. ๋ฐํ: (์ ์ df, ๋ณด์ ๊ฑด์)\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 5] ์ ํฌ์ ์ ์ฒด ์กฐ๊ฑด๋ณ ๊ฐ์ธํ ์ํ ์ง์ ์ฐ์ถ ๋ฐ ์ค๊ฒฝ๋ณด ๋ฐฉ์ง ์ ๋ขฐ๋ ์์ง ๊ฐ๋ฐ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def env_index(env):\n",
" # [TODO : ํ์ ์ค์ต 5] ์ ํฌ์ ์ ์ฒด ์กฐ๊ฑด๋ณ ๊ฐ์ธํ ์ํ ์ง์ ์ฐ์ถ ๋ฐ ์ค๊ฒฝ๋ณด ๋ฐฉ์ง ์ ๋ขฐ๋ ์์ง ๊ฐ๋ฐ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def features(s, use_env=True, personal=True):\n",
" \"\"\"์ธ์
1๊ฐ -> ๋ถ ๋จ์ ํน์งํ(์ต๊ทผ 5๋ถ ํ๊ท ยท๊ธฐ์ธ๊ธฐ ํฌํจ). ์ ๋ต stage/rtype ํฌํจ.\"\"\"\n",
" df, _ = clean(s[\"df\"]); p, e = s[\"person\"], s[\"env\"]\n",
" f = pd.DataFrame({\"sid\": s[\"id\"], \"min\": df[\"min\"]})\n",
" f[\"temp5\"] = df[\"temp\"].rolling(5, min_periods=1).mean()\n",
" f[\"hr5\"] = df[\"hr\"].rolling(5, min_periods=1).mean()\n",
" f[\"eda5\"] = df[\"eda\"].rolling(5, min_periods=1).mean()\n",
" f[\"dtemp\"] = (f[\"temp5\"] - f[\"temp5\"].shift(4)).fillna(0)\n",
" f[\"act\"] = df[\"act\"]\n",
" if personal:\n",
" f[\"hr_rel\"] = f[\"hr5\"] - p[\"rest_hr\"]\n",
" f[\"acclim\"], f[\"bmi\"], f[\"fit\"] = p[\"acclim\"], p[\"bmi\"], p[\"fit\"]\n",
" if use_env:\n",
" f[\"hi\"], f[\"wc\"] = env_index(e); f[\"wind\"] = e[\"wind\"]\n",
" f[\"stage\"], f[\"rtype\"], f[\"kind\"] = df[\"stage\"], df[\"rtype\"], s[\"kind\"]\n",
" return f[f[\"min\"] >= 5].reset_index(drop=True)\n",
"\n",
"\n",
"def table(sessions, **kw):\n",
" return pd.concat([features(s, **kw) for s in sessions], ignore_index=True)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"PF = [\"temp5\", \"dtemp\", \"hr_excess\", \"eda5\", \"hi\", \"wc\", \"acclim\", \"bmi\", \"tdev\"]\n",
"\n",
"\n",
"def personalize(t):\n",
" \"\"\"ํ๋ ๊ฐ๋ยท๊ฐ์ธ ์ฒด๋ ฅ์ผ๋ก ์์๋๋ ์ฌ๋ฐ์ ๋นผ '์ค๋ช
๋์ง ์๋ ์ฌ๋ฐ ์์น(hr_excess)'๊ณผ ์ฒด์จ ํธ์ฐจ(tdev)๋ฅผ ๋ง๋ ๋ค.\"\"\"\n",
" t = t.copy()\n",
" expected = t[\"hr5\"] - t[\"hr_rel\"] + 9.5 * (t[\"act\"].rolling(5, min_periods=1).mean() - 1.5) * (1.15 - 0.3 * t[\"fit\"])\n",
" t[\"hr_excess\"] = t[\"hr5\"] - expected\n",
" t[\"tdev\"] = (t[\"temp5\"] - 36.9).abs()\n",
" return t\n",
"\n",
"\n",
"def grade(p):\n",
" return \"High\" if p >= 0.8 else (\"Medium\" if p >= 0.5 else \"Low\")\n",
"\n",
"\n",
"train = personalize(table(make_dataset(180, seed=41)))\n",
"test = personalize(table(make_dataset(90, seed=43)))\n",
"model = make_pipeline(StandardScaler(), LogisticRegression(class_weight=\"balanced\", C=1.0, max_iter=3000, random_state=0))\n",
"model.fit(train[PF], train[\"stage\"] >= 1)\n",
"test[\"score\"] = model.predict_proba(test[PF])[:, 1]\n",
"test[\"grade\"] = test[\"score\"].map(grade)\n",
"true = (test[\"stage\"] >= 1).values\n",
"\n",
"fixed = ((test[\"temp5\"] >= 37.8) | (test[\"temp5\"] <= 36.5) | (test[\"hr5\"] >= 130)).values # ๊ธฐ์กด: ๊ณ ์ ์๊ณ๊ฐ ๊ฒฝ๋ณด\n",
"eng = (test[\"score\"] >= 0.5).values\n",
"\n",
"\n",
"def far_rec(alert):\n",
" return (alert & ~true).sum() / max(alert.sum(), 1), (alert & true).sum() / true.sum()\n",
"\n",
"\n",
"f0, r0 = far_rec(fixed); f1, r1 = far_rec(eng)\n",
"print(pd.DataFrame([{\"๋ฐฉ์\": \"๊ณ ์ ์๊ณ๊ฐ(์ฒด์จ 37.8 ์ด์ยท36.5 ์ดํยท์ฌ๋ฐ 130 ์ด์)\", \"๊ฒฝ๋ณด ๊ตฌ๊ฐ\": int(fixed.sum()), \"์ค๊ฒฝ๋ณด์จ\": round(f0, 3), \"์ํ ์ฌํ์จ\": round(r0, 3)},\n",
" {\"๋ฐฉ์\": \"๊ฐ์ธํ ์ ๋ขฐ๋ ์์ง(0.5 ์ด์)\", \"๊ฒฝ๋ณด ๊ตฌ๊ฐ\": int(eng.sum()), \"์ค๊ฒฝ๋ณด์จ\": round(f1, 3), \"์ํ ์ฌํ์จ\": round(r1, 3)}]).to_string(index=False))\n",
"cut = 1 - f1 / f0\n",
"print(f\"\\n์ค๊ฒฝ๋ณด์จ ๊ฐ์ถ {cut:.1%} (๋ชฉํ 40% ์ด์: {'๋ฌ์ฑ' if cut >= 0.4 else '๋ฏธ๋ฌ'}), ROC-AUC {roc_auc_score(true, test['score']):.3f}\")\n",
"\n",
"# ์ ์ ์ผ๊ด์ฑ: ์ผ์ ๊ฐ์ +-2% ์ธก์ ํ๋ค๋ฆผ์ ์ฃผ์ด๋ ๊ฐ์ ๋ฑ๊ธ์ด ์ ์ง๋๋ ๋น์จ\n",
"rng = np.random.default_rng(0)\n",
"X = test[PF].values; g0 = test[\"grade\"].values\n",
"same = [np.mean(np.array([grade(v) for v in model.predict_proba(pd.DataFrame(X * (1 + rng.normal(0, 0.02, X.shape)), columns=PF))[:, 1]]) == g0) for _ in range(10)]\n",
"print(f\"์ ๋ขฐ๋ ๋ฑ๊ธ ์ผ๊ด์ฑ {np.mean(same):.1%} (๊ฐ์ ์
๋ ฅ ์ฌ์คํ 100%, ๋ชฉํ 90% ์ด์: {'๋ฌ์ฑ' if np.mean(same) >= 0.9 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"print(\"\\n[๋จ๊ณ๋ณ ๋ฑ๊ธ ๋ถํฌ] (ํ: ์ ๋ต ๋จ๊ณ)\")\n",
"print(pd.crosstab(test[\"stage\"].map(STAGE).rename(\"์ ๋ต ๋จ๊ณ\"), test[\"grade\"].rename(\"๋ฑ๊ธ\")).to_string())\n",
"\n",
"# ๊ฐ์ธํ ํจ๊ณผ: ๊ฐ์ ์ฌ๋ฐยท์ฒด์จ์ด๋ผ๋ ์ฒด๋ ฅยท์์ ์ฌ๋ถ์ ๋ฐ๋ผ ์ง์๊ฐ ๋ฌ๋ผ์ง๋ ์\n",
"ex = test.iloc[[0]].copy()\n",
"rows = []\n",
"for name, fit, acc in ((\"์ฒด๋ ฅ ๋ฎ์ยท๋น์์\", 0.2, 0), (\"์ฒด๋ ฅ ๋์ยท์์\", 0.8, 1)):\n",
" e = ex.copy(); e[\"fit\"], e[\"acclim\"] = fit, acc\n",
" e[\"hr_rel\"] = e[\"hr5\"] - 64; e[\"hr_excess\"] = 0; e = personalize(e); e[\"act\"] = 4.5\n",
" e[\"hr5\"] = 128.0; e[\"temp5\"] = 38.3; e[\"hi\"], e[\"wc\"] = 41.0, 30.0; e[\"tdev\"] = 1.1; e[\"dtemp\"] = 0.2; e[\"eda5\"] = 3.5; e = personalize(e)\n",
" rows.append({\"๊ฐ์ ๋ณ์ฌ\": name, \"์ด๊ณผ ์ฌ๋ฐ\": round(float(e[\"hr_excess\"].iloc[0]), 1), \"์ํ ์ ์\": round(float(model.predict_proba(e[PF])[:, 1][0]), 3)})\n",
"print(\"\\n[๊ฐ์ธํ ์์: ๊ฐ์ ์ฌ๋ฐ 128ยท์ฒด์จ 38.3]\")\n",
"print(pd.DataFrame(rows).to_string(index=False))\n",
"print(\"๊ฐ์ ์ธก์ ๊ฐ์ด๋ผ๋ ์ฒด๋ ฅยท๋์ ์์ ์ฌ๋ถ์ ๋ฐ๋ผ ์ํ ์ ์๊ฐ ๋ฌ๋ผ์ง๋ค(์์ ๋ณ์ฌ๋ ๊ฐ์ ์ฒด์จ์์ ์ ์๊ฐ ๋ฎ์์ง).\")\n",
"\n",
"sc, lr = model[0], model[-1]\n",
"i = int(np.argmax(test[\"score\"].values)); c = (X[i] - sc.mean_) / sc.scale_ * lr.coef_[0]\n",
"print(f\"\\n์ต๊ณ ์ ์ ๊ตฌ๊ฐ ๊ทผ๊ฑฐ: \", \", \".join(f\"{PF[j]}({c[j]:+.2f})\" for j in np.argsort(-np.abs(c))[:3]))\n",
"print(\"๋ฑ๊ธ ๊ธฐ์ค: High(0.8 ์ด์)๋ ์ฆ์ ์๋์ฅ ํ์ธยท์๋ฌด๋ ์ฐ๋ฝ ๊ฒํ , Medium(0.5~0.8)์ ์ฌ์ธก์ ยทํด์ ํ์ธ, Low(0.5 ๋ฏธ๋ง)๋ ๋ชจ๋ํฐ๋ง ์ ์ง. ์ ์๋ ํ์ธ ์์๋ฅผ ์ ํ๋ ๋ณด์กฐ ์ ๋ณด์ด๋ฉฐ ์ง๋จ์ด ์๋๋ค.\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(11, 4))\n",
"for st, col in ((0, \"tab:gray\"), (1, \"tab:orange\"), (2, \"tab:red\")):\n",
" ax[0].hist(test.loc[test[\"stage\"] == st, \"score\"], bins=10, range=(0, 1), alpha=0.6, color=col, label=f\"stage {st}\")\n",
"ax[0].axvline(0.5, color=\"k\", ls=\"--\"); ax[0].set_xlabel(\"confidence score\"); ax[0].legend(); ax[0].set_title(\"Score by true stage\")\n",
"fpr, tpr, _ = roc_curve(true, test[\"score\"]); ax[1].plot(fpr, tpr); ax[1].plot([0, 1], [0, 1], \"k:\"); ax[1].set_title(\"ROC\"); ax[1].set_xlabel(\"FPR\"); ax[1].set_ylabel(\"TPR\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
05 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
" ๋ฐฉ์ ๊ฒฝ๋ณด ๊ตฌ๊ฐ ์ค๊ฒฝ๋ณด์จ ์ํ ์ฌํ์จ\n",
"๊ณ ์ ์๊ณ๊ฐ(์ฒด์จ 37.8 ์ด์ยท36.5 ์ดํยท์ฌ๋ฐ 130 ์ด์) 2614 0.392 0.999\n",
" ๊ฐ์ธํ ์ ๋ขฐ๋ ์์ง(0.5 ์ด์) 1797 0.165 0.943\n",
"\n",
"์ค๊ฒฝ๋ณด์จ ๊ฐ์ถ 57.9% (๋ชฉํ 40% ์ด์: ๋ฌ์ฑ), ROC-AUC 0.986\n",
"์ ๋ขฐ๋ ๋ฑ๊ธ ์ผ๊ด์ฑ 92.9% (๊ฐ์ ์
๋ ฅ ์ฌ์คํ 100%, ๋ชฉํ 90% ์ด์: ๋ฌ์ฑ)\n",
"\n",
"[๋จ๊ณ๋ณ ๋ฑ๊ธ ๋ถํฌ] (ํ: ์ ๋ต ๋จ๊ณ)\n",
"๋ฑ๊ธ High Low Medium\n",
"์ ๋ต ๋จ๊ณ \n",
"๊ฒฝ๋ณด 183 0 0\n",
"์ ์ 96 3063 201\n",
"์ฃผ์ 1153 90 164\n",
"\n",
"[๊ฐ์ธํ ์์: ๊ฐ์ ์ฌ๋ฐ 128ยท์ฒด์จ 38.3]\n",
" ๊ฐ์ ๋ณ์ฌ ์ด๊ณผ ์ฌ๋ฐ ์ํ ์ ์\n",
"์ฒด๋ ฅ ๋ฎ์ยท๋น์์ 1.9 0.288\n",
" ์ฒด๋ ฅ ๋์ยท์์ 7.0 0.170\n",
"๊ฐ์ ์ธก์ ๊ฐ์ด๋ผ๋ ์ฒด๋ ฅยท๋์ ์์ ์ฌ๋ถ์ ๋ฐ๋ผ ์ํ ์ ์๊ฐ ๋ฌ๋ผ์ง๋ค(์์ ๋ณ์ฌ๋ ๊ฐ์ ์ฒด์จ์์ ์ ์๊ฐ ๋ฎ์์ง).\n",
"\n",
"์ต๊ณ ์ ์ ๊ตฌ๊ฐ ๊ทผ๊ฑฐ: tdev(+21.02), hi(+8.18), wc(-7.12)\n",
"๋ฑ๊ธ ๊ธฐ์ค: High(0.8 ์ด์)๋ ์ฆ์ ์๋์ฅ ํ์ธยท์๋ฌด๋ ์ฐ๋ฝ ๊ฒํ , Medium(0.5~0.8)์ ์ฌ์ธก์ ยทํด์ ํ์ธ, Low(0.5 ๋ฏธ๋ง)๋ ๋ชจ๋ํฐ๋ง ์ ์ง. ์ ์๋ ํ์ธ ์์๋ฅผ ์ ํ๋ ๋ณด์กฐ ์ ๋ณด์ด๋ฉฐ ์ง๋จ์ด ์๋๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
06. ์๋์ฅ/์๋ฌด๊ด์ฉ ์ฅ๋ณ ์์ฒด ์ํ ์ ํธ ๋ชจ๋ํฐ๋ง ๋ฐ ๊ธด๊ธ ๊ฒฝ๋ณด ๋์๋ณด๋ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์งํ๊ด/์๋ฌด๊ด์ฉ ์ฅ๋ณ ์์ฒด ์ํ ์ ํธ ๋ชจ๋ํฐ๋ง ๋์๋ณด๋๋ฅผ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์๋์ฅ๊ณผ ์๋ฌด๊ด์ด ๊ฐ์ ์๋ ๋ณ์ฌ๋ค์ ์์ฒด ์ํ ์ ํธ๋ฅผ ํ๋์ ๋ณด๊ณ ํ์ธ ๋์์ ์ ํ๋ ๋ชจ๋ํฐ๋ง ์ํฉํ์ ๊ตฌํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ฐ์ ๋ณ์ฌ 24๋ช
์ ์ํ๋ฅผ ์ ์ยท์ฃผ์ยท๊ฒฝ๋ณดยท์ ํธ๋์ ๋ก ๋๋๊ณ ์ฒด์จยท์ฌ๋ฐยท์ถ์ธยท๊ถ๊ณ ๋ฌธ๊ตฌ๋ฅผ ํ๋ก ์์ฑํ์ฌ ์์ธ์ ๋ถ์ํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/ui/wel01_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": [
"\"\"\"์๋์ฅยท์๋ฌด๊ด์ฉ ๊ฐ์ ์๋ 24๋ช
์ ์์ฒด ์ํ ์ ํธ ๋ชจ๋ํฐ๋ง ์ํฉํ์ ๊ตฌํํ๋ค. ๋ณ์ฌ๋ณ ์ํ(์ ์ยท์ฃผ์ยท๊ฒฝ๋ณดยท์ ํธ๋์ ), ์ฒด์จยท์ฌ๋ฐ ์ถ์ธ, ์กฐ์น ๊ถ๊ณ ๋ฌธ๊ตฌ๋ฅผ 1์ด ์ฃผ๊ธฐ๋ก ๊ฐฑ์ ํ๊ณ ์ฒ๋ฆฌ์๊ฐ์ ์ธก์ ํ๋ค. ์ง๋จ ์๋ยท์๋ฌด๋ ์ฐ๊ณ.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.linear_model import LogisticRegression\n",
"from sklearn.pipeline import make_pipeline\n",
"from sklearn.preprocessing import StandardScaler\n",
"\n",
"# ---------------- [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋๋ฏธ ์์ฒด์ ํธ ์์ฑ๊ธฐ (์ค์ ์ฅ๋ณยท๋ถ๋ยท์๋ฌด ๊ธฐ๋ก ์์) ----------------\n",
"# ๊ฐ์ ๋ณ์ฌ(๊ฐ๋ช
ID)๊ฐ 60๋ถ๊ฐ ๊ฑท๊ธฐยทํด์ยท๊ณ ๊ฐ๋ ํ๋์ ํ๋ ๋์์ ์ฌ๋ฐ์ยท์ฒด์จ(์ถ์ ์ฌ๋ถ)ยทํผ๋ถ์ ๋๋๋ฅผ 1๋ถ ๊ฐ๊ฒฉ์ผ๋ก ๋ง๋ ๋ค.\n",
"# ์ง๋จ ๋๊ตฌ๊ฐ ์๋๋ฉฐ, ์ํ ๋ผ๋ฒจ์ ๊ฐ์ ์๋ฆฌ ๋ชจ๋ธ์ '๊ต์ก์ฉ ๋จ๊ณ'์ผ ๋ฟ์ด๋ค.\n",
"N_MIN = 60\n",
"STAGE = {0: \"์ ์\", 1: \"์ฃผ์\", 2: \"๊ฒฝ๋ณด\"}\n",
"TYPES = [\"normal\", \"heat\", \"cold\"]\n",
"\n",
"\n",
"def make_person(i, rng):\n",
" return {\"id\": f\"S{i:03d}\", \"rest_hr\": float(rng.normal(64, 7)), \"age\": int(rng.integers(19, 27)), \"bmi\": float(rng.normal(23.5, 2.5)),\n",
" \"acclim\": int(rng.random() < 0.4), \"fit\": float(np.clip(rng.normal(0.5, 0.2), 0.05, 0.95))}\n",
"\n",
"\n",
"def make_env(kind, rng):\n",
" if kind == \"heat\":\n",
" return {\"temp_c\": float(rng.uniform(30, 40)), \"rh\": float(rng.uniform(45, 85)), \"wind\": float(rng.uniform(0.3, 3))}\n",
" if kind == \"cold\":\n",
" return {\"temp_c\": float(rng.uniform(-22, 2)), \"rh\": float(rng.uniform(40, 80)), \"wind\": float(rng.uniform(2, 11))}\n",
" return {\"temp_c\": float(rng.uniform(8, 24)), \"rh\": float(rng.uniform(35, 70)), \"wind\": float(rng.uniform(0.3, 5))}\n",
"\n",
"\n",
"def make_session(sid, kind, rng, p=None, glitch=True):\n",
" \"\"\"๋ฐํ: dict(person, env, kind, df[hr,temp,eda,act + ์ ๋ต coreยทstageยทrtype]). ์ผ์ ๊ฒฐ์ธกยทํยท๊ณ ์ฐฉ ํฌํจ.\"\"\"\n",
" p = p or make_person(sid, rng)\n",
" env = make_env(kind, rng)\n",
" act = np.zeros(N_MIN); t = 0\n",
" while t < N_MIN: # 10~20๋ถ ๋จ์ ํ๋ ๋ธ๋ก: ํด์ 1.5 / ํ๊ตฐ 4.5 / ๊ณ ๊ฐ๋ 7 MET\n",
" d = int(rng.integers(10, 21)); act[t:t + d] = rng.choice([1.5, 4.5, 7.0], p=[0.25, 0.5, 0.25]); t += d\n",
" hi = env[\"temp_c\"] + (0.12 * (env[\"rh\"] - 40) if env[\"temp_c\"] > 25 else 0)\n",
" wc = env[\"temp_c\"] - 1.6 * env[\"wind\"]\n",
" core, cores = 36.9, []\n",
" for m in range(N_MIN):\n",
" heat = 0.085 * max(0, hi - 24) * (1 - 0.25 * p[\"acclim\"]) * (1 + 0.5 * m / N_MIN) * (1 + 0.012 * (p[\"bmi\"] - 23.5))\n",
" cold = -0.075 * max(0, 8 - wc) * (1 - 0.09 * act[m])\n",
" ss = 36.9 + 0.09 * act[m] + heat + cold\n",
" core += (ss - core) / 18 + rng.normal(0, 0.03)\n",
" cores.append(core)\n",
" core = np.array(cores)\n",
" hr = p[\"rest_hr\"] + 9.5 * (act - 1.5) * (1.15 - 0.3 * p[\"fit\"]) + 11 * (core - 36.9) + rng.normal(0, 3.2, N_MIN)\n",
" eda = 2.0 + 0.25 * act + 3.2 * np.maximum(core - 37.2, 0) - 0.04 * np.maximum(8 - wc, 0) + rng.normal(0, 0.25, N_MIN)\n",
" bias = rng.normal(0, 0.1)\n",
" temp = core + bias + rng.normal(0, 0.1, N_MIN) - np.where((kind == \"cold\") & (env[\"wind\"] > 7), 0.25, 0.0) # ์ผ์ ์ ์ด ๋ถ์ ๋ฐ๋ ๋๊ฐ ์ค์ฐจ\n",
" df = pd.DataFrame({\"min\": np.arange(N_MIN), \"hr\": hr, \"temp\": temp, \"eda\": np.maximum(eda, 0.2), \"act\": act})\n",
" if glitch:\n",
" for col in (\"hr\", \"temp\", \"eda\"):\n",
" df.loc[rng.random(N_MIN) < 0.03, col] = np.nan # ๊ฒฐ์ธก\n",
" s = int(rng.integers(5, N_MIN))\n",
" if rng.random() < 0.12:\n",
" df.loc[s, \"hr\"] = 230.0 # ์ผ์ ํ\n",
" if rng.random() < 0.12:\n",
" df.loc[s, \"temp\"] = 45.0\n",
" ht = np.where(core >= 39.0, 2, np.where(core >= 38.3, 1, 0)); cd = np.where(core <= 35.4, 2, np.where(core <= 36.2, 1, 0))\n",
" df[\"core\"] = core\n",
" df[\"stage\"] = np.maximum(ht, cd); df[\"rtype\"] = np.where(ht > 0, \"heat\", np.where(cd > 0, \"cold\", \"normal\"))\n",
" return {\"id\": p[\"id\"], \"person\": p, \"env\": env, \"kind\": kind, \"df\": df}\n",
"\n",
"\n",
"def make_dataset(n, seed):\n",
" rng = np.random.default_rng(seed)\n",
" kinds = [\"normal\", \"heat\", \"cold\"]\n",
" return [make_session(i, kinds[i % 3], rng) for i in range(n)]\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---------------- ์ ์ฒ๋ฆฌยทํน์ง ์ถ์ถ ๊ณตํต ํจ์ ----------------\n",
"LIMITS = {\"hr\": (30, 220), \"temp\": (30.0, 43.0), \"eda\": (0.0, 40.0)} # ์๋ฆฌ์ ์ผ๋ก ๊ฐ๋ฅํ ๋ฒ์(๋ฒ์ ๋ฐ = ์ผ์ ์ค๋ฅ)\n",
"\n",
"\n",
"def clean(df):\n",
" \"\"\"๋ฒ์ ๋ฐ ๊ฐ์ ๊ฒฐ์ธก ์ฒ๋ฆฌ -> ์ ํ ๋ณด๊ฐ(์ ๋์ ๊ฐ๊น์ด ๊ฐ) -> 3๋ถ ์ด๋ ์ค์๊ฐ์ผ๋ก ํ ์ ๊ฑฐ. ๋ฐํ: (์ ์ df, ๋ณด์ ๊ฑด์)\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 6] ์๋์ฅ/์๋ฌด๊ด์ฉ ์ฅ๋ณ ์์ฒด ์ํ ์ ํธ ๋ชจ๋ํฐ๋ง ๋ฐ ๊ธด๊ธ ๊ฒฝ๋ณด ๋์๋ณด๋ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def env_index(env):\n",
" # [TODO : ํ์ ์ค์ต 6] ์๋์ฅ/์๋ฌด๊ด์ฉ ์ฅ๋ณ ์์ฒด ์ํ ์ ํธ ๋ชจ๋ํฐ๋ง ๋ฐ ๊ธด๊ธ ๊ฒฝ๋ณด ๋์๋ณด๋ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def features(s, use_env=True, personal=True):\n",
" \"\"\"์ธ์
1๊ฐ -> ๋ถ ๋จ์ ํน์งํ(์ต๊ทผ 5๋ถ ํ๊ท ยท๊ธฐ์ธ๊ธฐ ํฌํจ). ์ ๋ต stage/rtype ํฌํจ.\"\"\"\n",
" df, _ = clean(s[\"df\"]); p, e = s[\"person\"], s[\"env\"]\n",
" f = pd.DataFrame({\"sid\": s[\"id\"], \"min\": df[\"min\"]})\n",
" f[\"temp5\"] = df[\"temp\"].rolling(5, min_periods=1).mean()\n",
" f[\"hr5\"] = df[\"hr\"].rolling(5, min_periods=1).mean()\n",
" f[\"eda5\"] = df[\"eda\"].rolling(5, min_periods=1).mean()\n",
" f[\"dtemp\"] = (f[\"temp5\"] - f[\"temp5\"].shift(4)).fillna(0)\n",
" f[\"act\"] = df[\"act\"]\n",
" if personal:\n",
" f[\"hr_rel\"] = f[\"hr5\"] - p[\"rest_hr\"]\n",
" f[\"acclim\"], f[\"bmi\"], f[\"fit\"] = p[\"acclim\"], p[\"bmi\"], p[\"fit\"]\n",
" if use_env:\n",
" f[\"hi\"], f[\"wc\"] = env_index(e); f[\"wind\"] = e[\"wind\"]\n",
" f[\"stage\"], f[\"rtype\"], f[\"kind\"] = df[\"stage\"], df[\"rtype\"], s[\"kind\"]\n",
" return f[f[\"min\"] >= 5].reset_index(drop=True)\n",
"\n",
"\n",
"def table(sessions, **kw):\n",
" return pd.concat([features(s, **kw) for s in sessions], ignore_index=True)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"FE = [\"temp5\", \"dtemp\", \"hr5\", \"eda5\", \"hr_rel\", \"acclim\", \"bmi\", \"fit\", \"hi\", \"wc\", \"wind\"]\n",
"tr = table(make_dataset(120, seed=51))\n",
"model = make_pipeline(StandardScaler(), LogisticRegression(class_weight=\"balanced\", max_iter=3000, random_state=0)).fit(tr[FE], tr[\"stage\"] >= 1)\n",
"\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์๋ 24๋ช
(๊ฐ๋ช
ID): ํญ์ผ 8, ํนํ 8, ํ์ 8 ๊ตฌ์ญ์์ ํ๋ ์ค์ธ ๋ณ์ฌ๋ค์ ํ๋ฉด\n",
"rng = np.random.default_rng(3)\n",
"kinds = [\"heat\"] * 8 + [\"cold\"] * 8 + [\"normal\"] * 8\n",
"squad = [make_session(i, k, rng) for i, k in enumerate(kinds)]\n",
"for s in squad[:2]:\n",
" s[\"df\"].loc[40:, [\"hr\", \"temp\", \"eda\"]] = np.nan # ์ ํธ ๋์ 2๋ช
(๊ธฐ๊ธฐ ์ดํยท๋ฐฐํฐ๋ฆฌ ๋ฐฉ์ ๋ชจ์)\n",
"FEAT = {s[\"id\"]: features(s) for s in squad} # ๋ณ์ฌ๋ณ ๋ถ ๋จ์ ํน์งํ(์ฌ์์ฉ)\n",
"RAW = {s[\"id\"]: s[\"df\"] for s in squad}\n",
"\n",
"\n",
"def snapshot(minute):\n",
" \"\"\"๋ถ ๋จ์ 1ํฑ: ๋ณ์ฌ๋ณ ์ํยท์ ์ยท๊ถ๊ณ ๋ฅผ ๊ณ์ฐํด ์ํฉํ ํ๋ฅผ ๋ง๋ ๋ค.\"\"\"\n",
" rows = []\n",
" for s in squad:\n",
" f = FEAT[s[\"id\"]]; r = f[f[\"min\"] == minute]\n",
" raw = RAW[s[\"id\"]].loc[max(minute - 4, 0):minute, [\"hr\", \"temp\"]]\n",
" if raw.isna().all().all():\n",
" rows.append({\"๋ณ์ฌ\": s[\"id\"], \"์ํ\": \"์ ํธ๋์ \", \"์ ์\": np.nan, \"์ฒด์จ\": np.nan, \"์ฌ๋ฐ\": np.nan, \"์ถ์ธ\": \"-\", \"๊ถ๊ณ \": \"๊ธฐ๊ธฐ ์ฐฉ์ฉยท๋ฐฐํฐ๋ฆฌ ํ์ธ, ์๋์ฅ ๋๋ฉด ํ์ธ\"}); continue\n",
" sc = float(model.predict_proba(r[FE])[:, 1][0])\n",
" st = \"๊ฒฝ๋ณด\" if sc >= 0.8 else (\"์ฃผ์\" if sc >= 0.5 else \"์ ์\")\n",
" rec = {\"๊ฒฝ๋ณด\": \"์๋์ฅ ์ฆ์ ํ์ธ, ์๋ฌด๋ ์ฐ๋ฝ ๊ฒํ \", \"์ฃผ์\": \"์ฌ์ธก์ ยทํด์ ์ํ ํ์ธ\", \"์ ์\": \"-\"}[st]\n",
" rows.append({\"๋ณ์ฌ\": s[\"id\"], \"์ํ\": st, \"์ ์\": round(sc, 2), \"์ฒด์จ\": round(float(r[\"temp5\"].iloc[0]), 1), \"์ฌ๋ฐ\": int(r[\"hr5\"].iloc[0]),\n",
" \"์ถ์ธ\": f\"{float(r['dtemp'].iloc[0]):+.1f}\", \"๊ถ๊ณ \": rec})\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"# 1) 1์ด ์ฃผ๊ธฐ ๊ฐฑ์ ๋ชจ์: 5ํฑ ๊ฐฑ์ ์๊ฐ ์ธก์ \n",
"times = []\n",
"for m in range(45, 50):\n",
" t0 = time.perf_counter(); board = snapshot(m); times.append((time.perf_counter() - t0) * 1000)\n",
"print(f\"์ํฉํ ๊ฐฑ์ ์ฒ๋ฆฌ์๊ฐ ํ๊ท {np.mean(times):.1f} ms, ์ต๋ {max(times):.1f} ms (๋ชฉํ: ๊ฐฑ์ ์ฃผ๊ธฐ 1์ด ์ด๋ด -> {'๋ฌ์ฑ' if max(times) < 1000 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# 2) ๋ณ์ฌ 24๋ช
-> ํ์ธ ๋์ ์์ฝ(์ธ์ง ๋ถํ ๊ฐ์)\n",
"order = {\"์ ํธ๋์ \": 0, \"๊ฒฝ๋ณด\": 1, \"์ฃผ์\": 2, \"์ ์\": 3}\n",
"board = board.sort_values([\"์ํ\", \"์ ์\"], key=lambda s: s.map(order) if s.name == \"์ํ\" else -s.fillna(2), kind=\"stable\")\n",
"cnt = board[\"์ํ\"].value_counts().reindex([\"๊ฒฝ๋ณด\", \"์ฃผ์\", \"์ ํธ๋์ \", \"์ ์\"]).fillna(0).astype(int)\n",
"print(f\"\\n[์๋ ํํฉ] ๋ณ์ฌ {len(board)}๋ช
-> ํ์ธ ๋์ {len(board) - cnt['์ ์']}๋ช
์ผ๋ก ์์ถ:\", cnt.to_dict())\n",
"print(\"\\n[์ฐ์ ํ์ธ ๋ชฉ๋ก: ์ ํธ๋์ > ๊ฒฝ๋ณด > ์ฃผ์ ์]\")\n",
"print(board[board[\"์ํ\"] != \"์ ์\"].head(12).fillna(\"-\").to_string(index=False))\n",
"\n",
"# 3) ๋ ๋๋ง ์๊ฐ(UI ์๋ต์ฑ)๊ณผ ์ํฉํ ๊ทธ๋ฆผ\n",
"t0 = time.perf_counter()\n",
"col = {\"๊ฒฝ๋ณด\": \"tab:red\", \"์ฃผ์\": \"orange\", \"์ ์\": \"tab:green\", \"์ ํธ๋์ \": \"dimgray\"}; EN = {\"๊ฒฝ๋ณด\": \"ALERT\", \"์ฃผ์\": \"CAUTION\", \"์ ์\": \"NORMAL\", \"์ ํธ๋์ \": \"NO SIGNAL\"}\n",
"fig, ax = plt.subplots(2, 3, figsize=(13, 6))\n",
"focus = board[board[\"์ํ\"].isin([\"๊ฒฝ๋ณด\", \"์ฃผ์\"])].head(5)[\"๋ณ์ฌ\"].tolist()\n",
"for a, sid in zip(ax.ravel()[:5], focus):\n",
" d = RAW[sid]; st = board[board[\"๋ณ์ฌ\"] == sid][\"์ํ\"].iloc[0]\n",
" a.plot(d[\"min\"], d[\"temp\"].interpolate(), color=col[st]); a.axvline(49, color=\"k\", ls=\":\"); a.set_ylim(34, 41)\n",
" a.set_title(f\"{sid} [{EN[st]}] temperature trend\", fontsize=9); a.set_xlabel(\"minute\")\n",
"ax.ravel()[5].bar([EN[k] for k in cnt.index], cnt.values, color=[col[k] for k in cnt.index]); ax.ravel()[5].set_title(\"Platoon status\")\n",
"plt.suptitle(\"Soldier bio-signal board (dummy data, not diagnosis)\"); plt.tight_layout(); fig.canvas.draw()\n",
"render = (time.perf_counter() - t0) * 1000\n",
"print(f\"\\nํ ํ๋ฉด ๋ ๋๋ง {render:.0f} ms (UI ์๋ต ๋ชฉํ 100 ms: ๋ฐ์ดํฐ ๊ฐฑ์ {np.mean(times):.1f} ms๋ ๋ฌ์ฑ, ๋ ๋๋ง์ ์คํ ํ๊ฒฝ์ ๋ฐ๋ผ ๋ฌ๋ผ์ง)\")\n",
"print(\"์ํฉํ์ ์ํ์ ๊ถ๊ณ ๋ ์๋์ฅยท์๋ฌด๊ด์ ํ์ธ์ ๋๋ ๋ณด์กฐ ์ ๋ณด์ด๋ฉฐ ์ง๋จ์ด ์๋๋ค. ์ํ์ ํ๋จ๊ณผ ์กฐ์น๋ ๊ตฐ์๊ดยท์๋ฌด๋๊ฐ ํ๋ฉฐ, ์กฐ์น ๋ช
๋ น์ ๋ฏธ์
7์ HITL ์ ์ฐจ๋ฅผ ๋ฐ๋ฅธ๋ค.\")\n",
"plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
06 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"์ํฉํ ๊ฐฑ์ ์ฒ๋ฆฌ์๊ฐ ํ๊ท 74.0 ms, ์ต๋ 77.3 ms (๋ชฉํ: ๊ฐฑ์ ์ฃผ๊ธฐ 1์ด ์ด๋ด -> ๋ฌ์ฑ)\n",
"\n",
"[์๋ ํํฉ] ๋ณ์ฌ 24๋ช
-> ํ์ธ ๋์ 12๋ช
์ผ๋ก ์์ถ: {'๊ฒฝ๋ณด': 7, '์ฃผ์': 3, '์ ํธ๋์ ': 2, '์ ์': 12}\n",
"\n",
"[์ฐ์ ํ์ธ ๋ชฉ๋ก: ์ ํธ๋์ > ๊ฒฝ๋ณด > ์ฃผ์ ์]\n",
" ๋ณ์ฌ ์ํ ์ ์ ์ฒด์จ ์ฌ๋ฐ ์ถ์ธ ๊ถ๊ณ \n",
"S000 ์ ํธ๋์ - - - - ๊ธฐ๊ธฐ ์ฐฉ์ฉยท๋ฐฐํฐ๋ฆฌ ํ์ธ, ์๋์ฅ ๋๋ฉด ํ์ธ\n",
"S001 ์ ํธ๋์ - - - - ๊ธฐ๊ธฐ ์ฐฉ์ฉยท๋ฐฐํฐ๋ฆฌ ํ์ธ, ์๋์ฅ ๋๋ฉด ํ์ธ\n",
"S003 ๊ฒฝ๋ณด 1.0 38.7 119.0 +0.1 ์๋์ฅ ์ฆ์ ํ์ธ, ์๋ฌด๋ ์ฐ๋ฝ ๊ฒํ \n",
"S010 ๊ฒฝ๋ณด 1.0 35.4 65.0 -0.1 ์๋์ฅ ์ฆ์ ํ์ธ, ์๋ฌด๋ ์ฐ๋ฝ ๊ฒํ \n",
"S009 ๊ฒฝ๋ณด 0.99 35.3 93.0 -0.1 ์๋์ฅ ์ฆ์ ํ์ธ, ์๋ฌด๋ ์ฐ๋ฝ ๊ฒํ \n",
"S006 ๊ฒฝ๋ณด 0.96 38.5 110.0 +0.1 ์๋์ฅ ์ฆ์ ํ์ธ, ์๋ฌด๋ ์ฐ๋ฝ ๊ฒํ \n",
"S014 ๊ฒฝ๋ณด 0.94 35.9 85.0 -0.1 ์๋์ฅ ์ฆ์ ํ์ธ, ์๋ฌด๋ ์ฐ๋ฝ ๊ฒํ \n",
"S004 ๊ฒฝ๋ณด 0.93 38.3 81.0 -0.0 ์๋์ฅ ์ฆ์ ํ์ธ, ์๋ฌด๋ ์ฐ๋ฝ ๊ฒํ \n",
"S005 ๊ฒฝ๋ณด 0.86 38.1 125.0 +0.0 ์๋์ฅ ์ฆ์ ํ์ธ, ์๋ฌด๋ ์ฐ๋ฝ ๊ฒํ \n",
"S007 ์ฃผ์ 0.67 38.5 110.0 +0.0 ์ฌ์ธก์ ยทํด์ ์ํ ํ์ธ\n",
"S002 ์ฃผ์ 0.62 38.3 91.0 +0.1 ์ฌ์ธก์ ยทํด์ ์ํ ํ์ธ\n",
"S012 ์ฃผ์ 0.52 36.2 92.0 -0.0 ์ฌ์ธก์ ยทํด์ ์ํ ํ์ธ\n",
"\n",
"ํ ํ๋ฉด ๋ ๋๋ง 565 ms (UI ์๋ต ๋ชฉํ 100 ms: ๋ฐ์ดํฐ ๊ฐฑ์ 74.0 ms๋ ๋ฌ์ฑ, ๋ ๋๋ง์ ์คํ ํ๊ฒฝ์ ๋ฐ๋ผ ๋ฌ๋ผ์ง)\n",
"์ํฉํ์ ์ํ์ ๊ถ๊ณ ๋ ์๋์ฅยท์๋ฌด๊ด์ ํ์ธ์ ๋๋ ๋ณด์กฐ ์ ๋ณด์ด๋ฉฐ ์ง๋จ์ด ์๋๋ค. ์ํ์ ํ๋จ๊ณผ ์กฐ์น๋ ๊ตฐ์๊ดยท์๋ฌด๋๊ฐ ํ๋ฉฐ, ์กฐ์น ๋ช
๋ น์ ๋ฏธ์
7์ HITL ์ ์ฐจ๋ฅผ ๋ฐ๋ฅธ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
07. ์ํ ์ฅ๋ณ ๋ฐ์ ์ ๊ตฐ์๊ด ์๊ฒฉ ๊ฒํ ๋ฐ ์๋ฌด ๋ฐฐ์ /ํด์ ๋ช
๋ น HITL ์ ์ฐจ ์ค๊ณ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ํ ์ฅ๋ณ ๋ฐ์ ์ ๊ตฐ์๊ด ์๊ฒฉ ๊ฒํ ๋ฐ ์กฐ์น ๋ช
๋ น HITL ์ ์ฐจ๋ฅผ ์ค๊ณํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ํ ์ ํธ๊ฐ ๋ฐ์ํ ๋ณ์ฌ์ ๋ํด ๊ตฐ์๊ด์ด ์๊ฒฉ์ผ๋ก ๊ฒํ ํ๊ณ ์กฐ์น๋ฅผ ๊ฒฐ์ ํ๋ ์ธ๊ฐ ๊ฐ์
(HITL) ์ ์ฐจ๋ฅผ ์ค๊ณํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **AI ์ํ ์ ํธ๋ ์ ์์ ๋ฌด๊ดํ๊ฒ ๊ฒํ ๋๊ธฐ๋ก ๋ฑ๋กํ๊ณ ๋ฑ๊ธยท์ ์ ์์ผ๋ก ๊ฒํ ์์๋ฅผ ์ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/hitl/wel01_approval_workflow.py ๋ฐ docs/wel01_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": [
"# [TODO : ํ์ ์ค์ต 7] ์ํ ์ฅ๋ณ ๋ฐ์ ์ ๊ตฐ์๊ด ์๊ฒฉ ๊ฒํ ๋ฐ ์๋ฌด ๋ฐฐ์ /ํด์ ๋ช
๋ น HITL ์ ์ฐจ ์ค๊ณ\n",
"# ๋ฏธ์
์ง์นจ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค์ ํ์ธํ๊ณ ์ฝ๋๋ฅผ ์คํ ๋ฐ ํ๋ํ์ธ์.\n",
"\"\"\"์ํ ์ ํธ๊ฐ ๋ฐ์ํ ๊ฐ์ ๋ณ์ฌ์ ๋ํด ๊ตฐ์๊ด์ด ์๊ฒฉ ๊ฒํ ํ๊ณ ํด์ ๊ถ๊ณ ยท์๋ฌด ๋ฐฐ์ ์์ฒญยท์๋ฌด๋ ์ธ๊ณ ์์ฒญยท์ฌ์ธก์ ๋ณด๋ฅ๋ฅผ ๊ฒฐ์ ํ๋ ์ธ๊ฐ ๊ฐ์
(HITL) ์ ์ฐจ๋ฅผ ๊ตฌํํ๋ค. AI๋ ์ด๋ค ์กฐ์น๋ ์๋์ผ๋ก ๋ช
๋ นํ์ง ์์ผ๋ฉฐ ๋ชจ๋ ๊ฒฐ์ ์ ๋ด๋น์ IDยท์ญํ ยท๊ทผ๊ฑฐ์ ํจ๊ป ์ยท๋ณ์กฐ ํ์ธ์ด ๊ฐ๋ฅํ ๊ฐ์ฌ ๋ก๊ทธ์ ๊ธฐ๋ก๋๋ค. ์ง๋จ ์๋.\"\"\"\n",
"import hashlib\n",
"import json\n",
"import pandas as pd\n",
"\n",
"ACTIONS = {\"REST\": \"ํด์ ๊ถ๊ณ (ํ์ฅ ํ์ธ ํ ์ดํ)\", \"EXCLUDE\": \"์๋ฌด ๋ฐฐ์ ์์ฒญ(์งํ๊ด ์น์ธ ๋์)\", \"REFER\": \"์๋ฌด๋ ์ธ๊ณ ์์ฒญ\", \"HOLD\": \"์ฌ์ธก์ ยท๋ณด๋ฅ\"}\n",
"ROLES = {\"platoon_leader\": \"์๋์ฅ\", \"medical_officer\": \"๊ตฐ์๊ด\", \"commander\": \"์งํ๊ด\"}\n",
"PRIORITY = {\"High\": 0, \"Medium\": 1, \"Low\": 2}\n",
"HOLD_LIMIT_S = 60\n",
"\n",
"\n",
"class WorkflowError(ValueError):\n",
" pass\n",
"\n",
"\n",
"class ReviewQueue:\n",
" def __init__(self, clock):\n",
" self.items, self.log, self.clock = {}, [], clock\n",
"\n",
" # ---- ๊ฐ์ฌ ๋ก๊ทธ: ์ง์ ํด์๋ฅผ ํฌํจํ ํด์ ์ฒด์ธ(์ถ๊ฐ ์ ์ฉ) ----\n",
" def _record(self, body):\n",
" prev = self.log[-1][\"hash\"] if self.log else \"GENESIS\"\n",
" body = {**body, \"ts\": round(self.clock(), 3)}\n",
" h = hashlib.sha256((prev + json.dumps(body, ensure_ascii=False, sort_keys=True)).encode()).hexdigest()\n",
" self.log.append({**body, \"prev\": prev, \"hash\": h})\n",
"\n",
" def verify_log(self):\n",
" prev = \"GENESIS\"\n",
" for n, rec in enumerate(self.log, start=1):\n",
" body = {k: v for k, v in rec.items() if k not in (\"prev\", \"hash\")}\n",
" ok_hash = hashlib.sha256((prev + json.dumps(body, ensure_ascii=False, sort_keys=True)).encode()).hexdigest()\n",
" if rec[\"prev\"] != prev or rec[\"hash\"] != ok_hash:\n",
" return False, n\n",
" prev = rec[\"hash\"]\n",
" return True, -1\n",
"\n",
" def submit(self, sid, score, grade, summary):\n",
" \"\"\"AI ๊ฒฝ๋ณด ๋ฑ๋ก. ์ ์์ ๋ฌด๊ดํ๊ฒ ํญ์ PENDING(๊ฒํ ๋๊ธฐ)์ผ๋ก ์์ํ๋ค.\"\"\"\n",
" self.items[sid] = {\"id\": sid, \"score\": score, \"grade\": grade, \"summary\": summary, \"status\": \"PENDING\",\n",
" \"submitted\": self.clock(), \"hold_since\": None, \"escalated\": False, \"decided\": None}\n",
" self._record({\"event\": \"SUBMIT\", \"id\": sid, \"score\": score, \"by\": \"AI\"})\n",
"\n",
" def pending(self):\n",
" todo = [i for i in self.items.values() if i[\"status\"] in (\"PENDING\", \"HOLD\", \"EXCLUDE_REQUESTED\")]\n",
" return sorted(todo, key=lambda i: (not i[\"escalated\"], PRIORITY[i[\"grade\"]], -i[\"score\"]))\n",
"\n",
" def decide(self, sid, action, operator, role, reason):\n",
" if action not in ACTIONS:\n",
" raise WorkflowError(f\"ํ์ฉ๋์ง ์์ ์กฐ์น: {action}\")\n",
" if role not in ROLES or not operator:\n",
" raise WorkflowError(\"๋ด๋น์ ID์ ์ญํ ํ์\")\n",
" item = self.items.get(sid)\n",
" if item is None:\n",
" raise WorkflowError(\"๋๊ธฐ์ด์ ์๋ ๋ณ์ฌ\")\n",
" if item[\"status\"] in (\"CLOSED\",):\n",
" raise WorkflowError(\"์ด๋ฏธ ์ข
๊ฒฐ๋ ๊ฑด\")\n",
" if len(reason.strip()) < 5:\n",
" raise WorkflowError(\"ํ๋จ ๊ทผ๊ฑฐ 5์ ์ด์ ํ์\")\n",
" if action in (\"EXCLUDE\", \"REFER\") and role == \"platoon_leader\":\n",
" if action == \"EXCLUDE\":\n",
" raise WorkflowError(\"์๋ฌด ๋ฐฐ์ ์์ฒญ์ ๊ตฐ์๊ด ๊ฒํ ์๊ฒฌ์ด ์์ด์ผ ํจ(์๋์ฅ ๋จ๋
๋ถ๊ฐ)\")\n",
" if action == \"EXCLUDE\" and item[\"status\"] == \"EXCLUDE_REQUESTED\" and role != \"commander\":\n",
" raise WorkflowError(\"์๋ฌด ๋ฐฐ์ ์ต์ข
์น์ธ์ ์งํ๊ด๋ง ๊ฐ๋ฅ\")\n",
" if action == \"EXCLUDE\" and item[\"status\"] != \"EXCLUDE_REQUESTED\":\n",
" if role != \"medical_officer\":\n",
" raise WorkflowError(\"์๋ฌด ๋ฐฐ์ ์์ฒญ์ ๊ตฐ์๊ด๋ง ์ฌ๋ฆด ์ ์์\")\n",
" item[\"status\"] = \"EXCLUDE_REQUESTED\" # ๊ตฐ์๊ด ์์ฒญ ๋จ๊ณ: ์งํ๊ด ์น์ธ ์ ๊น์ง ํ์ ์๋\n",
" elif action == \"EXCLUDE\":\n",
" item[\"status\"] = \"CLOSED\" # ์งํ๊ด ์น์ธ = ์กฐ์น ์ง์ ์์ฒญ๊น์ง\n",
" elif action == \"REST\" and item[\"grade\"] == \"High\" and role == \"platoon_leader\":\n",
" raise WorkflowError(\"High ๋ฑ๊ธ ํด์ ํ๋จ์ ๊ตฐ์๊ด ๊ฒํ ํ์\")\n",
" elif action in (\"REST\", \"REFER\"):\n",
" item[\"status\"] = \"CLOSED\"\n",
" else:\n",
" item[\"status\"] = \"HOLD\"\n",
" item[\"hold_since\"] = self.clock() if action == \"HOLD\" else None\n",
" item[\"decided\"] = self.clock() if item[\"status\"] == \"CLOSED\" else None\n",
" self._record({\"event\": \"DECIDE\", \"id\": sid, \"action\": action, \"operator\": operator, \"role\": role, \"reason\": reason})\n",
"\n",
" def check_holds(self):\n",
" \"\"\"๋ณด๋ฅ ๊ธฐํ ์ด๊ณผ ์ ์๋ ์กฐ์นํ์ง ์๊ณ ๊ตฐ์๊ด์๊ฒ ์์ ํ๋ค.\"\"\"\n",
" out = []\n",
" for i in self.items.values():\n",
" if i[\"status\"] == \"HOLD\" and not i[\"escalated\"] and self.clock() - i[\"hold_since\"] > HOLD_LIMIT_S:\n",
" i[\"escalated\"] = True; out.append(i[\"id\"])\n",
" self._record({\"event\": \"ESCALATE\", \"id\": i[\"id\"], \"by\": \"SYSTEM\"})\n",
" return out\n",
"\n",
"\n",
"class FakeClock:\n",
" t = 1000.0\n",
"\n",
" def __call__(self):\n",
" return self.t\n",
"\n",
"\n",
"clock = FakeClock()\n",
"q = ReviewQueue(clock)\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ฏธ์
5 ์ ๋ขฐ๋ ์์ง์์ ๋์ด์จ ์ํ ์ ํธ 6๊ฑด(๊ฐ๋ช
๋ณ์ฌ ID)\n",
"for sid, sc, g, sm in [(\"S003\", 0.98, \"High\", \"์ฒด์จ 38.7, ์ฌ๋ฐ 119\"), (\"S010\", 0.97, \"High\", \"์ฒด์จ 35.4, ํ๋ญ\"), (\"S007\", 0.66, \"Medium\", \"์ฒด์จ 38.5, ์ฌ๋ฐ 110\"),\n",
" (\"S012\", 0.52, \"Medium\", \"์ฒด์จ 36.2\"), (\"S002\", 0.55, \"Medium\", \"์ฒด์จ 38.3\"), (\"S015\", 0.35, \"Low\", \"์ฌ๋ฐ ์ผ์ ์์น\")]:\n",
" q.submit(sid, sc, g, sm)\n",
"print(\"๊ฒํ ์์:\", [i[\"id\"] for i in q.pending()])\n",
"print(\"์๋ ์กฐ์น(ํด์ยท๋ฐฐ์ ยท์ธ๊ณ)๋ ๊ฑด์:\", sum(i[\"status\"] == \"CLOSED\" for i in q.items.values()))\n",
"\n",
"# ๊ท์น ์๋ฐ ์๋๊ฐ ๋ชจ๋ ์ฐจ๋จ๋๋์ง ํ์ธ\n",
"tries = [(\"S003\", \"REST\", \"pl01\", \"platoon_leader\", \"๋์์ ์ฌ๊ฒ ํจ ํ์ธ ์๋ฃ\"), (\"S010\", \"EXCLUDE\", \"pl01\", \"platoon_leader\", \"์ ์ฒด์จ ์์ฌ๋์ด ๋ฐฐ์ ์์ฒญ\"),\n",
" (\"S007\", \"REST\", \"\", \"platoon_leader\", \"ํด์ ํ ์ฌ์ธก์ ์์ \"), (\"S007\", \"DISCHARGE\", \"md01\", \"medical_officer\", \"ํ์ฉ๋์ง ์์ ์กฐ์น\"),\n",
" (\"S012\", \"REFER\", \"pl01\", \"platoon_leader\", \"์งง์\")]\n",
"for args in tries:\n",
" try:\n",
" q.decide(*args)\n",
" except WorkflowError as e:\n",
" print(\"์ฐจ๋จ:\", e)\n",
"\n",
"clock.t += 3; q.decide(\"S003\", \"EXCLUDE\", \"md01\", \"medical_officer\", \"์๊ฒฉ ๊ฒํ : ์ฒด์จ ์์น ์ง์, ์๋ฌด ๋ฐฐ์ ๋ฐ ์๋ฌด๋ ์ธ๊ณ ์์ฒญ\")\n",
"clock.t += 4; q.decide(\"S003\", \"EXCLUDE\", \"cdr01\", \"commander\", \"๊ตฐ์๊ด ์๊ฒฌ ํ์ธ, ์๋ฌด ๋ฐฐ์ ์น์ธ ํ ์๋ฌด๋ ์ธ๊ณ ์ง์ ์์ฒญ\")\n",
"clock.t += 5; q.decide(\"S010\", \"HOLD\", \"md01\", \"medical_officer\", \"์ผ์ ๋๊ฐ ์ค์ฐจ ๊ฐ๋ฅ์ฑ, ์ฌ์ธก์ ํ ํ๋จ\")\n",
"clock.t += 4; q.decide(\"S007\", \"REST\", \"pl01\", \"platoon_leader\", \"๊ทธ๋ ํด์ ํ ์ฌ์ธก์ , ์ํ ํธ์ ํ์ธ\")\n",
"clock.t += 3; q.decide(\"S015\", \"REST\", \"pl01\", \"platoon_leader\", \"์ด๋ ์งํ ์ผ์ ์์น์ผ๋ก ํ์ธ\")\n",
"clock.t += 70\n",
"print(\"๋ณด๋ฅ ๊ธฐํ ์ด๊ณผ ์์ :\", q.check_holds())\n",
"print(\"์์ ํ ๊ฒํ ์์:\", [i[\"id\"] for i in q.pending()])\n",
"\n",
"df = pd.DataFrame(q.items.values())\n",
"df[\"๋๊ธฐ์๊ฐ(s)\"] = (df[\"decided\"] - df[\"submitted\"]).round(1)\n",
"print(\"\\n\" + df[[\"id\", \"grade\", \"score\", \"status\", \"escalated\", \"๋๊ธฐ์๊ฐ(s)\"]].fillna(\"-\").to_string(index=False))\n",
"ev = [r for r in q.log if r[\"event\"] == \"DECIDE\"]\n",
"print(f\"\\n์ฒ๋ฆฌ ๊ฒฐ์ {len(ev)}๊ฑด ์ค ๋ก๊ทธ ๊ธฐ๋ก {len(ev)}๊ฑด(์ฑ๊ณต๋ฅ {len(ev) / len(ev):.0%}), ์ ์ฒด ๊ฐ์ฌ ๋ก๊ทธ {len(q.log)}๊ฑด, ํด์ ์ฒด์ธ ๊ฒ์ฆ: {q.verify_log()}\")\n",
"q.log[7][\"operator\"] = \"md99\" # ์ยท๋ณ์กฐ ์๋\n",
"print(\"8๋ฒ์งธ ๊ธฐ๋ก ๋ณ์กฐ ํ ๊ฒ์ฆ(False, ๊นจ์ง ์์น):\", q.verify_log())\n",
"print(\"\\n์ฑ
์ ๋ถ๊ณ: AI๋ ์ํ ์ ํธ ์ ์๋ง, ํด์ ํ๋จ์ ์๋์ฅ(High๋ ๊ตฐ์๊ด), ์๋ฌด ๋ฐฐ์ ยท์๋ฌด๋ ์ธ๊ณ ์์ฒญ์ ๊ตฐ์๊ด, ์๋ฌด ๋ฐฐ์ ์ต์ข
์น์ธ์ ์งํ๊ด์ด๋ค.\")\n",
"print(\"๋ณธ ์ ์ฐจ๋ ํ์ต์ฉ ๋ชจ์์ด๋ฉฐ ์ง๋จยท์ฒ๋ฐฉ์ด ์๋๋ค. ์ค์ ์กฐ์น๋ ๊ตฐ์๊ดยท์๋ฌด๋์ ํ๋จ๊ณผ ์งํ๊ณํต์ ๋ฐ๋ฅธ๋ค.\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
07 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"๊ฒํ ์์: ['S003', 'S010', 'S007', 'S002', 'S012', 'S015']\n",
"์๋ ์กฐ์น(ํด์ยท๋ฐฐ์ ยท์ธ๊ณ)๋ ๊ฑด์: 0\n",
"์ฐจ๋จ: High ๋ฑ๊ธ ํด์ ํ๋จ์ ๊ตฐ์๊ด ๊ฒํ ํ์\n",
"์ฐจ๋จ: ์๋ฌด ๋ฐฐ์ ์์ฒญ์ ๊ตฐ์๊ด ๊ฒํ ์๊ฒฌ์ด ์์ด์ผ ํจ(์๋์ฅ ๋จ๋
๋ถ๊ฐ)\n",
"์ฐจ๋จ: ๋ด๋น์ ID์ ์ญํ ํ์\n",
"์ฐจ๋จ: ํ์ฉ๋์ง ์์ ์กฐ์น: DISCHARGE\n",
"์ฐจ๋จ: ํ๋จ ๊ทผ๊ฑฐ 5์ ์ด์ ํ์\n",
"๋ณด๋ฅ ๊ธฐํ ์ด๊ณผ ์์ : ['S010']\n",
"์์ ํ ๊ฒํ ์์: ['S010', 'S002', 'S012']\n",
"\n",
" id grade score status escalated ๋๊ธฐ์๊ฐ(s)\n",
"S003 High 0.98 CLOSED False 7.0\n",
"S010 High 0.97 HOLD True -\n",
"S007 Medium 0.66 CLOSED False 16.0\n",
"S012 Medium 0.52 PENDING False -\n",
"S002 Medium 0.55 PENDING False -\n",
"S015 Low 0.35 CLOSED False 19.0\n",
"\n",
"์ฒ๋ฆฌ ๊ฒฐ์ 5๊ฑด ์ค ๋ก๊ทธ ๊ธฐ๋ก 5๊ฑด(์ฑ๊ณต๋ฅ 100%), ์ ์ฒด ๊ฐ์ฌ ๋ก๊ทธ 12๊ฑด, ํด์ ์ฒด์ธ ๊ฒ์ฆ: (True, -1)\n",
"8๋ฒ์งธ ๊ธฐ๋ก ๋ณ์กฐ ํ ๊ฒ์ฆ(False, ๊นจ์ง ์์น): (False, 8)\n",
"\n",
"์ฑ
์ ๋ถ๊ณ: AI๋ ์ํ ์ ํธ ์ ์๋ง, ํด์ ํ๋จ์ ์๋์ฅ(High๋ ๊ตฐ์๊ด), ์๋ฌด ๋ฐฐ์ ยท์๋ฌด๋ ์ธ๊ณ ์์ฒญ์ ๊ตฐ์๊ด, ์๋ฌด ๋ฐฐ์ ์ต์ข
์น์ธ์ ์งํ๊ด์ด๋ค.\n",
"๋ณธ ์ ์ฐจ๋ ํ์ต์ฉ ๋ชจ์์ด๋ฉฐ ์ง๋จยท์ฒ๋ฐฉ์ด ์๋๋ค. ์ค์ ์กฐ์น๋ ๊ตฐ์๊ดยท์๋ฌด๋์ ํ๋จ๊ณผ ์งํ๊ณํต์ ๋ฐ๋ฅธ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
08. ํญ์ผ/ํนํ ์ผ์ ๊ธฐ๋ํ๋ จ ๋ชจ์ ๋๋ ์์ฒด ๋ฐ์ดํฐ ์ ์
์คํธ๋ ์ค ๋ถํ ํ
์คํธ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"ํนํ/ํญ์ผ ๊ธฐ๋ํ๋ จ ๋ชจ์ ๋๋ ์์ฒด ๋ฐ์ดํฐ ์ ์
๋ถํ ํ
์คํธ๋ฅผ ์ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **ํญ์ผยทํนํ ๊ธฐ๋ํ๋ จ์์ ์์ฒด ๋ฐ์ดํฐ ์ ์
์ด ํฌ๊ฒ ๋์ด๋ ์์คํ
์ด ๋ฉ์ถ์ง ์๊ณ ๊ฒฝ๋ณด๋ฅผ ์์ง ์๋์ง ์ํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ ์ ๋ณ์ฌ ์๋ฅผ ํ์ 20๋ช
์์ ์ต๋ 120๋ช
(ํธ๋ํฝ 500% ์ฆ๊ฐ)๊น์ง ๋๋ฆฌ๊ณ ํจํท ์์ค, ์ผ์ ํ, ๊ธฐ๊ธฐ ๋์ , ๊ฒฝ๋ณด ์์ ํ์ธ ์์ค ์ฃผ์
ํ์ฌ ์ฒ๋ฆฌ ์ง์ฐ ๋ฐ ๋ณต์๋ ฅ์ ์ํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `tests/wel01_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": [
"\"\"\"ํญ์ผยทํนํ ๊ธฐ๋ํ๋ จ์์ ์์ฒด์ ํธ๋ฅผ ๋ณด๋ด๋ ๋ณ์ฌ ์๊ฐ ํ์์ 6๋ฐฐ(ํธ๋ํฝ 500% ์ฆ๊ฐ)๋ก ๋๊ณ ์ผ์ ํยท๊ฒฐ์ธกยท๊ธฐ๊ธฐ ๋์ ยท์ ์ก ์์ค์ด ๊ฒน์ณ๋ ์์คํ
์ด ๋ฉ์ถ์ง ์๋์ง(๋ค์ด 0๊ฑด), ์ํ ๊ฒฝ๋ณด๊ฐ ์ ์ค๋์ง ์๋์ง(์ ์ค๋ฅ 0%), ์ฉ๋ ์ด๊ณผ ์ ๊ธฐ๋ฅ ์ถ์ ๋ชจ๋์ ์ ํธ๋์ ๊ฒฝ๋ณด๊ฐ ์์ ํ๊ฒ ๋์ํ๋์ง ๊ฒ์ฆํ๋ค. ์ง๋จ ์๋ยท์๋ฌด๋ ์ฐ๊ณ.\"\"\"\n",
"import time\n",
"import warnings\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.linear_model import LogisticRegression\n",
"from sklearn.pipeline import make_pipeline\n",
"from sklearn.preprocessing import StandardScaler\n",
"\n",
"# ---------------- [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋๋ฏธ ์์ฒด์ ํธ ์์ฑ๊ธฐ (์ค์ ์ฅ๋ณยท๋ถ๋ยท์๋ฌด ๊ธฐ๋ก ์์) ----------------\n",
"# ๊ฐ์ ๋ณ์ฌ(๊ฐ๋ช
ID)๊ฐ 60๋ถ๊ฐ ๊ฑท๊ธฐยทํด์ยท๊ณ ๊ฐ๋ ํ๋์ ํ๋ ๋์์ ์ฌ๋ฐ์ยท์ฒด์จ(์ถ์ ์ฌ๋ถ)ยทํผ๋ถ์ ๋๋๋ฅผ 1๋ถ ๊ฐ๊ฒฉ์ผ๋ก ๋ง๋ ๋ค.\n",
"# ์ง๋จ ๋๊ตฌ๊ฐ ์๋๋ฉฐ, ์ํ ๋ผ๋ฒจ์ ๊ฐ์ ์๋ฆฌ ๋ชจ๋ธ์ '๊ต์ก์ฉ ๋จ๊ณ'์ผ ๋ฟ์ด๋ค.\n",
"N_MIN = 60\n",
"STAGE = {0: \"์ ์\", 1: \"์ฃผ์\", 2: \"๊ฒฝ๋ณด\"}\n",
"TYPES = [\"normal\", \"heat\", \"cold\"]\n",
"\n",
"\n",
"def make_person(i, rng):\n",
" return {\"id\": f\"S{i:03d}\", \"rest_hr\": float(rng.normal(64, 7)), \"age\": int(rng.integers(19, 27)), \"bmi\": float(rng.normal(23.5, 2.5)),\n",
" \"acclim\": int(rng.random() < 0.4), \"fit\": float(np.clip(rng.normal(0.5, 0.2), 0.05, 0.95))}\n",
"\n",
"\n",
"def make_env(kind, rng):\n",
" if kind == \"heat\":\n",
" return {\"temp_c\": float(rng.uniform(30, 40)), \"rh\": float(rng.uniform(45, 85)), \"wind\": float(rng.uniform(0.3, 3))}\n",
" if kind == \"cold\":\n",
" return {\"temp_c\": float(rng.uniform(-22, 2)), \"rh\": float(rng.uniform(40, 80)), \"wind\": float(rng.uniform(2, 11))}\n",
" return {\"temp_c\": float(rng.uniform(8, 24)), \"rh\": float(rng.uniform(35, 70)), \"wind\": float(rng.uniform(0.3, 5))}\n",
"\n",
"\n",
"def make_session(sid, kind, rng, p=None, glitch=True):\n",
" \"\"\"๋ฐํ: dict(person, env, kind, df[hr,temp,eda,act + ์ ๋ต coreยทstageยทrtype]). ์ผ์ ๊ฒฐ์ธกยทํยท๊ณ ์ฐฉ ํฌํจ.\"\"\"\n",
" p = p or make_person(sid, rng)\n",
" env = make_env(kind, rng)\n",
" act = np.zeros(N_MIN); t = 0\n",
" while t < N_MIN: # 10~20๋ถ ๋จ์ ํ๋ ๋ธ๋ก: ํด์ 1.5 / ํ๊ตฐ 4.5 / ๊ณ ๊ฐ๋ 7 MET\n",
" d = int(rng.integers(10, 21)); act[t:t + d] = rng.choice([1.5, 4.5, 7.0], p=[0.25, 0.5, 0.25]); t += d\n",
" hi = env[\"temp_c\"] + (0.12 * (env[\"rh\"] - 40) if env[\"temp_c\"] > 25 else 0)\n",
" wc = env[\"temp_c\"] - 1.6 * env[\"wind\"]\n",
" core, cores = 36.9, []\n",
" for m in range(N_MIN):\n",
" heat = 0.085 * max(0, hi - 24) * (1 - 0.25 * p[\"acclim\"]) * (1 + 0.5 * m / N_MIN) * (1 + 0.012 * (p[\"bmi\"] - 23.5))\n",
" cold = -0.075 * max(0, 8 - wc) * (1 - 0.09 * act[m])\n",
" ss = 36.9 + 0.09 * act[m] + heat + cold\n",
" core += (ss - core) / 18 + rng.normal(0, 0.03)\n",
" cores.append(core)\n",
" core = np.array(cores)\n",
" hr = p[\"rest_hr\"] + 9.5 * (act - 1.5) * (1.15 - 0.3 * p[\"fit\"]) + 11 * (core - 36.9) + rng.normal(0, 3.2, N_MIN)\n",
" eda = 2.0 + 0.25 * act + 3.2 * np.maximum(core - 37.2, 0) - 0.04 * np.maximum(8 - wc, 0) + rng.normal(0, 0.25, N_MIN)\n",
" bias = rng.normal(0, 0.1)\n",
" temp = core + bias + rng.normal(0, 0.1, N_MIN) - np.where((kind == \"cold\") & (env[\"wind\"] > 7), 0.25, 0.0) # ์ผ์ ์ ์ด ๋ถ์ ๋ฐ๋ ๋๊ฐ ์ค์ฐจ\n",
" df = pd.DataFrame({\"min\": np.arange(N_MIN), \"hr\": hr, \"temp\": temp, \"eda\": np.maximum(eda, 0.2), \"act\": act})\n",
" if glitch:\n",
" for col in (\"hr\", \"temp\", \"eda\"):\n",
" df.loc[rng.random(N_MIN) < 0.03, col] = np.nan # ๊ฒฐ์ธก\n",
" s = int(rng.integers(5, N_MIN))\n",
" if rng.random() < 0.12:\n",
" df.loc[s, \"hr\"] = 230.0 # ์ผ์ ํ\n",
" if rng.random() < 0.12:\n",
" df.loc[s, \"temp\"] = 45.0\n",
" ht = np.where(core >= 39.0, 2, np.where(core >= 38.3, 1, 0)); cd = np.where(core <= 35.4, 2, np.where(core <= 36.2, 1, 0))\n",
" df[\"core\"] = core\n",
" df[\"stage\"] = np.maximum(ht, cd); df[\"rtype\"] = np.where(ht > 0, \"heat\", np.where(cd > 0, \"cold\", \"normal\"))\n",
" return {\"id\": p[\"id\"], \"person\": p, \"env\": env, \"kind\": kind, \"df\": df}\n",
"\n",
"\n",
"def make_dataset(n, seed):\n",
" rng = np.random.default_rng(seed)\n",
" kinds = [\"normal\", \"heat\", \"cold\"]\n",
" return [make_session(i, kinds[i % 3], rng) for i in range(n)]\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---------------- ์ ์ฒ๋ฆฌยทํน์ง ์ถ์ถ ๊ณตํต ํจ์ ----------------\n",
"LIMITS = {\"hr\": (30, 220), \"temp\": (30.0, 43.0), \"eda\": (0.0, 40.0)} # ์๋ฆฌ์ ์ผ๋ก ๊ฐ๋ฅํ ๋ฒ์(๋ฒ์ ๋ฐ = ์ผ์ ์ค๋ฅ)\n",
"\n",
"\n",
"def clean(df):\n",
" \"\"\"๋ฒ์ ๋ฐ ๊ฐ์ ๊ฒฐ์ธก ์ฒ๋ฆฌ -> ์ ํ ๋ณด๊ฐ(์ ๋์ ๊ฐ๊น์ด ๊ฐ) -> 3๋ถ ์ด๋ ์ค์๊ฐ์ผ๋ก ํ ์ ๊ฑฐ. ๋ฐํ: (์ ์ df, ๋ณด์ ๊ฑด์)\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 8] ํญ์ผ/ํนํ ์ผ์ ๊ธฐ๋ํ๋ จ ๋ชจ์ ๋๋ ์์ฒด ๋ฐ์ดํฐ ์ ์
์คํธ๋ ์ค ๋ถํ ํ
์คํธ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def env_index(env):\n",
" # [TODO : ํ์ ์ค์ต 8] ํญ์ผ/ํนํ ์ผ์ ๊ธฐ๋ํ๋ จ ๋ชจ์ ๋๋ ์์ฒด ๋ฐ์ดํฐ ์ ์
์คํธ๋ ์ค ๋ถํ ํ
์คํธ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def features(s, use_env=True, personal=True):\n",
" \"\"\"์ธ์
1๊ฐ -> ๋ถ ๋จ์ ํน์งํ(์ต๊ทผ 5๋ถ ํ๊ท ยท๊ธฐ์ธ๊ธฐ ํฌํจ). ์ ๋ต stage/rtype ํฌํจ.\"\"\"\n",
" df, _ = clean(s[\"df\"]); p, e = s[\"person\"], s[\"env\"]\n",
" f = pd.DataFrame({\"sid\": s[\"id\"], \"min\": df[\"min\"]})\n",
" f[\"temp5\"] = df[\"temp\"].rolling(5, min_periods=1).mean()\n",
" f[\"hr5\"] = df[\"hr\"].rolling(5, min_periods=1).mean()\n",
" f[\"eda5\"] = df[\"eda\"].rolling(5, min_periods=1).mean()\n",
" f[\"dtemp\"] = (f[\"temp5\"] - f[\"temp5\"].shift(4)).fillna(0)\n",
" f[\"act\"] = df[\"act\"]\n",
" if personal:\n",
" f[\"hr_rel\"] = f[\"hr5\"] - p[\"rest_hr\"]\n",
" f[\"acclim\"], f[\"bmi\"], f[\"fit\"] = p[\"acclim\"], p[\"bmi\"], p[\"fit\"]\n",
" if use_env:\n",
" f[\"hi\"], f[\"wc\"] = env_index(e); f[\"wind\"] = e[\"wind\"]\n",
" f[\"stage\"], f[\"rtype\"], f[\"kind\"] = df[\"stage\"], df[\"rtype\"], s[\"kind\"]\n",
" return f[f[\"min\"] >= 5].reset_index(drop=True)\n",
"\n",
"\n",
"def table(sessions, **kw):\n",
" return pd.concat([features(s, **kw) for s in sessions], ignore_index=True)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"warnings.simplefilter(\"ignore\", RuntimeWarning) # ์ ํธ ์ ์ฒด ๋์ ์ ๋น ํ๊ท ๊ฒฝ๊ณ ๋ ์ ํธ๋์ ์ฒ๋ฆฌ๋ก ๋์ฒด๋จ\n",
"BASE_N = 20 # ํ์ ๋์ ์ ์ ๋ณ์ฌ ์\n",
"FULL_CAPACITY = 60 # ํ ํฑ(1์ด)์ ์ ๋ฐ ๋ชจ๋ธ๋ก ์ฒ๋ฆฌํ๋ ๋ณ์ฌ ์(์ด๊ณผ๋ถ์ ๊ธฐ๋ฅ ์ถ์ ๋ชจ๋)\n",
"FE = [\"temp5\", \"dtemp\", \"hr5\", \"eda5\", \"hr_rel\", \"acclim\", \"bmi\", \"fit\", \"hi\", \"wc\", \"wind\"]\n",
"tr = table(make_dataset(120, seed=51))\n",
"model = make_pipeline(StandardScaler(), LogisticRegression(class_weight=\"balanced\", max_iter=3000, random_state=0)).fit(tr[FE], tr[\"stage\"] >= 1)\n",
"\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ณ์ฌ 120๋ช
ํ(ํญ์ผ 60 / ํนํ 60): ๋ถ ๋จ์ ์์ ์ ํธ๋ฅผ [๋ณ์ฌ, ๋ถ] ๋ฐฐ์ด๋ก ๋ณด๊ด\n",
"rng0 = np.random.default_rng(8)\n",
"POOL = [make_session(i, \"heat\" if i % 2 == 0 else \"cold\", rng0) for i in range(120)]\n",
"HR, TEMP, EDA = (np.array([s[\"df\"][c].values for s in POOL]) for c in (\"hr\", \"temp\", \"eda\"))\n",
"STAGE_ = np.array([s[\"df\"][\"stage\"].values for s in POOL])\n",
"STATIC = pd.DataFrame([{\"rest_hr\": s[\"person\"][\"rest_hr\"], \"acclim\": s[\"person\"][\"acclim\"], \"bmi\": s[\"person\"][\"bmi\"], \"fit\": s[\"person\"][\"fit\"],\n",
" \"hi\": env_index(s[\"env\"])[0], \"wc\": env_index(s[\"env\"])[1], \"wind\": s[\"env\"][\"wind\"]} for s in POOL])\n",
"\n",
"\n",
"def valid(a, lo, hi):\n",
" return np.where((a >= lo) & (a <= hi), a, np.nan)\n",
"\n",
"\n",
"def window_mean(arr, m, lo, hi, w=5):\n",
" return np.nanmean(valid(arr[:, max(m - w + 1, 0):m + 1], lo, hi), axis=1)\n",
"\n",
"\n",
"def tick(idx, m, hr, temp, eda, mode_full):\n",
" \"\"\"๋ณ์ฌ idx์ ๋ถ m ์ ํธ๋ฅผ ์ฒ๋ฆฌํด ๊ฒฝ๋ณด ๋ชฉ๋ก๊ณผ ์ ํธ๋์ ๋ชฉ๋ก์ ๋ฐํํ๋ค.\"\"\"\n",
" with np.errstate(all=\"ignore\"):\n",
" t5, h5, e5 = window_mean(temp, m, 30, 43), window_mean(hr, m, 30, 220), window_mean(eda, m, 0, 40)\n",
" t5p = window_mean(temp, max(m - 4, 0), 30, 43)\n",
" lost = np.isnan(t5) | np.isnan(h5) # ์ต๊ทผ 5๋ถ ์ ํจ ๊ฐ ์์ = ์ ํธ๋์ \n",
" ok = ~lost\n",
" alert = np.zeros(len(idx), bool)\n",
" if mode_full.any():\n",
" sel = ok & mode_full\n",
" if sel.any():\n",
" S = STATIC.iloc[idx[sel]]\n",
" X = pd.DataFrame({\"temp5\": t5[sel], \"dtemp\": np.nan_to_num(t5[sel] - t5p[sel]), \"hr5\": h5[sel], \"eda5\": np.nan_to_num(e5[sel], nan=2.0),\n",
" \"hr_rel\": h5[sel] - S[\"rest_hr\"].values, \"acclim\": S[\"acclim\"].values, \"bmi\": S[\"bmi\"].values, \"fit\": S[\"fit\"].values,\n",
" \"hi\": S[\"hi\"].values, \"wc\": S[\"wc\"].values, \"wind\": S[\"wind\"].values})[FE]\n",
" alert[np.where(sel)[0]] = model.predict_proba(X)[:, 1] >= 0.5\n",
" light = ok & ~mode_full\n",
" alert[light] = (t5[light] >= 38.0) | (t5[light] <= 36.3) # ๊ธฐ๋ฅ ์ถ์: ์ฒด์จ ๊ธฐ์ค๋ง(์ค๊ฒฝ๋ณด๋ ๋๊ณ ๋ฏธํ์ ํผํจ)\n",
" return alert, lost\n",
"\n",
"\n",
"def scenario(n, loss=0.0, spike=0.0, dead=0, ack_loss=0.0, seed=0):\n",
" rng = np.random.default_rng(seed)\n",
" idx = np.arange(n) % len(POOL)\n",
" hr, temp, eda = HR[idx].copy(), TEMP[idx].copy(), EDA[idx].copy()\n",
" for a in (hr, temp, eda):\n",
" a[rng.random(a.shape) < loss] = np.nan # ํจํท ์์ค๋ก ์ธํ ๊ฒฐ์ธก\n",
" m_sp = rng.random(hr.shape) < spike; hr[m_sp] = 230.0; temp[m_sp] = 45.0 # ์ผ์ ํ\n",
" dead_ids = rng.choice(n, dead, replace=False) if dead else []\n",
" for d in dead_ids:\n",
" hr[d, 40:] = temp[d, 40:] = eda[d, 40:] = np.nan # ๊ธฐ๊ธฐ ๋์ (๋ฐฐํฐ๋ฆฌยท์ดํ)\n",
" mode_full = np.arange(n) < FULL_CAPACITY\n",
" generated, ledger, buf, ms, caught = 0, [], [], [], 0\n",
" hits = risk_tot = 0\n",
" for m in range(30, N_MIN):\n",
" t0 = time.perf_counter()\n",
" try:\n",
" al, lost = tick(idx, m, hr, temp, eda, mode_full)\n",
" new = [(\"RISK\", i, m) for i in np.where(al)[0]] + [(\"LOST\", i, m) for i in np.where(lost)[0]]\n",
" truth = STAGE_[idx, m] >= 1; hits += int((al & truth).sum()); risk_tot += int(truth.sum())\n",
" except Exception as e: # ์ฒ๋ฆฌ ์ค๋ฅ๋ ๊ฒฉ๋ฆฌํ๊ณ ์ด๋ฒ ํฑ ์ ์ฒด๋ฅผ ์ ํธ๋์ ๊ฒฝ๋ณด๋ก ๋์ฒด\n",
" caught += 1; new = [(\"LOST\", i, m) for i in range(n)]\n",
" generated += len(new); buf += new\n",
" for a in list(buf): # ์ ์ก: ์์ค ์ ๋ฒํผ์ ๋จ๊ฒจ ๋ค์ ํฑ ์ฌ์ ์ก\n",
" if rng.random() >= ack_loss:\n",
" ledger.append(a); buf.remove(a)\n",
" ms.append((time.perf_counter() - t0) * 1000)\n",
" ledger += buf # ๋ง์ง๋ง์ ๋จ์ ๊ฑด์ ์์ ํ์ธ ํ ๋ฐ์\n",
" kinds = pd.Series([a[0] for a in ledger]).value_counts().to_dict()\n",
" return {\"๋ณ์ฌ ์\": n, \"๋ฐฐ์จ\": f\"x{n / BASE_N:.0f}\", \"์์ฑ ๊ฒฝ๋ณด\": generated, \"๊ธฐ๋ก ๊ฒฝ๋ณด\": len(ledger), \"์ ์ค๋ฅ \": f\"{1 - len(ledger) / max(generated, 1):.0%}\",\n",
" \"์ํ ์ฌํ์จ\": f\"{hits / max(risk_tot, 1):.0%}\", \"์ ํธ๋์ ๊ฒฝ๋ณด\": kinds.get(\"LOST\", 0), \"์ฒ๋ฆฌ์ค๋ฅ ๊ฒฉ๋ฆฌ\": caught, \"์ถ์๋ชจ๋ ๋ณ์ฌ\": max(n - FULL_CAPACITY, 0),\n",
" \"ํฑ ํ๊ท ms\": round(float(np.mean(ms)), 1)}, kinds\n",
"\n",
"\n",
"cases = [(\"ํ์ 20๋ช
\", dict(n=20)), (\"x2 40๋ช
\", dict(n=40)), (\"x4 80๋ช
\", dict(n=80)), (\"x6 120๋ช
(ํธ๋ํฝ 500% ์ฆ๊ฐ)\", dict(n=120)),\n",
" (\"x6 + ํจํท์์ค 20% + ์ผ์ํ 3%\", dict(n=120, loss=0.2, spike=0.03, ack_loss=0.3)),\n",
" (\"x6 + ๊ธฐ๊ธฐ๋์ 8๋ช
+ ํจํท์์ค 30%\", dict(n=120, loss=0.3, dead=8, ack_loss=0.3))]\n",
"rows = []\n",
"for i, (name, kw) in enumerate(cases):\n",
" try:\n",
" r, kinds = scenario(seed=i, **kw)\n",
" except Exception as e:\n",
" r = {\"๋ณ์ฌ ์\": kw[\"n\"], \"๋ฐฐ์จ\": \"-\", \"์์ฑ ๊ฒฝ๋ณด\": 0, \"๊ธฐ๋ก ๊ฒฝ๋ณด\": 0, \"์ ์ค๋ฅ \": \"100%\", \"์ํ ์ฌํ์จ\": \"-\", \"์ ํธ๋์ ๊ฒฝ๋ณด\": 0, \"์ฒ๋ฆฌ์ค๋ฅ ๊ฒฉ๋ฆฌ\": 0, \"์ถ์๋ชจ๋ ๋ณ์ฌ\": 0, \"ํฑ ํ๊ท ms\": 0}\n",
" print(\"๋ค์ด:\", e); r[\"๋ค์ด\"] = 1\n",
" r.setdefault(\"๋ค์ด\", 0)\n",
" rows.append({\"์ํฉ\": name, **r})\n",
"df = pd.DataFrame(rows)\n",
"print(df.drop(columns=[\"์ฒ๋ฆฌ์ค๋ฅ ๊ฒฉ๋ฆฌ\"]).to_string(index=False))\n",
"print(f\"\\n์์คํ
๋ค์ด {df['๋ค์ด'].sum()}๊ฑด(๋ชฉํ 0๊ฑด), ์ต๋ ์ ์ค๋ฅ {df['์ ์ค๋ฅ '].max()}(๋ชฉํ 0%) -> {'๋ฌ์ฑ' if df['๋ค์ด'].sum() == 0 and df['์ ์ค๋ฅ '].max() == '0%' else '๋ฏธ๋ฌ'}\")\n",
"\n",
"# ๊ธฐ๋ฅ ์ถ์ ๋ชจ๋์ ๋๊ฐ: ๊ฐ์ ๋ณ์ฌ๋ฅผ ์ ๋ฐยท์ถ์ ๋ชจ๋๋ก ์ฒ๋ฆฌํ์ ๋ ๊ฒฝ๋ณด ์์ ์ํ ์ฌํ์จ\n",
"n = 120; idx = np.arange(n)\n",
"res = {}\n",
"for name, full in ((\"์ ๋ฐ\", np.ones(n, bool)), (\"์ถ์\", np.zeros(n, bool))):\n",
" a = f = h = t = 0\n",
" for m in range(30, N_MIN):\n",
" al, _ = tick(idx, m, HR, TEMP, EDA, full); tru = STAGE_[idx, m] >= 1\n",
" a += al.sum(); f += (al & ~tru).sum(); h += (al & tru).sum(); t += tru.sum()\n",
" res[name] = (a, f / max(a, 1), h / t)\n",
"print(f\"์ ๋ฐ ๋ชจ๋: ๊ฒฝ๋ณด {res['์ ๋ฐ'][0]}๊ฑด(์ค๊ฒฝ๋ณด์จ {res['์ ๋ฐ'][1]:.1%}, ์ํ ์ฌํ์จ {res['์ ๋ฐ'][2]:.1%}) / ์ถ์ ๋ชจ๋: ๊ฒฝ๋ณด {res['์ถ์'][0]}๊ฑด(์ค๊ฒฝ๋ณด์จ {res['์ถ์'][1]:.1%}, ์ํ ์ฌํ์จ {res['์ถ์'][2]:.1%})\")\n",
"print(\"์์ ์คํจ ์์น: ์ฉ๋์ ๋์ผ๋ฉด ์ ๋ฐ๋๋ฅผ ๋ฎ์ถ๋๋ผ๋ ์ํ ๊ฒฝ๋ณด๋ฅผ ๋์น์ง ์๋ ์ชฝ(์ฒด์จ ๊ธฐ์ค ๋จ์ ๋ชจ๋)์ผ๋ก, ์ ํธ๊ฐ ๋๊ธฐ๋ฉด '์ ์'์ด ์๋๋ผ '์ ํธ๋์ ํ์ธ ํ์'๋ก ํ์ํ๋ค. ๊ฒฝ๋ณด๋ ์ด๋ ๊ฒฝ์ฐ์๋ ๋ฒ๋ฆฌ์ง ์๊ณ ์์ ํ์ธ ํ ์ญ์ ํ๋ค.\")\n",
"print(\"๋ณธ ๊ฒฐ๊ณผ๋ ๊ต์ก์ฉ ๊ฐ์ ์๋ฃ์ ์์คํ
์ํ์ด๋ฉฐ ์ง๋จ์ด ์๋๋ค. ์ค์ ์ด์ ์งํ๋ ์๋ฌด๋ยท๊ตฐ์๊ด์๊ฒ ์ฐ๊ณํ๋ค.\")\n",
"\n",
"plt.figure(figsize=(9, 4))\n",
"plt.bar(range(len(df)), df[\"ํฑ ํ๊ท ms\"], color=[\"tab:blue\"] * 3 + [\"tab:orange\"] * 3)\n",
"plt.xticks(range(len(df)), [f\"case{i + 1}\" for i in range(len(df))]); plt.axhline(1000, color=\"red\", ls=\"--\", label=\"1 s refresh budget\")\n",
"plt.ylabel(\"ms per tick\"); plt.legend(); plt.title(\"Stress test: tick latency (orange = 120 soldiers)\")\n",
"plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
08 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
" ์ํฉ ๋ณ์ฌ ์ ๋ฐฐ์จ ์์ฑ ๊ฒฝ๋ณด ๊ธฐ๋ก ๊ฒฝ๋ณด ์ ์ค๋ฅ ์ํ ์ฌํ์จ ์ ํธ๋์ ๊ฒฝ๋ณด ์ถ์๋ชจ๋ ๋ณ์ฌ ํฑ ํ๊ท ms ๋ค์ด\n",
" ํ์ 20๋ช
20 x1 377 377 0% 97% 0 0 3.2 0\n",
" x2 40๋ช
40 x2 790 790 0% 96% 0 0 3.4 0\n",
" x4 80๋ช
80 x4 1768 1768 0% 98% 0 20 4.4 0\n",
" x6 120๋ช
(ํธ๋ํฝ 500% ์ฆ๊ฐ) 120 x6 2818 2818 0% 98% 0 60 3.4 0\n",
" x6 + ํจํท์์ค 20% + ์ผ์ํ 3% 120 x6 2801 2801 0% 98% 10 60 4.2 0\n",
"x6 + ๊ธฐ๊ธฐ๋์ 8๋ช
+ ํจํท์์ค 30% 120 x6 2812 2812 0% 93% 158 60 3.6 0\n",
"\n",
"์์คํ
๋ค์ด 0๊ฑด(๋ชฉํ 0๊ฑด), ์ต๋ ์ ์ค๋ฅ 0%(๋ชฉํ 0%) -> ๋ฌ์ฑ\n",
"์ ๋ฐ ๋ชจ๋: ๊ฒฝ๋ณด 2655๊ฑด(์ค๊ฒฝ๋ณด์จ 14.6%, ์ํ ์ฌํ์จ 97.2%) / ์ถ์ ๋ชจ๋: ๊ฒฝ๋ณด 2986๊ฑด(์ค๊ฒฝ๋ณด์จ 22.1%, ์ํ ์ฌํ์จ 99.7%)\n",
"์์ ์คํจ ์์น: ์ฉ๋์ ๋์ผ๋ฉด ์ ๋ฐ๋๋ฅผ ๋ฎ์ถ๋๋ผ๋ ์ํ ๊ฒฝ๋ณด๋ฅผ ๋์น์ง ์๋ ์ชฝ(์ฒด์จ ๊ธฐ์ค ๋จ์ ๋ชจ๋)์ผ๋ก, ์ ํธ๊ฐ ๋๊ธฐ๋ฉด '์ ์'์ด ์๋๋ผ '์ ํธ๋์ ํ์ธ ํ์'๋ก ํ์ํ๋ค. ๊ฒฝ๋ณด๋ ์ด๋ ๊ฒฝ์ฐ์๋ ๋ฒ๋ฆฌ์ง ์๊ณ ์์ ํ์ธ ํ ์ญ์ ํ๋ค.\n",
"๋ณธ ๊ฒฐ๊ณผ๋ ๊ต์ก์ฉ ๊ฐ์ ์๋ฃ์ ์์คํ
์ํ์ด๋ฉฐ ์ง๋จ์ด ์๋๋ค. ์ค์ ์ด์ ์งํ๋ ์๋ฌด๋ยท๊ตฐ์๊ด์๊ฒ ์ฐ๊ณํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
09. AAR ๊ธฐ๋ฐ ์ค๊ฒฝ๋ณด ์ ๋ฐ ์์ฒด ํจํด ๋ถ์ ๋ฐ ์ํ ์๊ณ๊ฐ ํ๋ผ๋ฏธํฐ ์ฌ๋ณด์ \n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"AAR ๊ธฐ๋ฐ ์ค๊ฒฝ๋ณด ์ ๋ฐ ์์ฒด ํจํด ๋ถ์ ๋ฐ ์ํ ์๊ณ๊ฐ์ ์ฌ๋ณด์ ํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **AAR(์ฌํ๊ฒํ )๋ก ์ค๊ฒฝ๋ณด๋ฅผ ์ ๋ฐํ ์์ฒด ํจํด์ ๋ถ์ํ๊ณ ์ํ ์๊ณ๊ฐ ํ๋ผ๋ฏธํฐ๋ฅผ ์ฌ๋ณด์ ํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ด๊ธฐ ์ค์ (์ด ๊ธฐ์ค 37.8๋, ํ๋ญ ๊ธฐ์ค 36.6๋, ์ง์ 1๋ถ)์ ๋ชจ์ ์ด์ฉ ๊ฒฐ๊ณผ์์ ์ค๊ฒฝ๋ณด๋ฅผ ๊ฐํ ์ผ์ ๋๊ฐ, ์ผ์ ํ, ์๊ณ๊ฐ ๊ทผ์ ๋ฑ ์์ธ๋ณ๋ก ๋ถ๋ฅ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `docs/wel01_aar_report.md ๋ฐ configs/wel01_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(์ฌํ๊ฒํ ) ์ผ์ง ํ์์ผ๋ก ๋ชจ์ ์ด์ฉ์ ์ค๊ฒฝ๋ณดยท๋ฏธํ์ ์์ธ๋ณ(์ผ์ ๋๊ฐ ์ค์ฐจ, ์ผ์ ํ, ์๊ณ๊ฐ ๊ทผ์ ๋ฑ)๋ก ๋ถ๋ฅํ๊ณ , ์ฒด์จ ์๊ณ๊ฐยท์ง์ ์๊ฐยท์ผ์ ๋๊ฐ ๋ณด์ ์ ์ ์ฝ ์กฐ๊ฑด ์์์ ๊ฒฉ์ ํ์์ผ๋ก ์ฌ๋ณด์ ํด ๊ฐ์ ์ ํ ์ฑ๋ฅ๊ณผ ์ฌ๋ฐ ์ฌ๋ถ๋ฅผ ๋น๊ตํ๋ค. ์ง๋จ ์๋ยท์๋ฌด๋ ์ฐ๊ณ.\"\"\"\n",
"import itertools\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------- [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋๋ฏธ ์์ฒด์ ํธ ์์ฑ๊ธฐ (์ค์ ์ฅ๋ณยท๋ถ๋ยท์๋ฌด ๊ธฐ๋ก ์์) ----------------\n",
"# ๊ฐ์ ๋ณ์ฌ(๊ฐ๋ช
ID)๊ฐ 60๋ถ๊ฐ ๊ฑท๊ธฐยทํด์ยท๊ณ ๊ฐ๋ ํ๋์ ํ๋ ๋์์ ์ฌ๋ฐ์ยท์ฒด์จ(์ถ์ ์ฌ๋ถ)ยทํผ๋ถ์ ๋๋๋ฅผ 1๋ถ ๊ฐ๊ฒฉ์ผ๋ก ๋ง๋ ๋ค.\n",
"# ์ง๋จ ๋๊ตฌ๊ฐ ์๋๋ฉฐ, ์ํ ๋ผ๋ฒจ์ ๊ฐ์ ์๋ฆฌ ๋ชจ๋ธ์ '๊ต์ก์ฉ ๋จ๊ณ'์ผ ๋ฟ์ด๋ค.\n",
"N_MIN = 60\n",
"STAGE = {0: \"์ ์\", 1: \"์ฃผ์\", 2: \"๊ฒฝ๋ณด\"}\n",
"TYPES = [\"normal\", \"heat\", \"cold\"]\n",
"\n",
"\n",
"def make_person(i, rng):\n",
" return {\"id\": f\"S{i:03d}\", \"rest_hr\": float(rng.normal(64, 7)), \"age\": int(rng.integers(19, 27)), \"bmi\": float(rng.normal(23.5, 2.5)),\n",
" \"acclim\": int(rng.random() < 0.4), \"fit\": float(np.clip(rng.normal(0.5, 0.2), 0.05, 0.95))}\n",
"\n",
"\n",
"def make_env(kind, rng):\n",
" if kind == \"heat\":\n",
" return {\"temp_c\": float(rng.uniform(30, 40)), \"rh\": float(rng.uniform(45, 85)), \"wind\": float(rng.uniform(0.3, 3))}\n",
" if kind == \"cold\":\n",
" return {\"temp_c\": float(rng.uniform(-22, 2)), \"rh\": float(rng.uniform(40, 80)), \"wind\": float(rng.uniform(2, 11))}\n",
" return {\"temp_c\": float(rng.uniform(8, 24)), \"rh\": float(rng.uniform(35, 70)), \"wind\": float(rng.uniform(0.3, 5))}\n",
"\n",
"\n",
"def make_session(sid, kind, rng, p=None, glitch=True):\n",
" \"\"\"๋ฐํ: dict(person, env, kind, df[hr,temp,eda,act + ์ ๋ต coreยทstageยทrtype]). ์ผ์ ๊ฒฐ์ธกยทํยท๊ณ ์ฐฉ ํฌํจ.\"\"\"\n",
" p = p or make_person(sid, rng)\n",
" env = make_env(kind, rng)\n",
" act = np.zeros(N_MIN); t = 0\n",
" while t < N_MIN: # 10~20๋ถ ๋จ์ ํ๋ ๋ธ๋ก: ํด์ 1.5 / ํ๊ตฐ 4.5 / ๊ณ ๊ฐ๋ 7 MET\n",
" d = int(rng.integers(10, 21)); act[t:t + d] = rng.choice([1.5, 4.5, 7.0], p=[0.25, 0.5, 0.25]); t += d\n",
" hi = env[\"temp_c\"] + (0.12 * (env[\"rh\"] - 40) if env[\"temp_c\"] > 25 else 0)\n",
" wc = env[\"temp_c\"] - 1.6 * env[\"wind\"]\n",
" core, cores = 36.9, []\n",
" for m in range(N_MIN):\n",
" heat = 0.085 * max(0, hi - 24) * (1 - 0.25 * p[\"acclim\"]) * (1 + 0.5 * m / N_MIN) * (1 + 0.012 * (p[\"bmi\"] - 23.5))\n",
" cold = -0.075 * max(0, 8 - wc) * (1 - 0.09 * act[m])\n",
" ss = 36.9 + 0.09 * act[m] + heat + cold\n",
" core += (ss - core) / 18 + rng.normal(0, 0.03)\n",
" cores.append(core)\n",
" core = np.array(cores)\n",
" hr = p[\"rest_hr\"] + 9.5 * (act - 1.5) * (1.15 - 0.3 * p[\"fit\"]) + 11 * (core - 36.9) + rng.normal(0, 3.2, N_MIN)\n",
" eda = 2.0 + 0.25 * act + 3.2 * np.maximum(core - 37.2, 0) - 0.04 * np.maximum(8 - wc, 0) + rng.normal(0, 0.25, N_MIN)\n",
" bias = rng.normal(0, 0.1)\n",
" temp = core + bias + rng.normal(0, 0.1, N_MIN) - np.where((kind == \"cold\") & (env[\"wind\"] > 7), 0.25, 0.0) # ์ผ์ ์ ์ด ๋ถ์ ๋ฐ๋ ๋๊ฐ ์ค์ฐจ\n",
" df = pd.DataFrame({\"min\": np.arange(N_MIN), \"hr\": hr, \"temp\": temp, \"eda\": np.maximum(eda, 0.2), \"act\": act})\n",
" if glitch:\n",
" for col in (\"hr\", \"temp\", \"eda\"):\n",
" df.loc[rng.random(N_MIN) < 0.03, col] = np.nan # ๊ฒฐ์ธก\n",
" s = int(rng.integers(5, N_MIN))\n",
" if rng.random() < 0.12:\n",
" df.loc[s, \"hr\"] = 230.0 # ์ผ์ ํ\n",
" if rng.random() < 0.12:\n",
" df.loc[s, \"temp\"] = 45.0\n",
" ht = np.where(core >= 39.0, 2, np.where(core >= 38.3, 1, 0)); cd = np.where(core <= 35.4, 2, np.where(core <= 36.2, 1, 0))\n",
" df[\"core\"] = core\n",
" df[\"stage\"] = np.maximum(ht, cd); df[\"rtype\"] = np.where(ht > 0, \"heat\", np.where(cd > 0, \"cold\", \"normal\"))\n",
" return {\"id\": p[\"id\"], \"person\": p, \"env\": env, \"kind\": kind, \"df\": df}\n",
"\n",
"\n",
"def make_dataset(n, seed):\n",
" rng = np.random.default_rng(seed)\n",
" kinds = [\"normal\", \"heat\", \"cold\"]\n",
" return [make_session(i, kinds[i % 3], rng) for i in range(n)]\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---------------- ์ ์ฒ๋ฆฌยทํน์ง ์ถ์ถ ๊ณตํต ํจ์ ----------------\n",
"LIMITS = {\"hr\": (30, 220), \"temp\": (30.0, 43.0), \"eda\": (0.0, 40.0)} # ์๋ฆฌ์ ์ผ๋ก ๊ฐ๋ฅํ ๋ฒ์(๋ฒ์ ๋ฐ = ์ผ์ ์ค๋ฅ)\n",
"\n",
"\n",
"def clean(df):\n",
" \"\"\"๋ฒ์ ๋ฐ ๊ฐ์ ๊ฒฐ์ธก ์ฒ๋ฆฌ -> ์ ํ ๋ณด๊ฐ(์ ๋์ ๊ฐ๊น์ด ๊ฐ) -> 3๋ถ ์ด๋ ์ค์๊ฐ์ผ๋ก ํ ์ ๊ฑฐ. ๋ฐํ: (์ ์ df, ๋ณด์ ๊ฑด์)\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 9] AAR ๊ธฐ๋ฐ ์ค๊ฒฝ๋ณด ์ ๋ฐ ์์ฒด ํจํด ๋ถ์ ๋ฐ ์ํ ์๊ณ๊ฐ ํ๋ผ๋ฏธํฐ ์ฌ๋ณด์ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def env_index(env):\n",
" # [TODO : ํ์ ์ค์ต 9] AAR ๊ธฐ๋ฐ ์ค๊ฒฝ๋ณด ์ ๋ฐ ์์ฒด ํจํด ๋ถ์ ๋ฐ ์ํ ์๊ณ๊ฐ ํ๋ผ๋ฏธํฐ ์ฌ๋ณด์ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def features(s, use_env=True, personal=True):\n",
" \"\"\"์ธ์
1๊ฐ -> ๋ถ ๋จ์ ํน์งํ(์ต๊ทผ 5๋ถ ํ๊ท ยท๊ธฐ์ธ๊ธฐ ํฌํจ). ์ ๋ต stage/rtype ํฌํจ.\"\"\"\n",
" df, _ = clean(s[\"df\"]); p, e = s[\"person\"], s[\"env\"]\n",
" f = pd.DataFrame({\"sid\": s[\"id\"], \"min\": df[\"min\"]})\n",
" f[\"temp5\"] = df[\"temp\"].rolling(5, min_periods=1).mean()\n",
" f[\"hr5\"] = df[\"hr\"].rolling(5, min_periods=1).mean()\n",
" f[\"eda5\"] = df[\"eda\"].rolling(5, min_periods=1).mean()\n",
" f[\"dtemp\"] = (f[\"temp5\"] - f[\"temp5\"].shift(4)).fillna(0)\n",
" f[\"act\"] = df[\"act\"]\n",
" if personal:\n",
" f[\"hr_rel\"] = f[\"hr5\"] - p[\"rest_hr\"]\n",
" f[\"acclim\"], f[\"bmi\"], f[\"fit\"] = p[\"acclim\"], p[\"bmi\"], p[\"fit\"]\n",
" if use_env:\n",
" f[\"hi\"], f[\"wc\"] = env_index(e); f[\"wind\"] = e[\"wind\"]\n",
" f[\"stage\"], f[\"rtype\"], f[\"kind\"] = df[\"stage\"], df[\"rtype\"], s[\"kind\"]\n",
" return f[f[\"min\"] >= 5].reset_index(drop=True)\n",
"\n",
"\n",
"def table(sessions, **kw):\n",
" return pd.concat([features(s, **kw) for s in sessions], ignore_index=True)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"rng = np.random.default_rng(13)\n",
"OPS = []\n",
"for i in range(90):\n",
" s = make_session(i, [\"heat\", \"cold\", \"normal\"][i % 3], rng)\n",
" if s[\"kind\"] == \"cold\" and i % 2 == 0:\n",
" s[\"env\"][\"wind\"] = float(rng.uniform(7.5, 11)) # ๊ฐํ ํนํ: ์ผ์ ๋๊ฐ ์ค์ฐจ ๊ตฌ๊ฐ\n",
" OPS.append(s)\n",
"TAB = table(OPS)\n",
"SPIKE = {s[\"id\"]: set(np.where((s[\"df\"][\"hr\"] > 220) | (s[\"df\"][\"temp\"] > 43))[0]) for s in OPS}\n",
"\n",
"\n",
"def alerts(tab, th_heat, th_cold, persist, corr):\n",
" \"\"\"์ฒด์จ ๊ธฐ์ค ๊ฒฝ๋ณด: ์ผ์ ๋๊ฐ ๋ณด์ (๊ฐํยทํ๋ญ ํ๊ฒฝ์์ corr๋งํผ ๋ณด์ ) ํ ์๊ณ๊ฐ์ persist๋ถ ์ฐ์ ๋์ด์ผ ๊ฒฝ๋ณด.\"\"\"\n",
" t = tab[\"temp5\"] + np.where((tab[\"wind\"] >= 7) & (tab[\"wc\"] < 0), corr, 0.0)\n",
" raw = (t >= th_heat) | (t <= th_cold)\n",
" return raw.groupby(tab[\"sid\"]).transform(lambda v: v.rolling(persist, min_periods=persist).min().fillna(0).astype(bool))\n",
"\n",
"\n",
"def summarize(tab, al):\n",
" truth = tab[\"stage\"] >= 1\n",
" tp = (al & truth).sum(); far = (al & ~truth).sum() / max(al.sum(), 1); rec = tp / truth.sum()\n",
" return {\"๊ฒฝ๋ณด\": int(al.sum()), \"์ค๊ฒฝ๋ณด์จ\": round(float(far), 3), \"์ฌํ์จ\": round(float(rec), 3), \"์ข
ํฉ์ ํ๋(F1)\": round(float(2 * (1 - far) * rec / max(1 - far + rec, 1e-9)), 3)}\n",
"\n",
"\n",
"def cause(r):\n",
" near = min(abs(r[\"temp5\"] - 38.0), abs(r[\"temp5\"] - 36.3)) < 0.35\n",
" if r[\"wind\"] >= 7 and r[\"wc\"] < 0:\n",
" return \"๊ฐํ ์ผ์ ๋๊ฐ ์ค์ฐจ\"\n",
" if any(m in SPIKE[r[\"sid\"]] for m in range(int(r[\"min\"]) - 4, int(r[\"min\"]) + 1)):\n",
" return \"์ผ์ ํ ์ํฅ\"\n",
" return \"์๊ณ๊ฐ ๊ทผ์ (์กฐ๊ธฐ ๊ฒฝ๋ณด)\" if near else \"๊ธฐํ\"\n",
"\n",
"\n",
"def aar_log(tab, al):\n",
" rows = [{\"๊ตฌ๋ถ\": \"์ค๊ฒฝ๋ณด\", \"์์ธ\": cause(r), \"๋ณ์ฌ\": r[\"sid\"], \"์ ํ\": r[\"rtype\"]} for r in tab[al & (tab[\"stage\"] == 0)].to_dict(\"records\")]\n",
" miss = tab[~al & (tab[\"stage\"] >= 1)]\n",
" rows += [{\"๊ตฌ๋ถ\": \"๋ฏธํ\", \"์์ธ\": \"์๊ณ๊ฐยท์ง์์๊ฐ ๋ฏธ์ถฉ์กฑ\", \"๋ณ์ฌ\": sid, \"์ ํ\": \"-\"} for sid in miss[\"sid\"].unique() for _ in range(int(len(miss[miss[\"sid\"] == sid]) > 0))]\n",
" return pd.DataFrame(rows, columns=[\"๊ตฌ๋ถ\", \"์์ธ\", \"๋ณ์ฌ\", \"์ ํ\"])\n",
"\n",
"\n",
"# 1) ์ด๊ธฐ ์ค์ ์ผ๋ก ๋ชจ์ ์ด์ฉ: '๋์น์ง ์๊ธฐ' ์ฐ์ (๋ฎ์ ์ด ๊ธฐ์ค, ๋์ ํ๋ญ ๊ธฐ์ค, ์ง์ 1๋ถ)\n",
"INIT = {\"th_heat\": 37.8, \"th_cold\": 36.6, \"persist\": 1, \"corr\": 0.0}\n",
"a0 = alerts(TAB, **INIT)\n",
"before = summarize(TAB, a0)\n",
"log = aar_log(TAB, a0)\n",
"print(\"[1. ๋ฌด์์ด ์ผ์ด๋ฌ๋] ์ด๊ธฐ ์ค์ \", INIT, \"->\", before)\n",
"print(log.groupby([\"๊ตฌ๋ถ\", \"์์ธ\"]).size().rename(\"๊ฑด์\").to_string())\n",
"fa = log[log[\"๊ตฌ๋ถ\"] == \"์ค๊ฒฝ๋ณด\"]\n",
"print(\"\\n[2. ์ ์ผ์ด๋ฌ๋] ์ค๊ฒฝ๋ณด ์ ํ:\", fa[\"์ ํ\"].value_counts().to_dict(), \"/ ์ค๊ฒฝ๋ณด ๋ฐ์ ๋ณ์ฌ ์:\", fa[\"๋ณ์ฌ\"].nunique())\n",
"\n",
"# 2) ์ฌ๋ณด์ : ์ ์ฝ(์ค๊ฒฝ๋ณด์จ 15% ์ดํ, ์ฌํ์จ 90% ์ด์) ์์์ ์ฌ๋ฐ ์ต์ -> F1 ์ต๋ ์กฐํฉ ํ์\n",
"seen = set(map(tuple, fa[[\"๋ณ์ฌ\", \"์์ธ\"]].drop_duplicates().values))\n",
"rows = []\n",
"for th, tc, ps, co in itertools.product((37.8, 38.0, 38.2, 38.4), (36.6, 36.4, 36.2, 36.0), (1, 2, 3), (0.0, 0.25)):\n",
" al = alerts(TAB, th, tc, ps, co)\n",
" new = aar_log(TAB, al); new = new[new[\"๊ตฌ๋ถ\"] == \"์ค๊ฒฝ๋ณด\"]\n",
" rep = len(seen & set(map(tuple, new[[\"๋ณ์ฌ\", \"์์ธ\"]].drop_duplicates().values)))\n",
" rows.append({\"th_heat\": th, \"th_cold\": tc, \"persist\": ps, \"corr\": co, **summarize(TAB, al), \"์ฌ๋ฐ\": rep})\n",
"g = pd.DataFrame(rows)\n",
"ok = g[(g[\"์ค๊ฒฝ๋ณด์จ\"] <= 0.15) & (g[\"์ฌํ์จ\"] >= 0.90)].sort_values([\"์ฌ๋ฐ\", \"์ข
ํฉ์ ํ๋(F1)\", \"์ฌํ์จ\"], ascending=[True, False, False])\n",
"best = ok.iloc[0]\n",
"print(\"\\n[3. ๋ฌด์์ ๋ฐ๊พธ๋] ์ ์ฝ ๋ง์กฑ ์กฐํฉ ์์ 5๊ฐ(์ฌ๋ฐ ์ ์ ์) (์ ์ฒด ์กฐํฉ %d๊ฐ ์ค %d๊ฐ ๋ง์กฑ)\" % (len(g), len(ok)))\n",
"print(ok.head().to_string(index=False))\n",
"after = summarize(TAB, alerts(TAB, best[\"th_heat\"], best[\"th_cold\"], int(best[\"persist\"]), best[\"corr\"]))\n",
"gain = (after[\"์ข
ํฉ์ ํ๋(F1)\"] - before[\"์ข
ํฉ์ ํ๋(F1)\"]) * 100\n",
"print(f\"\\n์ฌ๋ณด์ ํ: {after}\")\n",
"print(f\"์ข
ํฉ์ ํ๋ ๋ณํ {before['์ข
ํฉ์ ํ๋(F1)']:.3f} -> {after['์ข
ํฉ์ ํ๋(F1)']:.3f} ({gain:+.1f}%p, ๋ชฉํ +10%p ์ด์: {'๋ฌ์ฑ' if gain >= 10 else '๋ฏธ๋ฌ'})\")\n",
"print(f\"AAR์์ ์๋ณํ (๋ณ์ฌ, ์์ธ) ์ค๊ฒฝ๋ณด {len(seen)}์ข
-> ์ฌ๋ณด์ ํ ๊ฐ์ ๋ณ์ฌยท๊ฐ์ ์์ธ์ผ๋ก ์ฌ๋ฐ {int(best['์ฌ๋ฐ'])}๊ฑด (๋ชฉํ 0๊ฑด: {'๋ฌ์ฑ' if best['์ฌ๋ฐ'] == 0 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"tuned = (f\"heat_temp_threshold: {best['th_heat']}\\ncold_temp_threshold: {best['th_cold']}\\nmin_persist_minutes: {int(best['persist'])}\\n\"\n",
" f\"wind_cooling_correction_c: {best['corr']}\\nconstraints: {{max_far: 0.15, min_recall: 0.90}}\")\n",
"print(\"\\n[์ฌ๋ณด์ ํ๋ผ๋ฏธํฐ(wel01_tuned_params.yaml ๋ด์ฉ)]\\n\" + tuned)\n",
"print(f\"๊ฐ์ ์กฐ์น: ์ด ๊ธฐ์ค {best['th_heat']}๋ยทํ๋ญ ๊ธฐ์ค {best['th_cold']}๋, {int(best['persist'])}๋ถ ์ฐ์ ์ง์ ์กฐ๊ฑด์ผ๋ก ์ค๊ฒฝ๋ณด๋ฅผ ์ค์๋ค(์ผ์ ๋๊ฐ ๋ณด์ {best['corr']}๋). ํ๋ผ๋ฏธํฐ๋ ์ค์ ์ฅ๋นยท์ธ์ ์๋ฃ๋ก ์ฌ๊ฒ์ฆํ๊ณ ๊ตฐ์๊ด ๊ฒํ ๋ฅผ ๊ฑฐ์ณ์ผ ํ๋ฉฐ ์ง๋จ ๊ธฐ์ค์ด ์๋๋ค.\")\n",
"\n",
"plt.figure(figsize=(6, 4))\n",
"plt.scatter(g[\"์ค๊ฒฝ๋ณด์จ\"], g[\"์ฌํ์จ\"], c=g[\"persist\"], cmap=\"viridis\", s=18)\n",
"plt.axvline(0.15, ls=\"--\", c=\"gray\"); plt.axhline(0.90, ls=\"--\", c=\"gray\"); plt.colorbar(label=\"persist (min)\")\n",
"plt.xlabel(\"false alarm rate\"); plt.ylabel(\"risk recall\"); plt.title(\"AAR re-tuning grid (dummy data)\")\n",
"plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
09 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[1. ๋ฌด์์ด ์ผ์ด๋ฌ๋] ์ด๊ธฐ ์ค์ {'th_heat': 37.8, 'th_cold': 36.6, 'persist': 1, 'corr': 0.0} -> {'๊ฒฝ๋ณด': 2570, '์ค๊ฒฝ๋ณด์จ': 0.405, '์ฌํ์จ': 1.0, '์ข
ํฉ์ ํ๋(F1)': 0.746}\n",
"๊ตฌ๋ถ ์์ธ \n",
"์ค๊ฒฝ๋ณด ๊ฐํ ์ผ์ ๋๊ฐ ์ค์ฐจ 380\n",
" ๊ธฐํ 5\n",
" ์ผ์ ํ ์ํฅ 6\n",
" ์๊ณ๊ฐ ๊ทผ์ (์กฐ๊ธฐ ๊ฒฝ๋ณด) 650\n",
"\n",
"[2. ์ ์ผ์ด๋ฌ๋] ์ค๊ฒฝ๋ณด ์ ํ: {'normal': 1041} / ์ค๊ฒฝ๋ณด ๋ฐ์ ๋ณ์ฌ ์: 58\n",
"\n",
"[3. ๋ฌด์์ ๋ฐ๊พธ๋] ์ ์ฝ ๋ง์กฑ ์กฐํฉ ์์ 5๊ฐ(์ฌ๋ฐ ์ ์ ์) (์ ์ฒด ์กฐํฉ 96๊ฐ ์ค 8๊ฐ ๋ง์กฑ)\n",
" th_heat th_cold persist corr ๊ฒฝ๋ณด ์ค๊ฒฝ๋ณด์จ ์ฌํ์จ ์ข
ํฉ์ ํ๋(F1) ์ฌ๋ฐ\n",
" 38.2 36.2 3 0.00 1487 0.073 0.902 0.914 20\n",
" 38.2 36.4 3 0.25 1545 0.103 0.906 0.902 21\n",
" 38.2 36.2 2 0.00 1543 0.086 0.923 0.919 23\n",
" 38.2 36.4 2 0.25 1603 0.116 0.927 0.905 27\n",
" 38.2 36.2 1 0.00 1601 0.102 0.940 0.918 29\n",
"\n",
"์ฌ๋ณด์ ํ: {'๊ฒฝ๋ณด': 1487, '์ค๊ฒฝ๋ณด์จ': 0.073, '์ฌํ์จ': 0.902, '์ข
ํฉ์ ํ๋(F1)': 0.914}\n",
"์ข
ํฉ์ ํ๋ ๋ณํ 0.746 -> 0.914 (+16.8%p, ๋ชฉํ +10%p ์ด์: ๋ฌ์ฑ)\n",
"AAR์์ ์๋ณํ (๋ณ์ฌ, ์์ธ) ์ค๊ฒฝ๋ณด 62์ข
-> ์ฌ๋ณด์ ํ ๊ฐ์ ๋ณ์ฌยท๊ฐ์ ์์ธ์ผ๋ก ์ฌ๋ฐ 20๊ฑด (๋ชฉํ 0๊ฑด: ๋ฏธ๋ฌ)\n",
"\n",
"[์ฌ๋ณด์ ํ๋ผ๋ฏธํฐ(wel01_tuned_params.yaml ๋ด์ฉ)]\n",
"heat_temp_threshold: 38.2\n",
"cold_temp_threshold: 36.2\n",
"min_persist_minutes: 3\n",
"wind_cooling_correction_c: 0.0\n",
"constraints: {max_far: 0.15, min_recall: 0.90}\n",
"๊ฐ์ ์กฐ์น: ์ด ๊ธฐ์ค 38.2๋ยทํ๋ญ ๊ธฐ์ค 36.2๋, 3๋ถ ์ฐ์ ์ง์ ์กฐ๊ฑด์ผ๋ก ์ค๊ฒฝ๋ณด๋ฅผ ์ค์๋ค(์ผ์ ๋๊ฐ ๋ณด์ 0.0๋). ํ๋ผ๋ฏธํฐ๋ ์ค์ ์ฅ๋นยท์ธ์ ์๋ฃ๋ก ์ฌ๊ฒ์ฆํ๊ณ ๊ตฐ์๊ด ๊ฒํ ๋ฅผ ๊ฑฐ์ณ์ผ ํ๋ฉฐ ์ง๋จ ๊ธฐ์ค์ด ์๋๋ค.\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_wel01.py, README.md, docs/wel01_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": [
"\"\"\"๋ฐ์ดํฐ ๊ฒ์ฆ, ์ ์ฒ๋ฆฌ, ๊ฐ์ธํ ์ํ ์ ์, ์ง์ ์กฐ๊ฑด ๊ฒฝ๋ณด, ๊ตฐ์๊ด ๊ฒํ ๋๊ธฐ์ด(HITL), ์๋ ์ํฉํ, AAR ์ฌ๋ณด์ ๊ฐ ์ ์ฉ๊น์ง ์จ์ดยทํ๋ญ ์คํธ๋ ์ค ๊ฒฝ๋ณด ์ ์ฒด ํ์ดํ๋ผ์ธ์ ํ ํ์ผ๋ก ํตํฉ ์คํํ๊ณ , ์์ฒด ์ ๊ฒ(์คํ ๋ผ์ธ ์ปค๋ฒ๋ฆฌ์ง ํฌํจ)๊ณผ ์งํ๊ดยท์๋ฌด๊ด ๋ธ๋ฆฌํ ์์ฝ์ ์ถ๋ ฅํ๋ค. ์ง๋จ ์๋ยท์๋ฌด๋ ์ฐ๊ณ.\"\"\"\n",
"import dis\n",
"import hashlib\n",
"import json\n",
"import sys\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.linear_model import LogisticRegression\n",
"from sklearn.pipeline import make_pipeline\n",
"from sklearn.preprocessing import StandardScaler\n",
"\n",
"# ---------------- [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋๋ฏธ ์์ฒด์ ํธ ์์ฑ๊ธฐ (์ค์ ์ฅ๋ณยท๋ถ๋ยท์๋ฌด ๊ธฐ๋ก ์์) ----------------\n",
"# ๊ฐ์ ๋ณ์ฌ(๊ฐ๋ช
ID)๊ฐ 60๋ถ๊ฐ ๊ฑท๊ธฐยทํด์ยท๊ณ ๊ฐ๋ ํ๋์ ํ๋ ๋์์ ์ฌ๋ฐ์ยท์ฒด์จ(์ถ์ ์ฌ๋ถ)ยทํผ๋ถ์ ๋๋๋ฅผ 1๋ถ ๊ฐ๊ฒฉ์ผ๋ก ๋ง๋ ๋ค.\n",
"# ์ง๋จ ๋๊ตฌ๊ฐ ์๋๋ฉฐ, ์ํ ๋ผ๋ฒจ์ ๊ฐ์ ์๋ฆฌ ๋ชจ๋ธ์ '๊ต์ก์ฉ ๋จ๊ณ'์ผ ๋ฟ์ด๋ค.\n",
"N_MIN = 60\n",
"STAGE = {0: \"์ ์\", 1: \"์ฃผ์\", 2: \"๊ฒฝ๋ณด\"}\n",
"TYPES = [\"normal\", \"heat\", \"cold\"]\n",
"\n",
"\n",
"def make_person(i, rng):\n",
" return {\"id\": f\"S{i:03d}\", \"rest_hr\": float(rng.normal(64, 7)), \"age\": int(rng.integers(19, 27)), \"bmi\": float(rng.normal(23.5, 2.5)),\n",
" \"acclim\": int(rng.random() < 0.4), \"fit\": float(np.clip(rng.normal(0.5, 0.2), 0.05, 0.95))}\n",
"\n",
"\n",
"def make_env(kind, rng):\n",
" if kind == \"heat\":\n",
" return {\"temp_c\": float(rng.uniform(30, 40)), \"rh\": float(rng.uniform(45, 85)), \"wind\": float(rng.uniform(0.3, 3))}\n",
" if kind == \"cold\":\n",
" return {\"temp_c\": float(rng.uniform(-22, 2)), \"rh\": float(rng.uniform(40, 80)), \"wind\": float(rng.uniform(2, 11))}\n",
" return {\"temp_c\": float(rng.uniform(8, 24)), \"rh\": float(rng.uniform(35, 70)), \"wind\": float(rng.uniform(0.3, 5))}\n",
"\n",
"\n",
"def make_session(sid, kind, rng, p=None, glitch=True):\n",
" \"\"\"๋ฐํ: dict(person, env, kind, df[hr,temp,eda,act + ์ ๋ต coreยทstageยทrtype]). ์ผ์ ๊ฒฐ์ธกยทํยท๊ณ ์ฐฉ ํฌํจ.\"\"\"\n",
" p = p or make_person(sid, rng)\n",
" env = make_env(kind, rng)\n",
" act = np.zeros(N_MIN); t = 0\n",
" while t < N_MIN: # 10~20๋ถ ๋จ์ ํ๋ ๋ธ๋ก: ํด์ 1.5 / ํ๊ตฐ 4.5 / ๊ณ ๊ฐ๋ 7 MET\n",
" d = int(rng.integers(10, 21)); act[t:t + d] = rng.choice([1.5, 4.5, 7.0], p=[0.25, 0.5, 0.25]); t += d\n",
" hi = env[\"temp_c\"] + (0.12 * (env[\"rh\"] - 40) if env[\"temp_c\"] > 25 else 0)\n",
" wc = env[\"temp_c\"] - 1.6 * env[\"wind\"]\n",
" core, cores = 36.9, []\n",
" for m in range(N_MIN):\n",
" heat = 0.085 * max(0, hi - 24) * (1 - 0.25 * p[\"acclim\"]) * (1 + 0.5 * m / N_MIN) * (1 + 0.012 * (p[\"bmi\"] - 23.5))\n",
" cold = -0.075 * max(0, 8 - wc) * (1 - 0.09 * act[m])\n",
" ss = 36.9 + 0.09 * act[m] + heat + cold\n",
" core += (ss - core) / 18 + rng.normal(0, 0.03)\n",
" cores.append(core)\n",
" core = np.array(cores)\n",
" hr = p[\"rest_hr\"] + 9.5 * (act - 1.5) * (1.15 - 0.3 * p[\"fit\"]) + 11 * (core - 36.9) + rng.normal(0, 3.2, N_MIN)\n",
" eda = 2.0 + 0.25 * act + 3.2 * np.maximum(core - 37.2, 0) - 0.04 * np.maximum(8 - wc, 0) + rng.normal(0, 0.25, N_MIN)\n",
" bias = rng.normal(0, 0.1)\n",
" temp = core + bias + rng.normal(0, 0.1, N_MIN) - np.where((kind == \"cold\") & (env[\"wind\"] > 7), 0.25, 0.0) # ์ผ์ ์ ์ด ๋ถ์ ๋ฐ๋ ๋๊ฐ ์ค์ฐจ\n",
" df = pd.DataFrame({\"min\": np.arange(N_MIN), \"hr\": hr, \"temp\": temp, \"eda\": np.maximum(eda, 0.2), \"act\": act})\n",
" if glitch:\n",
" for col in (\"hr\", \"temp\", \"eda\"):\n",
" df.loc[rng.random(N_MIN) < 0.03, col] = np.nan # ๊ฒฐ์ธก\n",
" s = int(rng.integers(5, N_MIN))\n",
" if rng.random() < 0.12:\n",
" df.loc[s, \"hr\"] = 230.0 # ์ผ์ ํ\n",
" if rng.random() < 0.12:\n",
" df.loc[s, \"temp\"] = 45.0\n",
" ht = np.where(core >= 39.0, 2, np.where(core >= 38.3, 1, 0)); cd = np.where(core <= 35.4, 2, np.where(core <= 36.2, 1, 0))\n",
" df[\"core\"] = core\n",
" df[\"stage\"] = np.maximum(ht, cd); df[\"rtype\"] = np.where(ht > 0, \"heat\", np.where(cd > 0, \"cold\", \"normal\"))\n",
" return {\"id\": p[\"id\"], \"person\": p, \"env\": env, \"kind\": kind, \"df\": df}\n",
"\n",
"\n",
"def make_dataset(n, seed):\n",
" rng = np.random.default_rng(seed)\n",
" kinds = [\"normal\", \"heat\", \"cold\"]\n",
" return [make_session(i, kinds[i % 3], rng) for i in range(n)]\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---------------- ์ ์ฒ๋ฆฌยทํน์ง ์ถ์ถ ๊ณตํต ํจ์ ----------------\n",
"LIMITS = {\"hr\": (30, 220), \"temp\": (30.0, 43.0), \"eda\": (0.0, 40.0)} # ์๋ฆฌ์ ์ผ๋ก ๊ฐ๋ฅํ ๋ฒ์(๋ฒ์ ๋ฐ = ์ผ์ ์ค๋ฅ)\n",
"\n",
"\n",
"def clean(df):\n",
" \"\"\"๋ฒ์ ๋ฐ ๊ฐ์ ๊ฒฐ์ธก ์ฒ๋ฆฌ -> ์ ํ ๋ณด๊ฐ(์ ๋์ ๊ฐ๊น์ด ๊ฐ) -> 3๋ถ ์ด๋ ์ค์๊ฐ์ผ๋ก ํ ์ ๊ฑฐ. ๋ฐํ: (์ ์ df, ๋ณด์ ๊ฑด์)\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 10] ๋๋ฏธ ์์ฒด์ ํธ ๊ธฐ๋ฐ ์จ์ดยทํ๋ญ ์คํธ๋ ์ค ๊ฒฝ๋ณด ์์คํ
ํตํฉ ํจํค์ง ์์ฑ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def env_index(env):\n",
" # [TODO : ํ์ ์ค์ต 10] ๋๋ฏธ ์์ฒด์ ํธ ๊ธฐ๋ฐ ์จ์ดยทํ๋ญ ์คํธ๋ ์ค ๊ฒฝ๋ณด ์์คํ
ํตํฉ ํจํค์ง ์์ฑ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def features(s, use_env=True, personal=True):\n",
" \"\"\"์ธ์
1๊ฐ -> ๋ถ ๋จ์ ํน์งํ(์ต๊ทผ 5๋ถ ํ๊ท ยท๊ธฐ์ธ๊ธฐ ํฌํจ). ์ ๋ต stage/rtype ํฌํจ.\"\"\"\n",
" df, _ = clean(s[\"df\"]); p, e = s[\"person\"], s[\"env\"]\n",
" f = pd.DataFrame({\"sid\": s[\"id\"], \"min\": df[\"min\"]})\n",
" f[\"temp5\"] = df[\"temp\"].rolling(5, min_periods=1).mean()\n",
" f[\"hr5\"] = df[\"hr\"].rolling(5, min_periods=1).mean()\n",
" f[\"eda5\"] = df[\"eda\"].rolling(5, min_periods=1).mean()\n",
" f[\"dtemp\"] = (f[\"temp5\"] - f[\"temp5\"].shift(4)).fillna(0)\n",
" f[\"act\"] = df[\"act\"]\n",
" if personal:\n",
" f[\"hr_rel\"] = f[\"hr5\"] - p[\"rest_hr\"]\n",
" f[\"acclim\"], f[\"bmi\"], f[\"fit\"] = p[\"acclim\"], p[\"bmi\"], p[\"fit\"]\n",
" if use_env:\n",
" f[\"hi\"], f[\"wc\"] = env_index(e); f[\"wind\"] = e[\"wind\"]\n",
" f[\"stage\"], f[\"rtype\"], f[\"kind\"] = df[\"stage\"], df[\"rtype\"], s[\"kind\"]\n",
" return f[f[\"min\"] >= 5].reset_index(drop=True)\n",
"\n",
"\n",
"def table(sessions, **kw):\n",
" return pd.concat([features(s, **kw) for s in sessions], ignore_index=True)\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"CONFIG = {\"alert_threshold\": 0.4, \"min_persist\": 3, \"heat_temp\": 38.2, \"cold_temp\": 36.2} # ๋ฏธ์
9 ์ฌ๋ณด์ ๊ฐ + ๋ชจ๋ธ ์๊ณ๊ฐ(์ง์ ์กฐ๊ฑด์ด ์ค๊ฒฝ๋ณด๋ฅผ ๋ง์ผ๋ฏ๋ก ๋ฏธ์
5์ 0.5๋ณด๋ค ๋ฎ์ถฐ ๋ฏธํ์ ์ค์)\n",
"FE = [\"temp5\", \"dtemp\", \"hr5\", \"eda5\", \"hr_rel\", \"acclim\", \"bmi\", \"fit\", \"hi\", \"wc\", \"wind\"]\n",
"\n",
"\n",
"def validate(s):\n",
" errs = []\n",
" if s[\"kind\"] not in TYPES:\n",
" errs.append(\"kind ํ์ฉ๊ฐ ์๋\")\n",
" if len(s[\"df\"]) != N_MIN or not {\"hr\", \"temp\", \"eda\"} <= set(s[\"df\"].columns):\n",
" errs.append(\"์ ํธ ๊ธธ์ดยทํ๋ ์ค๋ฅ\")\n",
" if not str(s[\"id\"]).startswith(\"S\"):\n",
" errs.append(\"soldier_id ํ์ ์ค๋ฅ\")\n",
" return errs\n",
"\n",
"\n",
"def run_session(s, model):\n",
" \"\"\"์ธ์
1๊ฐ: ์ ์ -> ํน์ง -> ์ ์ -> ์ง์ ์กฐ๊ฑด ๊ฒฝ๋ณด. ์ ํธ๊ฐ 5๋ถ ์ด์ ๋๊ธฐ๋ฉด '์ ํธ๋์ ' ํ๋๊ทธ.\"\"\"\n",
" f = features(s)\n",
" raw = s[\"df\"].set_index(\"min\")\n",
" f[\"lost\"] = [bool(raw.loc[max(m - 4, 0):m, [\"hr\", \"temp\"]].isna().all().all()) for m in f[\"min\"]]\n",
" f[\"score\"] = model.predict_proba(f[FE])[:, 1]\n",
" hot = (f[\"temp5\"] >= CONFIG[\"heat_temp\"]) | (f[\"temp5\"] <= CONFIG[\"cold_temp\"])\n",
" cond = (f[\"score\"] >= CONFIG[\"alert_threshold\"]) & (hot | (f[\"score\"] >= 0.8))\n",
" f[\"alert\"] = cond.rolling(CONFIG[\"min_persist\"], min_periods=CONFIG[\"min_persist\"]).min().fillna(0).astype(bool) & ~f[\"lost\"]\n",
" f[\"grade\"] = np.where(f[\"score\"] >= 0.8, \"High\", np.where(f[\"score\"] >= 0.5, \"Medium\", \"Low\"))\n",
" return f\n",
"\n",
"\n",
"class Queue:\n",
" def __init__(self):\n",
" self.items, self.log = [], []\n",
"\n",
" def submit(self, sid, minute, score, grade):\n",
" prev = self.log[-1][\"hash\"] if self.log else \"GENESIS\"\n",
" body = {\"event\": \"SUBMIT\", \"id\": f\"{sid}@{minute}\", \"score\": round(score, 3), \"by\": \"AI\"}\n",
" self.log.append({**body, \"prev\": prev, \"hash\": hashlib.sha256((prev + json.dumps(body, sort_keys=True)).encode()).hexdigest()})\n",
" self.items.append({\"id\": f\"{sid}@{minute}\", \"grade\": grade, \"status\": \"PENDING\"})\n",
"\n",
" def verify(self):\n",
" prev = \"GENESIS\"\n",
" for rec in self.log:\n",
" body = {k: v for k, v in rec.items() if k not in (\"prev\", \"hash\")}\n",
" if rec[\"prev\"] != prev or rec[\"hash\"] != hashlib.sha256((prev + json.dumps(body, sort_keys=True)).encode()).hexdigest():\n",
" return False\n",
" prev = rec[\"hash\"]\n",
" return True\n",
"\n",
"\n",
"def episodes(f):\n",
" \"\"\"์ฐ์ ๊ฒฝ๋ณด ๋ถ์ ํ๋์ ์ฌ๊ฑด์ผ๋ก ๋ฌถ์ด ์ฒซ ๋ถ๋ง ๊ฒํ ๋๊ธฐ์ด์ ์ฌ๋ฆฐ๋ค(๊ฐ์ ์ฌ๊ฑด ์ค๋ณต ํต๋ณด ๋ฐฉ์ง).\"\"\"\n",
" a = f[\"alert\"].values\n",
" return [i for i in range(len(a)) if a[i] and (i == 0 or not a[i - 1])]\n",
"\n",
"\n",
"t_start = time.perf_counter()\n",
"# ---------------- 1. ํ์ต(์ ์ ์์ง ์ค๋น) ----------------\n",
"tr = table(make_dataset(150, seed=51))\n",
"model = make_pipeline(StandardScaler(), LogisticRegression(class_weight=\"balanced\", max_iter=3000, random_state=0)).fit(tr[FE], tr[\"stage\"] >= 1)\n",
"\n",
"# ---------------- 2. ์ด์ฉ ๋ฐ์ดํฐ ์ฒ๋ฆฌ(์์ 1๊ฑด, ๊ธฐ๊ธฐ ๋์ 1๊ฑด ์ฃผ์
) ----------------\n",
"rng = np.random.default_rng(61)\n",
"ops = [make_session(i, TYPES[i % 3], rng) for i in range(60)]\n",
"ops[4] = {**ops[4], \"kind\": \"tundra\"} # ํ์ฉ๋์ง ์์ ์ ํ\n",
"dead = ops[8][\"df\"].copy(); dead.loc[35:, [\"hr\", \"temp\", \"eda\"]] = np.nan; ops[8] = {**ops[8], \"df\": dead} # ๊ธฐ๊ธฐ ๋์ \n",
"queue, rejected, runs, results = Queue(), {}, [], []\n",
"codes = {f.__code__ for f in (validate, run_session, episodes, Queue.submit, Queue.verify, clean, env_index, features, table)}\n",
"lines_total = {c: {ln for _, ln in dis.findlinestarts(c) if ln != c.co_firstlineno} for c in codes}\n",
"trace_lines = set()\n",
"\n",
"\n",
"def tracer(frame, event, arg):\n",
" if frame.f_code in codes:\n",
" def local(fr, ev, ar):\n",
" if ev == \"line\":\n",
" trace_lines.add((fr.f_code, fr.f_lineno))\n",
" return local\n",
" return local\n",
"\n",
"\n",
"sys.settrace(tracer)\n",
"for s in ops:\n",
" errs = validate(s)\n",
" if errs:\n",
" rejected[s[\"id\"]] = errs; continue\n",
" t0 = time.perf_counter()\n",
" f = run_session(s, model)\n",
" for i in episodes(f):\n",
" queue.submit(s[\"id\"], int(f[\"min\"].iloc[i]), float(f[\"score\"].iloc[i]), f[\"grade\"].iloc[i])\n",
" runs.append({\"sid\": s[\"id\"], \"ms_per_min\": (time.perf_counter() - t0) * 1000 / len(f), \"events\": len(episodes(f)), \"lost\": int(f[\"lost\"].any())})\n",
" results.append(f)\n",
"queue.verify()\n",
"sys.settrace(None)\n",
"covered = sum(len({ln for c2, ln in trace_lines if c2 is c} & lines_total[c]) for c in codes)\n",
"cov = covered / sum(len(v) for v in lines_total.values())\n",
"\n",
"# ---------------- 3. ์ฑ๋ฅ ํ๊ฐ ----------------\n",
"allf = pd.concat(results, ignore_index=True)\n",
"truth = allf[\"stage\"] >= 1\n",
"far = (allf[\"alert\"] & ~truth).sum() / max(allf[\"alert\"].sum(), 1)\n",
"rec = (allf[\"alert\"] & truth).sum() / truth.sum()\n",
"risk_sids = set(allf.loc[truth, \"sid\"]); alerted = set(allf.loc[allf[\"alert\"] & truth, \"sid\"])\n",
"ms = np.mean([r[\"ms_per_min\"] for r in runs])\n",
"\n",
"# ---------------- 4. ์์ฒด ์ ๊ฒ(ํตํฉ ํ
์คํธ) ----------------\n",
"chk = {\n",
" \"์์ ๋ฐ์ดํฐ ์ฐจ๋จ\": \"S004\" in rejected,\n",
" \"์ฒ๋ฆฌ ์ธ์
59๊ฐ ์ ๋ ์ฑ๊ณต\": len(runs) == 59,\n",
" \"์ ํธ๋์ ๋ณ์ฌ ์๋ณ\": any(r[\"lost\"] for r in runs) and bool(allf.loc[allf[\"sid\"] == \"S008\", \"lost\"].any()),\n",
" \"์ํ ๊ตฌ๊ฐ ์ฌํ์จ 85% ์ด์\": rec >= 0.85,\n",
" \"์ค๊ฒฝ๋ณด์จ 15% ์ดํ\": far <= 0.15,\n",
" \"์ํ ๋ณ์ฌ ์ฌ๊ฑด ์๋ณ 90% ์ด์\": len(alerted) / max(len(risk_sids), 1) >= 0.9,\n",
" \"์๋ ์กฐ์น 0๊ฑด\": all(i[\"status\"] == \"PENDING\" for i in queue.items),\n",
" \"๊ฐ์ฌ ๋ก๊ทธ ๊ฑด์ ์ผ์นยท๋ฌด๊ฒฐ\": len(queue.log) == len(queue.items) and queue.verify(),\n",
" \"๋ถ๋น ์ฒ๋ฆฌ 50ms ์ด๋ด\": ms < 50,\n",
" \"์คํ ๋ผ์ธ ์ปค๋ฒ๋ฆฌ์ง 85% ์ด์\": cov >= 0.85,\n",
"}\n",
"passed = sum(chk.values())\n",
"for k, v in chk.items():\n",
" print(f\"[{'PASS' if v else 'FAIL'}] {k}\")\n",
"print(f\"์์ฒด ์ ๊ฒ {passed}/{len(chk)} ํต๊ณผ ({passed / len(chk):.0%}), ํ์ดํ๋ผ์ธ ์คํ ์ฑ๊ณต {'์' if len(runs) + len(rejected) == len(ops) else '์๋์ค'}, ํต์ฌ ๋ชจ๋ ์คํ ๋ผ์ธ ์ปค๋ฒ๋ฆฌ์ง {cov:.1%}\")\n",
"\n",
"# ---------------- 5. ์๋ ์ํฉํ ----------------\n",
"colr = {\"High\": \"tab:red\", \"Medium\": \"orange\", \"Low\": \"tab:gray\"}\n",
"fig, ax = plt.subplots(2, 2, figsize=(12, 8))\n",
"top = sorted(results, key=lambda f: -f[\"score\"].max())[:2]\n",
"for a, f in zip(ax[0], top):\n",
" a.plot(f[\"min\"], f[\"temp5\"], label=\"temp (5-min mean)\"); a.scatter(f[\"min\"][f[\"alert\"]], f[\"temp5\"][f[\"alert\"]], color=\"red\", s=12, label=\"alert\")\n",
" a.set_title(f\"{f['sid'].iloc[0]} temperature and alerts\"); a.set_xlabel(\"minute\"); a.legend()\n",
"g = pd.Series([i[\"grade\"] for i in queue.items]).value_counts().reindex([\"High\", \"Medium\", \"Low\"]).fillna(0)\n",
"ax[1, 0].bar(g.index, g.values, color=[colr[i] for i in g.index]); ax[1, 0].set_title(\"Review queue events by grade (pending human review)\")\n",
"ax[1, 1].axis(\"off\")\n",
"ax[1, 1].text(0, 0.9, f\"Risk recall {rec:.1%}\\nFalse alarm {far:.1%}\\nQueue {len(queue.items)} pending\\nLatency {ms:.2f} ms/min\\nSelf-check {passed}/{len(chk)}\", family=\"monospace\", fontsize=13, va=\"top\")\n",
"plt.suptitle(\"WEL01 integrated pipeline - dummy data, not diagnosis\"); plt.tight_layout(); plt.show()\n",
"\n",
"# ---------------- 6. ๋ธ๋ฆฌํ ์์ฝ ----------------\n",
"print(f\"\"\"\n",
"[์ดยทํ๋ญ ์คํธ๋ ์ค ๊ฒฝ๋ณด ๋ธ๋ฆฌํ ์์ฝ - ๊ต์ก์ฉ๊ฐ์์๋ฃ, ์ง๋จ ์๋]\n",
"1. ์ฒ๋ฆฌ: ๊ฐ์ ๋ณ์ฌ {len(ops)}๋ช
์ค {len(runs)}๋ช
์ฒ๋ฆฌ, ์์ ๋ฐ์ดํฐ {len(rejected)}๊ฑด ์ฐจ๋จ({', '.join(rejected)}), ์ ํธ๋์ {sum(r['lost'] for r in runs)}๋ช
ํ์ธ ํ์ ํ์\n",
"2. ์ฑ๋ฅ: ์ํ ๊ตฌ๊ฐ ์ฌํ์จ {rec:.1%}, ์ค๊ฒฝ๋ณด์จ {far:.1%}, ๋ถ๋น {ms:.2f} ms, ์ ์ฒด ์์ {time.perf_counter() - t_start:.1f} s\n",
"3. ๊ฒฝ๋ณด: {len(queue.items)}๊ฑด ๊ตฐ์๊ดยท์๋์ฅ ๊ฒํ ๋๊ธฐ (High {sum(i['grade'] == 'High' for i in queue.items)} / Medium {sum(i['grade'] == 'Medium' for i in queue.items)} / Low {sum(i['grade'] == 'Low' for i in queue.items)}), ์๋ ์กฐ์น 0๊ฑด\n",
"4. ์ด์ฉ ์ ์: ํด์ ๊ถ๊ณ ยท์๋ฌด ๋ฐฐ์ ยท์๋ฌด๋ ์ธ๊ณ๋ ๊ตฐ์๊ดยท์๋์ฅยท์งํ๊ด์ ์น์ธ ํ์๋ง ์ด๋ฃจ์ด์ง๋ฉฐ, ์ํ์ ํ๋จ์ ์๋ฌด๋ยท๊ตฐ์๊ด์๊ฒ ์ฐ๊ณํ๋ค.\n",
"5. ํ๊ณ: ๊ฐ์ ์๋ฆฌ ๋ชจ๋ธ๋ก ๋ง๋ ๋๋ฏธ ์ ํธ ๊ธฐ์ค ๊ฒฐ๊ณผ๋ก, ์ง๋จ ๋๊ตฌ๊ฐ ์๋๋ฉฐ ์ค์ ์ฅ๋นยท์ธ์ ์๋ฃ๋ก ์ฌ๊ฒ์ฆ์ด ํ์ํ๋ค.\"\"\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
10 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[PASS] ์์ ๋ฐ์ดํฐ ์ฐจ๋จ\n",
"[PASS] ์ฒ๋ฆฌ ์ธ์
59๊ฐ ์ ๋ ์ฑ๊ณต\n",
"[PASS] ์ ํธ๋์ ๋ณ์ฌ ์๋ณ\n",
"[PASS] ์ํ ๊ตฌ๊ฐ ์ฌํ์จ 85% ์ด์\n",
"[PASS] ์ค๊ฒฝ๋ณด์จ 15% ์ดํ\n",
"[PASS] ์ํ ๋ณ์ฌ ์ฌ๊ฑด ์๋ณ 90% ์ด์\n",
"[PASS] ์๋ ์กฐ์น 0๊ฑด\n",
"[PASS] ๊ฐ์ฌ ๋ก๊ทธ ๊ฑด์ ์ผ์นยท๋ฌด๊ฒฐ\n",
"[PASS] ๋ถ๋น ์ฒ๋ฆฌ 50ms ์ด๋ด\n",
"[PASS] ์คํ ๋ผ์ธ ์ปค๋ฒ๋ฆฌ์ง 85% ์ด์\n",
"์์ฒด ์ ๊ฒ 10/10 ํต๊ณผ (100%), ํ์ดํ๋ผ์ธ ์คํ ์ฑ๊ณต ์, ํต์ฌ ๋ชจ๋ ์คํ ๋ผ์ธ ์ปค๋ฒ๋ฆฌ์ง 92.7%\n",
"\n",
"[์ดยทํ๋ญ ์คํธ๋ ์ค ๊ฒฝ๋ณด ๋ธ๋ฆฌํ ์์ฝ - ๊ต์ก์ฉ๊ฐ์์๋ฃ, ์ง๋จ ์๋]\n",
"1. ์ฒ๋ฆฌ: ๊ฐ์ ๋ณ์ฌ 60๋ช
์ค 59๋ช
์ฒ๋ฆฌ, ์์ ๋ฐ์ดํฐ 1๊ฑด ์ฐจ๋จ(S004), ์ ํธ๋์ 1๋ช
ํ์ธ ํ์ ํ์\n",
"2. ์ฑ๋ฅ: ์ํ ๊ตฌ๊ฐ ์ฌํ์จ 85.6%, ์ค๊ฒฝ๋ณด์จ 8.3%, ๋ถ๋น 2.24 ms, ์ ์ฒด ์์ 10.2 s\n",
"3. ๊ฒฝ๋ณด: 40๊ฑด ๊ตฐ์๊ดยท์๋์ฅ ๊ฒํ ๋๊ธฐ (High 24 / Medium 15 / Low 1), ์๋ ์กฐ์น 0๊ฑด\n",
"4. ์ด์ฉ ์ ์: ํด์ ๊ถ๊ณ ยท์๋ฌด ๋ฐฐ์ ยท์๋ฌด๋ ์ธ๊ณ๋ ๊ตฐ์๊ดยท์๋์ฅยท์งํ๊ด์ ์น์ธ ํ์๋ง ์ด๋ฃจ์ด์ง๋ฉฐ, ์ํ์ ํ๋จ์ ์๋ฌด๋ยท๊ตฐ์๊ด์๊ฒ ์ฐ๊ณํ๋ค.\n",
"5. ํ๊ณ: ๊ฐ์ ์๋ฆฌ ๋ชจ๋ธ๋ก ๋ง๋ ๋๋ฏธ ์ ํธ ๊ธฐ์ค ๊ฒฐ๊ณผ๋ก, ์ง๋จ ๋๊ตฌ๊ฐ ์๋๋ฉฐ ์ค์ ์ฅ๋นยท์ธ์ ์๋ฃ๋ก ์ฌ๊ฒ์ฆ์ด ํ์ํ๋ค.\n",
"```\n",
" \n"
]
}
],
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}